client hub bloc updated

This commit is contained in:
Achintha Isuru
2026-02-24 13:53:36 -05:00
parent 7591e71c3d
commit e78d5938dd
9 changed files with 66 additions and 309 deletions

View File

@@ -1,10 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:design_system/src/ui_typography.dart';
import 'package:flutter/material.dart';
import '../ui_icons.dart';
import 'ui_icon_button.dart';
/// A custom AppBar for the Krow UI design system.
///
/// This widget provides a consistent look and feel for top app bars across the application.

View File

@@ -2,10 +2,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
import '../../domain/arguments/delete_hub_arguments.dart';
import '../../domain/usecases/assign_nfc_tag_usecase.dart';
import '../../domain/usecases/delete_hub_usecase.dart';
import '../../domain/usecases/get_hubs_usecase.dart';
import 'client_hubs_event.dart';
import 'client_hubs_state.dart';
@@ -13,39 +9,18 @@ import 'client_hubs_state.dart';
/// BLoC responsible for managing the state of the Client Hubs feature.
///
/// It orchestrates the flow between the UI and the domain layer by invoking
/// specific use cases for fetching, deleting, and assigning tags to hubs.
/// specific use cases for fetching hubs.
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
with BlocErrorHandler<ClientHubsState>
implements Disposable {
ClientHubsBloc({
required GetHubsUseCase getHubsUseCase,
required DeleteHubUseCase deleteHubUseCase,
required AssignNfcTagUseCase assignNfcTagUseCase,
}) : _getHubsUseCase = getHubsUseCase,
_deleteHubUseCase = deleteHubUseCase,
_assignNfcTagUseCase = assignNfcTagUseCase,
super(const ClientHubsState()) {
ClientHubsBloc({required GetHubsUseCase getHubsUseCase})
: _getHubsUseCase = getHubsUseCase,
super(const ClientHubsState()) {
on<ClientHubsFetched>(_onFetched);
on<ClientHubsDeleteRequested>(_onDeleteRequested);
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
on<ClientHubsMessageCleared>(_onMessageCleared);
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
}
final GetHubsUseCase _getHubsUseCase;
final DeleteHubUseCase _deleteHubUseCase;
final AssignNfcTagUseCase _assignNfcTagUseCase;
void _onIdentifyDialogToggled(
ClientHubsIdentifyDialogToggled event,
Emitter<ClientHubsState> emit,
) {
if (event.hub == null) {
emit(state.copyWith(clearHubToIdentify: true));
} else {
emit(state.copyWith(hubToIdentify: event.hub));
}
}
Future<void> _onFetched(
ClientHubsFetched event,
@@ -66,61 +41,6 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
);
}
Future<void> _onDeleteRequested(
ClientHubsDeleteRequested event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError(
emit: emit.call,
action: () async {
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase.call();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub deleted successfully',
),
);
},
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
);
}
Future<void> _onNfcTagAssignRequested(
ClientHubsNfcTagAssignRequested event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError(
emit: emit.call,
action: () async {
await _assignNfcTagUseCase.call(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
);
final List<Hub> hubs = await _getHubsUseCase.call();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'NFC tag assigned successfully',
clearHubToIdentify: true,
),
);
},
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
);
}
void _onMessageCleared(
ClientHubsMessageCleared event,
Emitter<ClientHubsState> emit,
@@ -130,8 +50,8 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
clearErrorMessage: true,
clearSuccessMessage: true,
status:
state.status == ClientHubsStatus.actionSuccess ||
state.status == ClientHubsStatus.actionFailure
state.status == ClientHubsStatus.success ||
state.status == ClientHubsStatus.failure
? ClientHubsStatus.success
: state.status,
),

View File

@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base class for all client hubs events.
abstract class ClientHubsEvent extends Equatable {
@@ -14,38 +13,7 @@ class ClientHubsFetched extends ClientHubsEvent {
const ClientHubsFetched();
}
/// Event triggered to delete a hub.
class ClientHubsDeleteRequested extends ClientHubsEvent {
const ClientHubsDeleteRequested(this.hubId);
final String hubId;
@override
List<Object?> get props => <Object?>[hubId];
}
/// Event triggered to assign an NFC tag to a hub.
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
const ClientHubsNfcTagAssignRequested({
required this.hubId,
required this.nfcTagId,
});
final String hubId;
final String nfcTagId;
@override
List<Object?> get props => <Object?>[hubId, nfcTagId];
}
/// Event triggered to clear any error or success messages.
class ClientHubsMessageCleared extends ClientHubsEvent {
const ClientHubsMessageCleared();
}
/// Event triggered to toggle the visibility of the "Identify NFC" dialog.
class ClientHubsIdentifyDialogToggled extends ClientHubsEvent {
const ClientHubsIdentifyDialogToggled({this.hub});
final Hub? hub;
@override
List<Object?> get props => <Object?>[hub];
}

