diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 0fbaa7bd..4e60c7fe 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -239,6 +239,24 @@ "address_hint": "Full address", "create_button": "Create Hub" }, + "edit_hub": { + "title": "Edit Hub", + "subtitle": "Update hub details", + "name_label": "Hub Name *", + "name_hint": "e.g., Main Kitchen, Front Desk", + "address_label": "Address", + "address_hint": "Full address", + "save_button": "Save Changes", + "success": "Hub updated successfully!" + }, + "hub_details": { + "title": "Hub Details", + "name_label": "Name", + "address_label": "Address", + "nfc_label": "NFC Tag", + "nfc_not_assigned": "Not Assigned", + "edit_button": "Edit Hub" + }, "nfc_dialog": { "title": "Identify NFC Tag", "instruction": "Tap your phone to the NFC tag to identify it", @@ -1154,6 +1172,7 @@ "success": { "hub": { "created": "Hub created successfully!", + "updated": "Hub updated successfully!", "deleted": "Hub deleted successfully!", "nfc_assigned": "NFC tag assigned successfully!" }, diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 8f9451a9..18ec6f7c 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -253,6 +253,24 @@ "dependency_warning": "Ten en cuenta que si hay turnos/órdenes asignados a este hub no deberíamos poder eliminarlo.", "cancel": "Cancelar", "delete": "Eliminar" + }, + "edit_hub": { + "title": "Editar Hub", + "subtitle": "Actualizar detalles del hub", + "name_label": "Nombre del Hub", + "name_hint": "Ingresar nombre del hub", + "address_label": "Dirección", + "address_hint": "Ingresar dirección", + "save_button": "Guardar Cambios", + "success": "¡Hub actualizado exitosamente!" + }, + "hub_details": { + "title": "Detalles del Hub", + "edit_button": "Editar", + "name_label": "Nombre del Hub", + "address_label": "Dirección", + "nfc_label": "Etiqueta NFC", + "nfc_not_assigned": "No asignada" } }, "client_create_order": { @@ -1154,6 +1172,7 @@ "success": { "hub": { "created": "¡Hub creado exitosamente!", + "updated": "¡Hub actualizado exitosamente!", "deleted": "¡Hub eliminado exitosamente!", "nfc_assigned": "¡Etiqueta NFC asignada exitosamente!" }, diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart index df942ad3..f11b63ec 100644 --- a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart +++ b/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart @@ -1,23 +1,13 @@ import 'package:equatable/equatable.dart'; -<<<<<<< Updated upstream import 'recurring_order_position.dart'; /// Represents a recurring staffing request spanning a date range. -======= -import 'one_time_order.dart'; -import 'one_time_order_position.dart'; - -/// Represents a customer's request for recurring staffing. ->>>>>>> Stashed changes class RecurringOrder extends Equatable { const RecurringOrder({ required this.startDate, required this.endDate, required this.recurringDays, -<<<<<<< Updated upstream required this.location, -======= ->>>>>>> Stashed changes required this.positions, this.hub, this.eventName, @@ -25,7 +15,6 @@ class RecurringOrder extends Equatable { this.roleRates = const {}, }); -<<<<<<< Updated upstream /// Start date for the recurring schedule. final DateTime startDate; @@ -59,25 +48,6 @@ class RecurringOrder extends Equatable { endDate, recurringDays, location, -======= - final DateTime startDate; - final DateTime endDate; - - /// List of days (e.g., ['Monday', 'Wednesday']) or bitmask. - final List recurringDays; - - final List positions; - final OneTimeOrderHubDetails? hub; - final String? eventName; - final String? vendorId; - final Map roleRates; - - @override - List get props => [ - startDate, - endDate, - recurringDays, ->>>>>>> Stashed changes positions, hub, eventName, @@ -85,7 +55,6 @@ class RecurringOrder extends Equatable { roleRates, ]; } -<<<<<<< Updated upstream /// Minimal hub details used during recurring order creation. class RecurringOrderHubDetails extends Equatable { @@ -130,5 +99,3 @@ class RecurringOrderHubDetails extends Equatable { zipCode, ]; } -======= ->>>>>>> Stashed changes diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index 4012ebc4..fff9a19c 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -179,7 +179,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte .date(orderTimestamp) .startDate(startTimestamp) .endDate(endTimestamp) - .recurringDays(fdc.AnyValue(order.recurringDays)) + .recurringDays(order.recurringDays) .execute(); final String orderId = orderResult.data.order_insert.id; @@ -274,7 +274,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte if (vendorId == null || vendorId.isEmpty) { throw Exception('Vendor is missing.'); } - final domain.PermanentOrderHubDetails? hub = order.hub; + final domain.OneTimeOrderHubDetails? hub = order.hub; if (hub == null || hub.id.isEmpty) { throw Exception('Hub is missing.'); } @@ -299,7 +299,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte .status(dc.OrderStatus.POSTED) .date(orderTimestamp) .startDate(startTimestamp) - .permanentDays(fdc.AnyValue(order.permanentDays)) + .permanentDays(order.permanentDays) .execute(); final String orderId = orderResult.data.order_insert.id; @@ -311,7 +311,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte final Set selectedDays = Set.from(order.permanentDays); final int workersNeeded = order.positions.fold( 0, - (int sum, domain.PermanentOrderPosition position) => sum + position.count, + (int sum, domain.OneTimeOrderPosition position) => sum + position.count, ); final double shiftCost = _calculatePermanentShiftCost(order); @@ -352,7 +352,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte final String shiftId = shiftResult.data.shift_insert.id; shiftIds.add(shiftId); - for (final domain.PermanentOrderPosition position in order.positions) { + for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(day, position.startTime); final DateTime end = _parseTime(day, position.endTime); final DateTime normalizedEnd = @@ -420,7 +420,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte double _calculatePermanentShiftCost(domain.PermanentOrder order) { double total = 0; - for (final domain.PermanentOrderPosition position in order.positions) { + for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(order.startDate, position.startTime); final DateTime end = _parseTime(order.startDate, position.endTime); final DateTime normalizedEnd = diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart index 68aa0aa1..b79b3359 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart @@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../repositories/client_create_order_repository_interface.dart'; /// Use case for creating a permanent staffing order. -class CreatePermanentOrderUseCase implements UseCase, PermanentOrder> { +class CreatePermanentOrderUseCase implements UseCase { const CreatePermanentOrderUseCase(this._repository); final ClientCreateOrderRepositoryInterface _repository; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart index 193b20ef..561a5ef8 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart @@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../repositories/client_create_order_repository_interface.dart'; /// Use case for creating a recurring staffing order. -class CreateRecurringOrderUseCase implements UseCase, RecurringOrder> { +class CreateRecurringOrderUseCase implements UseCase { const CreateRecurringOrderUseCase(this._repository); final ClientCreateOrderRepositoryInterface _repository; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/permanent_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/permanent_order_bloc.dart index 731a8018..48a75b27 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/permanent_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/permanent_order_bloc.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; -import '../../domain/arguments/permanent_order_arguments.dart'; import '../../domain/usecases/create_permanent_order_usecase.dart'; import 'permanent_order_event.dart'; import 'permanent_order_state.dart'; @@ -286,10 +285,9 @@ class PermanentOrderBloc extends Bloc final domain.PermanentOrder order = domain.PermanentOrder( startDate: state.startDate, permanentDays: state.permanentDays, - location: selectedHub.name, positions: state.positions .map( - (PermanentOrderPosition p) => domain.PermanentOrderPosition( + (PermanentOrderPosition p) => domain.OneTimeOrderPosition( role: p.role, count: p.count, startTime: p.startTime, @@ -299,7 +297,7 @@ class PermanentOrderBloc extends Bloc ), ) .toList(), - hub: domain.PermanentOrderHubDetails( + hub: domain.OneTimeOrderHubDetails( id: selectedHub.id, name: selectedHub.name, address: selectedHub.address, @@ -316,9 +314,7 @@ class PermanentOrderBloc extends Bloc vendorId: state.selectedVendor?.id, roleRates: roleRates, ); - await _createPermanentOrderUseCase( - PermanentOrderArguments(order: order), - ); + await _createPermanentOrderUseCase(order); emit(state.copyWith(status: PermanentOrderStatus.success)); }, onError: (String errorKey) => state.copyWith( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart index b94ed6c1..fc975068 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; -import '../../domain/arguments/recurring_order_arguments.dart'; import '../../domain/usecases/create_recurring_order_usecase.dart'; import 'recurring_order_event.dart'; import 'recurring_order_state.dart'; @@ -334,9 +333,7 @@ class RecurringOrderBloc extends Bloc vendorId: state.selectedVendor?.id, roleRates: roleRates, ); - await _createRecurringOrderUseCase( - RecurringOrderArguments(order: order), - ); + await _createRecurringOrderUseCase(order); emit(state.copyWith(status: RecurringOrderStatus.success)); }, onError: (String errorKey) => state.copyWith( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart index 9c2931d7..bcfe0d31 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart @@ -26,7 +26,7 @@ class ClientHomeEditBanner extends StatelessWidget { builder: (BuildContext context, ClientHomeState state) { return AnimatedContainer( duration: const Duration(milliseconds: 300), - height: state.isEditMode ? 76 : 0, + height: state.isEditMode ? 80 : 0, clipBehavior: Clip.antiAlias, margin: const EdgeInsets.symmetric( horizontal: UiConstants.space4, @@ -43,21 +43,23 @@ class ClientHomeEditBanner extends StatelessWidget { children: [ const Icon(UiIcons.edit, size: 16, color: UiColors.primary), const SizedBox(width: UiConstants.space2), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - i18n.dashboard.edit_mode_active, - style: UiTypography.footnote1b.copyWith( - color: UiColors.primary, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + i18n.dashboard.edit_mode_active, + style: UiTypography.footnote1b.copyWith( + color: UiColors.primary, + ), ), - ), - Text( - i18n.dashboard.drag_instruction, - style: UiTypography.footnote2r.textSecondary, - ), - ], + Text( + i18n.dashboard.drag_instruction, + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), ), UiButton.secondary( text: i18n.dashboard.reset, 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 1f7c0eb9..e3dd08f4 100644 --- a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart +++ b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart @@ -9,6 +9,7 @@ import 'src/domain/usecases/assign_nfc_tag_usecase.dart'; import 'src/domain/usecases/create_hub_usecase.dart'; 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/pages/client_hubs_page.dart'; @@ -29,6 +30,7 @@ class ClientHubsModule extends Module { i.addLazySingleton(CreateHubUseCase.new); i.addLazySingleton(DeleteHubUseCase.new); i.addLazySingleton(AssignNfcTagUseCase.new); + i.addLazySingleton(UpdateHubUseCase.new); // BLoCs i.add(ClientHubsBloc.new); diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 91de3bdf..c79d15cd 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -124,6 +124,78 @@ class HubRepositoryImpl implements HubRepositoryInterface { ); } + @override + Future updateHub({ + required String id, + String? name, + String? address, + String? placeId, + double? latitude, + double? longitude, + String? city, + String? state, + String? street, + String? country, + String? zipCode, + }) async { + return _service.run(() async { + final _PlaceAddress? placeAddress = + placeId == null || placeId.isEmpty + ? null + : await _fetchPlaceAddress(placeId); + + final dc.UpdateTeamHubVariablesBuilder builder = _service.connector + .updateTeamHub(id: id); + + if (name != null) builder.hubName(name); + if (address != null) builder.address(address); + if (placeId != null || placeAddress != null) { + builder.placeId(placeId ?? placeAddress?.street); + } + if (latitude != null) builder.latitude(latitude); + if (longitude != null) builder.longitude(longitude); + if (city != null || placeAddress?.city != null) { + builder.city(city ?? placeAddress?.city); + } + if (state != null || placeAddress?.state != null) { + builder.state(state ?? placeAddress?.state); + } + if (street != null || placeAddress?.street != null) { + builder.street(street ?? placeAddress?.street); + } + if (country != null || placeAddress?.country != null) { + builder.country(country ?? placeAddress?.country); + } + if (zipCode != null || placeAddress?.zipCode != null) { + builder.zipCode(zipCode ?? placeAddress?.zipCode); + } + + await builder.execute(); + + final dc.GetBusinessesByUserIdBusinesses business = + await _getBusinessForCurrentUser(); + final String teamId = await _getOrCreateTeamId(business); + final List hubs = await _fetchHubsForTeam( + teamId: teamId, + businessId: business.id, + ); + + for (final domain.Hub hub in hubs) { + if (hub.id == id) return hub; + } + + // Fallback: return a reconstructed Hub from the update inputs. + return domain.Hub( + id: id, + businessId: business.id, + name: name ?? '', + address: address ?? '', + nfcTagId: null, + status: domain.HubStatus.active, + ); + }); + } + Future _getBusinessForCurrentUser() async { final dc.ClientSession? session = dc.ClientSessionStore.instance.session; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart index 5580e6e4..0288d180 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart @@ -35,4 +35,21 @@ abstract interface class HubRepositoryInterface { /// /// Takes the [hubId] and the [nfcTagId] to be associated. Future assignNfcTag({required String hubId, required String nfcTagId}); + + /// Updates an existing hub by its [id]. + /// + /// All fields other than [id] are optional — only supplied values are updated. + Future updateHub({ + required String id, + String? name, + String? address, + String? placeId, + double? latitude, + double? longitude, + String? city, + String? state, + String? street, + String? country, + String? zipCode, + }); } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart index d62e0f92..97af203e 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart @@ -1,10 +1,10 @@ +import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import '../repositories/hub_repository_interface.dart'; -import '../../domain/arguments/create_hub_arguments.dart'; /// Arguments for the UpdateHubUseCase. -class UpdateHubArguments { +class UpdateHubArguments extends UseCaseArgument { const UpdateHubArguments({ required this.id, this.name, @@ -30,10 +30,25 @@ class UpdateHubArguments { final String? street; final String? country; final String? zipCode; + + @override + List get props => [ + id, + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; } /// Use case for updating an existing hub. -class UpdateHubUseCase implements UseCase, UpdateHubArguments> { +class UpdateHubUseCase implements UseCase { UpdateHubUseCase(this.repository); final HubRepositoryInterface repository; 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 2c2acb02..5096ed70 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 @@ -9,6 +9,7 @@ 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'; @@ -25,13 +26,16 @@ class ClientHubsBloc extends Bloc 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); @@ -42,6 +46,7 @@ class ClientHubsBloc extends Bloc final CreateHubUseCase _createHubUseCase; final DeleteHubUseCase _deleteHubUseCase; final AssignNfcTagUseCase _assignNfcTagUseCase; + final UpdateHubUseCase _updateHubUseCase; void _onAddDialogToggled( ClientHubsAddDialogToggled event, @@ -120,6 +125,46 @@ class ClientHubsBloc extends Bloc ); } + Future _onUpdateRequested( + ClientHubsUpdateRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); + + await handleError( + emit: emit, + 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, 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 9e539c8e..03fd5194 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 @@ -55,6 +55,50 @@ class ClientHubsAddRequested extends ClientHubsEvent { ]; } +/// 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 { 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 new file mode 100644 index 00000000..c5b53a91 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/edit_hub_page.dart @@ -0,0 +1,240 @@ +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: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 '../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. +class EditHubPage extends StatefulWidget { + const EditHubPage({ + required this.hub, + required this.bloc, + super.key, + }); + + final Hub hub; + final ClientHubsBloc bloc; + + @override + State createState() => _EditHubPageState(); +} + +class _EditHubPageState extends State { + final GlobalKey _formKey = GlobalKey(); + late final TextEditingController _nameController; + late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + Prediction? _selectedPrediction; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.hub.name); + _addressController = TextEditingController(text: widget.hub.address); + _addressFocusNode = FocusNode(); + } + + @override + void dispose() { + _nameController.dispose(); + _addressController.dispose(); + _addressFocusNode.dispose(); + super.dispose(); + } + + void _onSave() { + if (!_formKey.currentState!.validate()) return; + + if (_addressController.text.trim().isEmpty) { + UiSnackbar.show( + context, + message: t.client_hubs.add_hub_dialog.address_hint, + type: UiSnackbarType.error, + ); + return; + } + + 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 ?? ''), + ), + ); + } + + @override + Widget build(BuildContext context) { + 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 && + 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); + } + if (state.status == ClientHubsStatus.actionFailure && + state.errorMessage != null) { + UiSnackbar.show( + context, + message: state.errorMessage!, + type: UiSnackbarType.error, + ); + } + }, + child: BlocBuilder( + builder: (BuildContext context, ClientHubsState state) { + final bool isSaving = + state.status == ClientHubsStatus.actionInProgress; + + return Scaffold( + backgroundColor: UiColors.bgMenu, + appBar: AppBar( + backgroundColor: UiColors.foreground, + leading: IconButton( + icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), + onPressed: () => Navigator.of(context).pop(), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.client_hubs.edit_hub.title, + style: UiTypography.headline3m.white, + ), + Text( + t.client_hubs.edit_hub.subtitle, + style: UiTypography.footnote1r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + body: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Name field ────────────────────────────────── + _FieldLabel(t.client_hubs.edit_hub.name_label), + TextFormField( + controller: _nameController, + style: UiTypography.body1r.textPrimary, + textInputAction: TextInputAction.next, + validator: (String? value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required'; + } + return null; + }, + decoration: _inputDecoration( + t.client_hubs.edit_hub.name_hint, + ), + ), + + const SizedBox(height: UiConstants.space4), + + // ── Address field ──────────────────────────────── + _FieldLabel(t.client_hubs.edit_hub.address_label), + HubAddressAutocomplete( + controller: _addressController, + hintText: t.client_hubs.edit_hub.address_hint, + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, + ), + + const SizedBox(height: UiConstants.space8), + + // ── Save button ────────────────────────────────── + UiButton.primary( + onPressed: isSaving ? null : _onSave, + text: t.client_hubs.edit_hub.save_button, + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + + // ── Loading overlay ────────────────────────────────────── + if (isSaving) + Container( + color: UiColors.black.withValues(alpha: 0.1), + child: const Center(child: CircularProgressIndicator()), + ), + ], + ), + ); + }, + ), + ), + ); + } + + InputDecoration _inputDecoration(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), + ), + ); + } +} + +class _FieldLabel extends StatelessWidget { + const _FieldLabel(this.text); + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space2), + child: Text(text, style: UiTypography.body2m.textPrimary), + ); + } +} 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 e3eccc0a..bcb9255b 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,12 +1,16 @@ +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_domain/krow_domain.dart'; -import '../blocs/client_hubs_bloc.dart'; -import '../blocs/client_hubs_event.dart'; -import '../widgets/hub_form_dialog.dart'; +import '../blocs/client_hubs_bloc.dart'; +import 'edit_hub_page.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, @@ -19,50 +23,51 @@ class HubDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, - 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(), + 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), + ), ), - actions: [ - IconButton( - icon: const Icon(UiIcons.edit, color: UiColors.white), - onPressed: () => _showEditDialog(context), + ], + ), + 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: 'Name', - value: hub.name, - icon: UiIcons.home, - ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: 'Address', - value: hub.address, - icon: UiIcons.mapPin, - ), - const SizedBox(height: UiConstants.space4), - _buildDetailItem( - label: 'NFC Tag', - value: hub.nfcTagId ?? 'Not Assigned', - icon: UiIcons.nfc, - isHighlight: hub.nfcTagId != null, - ), - ], - ), - ), ), ); } @@ -78,7 +83,7 @@ class HubDetailsPage extends StatelessWidget { decoration: BoxDecoration( color: UiColors.white, borderRadius: BorderRadius.circular(UiConstants.radiusBase), - boxShadow: const [ + boxShadow: const [ BoxShadow( color: UiColors.popupShadow, blurRadius: 10, @@ -87,11 +92,11 @@ class HubDetailsPage extends StatelessWidget { ], ), child: Row( - children: [ + children: [ Container( padding: const EdgeInsets.all(UiConstants.space3), decoration: BoxDecoration( - color: isHighlight ? UiColors.tagInProgress : UiColors.bgInput, + color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField, borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), child: Icon( @@ -104,16 +109,10 @@ class HubDetailsPage extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: UiTypography.footnote1r.textSecondary, - ), + children: [ + Text(label, style: UiTypography.footnote1r.textSecondary), const SizedBox(height: UiConstants.space1), - Text( - value, - style: UiTypography.body1m.textPrimary, - ), + Text(value, style: UiTypography.body1m.textPrimary), ], ), ), @@ -122,33 +121,17 @@ class HubDetailsPage extends StatelessWidget { ); } - void _showEditDialog(BuildContext context) { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => HubFormDialog( - hub: hub, - onSave: (name, address, {placeId, latitude, longitude, city, state, street, country, zipCode}) { - bloc.add( - ClientHubsUpdateRequested( - id: hub.id, - name: name, - address: address, - placeId: placeId, - latitude: latitude, - longitude: longitude, - city: city, - state: state, - street: street, - country: country, - zipCode: zipCode, - ), - ); - Navigator.of(context).pop(); // Close dialog - Navigator.of(context).pop(); // Go back to list to refresh - }, - onCancel: () => Navigator.of(context).pop(), + 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), ), ); + if (saved == true && context.mounted) { + Navigator.of(context).pop(); + } } } diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart index 392a4300..d2411711 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart @@ -52,7 +52,14 @@ class _NoShowReportPageState extends State { bottom: 32, ), decoration: const BoxDecoration( - color: Color(0xFF1A1A2E), + gradient: LinearGradient( + colors: [ + UiColors.primary, + UiColors.buttonPrimaryHover, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart index edf6b8e3..508b5396 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart @@ -42,6 +42,7 @@ class ClientSettingsPage extends StatelessWidget { } }, child: const Scaffold( + backgroundColor: UiColors.bgMenu, body: CustomScrollView( slivers: [ SettingsProfileHeader(), diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart index 5f275b01..64543f96 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -14,27 +14,46 @@ class SettingsActions extends StatelessWidget { @override /// Builds the settings actions UI. Widget build(BuildContext context) { - // Get the translations for the client settings profile. final TranslationsClientSettingsProfileEn labels = t.client_settings.profile; + // Yellow button style matching the prototype + final ButtonStyle yellowStyle = ElevatedButton.styleFrom( + backgroundColor: UiColors.accent, + foregroundColor: UiColors.accentForeground, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + ), + ); + return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), sliver: SliverList( delegate: SliverChildListDelegate([ const SizedBox(height: UiConstants.space5), - /// TODO: FEATURE_NOT_YET_IMPLEMENTED - // Edit profile is not yet implemented + // Edit Profile button (yellow) + UiButton.primary( + text: labels.edit_profile, + style: yellowStyle, + onPressed: () {}, + ), + const SizedBox(height: UiConstants.space4), - // Hubs button + // Hubs button (yellow) UiButton.primary( text: labels.hubs, + style: yellowStyle, onPressed: () => Modular.to.toClientHubs(), ), const SizedBox(height: UiConstants.space4), - // Log out button + // Quick Links card + _QuickLinksCard(labels: labels), + const SizedBox(height: UiConstants.space4), + + // Log Out button (outlined) BlocBuilder( builder: (BuildContext context, ClientSettingsState state) { return UiButton.secondary( @@ -45,17 +64,11 @@ class SettingsActions extends StatelessWidget { ); }, ), + const SizedBox(height: UiConstants.space8), ]), ), ); } - - /// Handles the sign-out button click event. - void _onSignoutClicked(BuildContext context) { - ReadContext( - context, - ).read().add(const ClientSettingsSignOutRequested()); - } /// Shows a confirmation dialog for signing out. Future _showSignOutDialog(BuildContext context) { @@ -74,13 +87,10 @@ class SettingsActions extends StatelessWidget { style: UiTypography.body2r.textSecondary, ), actions: [ - // Log out button UiButton.secondary( text: t.client_settings.profile.log_out, onPressed: () => _onSignoutClicked(context), ), - - // Cancel button UiButton.secondary( text: t.common.cancel, onPressed: () => Modular.to.pop(), @@ -89,4 +99,97 @@ class SettingsActions extends StatelessWidget { ), ); } + + /// Handles the sign-out button click event. + void _onSignoutClicked(BuildContext context) { + ReadContext(context) + .read() + .add(const ClientSettingsSignOutRequested()); + } +} + +/// Quick Links card — inline here since it's always part of SettingsActions ordering. +class _QuickLinksCard extends StatelessWidget { + final TranslationsClientSettingsProfileEn labels; + + const _QuickLinksCard({required this.labels}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + side: const BorderSide(color: UiColors.border), + ), + color: UiColors.white, + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + labels.quick_links, + style: UiTypography.footnote1b.textPrimary, + ), + const SizedBox(height: UiConstants.space3), + _QuickLinkItem( + icon: UiIcons.nfc, + title: labels.clock_in_hubs, + onTap: () => Modular.to.toClientHubs(), + ), + _QuickLinkItem( + icon: UiIcons.building, + title: labels.billing_payments, + onTap: () => Modular.to.toClientBilling(), + ), + ], + ), + ), + ); + } +} + +/// A single quick link row item. +class _QuickLinkItem extends StatelessWidget { + final IconData icon; + final String title; + final VoidCallback onTap; + + const _QuickLinkItem({ + required this.icon, + required this.title, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: UiConstants.radiusMd, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space3, + horizontal: UiConstants.space2, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, size: 20, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space3), + Text(title, style: UiTypography.footnote1m.textPrimary), + ], + ), + const Icon( + UiIcons.chevronRight, + size: 20, + color: UiColors.iconThird, + ), + ], + ), + ), + ); + } } diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index b9ddd93e..706e1e4b 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -11,7 +11,6 @@ class SettingsProfileHeader extends StatelessWidget { const SettingsProfileHeader({super.key}); @override - /// Builds the profile header UI. Widget build(BuildContext context) { final TranslationsClientSettingsProfileEn labels = t.client_settings.profile; final dc.ClientSession? session = dc.ClientSessionStore.instance.session; @@ -23,78 +22,115 @@ class SettingsProfileHeader extends StatelessWidget { ? businessName.trim()[0].toUpperCase() : 'C'; - return SliverAppBar( - backgroundColor: UiColors.bgSecondary, - expandedHeight: 140, - pinned: true, - elevation: 0, - shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), - onPressed: () => Modular.to.toClientHome(), - ), - flexibleSpace: FlexibleSpaceBar( - background: Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8), - margin: const EdgeInsets.only(top: UiConstants.space24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - spacing: UiConstants.space4, - children: [ - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: UiColors.border, width: 2), - color: UiColors.white, + return SliverToBoxAdapter( + child: Container( + width: double.infinity, + padding: const EdgeInsets.only(bottom: 36), + decoration: const BoxDecoration( + color: UiColors.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // ── Top bar: back arrow + title ────────────────── + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space2, ), - child: CircleAvatar( - backgroundColor: UiColors.primary.withValues(alpha: 0.1), - backgroundImage: - photoUrl != null && photoUrl.isNotEmpty - ? NetworkImage(photoUrl) - : null, - child: - photoUrl != null && photoUrl.isNotEmpty - ? null - : Text( - avatarLetter, - style: UiTypography.headline1m.copyWith( - color: UiColors.primary, - ), - ), + child: Row( + children: [ + GestureDetector( + onTap: () => Modular.to.toClientHome(), + child: const Icon( + UiIcons.arrowLeft, + color: UiColors.white, + size: 22, + ), + ), + const SizedBox(width: UiConstants.space3), + Text( + labels.title, + style: UiTypography.body1b.copyWith( + color: UiColors.white, + ), + ), + ], ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(businessName, style: UiTypography.body1b.textPrimary), - const SizedBox(height: UiConstants.space1), - Row( - mainAxisAlignment: MainAxisAlignment.start, - spacing: UiConstants.space1, - children: [ - Icon( - UiIcons.mail, - size: 14, - color: UiColors.textSecondary, - ), - Text( - email, - style: UiTypography.footnote1r.textSecondary, - ), - ], + ), + + const SizedBox(height: UiConstants.space6), + + // ── Avatar ─────────────────────────────────────── + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: UiColors.white, + border: Border.all( + color: UiColors.white.withValues(alpha: 0.6), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.15), + blurRadius: 16, + offset: const Offset(0, 6), ), ], ), - ], - ), + child: ClipOval( + child: photoUrl != null && photoUrl.isNotEmpty + ? Image.network(photoUrl, fit: BoxFit.cover) + : Center( + child: Text( + avatarLetter, + style: UiTypography.headline1m.copyWith( + color: UiColors.primary, + fontSize: 32, + ), + ), + ), + ), + ), + + const SizedBox(height: UiConstants.space4), + + // ── Business Name ───────────────────────────────── + Text( + businessName, + style: UiTypography.headline3m.copyWith( + color: UiColors.white, + ), + ), + + const SizedBox(height: UiConstants.space2), + + // ── Email ───────────────────────────────────────── + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + UiIcons.mail, + size: 14, + color: UiColors.white.withValues(alpha: 0.75), + ), + const SizedBox(width: 6), + Text( + email, + style: UiTypography.footnote1r.copyWith( + color: UiColors.white.withValues(alpha: 0.75), + ), + ), + ], + ), + ], ), ), - title: Text(labels.title, style: UiTypography.body1b.textPrimary), ); } }