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/design_system.dart';
import 'package:design_system/src/ui_typography.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../ui_icons.dart';
import 'ui_icon_button.dart';
/// A custom AppBar for the Krow UI design system. /// A custom AppBar for the Krow UI design system.
/// ///
/// This widget provides a consistent look and feel for top app bars across the application. /// 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:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.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 '../../domain/usecases/get_hubs_usecase.dart';
import 'client_hubs_event.dart'; import 'client_hubs_event.dart';
import 'client_hubs_state.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. /// BLoC responsible for managing the state of the Client Hubs feature.
/// ///
/// It orchestrates the flow between the UI and the domain layer by invoking /// 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> class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
with BlocErrorHandler<ClientHubsState> with BlocErrorHandler<ClientHubsState>
implements Disposable { implements Disposable {
ClientHubsBloc({ ClientHubsBloc({required GetHubsUseCase getHubsUseCase})
required GetHubsUseCase getHubsUseCase, : _getHubsUseCase = getHubsUseCase,
required DeleteHubUseCase deleteHubUseCase, super(const ClientHubsState()) {
required AssignNfcTagUseCase assignNfcTagUseCase,
}) : _getHubsUseCase = getHubsUseCase,
_deleteHubUseCase = deleteHubUseCase,
_assignNfcTagUseCase = assignNfcTagUseCase,
super(const ClientHubsState()) {
on<ClientHubsFetched>(_onFetched); on<ClientHubsFetched>(_onFetched);
on<ClientHubsDeleteRequested>(_onDeleteRequested);
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
on<ClientHubsMessageCleared>(_onMessageCleared); on<ClientHubsMessageCleared>(_onMessageCleared);
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
} }
final GetHubsUseCase _getHubsUseCase; 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( Future<void> _onFetched(
ClientHubsFetched event, 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( void _onMessageCleared(
ClientHubsMessageCleared event, ClientHubsMessageCleared event,
Emitter<ClientHubsState> emit, Emitter<ClientHubsState> emit,
@@ -130,8 +50,8 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
clearErrorMessage: true, clearErrorMessage: true,
clearSuccessMessage: true, clearSuccessMessage: true,
status: status:
state.status == ClientHubsStatus.actionSuccess || state.status == ClientHubsStatus.success ||
state.status == ClientHubsStatus.actionFailure state.status == ClientHubsStatus.failure
? ClientHubsStatus.success ? ClientHubsStatus.success
: state.status, : state.status,
), ),

View File

@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base class for all client hubs events. /// Base class for all client hubs events.
abstract class ClientHubsEvent extends Equatable { abstract class ClientHubsEvent extends Equatable {
@@ -14,38 +13,7 @@ class ClientHubsFetched extends ClientHubsEvent {
const ClientHubsFetched(); 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. /// Event triggered to clear any error or success messages.
class ClientHubsMessageCleared extends ClientHubsEvent { class ClientHubsMessageCleared extends ClientHubsEvent {
const ClientHubsMessageCleared(); 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'; import 'package:krow_domain/krow_domain.dart';
/// Enum representing the status of the client hubs state. /// Enum representing the status of the client hubs state.
enum ClientHubsStatus { enum ClientHubsStatus { initial, loading, success, failure }
initial,
loading,
success,
failure,
actionInProgress,
actionSuccess,
actionFailure,
}
/// State class for the ClientHubs BLoC. /// State class for the ClientHubs BLoC.
class ClientHubsState extends Equatable { class ClientHubsState extends Equatable {
@@ -19,7 +11,6 @@ class ClientHubsState extends Equatable {
this.hubs = const <Hub>[], this.hubs = const <Hub>[],
this.errorMessage, this.errorMessage,
this.successMessage, this.successMessage,
this.hubToIdentify,
}); });
final ClientHubsStatus status; final ClientHubsStatus status;
@@ -27,17 +18,11 @@ class ClientHubsState extends Equatable {
final String? errorMessage; final String? errorMessage;
final String? successMessage; final String? successMessage;
/// The hub currently being identified/assigned an NFC tag.
/// If null, the identification dialog is closed.
final Hub? hubToIdentify;
ClientHubsState copyWith({ ClientHubsState copyWith({
ClientHubsStatus? status, ClientHubsStatus? status,
List<Hub>? hubs, List<Hub>? hubs,
String? errorMessage, String? errorMessage,
String? successMessage, String? successMessage,
Hub? hubToIdentify,
bool clearHubToIdentify = false,
bool clearErrorMessage = false, bool clearErrorMessage = false,
bool clearSuccessMessage = false, bool clearSuccessMessage = false,
}) { }) {
@@ -50,9 +35,6 @@ class ClientHubsState extends Equatable {
successMessage: clearSuccessMessage successMessage: clearSuccessMessage
? null ? null
: (successMessage ?? this.successMessage), : (successMessage ?? this.successMessage),
hubToIdentify: clearHubToIdentify
? null
: (hubToIdentify ?? this.hubToIdentify),
); );
} }
@@ -62,6 +44,5 @@ class ClientHubsState extends Equatable {
hubs, hubs,
errorMessage, errorMessage,
successMessage, successMessage,
hubToIdentify,
]; ];
} }

View File

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

View File

@@ -30,7 +30,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
emit(state.copyWith(status: HubDetailsStatus.loading)); emit(state.copyWith(status: HubDetailsStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id)); await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id));
emit( emit(
@@ -54,7 +54,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
emit(state.copyWith(status: HubDetailsStatus.loading)); emit(state.copyWith(status: HubDetailsStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _assignNfcTagUseCase.call( await _assignNfcTagUseCase.call(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), 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_card.dart';
import '../widgets/hub_empty_state.dart'; import '../widgets/hub_empty_state.dart';
import '../widgets/hub_info_card.dart'; import '../widgets/hub_info_card.dart';
import '../widgets/identify_nfc_dialog.dart';
/// The main page for the client hubs feature. /// The main page for the client hubs feature.
/// ///
@@ -72,84 +71,54 @@ class ClientHubsPage extends StatelessWidget {
), ),
child: const Icon(UiIcons.add), child: const Icon(UiIcons.add),
), ),
body: Stack( body: CustomScrollView(
children: <Widget>[ slivers: <Widget>[
CustomScrollView( _buildAppBar(context),
slivers: <Widget>[ SliverPadding(
_buildAppBar(context), padding: const EdgeInsets.symmetric(
SliverPadding( horizontal: UiConstants.space5,
padding: const EdgeInsets.symmetric( vertical: UiConstants.space5,
horizontal: UiConstants.space5, ).copyWith(bottom: 100),
vertical: UiConstants.space5, sliver: SliverList(
).copyWith(bottom: 100), delegate: SliverChildListDelegate(<Widget>[
sliver: SliverList( const Padding(
delegate: SliverChildListDelegate(<Widget>[ padding: EdgeInsets.only(bottom: UiConstants.space5),
if (state.status == ClientHubsStatus.loading) child: HubInfoCard(),
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(),
]),
), ),
),
],
),
if (state.hubToIdentify != null) if (state.status == ClientHubsStatus.loading)
IdentifyNfcDialog( const Center(child: CircularProgressIndicator())
hub: state.hubToIdentify!, else if (state.hubs.isEmpty)
onAssign: (String tagId) { HubEmptyState(
BlocProvider.of<ClientHubsBloc>(context).add( onAddPressed: () async {
ClientHubsNfcTagAssignRequested( final bool? success = await Modular.to.toEditHub();
hubId: state.hubToIdentify!.id, if (success == true && context.mounted) {
nfcTagId: tagId, 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());
}
},
),
), ),
); ],
}, const SizedBox(height: UiConstants.space5),
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()),
), ),
),
], ],
), ),
); );
@@ -160,7 +129,7 @@ class ClientHubsPage extends StatelessWidget {
Widget _buildAppBar(BuildContext context) { Widget _buildAppBar(BuildContext context) {
return SliverAppBar( return SliverAppBar(
backgroundColor: UiColors.foreground, // Dark Slate equivalent backgroundColor: UiColors.foreground,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
expandedHeight: 140, expandedHeight: 140,
pinned: true, 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. /// A card displaying information about a single hub.
class HubCard extends StatelessWidget { class HubCard extends StatelessWidget {
/// Creates a [HubCard]. /// Creates a [HubCard].
const HubCard({ const HubCard({required this.hub, required this.onTap, super.key});
required this.hub,
required this.onNfcPressed,
required this.onDeletePressed,
required this.onTap,
super.key,
});
/// The hub to display. /// The hub to display.
final Hub hub; 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. /// Callback when the card is tapped.
final VoidCallback onTap; final VoidCallback onTap;
@@ -37,13 +25,7 @@ class HubCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.white, color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const <BoxShadow>[ border: Border.all(color: UiColors.border),
BoxShadow(
color: UiColors.popupShadow,
blurRadius: 10,
offset: Offset(0, 4),
),
],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space4),
@@ -72,6 +54,7 @@ class HubCard extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: UiConstants.space1), padding: const EdgeInsets.only(top: UiConstants.space1),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
const Icon( const Icon(
UiIcons.mapPin, UiIcons.mapPin,
@@ -79,7 +62,7 @@ class HubCard extends StatelessWidget {
color: UiColors.iconThird, color: UiColors.iconThird,
), ),
const SizedBox(width: UiConstants.space1), const SizedBox(width: UiConstants.space1),
Expanded( Flexible(
child: Text( child: Text(
hub.address, hub.address,
style: UiTypography.footnote1r.textSecondary, style: UiTypography.footnote1r.textSecondary,
@@ -104,20 +87,10 @@ class HubCard extends StatelessWidget {
], ],
), ),
), ),
Row( const Icon(
children: <Widget>[ UiIcons.chevronRight,
IconButton( size: 16,
onPressed: onDeletePressed, color: UiColors.iconSecondary,
icon: const Icon(
UiIcons.delete,
color: UiColors.destructive,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
), ),
], ],
), ),

View File

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