View File

@@ -2,15 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Enum representing the status of the client hubs state.
enum ClientHubsStatus {
initial,
loading,
success,
failure,
actionInProgress,
actionSuccess,
actionFailure,
}
enum ClientHubsStatus { initial, loading, success, failure }
/// State class for the ClientHubs BLoC.
class ClientHubsState extends Equatable {
@@ -19,7 +11,6 @@ class ClientHubsState extends Equatable {
this.hubs = const <Hub>[],
this.errorMessage,
this.successMessage,
this.hubToIdentify,
});
final ClientHubsStatus status;
@@ -27,17 +18,11 @@ class ClientHubsState extends Equatable {
final String? errorMessage;
final String? successMessage;
/// The hub currently being identified/assigned an NFC tag.
/// If null, the identification dialog is closed.
final Hub? hubToIdentify;
ClientHubsState copyWith({
ClientHubsStatus? status,
List<Hub>? hubs,
String? errorMessage,
String? successMessage,
Hub? hubToIdentify,
bool clearHubToIdentify = false,
bool clearErrorMessage = false,
bool clearSuccessMessage = false,
}) {
@@ -50,9 +35,6 @@ class ClientHubsState extends Equatable {
successMessage: clearSuccessMessage
? null
: (successMessage ?? this.successMessage),
hubToIdentify: clearHubToIdentify
? null
: (hubToIdentify ?? this.hubToIdentify),
);
}
@@ -62,6 +44,5 @@ class ClientHubsState extends Equatable {
hubs,
errorMessage,
successMessage,
hubToIdentify,
];
}

View File

@@ -29,7 +29,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
emit(state.copyWith(status: EditHubStatus.loading));
await handleError(
emit: emit,
emit: emit.call,
action: () async {
await _createHubUseCase.call(
CreateHubArguments(
@@ -64,7 +64,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
emit(state.copyWith(status: EditHubStatus.loading));
await handleError(
emit: emit,
emit: emit.call,
action: () async {
await _updateHubUseCase.call(
UpdateHubArguments(

View File

@@ -30,7 +30,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
emit(state.copyWith(status: HubDetailsStatus.loading));
await handleError(
emit: emit,
emit: emit.call,
action: () async {
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id));
emit(
@@ -54,7 +54,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
emit(state.copyWith(status: HubDetailsStatus.loading));
await handleError(
emit: emit,
emit: emit.call,
action: () async {
await _assignNfcTagUseCase.call(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),

View File

@@ -12,7 +12,6 @@ import '../blocs/client_hubs_state.dart';
import '../widgets/hub_card.dart';
import '../widgets/hub_empty_state.dart';
import '../widgets/hub_info_card.dart';
import '../widgets/identify_nfc_dialog.dart';
/// The main page for the client hubs feature.
///
@@ -72,84 +71,54 @@ class ClientHubsPage extends StatelessWidget {
),
child: const Icon(UiIcons.add),
),
body: Stack(
children: <Widget>[
CustomScrollView(
slivers: <Widget>[
_buildAppBar(context),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space5,
).copyWith(bottom: 100),
sliver: SliverList(
delegate: SliverChildListDelegate(<Widget>[
if (state.status == ClientHubsStatus.loading)
const Center(child: CircularProgressIndicator())
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () async {
final bool? success = await Modular.to
.toEditHub();
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsFetched());
}
},
)
else ...<Widget>[
...state.hubs.map(
(Hub hub) => HubCard(
hub: hub,
onTap: () async {
final bool? success = await Modular.to
.toHubDetails(hub);
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsFetched());
}
},
onNfcPressed: () =>
BlocProvider.of<ClientHubsBloc>(
context,
).add(
ClientHubsIdentifyDialogToggled(hub: hub),
),
onDeletePressed: () =>
_confirmDeleteHub(context, hub),
),
),
],
const SizedBox(height: UiConstants.space5),
const HubInfoCard(),
]),
body: CustomScrollView(
slivers: <Widget>[
_buildAppBar(context),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space5,
).copyWith(bottom: 100),
sliver: SliverList(
delegate: SliverChildListDelegate(<Widget>[
const Padding(
padding: EdgeInsets.only(bottom: UiConstants.space5),
child: HubInfoCard(),
),
),
],
),
if (state.hubToIdentify != null)
IdentifyNfcDialog(
hub: state.hubToIdentify!,
onAssign: (String tagId) {
BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsNfcTagAssignRequested(
hubId: state.hubToIdentify!.id,
nfcTagId: tagId,
if (state.status == ClientHubsStatus.loading)
const Center(child: CircularProgressIndicator())
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () async {
final bool? success = await Modular.to.toEditHub();
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsFetched());
}
},
)
else ...<Widget>[
...state.hubs.map(
(Hub hub) => HubCard(
hub: hub,
onTap: () async {
final bool? success = await Modular.to
.toHubDetails(hub);
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsFetched());
}
},
),
),
);
},
onCancel: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsIdentifyDialogToggled()),
),
if (state.status == ClientHubsStatus.actionInProgress)
Container(
color: UiColors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()),
],
const SizedBox(height: UiConstants.space5),
]),
),
),
],
),
);
@@ -160,7 +129,7 @@ class ClientHubsPage extends StatelessWidget {
Widget _buildAppBar(BuildContext context) {
return SliverAppBar(
backgroundColor: UiColors.foreground, // Dark Slate equivalent
backgroundColor: UiColors.foreground,
automaticallyImplyLeading: false,
expandedHeight: 140,
pinned: true,
@@ -219,51 +188,4 @@ class ClientHubsPage extends StatelessWidget {
),
);
}
Future<void> _confirmDeleteHub(BuildContext context, Hub hub) async {
final String hubName = hub.name.isEmpty ? t.client_hubs.title : hub.name;
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(t.client_hubs.delete_dialog.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(t.client_hubs.delete_dialog.message(hubName: hubName)),
const SizedBox(height: UiConstants.space2),
Text(t.client_hubs.delete_dialog.undo_warning),
const SizedBox(height: UiConstants.space2),
Text(
t.client_hubs.delete_dialog.dependency_warning,
style: UiTypography.footnote1r.copyWith(
color: UiColors.textSecondary,
),
),
],
),
actions: <Widget>[
TextButton(
onPressed: () => Modular.to.pop(),
child: Text(t.client_hubs.delete_dialog.cancel),
),
TextButton(
onPressed: () {
BlocProvider.of<ClientHubsBloc>(
context,
).add(ClientHubsDeleteRequested(hub.id));
Modular.to.pop();
},
style: TextButton.styleFrom(
foregroundColor: UiColors.destructive,
),
child: Text(t.client_hubs.delete_dialog.delete),
),
],
);
},
);
}
}

