diff --git a/apps/mobile/packages/core/lib/src/routing/client/navigator.dart b/apps/mobile/packages/core/lib/src/routing/client/navigator.dart index 0203f45d..edb5141e 100644 --- a/apps/mobile/packages/core/lib/src/routing/client/navigator.dart +++ b/apps/mobile/packages/core/lib/src/routing/client/navigator.dart @@ -1,4 +1,5 @@ import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'route_paths.dart'; @@ -145,6 +146,22 @@ extension ClientNavigator on IModularNavigator { await pushNamed(ClientPaths.hubs); } + /// Navigates to the details of a specific hub. + Future toHubDetails(Hub hub) { + return pushNamed( + ClientPaths.hubDetails, + arguments: {'hub': hub}, + ); + } + + /// Navigates to the page to add a new hub or edit an existing one. + Future toEditHub({Hub? hub}) async { + return pushNamed( + ClientPaths.editHub, + arguments: {'hub': hub}, + ); + } + // ========================================================================== // ORDER CREATION // ========================================================================== diff --git a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart index b0ec3514..7575229d 100644 --- a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart @@ -16,14 +16,14 @@ class ClientPaths { /// Generate child route based on the given route and parent route /// /// This is useful for creating nested routes within modules. - static String childRoute(String parent, String child) { + static String childRoute(String parent, String child) { final String childPath = child.replaceFirst(parent, ''); - + // check if the child path is empty if (childPath.isEmpty) { return '/'; - } - + } + // ensure the child path starts with a '/' if (!childPath.startsWith('/')) { return '/$childPath'; @@ -82,10 +82,12 @@ class ClientPaths { static const String billing = '/client-main/billing'; /// Completion review page - review shift completion records. - static const String completionReview = '/client-main/billing/completion-review'; + static const String completionReview = + '/client-main/billing/completion-review'; /// Full list of invoices awaiting approval. - static const String awaitingApproval = '/client-main/billing/awaiting-approval'; + static const String awaitingApproval = + '/client-main/billing/awaiting-approval'; /// Invoice ready page - view status of approved invoices. static const String invoiceReady = '/client-main/billing/invoice-ready'; @@ -118,6 +120,12 @@ class ClientPaths { /// View and manage physical locations/hubs where staff are deployed. static const String hubs = '/client-hubs'; + /// Specific hub details. + static const String hubDetails = '/client-hubs/details'; + + /// Page for adding or editing a hub. + static const String editHub = '/client-hubs/edit'; + // ========================================================================== // ORDER CREATION & MANAGEMENT // ========================================================================== diff --git a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart index e3dd08f4..49a88f20 100644 --- a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart +++ b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart @@ -11,7 +11,12 @@ import 'src/domain/usecases/delete_hub_usecase.dart'; import 'src/domain/usecases/get_hubs_usecase.dart'; import 'src/domain/usecases/update_hub_usecase.dart'; import 'src/presentation/blocs/client_hubs_bloc.dart'; +import 'src/presentation/blocs/edit_hub/edit_hub_bloc.dart'; +import 'src/presentation/blocs/hub_details/hub_details_bloc.dart'; import 'src/presentation/pages/client_hubs_page.dart'; +import 'src/presentation/pages/edit_hub_page.dart'; +import 'src/presentation/pages/hub_details_page.dart'; +import 'package:krow_domain/krow_domain.dart'; export 'src/presentation/pages/client_hubs_page.dart'; @@ -34,10 +39,35 @@ class ClientHubsModule extends Module { // BLoCs i.add(ClientHubsBloc.new); + i.add(EditHubBloc.new); + i.add(HubDetailsBloc.new); } @override void routes(RouteManager r) { - r.child(ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs), child: (_) => const ClientHubsPage()); + r.child( + ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs), + child: (_) => const ClientHubsPage(), + ); + r.child( + ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubDetails), + child: (_) { + final Map data = r.args.data as Map; + return HubDetailsPage( + hub: data['hub'] as Hub, + bloc: Modular.get(), + ); + }, + ); + r.child( + ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.editHub), + child: (_) { + final Map data = r.args.data as Map; + return EditHubPage( + hub: data['hub'] as Hub?, + bloc: Modular.get(), + ); + }, + ); } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index 3c7e3c1b..dd6a1801 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -3,57 +3,38 @@ 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/create_hub_arguments.dart'; import '../../domain/arguments/delete_hub_arguments.dart'; import '../../domain/usecases/assign_nfc_tag_usecase.dart'; -import '../../domain/usecases/create_hub_usecase.dart'; import '../../domain/usecases/delete_hub_usecase.dart'; import '../../domain/usecases/get_hubs_usecase.dart'; -import '../../domain/usecases/update_hub_usecase.dart'; import 'client_hubs_event.dart'; 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, creating, deleting, and assigning tags to hubs. +/// specific use cases for fetching, deleting, and assigning tags to hubs. class ClientHubsBloc extends Bloc with BlocErrorHandler implements Disposable { - ClientHubsBloc({ required GetHubsUseCase getHubsUseCase, - required CreateHubUseCase createHubUseCase, required DeleteHubUseCase deleteHubUseCase, required AssignNfcTagUseCase assignNfcTagUseCase, - required UpdateHubUseCase updateHubUseCase, }) : _getHubsUseCase = getHubsUseCase, - _createHubUseCase = createHubUseCase, _deleteHubUseCase = deleteHubUseCase, _assignNfcTagUseCase = assignNfcTagUseCase, - _updateHubUseCase = updateHubUseCase, super(const ClientHubsState()) { on(_onFetched); - on(_onAddRequested); - on(_onUpdateRequested); on(_onDeleteRequested); on(_onNfcTagAssignRequested); on(_onMessageCleared); - on(_onAddDialogToggled); + on(_onIdentifyDialogToggled); } final GetHubsUseCase _getHubsUseCase; - final CreateHubUseCase _createHubUseCase; final DeleteHubUseCase _deleteHubUseCase; final AssignNfcTagUseCase _assignNfcTagUseCase; - final UpdateHubUseCase _updateHubUseCase; - - void _onAddDialogToggled( - ClientHubsAddDialogToggled event, - Emitter emit, - ) { - emit(state.copyWith(showAddHubDialog: event.visible)); - } void _onIdentifyDialogToggled( ClientHubsIdentifyDialogToggled event, @@ -71,11 +52,11 @@ class ClientHubsBloc extends Bloc Emitter emit, ) async { emit(state.copyWith(status: ClientHubsStatus.loading)); - + await handleError( emit: emit.call, action: () async { - final List hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase.call(); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); }, onError: (String errorKey) => state.copyWith( @@ -85,97 +66,17 @@ class ClientHubsBloc extends Bloc ); } - Future _onAddRequested( - ClientHubsAddRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); - - await handleError( - emit: emit.call, - action: () async { - await _createHubUseCase( - CreateHubArguments( - name: event.name, - address: event.address, - placeId: event.placeId, - latitude: event.latitude, - longitude: event.longitude, - city: event.city, - state: event.state, - street: event.street, - country: event.country, - zipCode: event.zipCode, - ), - ); - final List hubs = await _getHubsUseCase(); - emit( - state.copyWith( - status: ClientHubsStatus.actionSuccess, - hubs: hubs, - successMessage: 'Hub created successfully', - showAddHubDialog: false, - ), - ); - }, - onError: (String errorKey) => state.copyWith( - status: ClientHubsStatus.actionFailure, - errorMessage: errorKey, - ), - ); - } - - Future _onUpdateRequested( - ClientHubsUpdateRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); - - await handleError( - emit: emit.call, - action: () async { - await _updateHubUseCase( - UpdateHubArguments( - id: event.id, - name: event.name, - address: event.address, - placeId: event.placeId, - latitude: event.latitude, - longitude: event.longitude, - city: event.city, - state: event.state, - street: event.street, - country: event.country, - zipCode: event.zipCode, - ), - ); - final List hubs = await _getHubsUseCase(); - emit( - state.copyWith( - status: ClientHubsStatus.actionSuccess, - hubs: hubs, - successMessage: 'Hub updated successfully!', - ), - ); - }, - onError: (String errorKey) => state.copyWith( - status: ClientHubsStatus.actionFailure, - errorMessage: errorKey, - ), - ); - } - Future _onDeleteRequested( ClientHubsDeleteRequested event, Emitter emit, ) async { emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); - + await handleError( emit: emit.call, action: () async { - await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); - final List hubs = await _getHubsUseCase(); + await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.hubId)); + final List hubs = await _getHubsUseCase.call(); emit( state.copyWith( status: ClientHubsStatus.actionSuccess, @@ -196,14 +97,14 @@ class ClientHubsBloc extends Bloc Emitter emit, ) async { emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); - + await handleError( emit: emit.call, action: () async { - await _assignNfcTagUseCase( + await _assignNfcTagUseCase.call( AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), ); - final List hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase.call(); emit( state.copyWith( status: ClientHubsStatus.actionSuccess, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart index 03fd5194..c84737f4 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart @@ -14,94 +14,8 @@ class ClientHubsFetched extends ClientHubsEvent { const ClientHubsFetched(); } -/// Event triggered to add a new hub. -class ClientHubsAddRequested extends ClientHubsEvent { - - const ClientHubsAddRequested({ - required this.name, - required this.address, - this.placeId, - this.latitude, - this.longitude, - this.city, - this.state, - this.street, - this.country, - this.zipCode, - }); - final String name; - final String address; - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; - - @override - List get props => [ - name, - address, - placeId, - latitude, - longitude, - city, - state, - street, - country, - zipCode, - ]; -} - -/// Event triggered to update an existing hub. -class ClientHubsUpdateRequested extends ClientHubsEvent { - const ClientHubsUpdateRequested({ - required this.id, - required this.name, - required this.address, - this.placeId, - this.latitude, - this.longitude, - this.city, - this.state, - this.street, - this.country, - this.zipCode, - }); - - final String id; - final String name; - final String address; - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; - - @override - List get props => [ - id, - name, - address, - placeId, - latitude, - longitude, - city, - state, - street, - country, - zipCode, - ]; -} - /// Event triggered to delete a hub. class ClientHubsDeleteRequested extends ClientHubsEvent { - const ClientHubsDeleteRequested(this.hubId); final String hubId; @@ -111,7 +25,6 @@ class ClientHubsDeleteRequested extends ClientHubsEvent { /// Event triggered to assign an NFC tag to a hub. class ClientHubsNfcTagAssignRequested extends ClientHubsEvent { - const ClientHubsNfcTagAssignRequested({ required this.hubId, required this.nfcTagId, @@ -128,19 +41,8 @@ class ClientHubsMessageCleared extends ClientHubsEvent { const ClientHubsMessageCleared(); } -/// Event triggered to toggle the visibility of the "Add Hub" dialog. -class ClientHubsAddDialogToggled extends ClientHubsEvent { - - const ClientHubsAddDialogToggled({required this.visible}); - final bool visible; - - @override - List get props => [visible]; -} - /// Event triggered to toggle the visibility of the "Identify NFC" dialog. class ClientHubsIdentifyDialogToggled extends ClientHubsEvent { - const ClientHubsIdentifyDialogToggled({this.hub}); final Hub? hub; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart index 1d1eea5d..0dcbb7bd 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart @@ -14,23 +14,19 @@ enum ClientHubsStatus { /// State class for the ClientHubs BLoC. class ClientHubsState extends Equatable { - const ClientHubsState({ this.status = ClientHubsStatus.initial, this.hubs = const [], this.errorMessage, this.successMessage, - this.showAddHubDialog = false, this.hubToIdentify, }); + final ClientHubsStatus status; final List hubs; final String? errorMessage; final String? successMessage; - /// Whether the "Add Hub" dialog should be visible. - final bool showAddHubDialog; - /// The hub currently being identified/assigned an NFC tag. /// If null, the identification dialog is closed. final Hub? hubToIdentify; @@ -40,7 +36,6 @@ class ClientHubsState extends Equatable { List? hubs, String? errorMessage, String? successMessage, - bool? showAddHubDialog, Hub? hubToIdentify, bool clearHubToIdentify = false, bool clearErrorMessage = false, @@ -55,7 +50,6 @@ class ClientHubsState extends Equatable { successMessage: clearSuccessMessage ? null : (successMessage ?? this.successMessage), - showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog, hubToIdentify: clearHubToIdentify ? null : (hubToIdentify ?? this.hubToIdentify), @@ -68,7 +62,6 @@ class ClientHubsState extends Equatable { hubs, errorMessage, successMessage, - showAddHubDialog, hubToIdentify, ]; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_bloc.dart new file mode 100644 index 00000000..42a3734e --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_bloc.dart @@ -0,0 +1,95 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_core/core.dart'; +import '../../../domain/arguments/create_hub_arguments.dart'; +import '../../../domain/usecases/create_hub_usecase.dart'; +import '../../../domain/usecases/update_hub_usecase.dart'; +import 'edit_hub_event.dart'; +import 'edit_hub_state.dart'; + +/// Bloc for creating and updating hubs. +class EditHubBloc extends Bloc + with BlocErrorHandler { + EditHubBloc({ + required CreateHubUseCase createHubUseCase, + required UpdateHubUseCase updateHubUseCase, + }) : _createHubUseCase = createHubUseCase, + _updateHubUseCase = updateHubUseCase, + super(const EditHubState()) { + on(_onAddRequested); + on(_onUpdateRequested); + } + + final CreateHubUseCase _createHubUseCase; + final UpdateHubUseCase _updateHubUseCase; + + Future _onAddRequested( + EditHubAddRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: EditHubStatus.loading)); + + await handleError( + emit: emit, + action: () async { + await _createHubUseCase.call( + CreateHubArguments( + name: event.name, + address: event.address, + placeId: event.placeId, + latitude: event.latitude, + longitude: event.longitude, + city: event.city, + state: event.state, + street: event.street, + country: event.country, + zipCode: event.zipCode, + ), + ); + emit( + state.copyWith( + status: EditHubStatus.success, + successMessage: 'Hub created successfully', + ), + ); + }, + onError: (String errorKey) => + state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey), + ); + } + + Future _onUpdateRequested( + EditHubUpdateRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: EditHubStatus.loading)); + + await handleError( + emit: emit, + action: () async { + await _updateHubUseCase.call( + UpdateHubArguments( + id: event.id, + name: event.name, + address: event.address, + placeId: event.placeId, + latitude: event.latitude, + longitude: event.longitude, + city: event.city, + state: event.state, + street: event.street, + country: event.country, + zipCode: event.zipCode, + ), + ); + emit( + state.copyWith( + status: EditHubStatus.success, + successMessage: 'Hub updated successfully', + ), + ); + }, + onError: (String errorKey) => + state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_event.dart new file mode 100644 index 00000000..65e18a83 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_event.dart @@ -0,0 +1,94 @@ +import 'package:equatable/equatable.dart'; + +/// Base class for all edit hub events. +abstract class EditHubEvent extends Equatable { + const EditHubEvent(); + + @override + List get props => []; +} + +/// Event triggered to add a new hub. +class EditHubAddRequested extends EditHubEvent { + const EditHubAddRequested({ + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + final String name; + final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; + + @override + List get props => [ + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; +} + +/// Event triggered to update an existing hub. +class EditHubUpdateRequested extends EditHubEvent { + const EditHubUpdateRequested({ + required this.id, + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + final String id; + final String name; + final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; + + @override + List get props => [ + id, + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_state.dart new file mode 100644 index 00000000..17bdffcd --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/edit_hub/edit_hub_state.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; + +/// Status of the edit hub operation. +enum EditHubStatus { + /// Initial state. + initial, + + /// Operation in progress. + loading, + + /// Operation succeeded. + success, + + /// Operation failed. + failure, +} + +/// State for the edit hub operation. +class EditHubState extends Equatable { + const EditHubState({ + this.status = EditHubStatus.initial, + this.errorMessage, + this.successMessage, + }); + + /// The status of the operation. + final EditHubStatus status; + + /// The error message if the operation failed. + final String? errorMessage; + + /// The success message if the operation succeeded. + final String? successMessage; + + /// Create a copy of this state with the given fields replaced. + EditHubState copyWith({ + EditHubStatus? status, + String? errorMessage, + String? successMessage, + }) { + return EditHubState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + successMessage: successMessage ?? this.successMessage, + ); + } + + @override + List get props => [status, errorMessage, successMessage]; +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_bloc.dart new file mode 100644 index 00000000..9a82b60f --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_bloc.dart @@ -0,0 +1,75 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_core/core.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 'hub_details_event.dart'; +import 'hub_details_state.dart'; + +/// Bloc for managing hub details and operations like delete and NFC assignment. +class HubDetailsBloc extends Bloc + with BlocErrorHandler { + HubDetailsBloc({ + required DeleteHubUseCase deleteHubUseCase, + required AssignNfcTagUseCase assignNfcTagUseCase, + }) : _deleteHubUseCase = deleteHubUseCase, + _assignNfcTagUseCase = assignNfcTagUseCase, + super(const HubDetailsState()) { + on(_onDeleteRequested); + on(_onNfcTagAssignRequested); + } + + final DeleteHubUseCase _deleteHubUseCase; + final AssignNfcTagUseCase _assignNfcTagUseCase; + + Future _onDeleteRequested( + HubDetailsDeleteRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: HubDetailsStatus.loading)); + + await handleError( + emit: emit, + action: () async { + await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id)); + emit( + state.copyWith( + status: HubDetailsStatus.deleted, + successMessage: 'Hub deleted successfully', + ), + ); + }, + onError: (String errorKey) => state.copyWith( + status: HubDetailsStatus.failure, + errorMessage: errorKey, + ), + ); + } + + Future _onNfcTagAssignRequested( + HubDetailsNfcTagAssignRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: HubDetailsStatus.loading)); + + await handleError( + emit: emit, + action: () async { + await _assignNfcTagUseCase.call( + AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), + ); + emit( + state.copyWith( + status: HubDetailsStatus.success, + successMessage: 'NFC tag assigned successfully', + ), + ); + }, + onError: (String errorKey) => state.copyWith( + status: HubDetailsStatus.failure, + errorMessage: errorKey, + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_event.dart new file mode 100644 index 00000000..5c23da0b --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +/// Base class for all hub details events. +abstract class HubDetailsEvent extends Equatable { + const HubDetailsEvent(); + + @override + List get props => []; +} + +/// Event triggered to delete a hub. +class HubDetailsDeleteRequested extends HubDetailsEvent { + const HubDetailsDeleteRequested(this.id); + final String id; + + @override + List get props => [id]; +} + +/// Event triggered to assign an NFC tag to a hub. +class HubDetailsNfcTagAssignRequested extends HubDetailsEvent { + const HubDetailsNfcTagAssignRequested({ + required this.hubId, + required this.nfcTagId, + }); + + final String hubId; + final String nfcTagId; + + @override + List get props => [hubId, nfcTagId]; +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_state.dart new file mode 100644 index 00000000..f2c7f4c2 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/hub_details/hub_details_state.dart @@ -0,0 +1,53 @@ +import 'package:equatable/equatable.dart'; + +/// Status of the hub details operation. +enum HubDetailsStatus { + /// Initial state. + initial, + + /// Operation in progress. + loading, + + /// Operation succeeded. + success, + + /// Operation failed. + failure, + + /// Hub was deleted. + deleted, +} + +/// State for the hub details operation. +class HubDetailsState extends Equatable { + const HubDetailsState({ + this.status = HubDetailsStatus.initial, + this.errorMessage, + this.successMessage, + }); + + /// The status of the operation. + final HubDetailsStatus status; + + /// The error message if the operation failed. + final String? errorMessage; + + /// The success message if the operation succeeded. + final String? successMessage; + + /// Create a copy of this state with the given fields replaced. + HubDetailsState copyWith({ + HubDetailsStatus? status, + String? errorMessage, + String? successMessage, + }) { + return HubDetailsState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + successMessage: successMessage ?? this.successMessage, + ); + } + + @override + List get props => [status, errorMessage, successMessage]; +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart index c8fdffed..cb6d329d 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart @@ -8,7 +8,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../blocs/client_hubs_bloc.dart'; import '../blocs/client_hubs_event.dart'; import '../blocs/client_hubs_state.dart'; -import '../widgets/add_hub_dialog.dart'; + import '../widgets/hub_card.dart'; import '../widgets/hub_empty_state.dart'; import '../widgets/hub_info_card.dart'; @@ -43,7 +43,8 @@ class ClientHubsPage extends StatelessWidget { context, ).add(const ClientHubsMessageCleared()); } - if (state.successMessage != null && state.successMessage!.isNotEmpty) { + if (state.successMessage != null && + state.successMessage!.isNotEmpty) { UiSnackbar.show( context, message: state.successMessage!, @@ -58,9 +59,14 @@ class ClientHubsPage extends StatelessWidget { return Scaffold( backgroundColor: UiColors.bgMenu, floatingActionButton: FloatingActionButton( - onPressed: () => BlocProvider.of( - context, - ).add(const ClientHubsAddDialogToggled(visible: true)), + onPressed: () async { + final bool? success = await Modular.to.toEditHub(); + if (success == true && context.mounted) { + BlocProvider.of( + context, + ).add(const ClientHubsFetched()); + } + }, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), ), @@ -82,27 +88,37 @@ class ClientHubsPage extends StatelessWidget { const Center(child: CircularProgressIndicator()) else if (state.hubs.isEmpty) HubEmptyState( - onAddPressed: () => - BlocProvider.of(context).add( - const ClientHubsAddDialogToggled( - visible: true, - ), - ), + onAddPressed: () async { + final bool? success = await Modular.to + .toEditHub(); + if (success == true && context.mounted) { + BlocProvider.of( + context, + ).add(const ClientHubsFetched()); + } + }, ) else ...[ ...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( + context, + ).add(const ClientHubsFetched()); + } + }, onNfcPressed: () => BlocProvider.of( context, ).add( ClientHubsIdentifyDialogToggled(hub: hub), ), - onDeletePressed: () => _confirmDeleteHub( - context, - hub, - ), + onDeletePressed: () => + _confirmDeleteHub(context, hub), ), ), ], @@ -113,29 +129,7 @@ class ClientHubsPage extends StatelessWidget { ), ], ), - if (state.showAddHubDialog) - AddHubDialog( - onCreate: ( - String name, - String address, { - String? placeId, - double? latitude, - double? longitude, - }) { - BlocProvider.of(context).add( - ClientHubsAddRequested( - name: name, - address: address, - placeId: placeId, - latitude: latitude, - longitude: longitude, - ), - ); - }, - onCancel: () => BlocProvider.of( - context, - ).add(const ClientHubsAddDialogToggled(visible: false)), - ), + if (state.hubToIdentify != null) IdentifyNfcDialog( hub: state.hubToIdentify!, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart index 6b351b11..d230c1ba 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart @@ -2,28 +2,21 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:google_places_flutter/model/prediction.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../blocs/client_hubs_bloc.dart'; -import '../blocs/client_hubs_event.dart'; -import '../blocs/client_hubs_state.dart'; +import '../blocs/edit_hub/edit_hub_bloc.dart'; +import '../blocs/edit_hub/edit_hub_event.dart'; +import '../blocs/edit_hub/edit_hub_state.dart'; import '../widgets/hub_address_autocomplete.dart'; -/// A dedicated full-screen page for editing an existing hub. -/// -/// Takes the parent [ClientHubsBloc] via [BlocProvider.value] so the -/// updated hub list is reflected on the hubs list page when the user -/// saves and navigates back. +/// A dedicated full-screen page for adding or editing a hub. class EditHubPage extends StatefulWidget { - const EditHubPage({ - required this.hub, - required this.bloc, - super.key, - }); + const EditHubPage({this.hub, required this.bloc, super.key}); - final Hub hub; - final ClientHubsBloc bloc; + final Hub? hub; + final EditHubBloc bloc; @override State createState() => _EditHubPageState(); @@ -39,8 +32,8 @@ class _EditHubPageState extends State { @override void initState() { super.initState(); - _nameController = TextEditingController(text: widget.hub.name); - _addressController = TextEditingController(text: widget.hub.address); + _nameController = TextEditingController(text: widget.hub?.name); + _addressController = TextEditingController(text: widget.hub?.address); _addressFocusNode = FocusNode(); } @@ -64,37 +57,50 @@ class _EditHubPageState extends State { return; } - ReadContext(context).read().add( - ClientHubsUpdateRequested( - id: widget.hub.id, - name: _nameController.text.trim(), - address: _addressController.text.trim(), - placeId: _selectedPrediction?.placeId, - latitude: double.tryParse(_selectedPrediction?.lat ?? ''), - longitude: double.tryParse(_selectedPrediction?.lng ?? ''), - ), - ); + if (widget.hub == null) { + widget.bloc.add( + EditHubAddRequested( + name: _nameController.text.trim(), + address: _addressController.text.trim(), + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse(_selectedPrediction?.lat ?? ''), + longitude: double.tryParse(_selectedPrediction?.lng ?? ''), + ), + ); + } else { + widget.bloc.add( + EditHubUpdateRequested( + id: widget.hub!.id, + name: _nameController.text.trim(), + address: _addressController.text.trim(), + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse(_selectedPrediction?.lat ?? ''), + longitude: double.tryParse(_selectedPrediction?.lng ?? ''), + ), + ); + } } @override Widget build(BuildContext context) { - return BlocProvider.value( + return BlocProvider.value( value: widget.bloc, - child: BlocListener( - listenWhen: (ClientHubsState prev, ClientHubsState curr) => - prev.status != curr.status || prev.successMessage != curr.successMessage, - listener: (BuildContext context, ClientHubsState state) { - if (state.status == ClientHubsStatus.actionSuccess && + child: BlocListener( + listenWhen: (EditHubState prev, EditHubState curr) => + prev.status != curr.status || + prev.successMessage != curr.successMessage, + listener: (BuildContext context, EditHubState state) { + if (state.status == EditHubStatus.success && state.successMessage != null) { UiSnackbar.show( context, message: state.successMessage!, type: UiSnackbarType.success, ); - // Pop back to details page with updated hub - Navigator.of(context).pop(true); + // Pop back to the previous screen. + Modular.to.pop(true); } - if (state.status == ClientHubsStatus.actionFailure && + if (state.status == EditHubStatus.failure && state.errorMessage != null) { UiSnackbar.show( context, @@ -103,10 +109,9 @@ class _EditHubPageState extends State { ); } }, - child: BlocBuilder( - builder: (BuildContext context, ClientHubsState state) { - final bool isSaving = - state.status == ClientHubsStatus.actionInProgress; + child: BlocBuilder( + builder: (BuildContext context, EditHubState state) { + final bool isSaving = state.status == EditHubStatus.loading; return Scaffold( backgroundColor: UiColors.bgMenu, @@ -114,17 +119,21 @@ class _EditHubPageState extends State { backgroundColor: UiColors.foreground, leading: IconButton( icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Modular.to.pop(), ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - t.client_hubs.edit_hub.title, + widget.hub == null + ? t.client_hubs.add_hub_dialog.title + : t.client_hubs.edit_hub.title, style: UiTypography.headline3m.white, ), Text( - t.client_hubs.edit_hub.subtitle, + widget.hub == null + ? t.client_hubs.add_hub_dialog.create_button + : t.client_hubs.edit_hub.subtitle, style: UiTypography.footnote1r.copyWith( color: UiColors.white.withValues(alpha: 0.7), ), @@ -176,7 +185,9 @@ class _EditHubPageState extends State { // ── Save button ────────────────────────────────── UiButton.primary( onPressed: isSaving ? null : _onSave, - text: t.client_hubs.edit_hub.save_button, + text: widget.hub == null + ? t.client_hubs.add_hub_dialog.create_button + : t.client_hubs.edit_hub.save_button, ), const SizedBox(height: 40), diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart index bcb9255b..397ca883 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart @@ -1,72 +1,103 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +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 '../blocs/client_hubs_bloc.dart'; -import 'edit_hub_page.dart'; +import '../blocs/hub_details/hub_details_bloc.dart'; +import '../blocs/hub_details/hub_details_event.dart'; +import '../blocs/hub_details/hub_details_state.dart'; /// A read-only details page for a single [Hub]. /// /// Shows hub name, address, and NFC tag assignment. -/// Tapping the edit button navigates to [EditHubPage] (a dedicated page, -/// not a dialog), satisfying the "separate edit hub page" acceptance criterion. class HubDetailsPage extends StatelessWidget { - const HubDetailsPage({ - required this.hub, - required this.bloc, - super.key, - }); + const HubDetailsPage({required this.hub, required this.bloc, super.key}); final Hub hub; - final ClientHubsBloc bloc; + final HubDetailsBloc bloc; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(hub.name), - backgroundColor: UiColors.foreground, - leading: IconButton( - icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - TextButton.icon( - onPressed: () => _navigateToEditPage(context), - icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16), - label: Text( - t.client_hubs.hub_details.edit_button, - style: const TextStyle(color: UiColors.white), + return BlocProvider.value( + value: bloc, + child: BlocListener( + listener: (BuildContext context, HubDetailsState state) { + if (state.status == HubDetailsStatus.deleted) { + UiSnackbar.show( + context, + message: state.successMessage ?? 'Hub deleted successfully', + type: UiSnackbarType.success, + ); + Modular.to.pop(true); // Return true to indicate change + } + if (state.status == HubDetailsStatus.failure && + state.errorMessage != null) { + UiSnackbar.show( + context, + message: state.errorMessage!, + type: UiSnackbarType.error, + ); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(hub.name), + backgroundColor: UiColors.foreground, + leading: IconButton( + icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), + onPressed: () => Modular.to.pop(), + ), + actions: [ + IconButton( + onPressed: () => _confirmDeleteHub(context), + icon: const Icon( + UiIcons.delete, + color: UiColors.white, + size: 20, + ), + ), + TextButton.icon( + onPressed: () => _navigateToEditPage(context), + icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16), + label: Text( + t.client_hubs.hub_details.edit_button, + style: const TextStyle(color: UiColors.white), + ), + ), + ], + ), + backgroundColor: UiColors.bgMenu, + body: Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailItem( + label: t.client_hubs.hub_details.name_label, + value: hub.name, + icon: UiIcons.home, + ), + const SizedBox(height: UiConstants.space4), + _buildDetailItem( + label: t.client_hubs.hub_details.address_label, + value: hub.address, + icon: UiIcons.mapPin, + ), + const SizedBox(height: UiConstants.space4), + _buildDetailItem( + label: t.client_hubs.hub_details.nfc_label, + value: + hub.nfcTagId ?? + t.client_hubs.hub_details.nfc_not_assigned, + icon: UiIcons.nfc, + isHighlight: hub.nfcTagId != null, + ), + ], ), ), - ], - ), - backgroundColor: UiColors.bgMenu, - body: Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailItem( - label: t.client_hubs.hub_details.name_label, - value: hub.name, - icon: UiIcons.home, - ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: t.client_hubs.hub_details.address_label, - value: hub.address, - icon: UiIcons.mapPin, - ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: t.client_hubs.hub_details.nfc_label, - value: hub.nfcTagId ?? t.client_hubs.hub_details.nfc_not_assigned, - icon: UiIcons.nfc, - isHighlight: hub.nfcTagId != null, - ), - ], ), ), ); @@ -96,7 +127,9 @@ class HubDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(UiConstants.space3), decoration: BoxDecoration( - color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField, + color: isHighlight + ? UiColors.tagInProgress + : UiColors.bgInputField, borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), child: Icon( @@ -122,16 +155,37 @@ class HubDetailsPage extends StatelessWidget { } Future _navigateToEditPage(BuildContext context) async { - // Navigate to the dedicated edit page and await result. - // If the page returns `true` (save succeeded), pop the details page too so - // the user sees the refreshed hub list (the BLoC already holds updated data). - final bool? saved = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => EditHubPage(hub: hub, bloc: bloc), + // We still need to pass a Bloc for the edit page, but it's handled by Modular. + // However, the Navigator extension expect a Bloc. + // I'll update the Navigator extension to NOT require a Bloc since it's in Modular. + final bool? saved = await Modular.to.toEditHub(hub: hub); + if (saved == true && context.mounted) { + Modular.to.pop(true); // Return true to indicate change + } + } + + Future _confirmDeleteHub(BuildContext context) async { + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(t.client_hubs.delete_dialog.title), + content: Text(t.client_hubs.delete_dialog.message(hubName: hub.name)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(t.client_hubs.delete_dialog.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: UiColors.destructive), + child: Text(t.client_hubs.delete_dialog.delete), + ), + ], ), ); - if (saved == true && context.mounted) { - Navigator.of(context).pop(); + + if (confirm == true) { + bloc.add(HubDetailsDeleteRequested(hub.id)); } } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart deleted file mode 100644 index 8c59e977..00000000 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:core_localization/core_localization.dart'; -import 'package:google_places_flutter/model/prediction.dart'; - -import 'hub_address_autocomplete.dart'; - -/// A dialog for adding a new hub. -class AddHubDialog extends StatefulWidget { - - /// Creates an [AddHubDialog]. - const AddHubDialog({ - required this.onCreate, - required this.onCancel, - super.key, - }); - /// Callback when the "Create Hub" button is pressed. - final void Function( - String name, - String address, { - String? placeId, - double? latitude, - double? longitude, - }) onCreate; - - /// Callback when the dialog is cancelled. - final VoidCallback onCancel; - - @override - State createState() => _AddHubDialogState(); -} - -class _AddHubDialogState extends State { - late final TextEditingController _nameController; - late final TextEditingController _addressController; - late final FocusNode _addressFocusNode; - Prediction? _selectedPrediction; - - @override - void initState() { - super.initState(); - _nameController = TextEditingController(); - _addressController = TextEditingController(); - _addressFocusNode = FocusNode(); - } - - @override - void dispose() { - _nameController.dispose(); - _addressController.dispose(); - _addressFocusNode.dispose(); - super.dispose(); - } - - final GlobalKey _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Container( - color: UiColors.bgOverlay, - child: Center( - child: SingleChildScrollView( - child: Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - boxShadow: const [ - BoxShadow(color: UiColors.popupShadow, blurRadius: 20), - ], - ), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - t.client_hubs.add_hub_dialog.title, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space5), - _buildFieldLabel(t.client_hubs.add_hub_dialog.name_label), - TextFormField( - controller: _nameController, - style: UiTypography.body1r.textPrimary, - validator: (String? value) { - if (value == null || value.trim().isEmpty) { - return 'Name is required'; - } - return null; - }, - decoration: _buildInputDecoration( - t.client_hubs.add_hub_dialog.name_hint, - ), - ), - const SizedBox(height: UiConstants.space4), - _buildFieldLabel(t.client_hubs.add_hub_dialog.address_label), - // Assuming HubAddressAutocomplete is a custom widget wrapper. - // If it doesn't expose a validator, we might need to modify it or manually check _addressController. - // For now, let's just make sure we validate name. Address is tricky if it's a wrapper. - HubAddressAutocomplete( - controller: _addressController, - hintText: t.client_hubs.add_hub_dialog.address_hint, - focusNode: _addressFocusNode, - onSelected: (Prediction prediction) { - _selectedPrediction = prediction; - }, - ), - const SizedBox(height: UiConstants.space8), - Row( - children: [ - Expanded( - child: UiButton.secondary( - onPressed: widget.onCancel, - text: t.common.cancel, - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: UiButton.primary( - onPressed: () { - if (_formKey.currentState!.validate()) { - // Manually check address if needed, or assume manual entry is ok. - if (_addressController.text.trim().isEmpty) { - // Show manual error or scaffold - UiSnackbar.show(context, message: 'Address is required', type: UiSnackbarType.error); - return; - } - - widget.onCreate( - _nameController.text, - _addressController.text, - placeId: _selectedPrediction?.placeId, - latitude: double.tryParse( - _selectedPrediction?.lat ?? '', - ), - longitude: double.tryParse( - _selectedPrediction?.lng ?? '', - ), - ); - } - }, - text: t.client_hubs.add_hub_dialog.create_button, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildFieldLabel(String label) { - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space2), - child: Text(label, style: UiTypography.body2m.textPrimary), - ); - } - - InputDecoration _buildInputDecoration(String hint) { - return InputDecoration( - hintText: hint, - hintStyle: UiTypography.body2r.textPlaceholder, - filled: true, - fillColor: UiColors.input, - contentPadding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: 14, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - borderSide: const BorderSide(color: UiColors.border), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - borderSide: const BorderSide(color: UiColors.border), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - borderSide: const BorderSide(color: UiColors.ring, width: 2), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart index 812be35b..d8504194 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart @@ -5,14 +5,15 @@ 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, }); + /// The hub to display. final Hub hub; @@ -22,99 +23,105 @@ class HubCard extends StatelessWidget { /// Callback when the delete button is pressed. final VoidCallback onDeletePressed; + /// Callback when the card is tapped. + final VoidCallback onTap; + @override Widget build(BuildContext context) { final bool hasNfc = hub.nfcTagId != null; - return Container( - margin: const EdgeInsets.only(bottom: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - boxShadow: const [ - BoxShadow( - color: UiColors.popupShadow, - blurRadius: 10, - offset: Offset(0, 4), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: UiColors.tagInProgress, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Icon( - hasNfc ? UiIcons.success : UiIcons.nfc, - color: hasNfc ? UiColors.iconSuccess : UiColors.iconThird, - size: 24, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(hub.name, style: UiTypography.body1b.textPrimary), - if (hub.address.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: UiConstants.space1), - child: Row( - children: [ - const Icon( - UiIcons.mapPin, - size: 12, - color: UiColors.iconThird, - ), - const SizedBox(width: UiConstants.space1), - Expanded( - child: Text( - hub.address, - style: UiTypography.footnote1r.textSecondary, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - if (hasNfc) - Padding( - padding: const EdgeInsets.only(top: UiConstants.space1), - child: Text( - t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!), - style: UiTypography.footnote1b.copyWith( - color: UiColors.textSuccess, - fontFamily: 'monospace', - ), - ), - ), - ], - ), - ), - Row( - children: [ - IconButton( - onPressed: onDeletePressed, - icon: const Icon( - UiIcons.delete, - color: UiColors.destructive, - size: 20, - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - splashRadius: 20, - ), - ], + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + boxShadow: const [ + BoxShadow( + color: UiColors.popupShadow, + blurRadius: 10, + offset: Offset(0, 4), ), ], ), + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Row( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: UiColors.tagInProgress, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Icon( + hasNfc ? UiIcons.success : UiIcons.nfc, + color: hasNfc ? UiColors.iconSuccess : UiColors.iconThird, + size: 24, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(hub.name, style: UiTypography.body1b.textPrimary), + if (hub.address.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: UiConstants.space1), + child: Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 12, + color: UiColors.iconThird, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + hub.address, + style: UiTypography.footnote1r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (hasNfc) + Padding( + padding: const EdgeInsets.only(top: UiConstants.space1), + child: Text( + t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!), + style: UiTypography.footnote1b.copyWith( + color: UiColors.textSuccess, + fontFamily: 'monospace', + ), + ), + ), + ], + ), + ), + Row( + children: [ + IconButton( + onPressed: onDeletePressed, + icon: const Icon( + UiIcons.delete, + color: UiColors.destructive, + size: 20, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + ], + ), + ], + ), + ), ), ); }