View File

@@ -6,23 +6,11 @@ import 'package:core_localization/core_localization.dart';
/// A card displaying information about a single hub.
class HubCard extends StatelessWidget {
/// Creates a [HubCard].
const HubCard({
required this.hub,
required this.onNfcPressed,
required this.onDeletePressed,
required this.onTap,
super.key,
});
const HubCard({required this.hub, required this.onTap, super.key});
/// The hub to display.
final Hub hub;
/// Callback when the NFC button is pressed.
final VoidCallback onNfcPressed;
/// Callback when the delete button is pressed.
final VoidCallback onDeletePressed;
/// Callback when the card is tapped.
final VoidCallback onTap;
@@ -37,13 +25,7 @@ class HubCard extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const <BoxShadow>[
BoxShadow(
color: UiColors.popupShadow,
blurRadius: 10,
offset: Offset(0, 4),
),
],
border: Border.all(color: UiColors.border),
),
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
@@ -72,6 +54,7 @@ class HubCard extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(top: UiConstants.space1),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
UiIcons.mapPin,
@@ -79,7 +62,7 @@ class HubCard extends StatelessWidget {
color: UiColors.iconThird,
),
const SizedBox(width: UiConstants.space1),
Expanded(
Flexible(
child: Text(
hub.address,
style: UiTypography.footnote1r.textSecondary,
@@ -104,20 +87,10 @@ class HubCard extends StatelessWidget {
],
),
),
Row(
children: <Widget>[
IconButton(
onPressed: onDeletePressed,
icon: const Icon(
UiIcons.delete,
color: UiColors.destructive,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
const Icon(
UiIcons.chevronRight,
size: 16,
color: UiColors.iconSecondary,
),
],
),

View File

@@ -31,10 +31,7 @@ class HubInfoCard extends StatelessWidget {
const SizedBox(height: UiConstants.space1),
Text(
t.client_hubs.about_hubs.description,
style: UiTypography.footnote1r.copyWith(
color: UiColors.textSecondary,
height: 1.4,
),
style: UiTypography.footnote1r.textSecondary,
),
],
),