From 94e15ae05d4764764211677c94f943d2383533d6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 26 Feb 2026 13:39:33 -0500 Subject: [PATCH] refactor: Refactor hub form from a dialog to a dedicated widget and streamline HubDetailsPage UI and bloc initialization. --- .../features/client/hubs/lib/client_hubs.dart | 28 +- .../src/presentation/pages/edit_hub_page.dart | 153 ++++---- .../presentation/pages/hub_details_page.dart | 35 +- .../src/presentation/widgets/hub_form.dart | 307 +++++++++++++++ .../presentation/widgets/hub_form_dialog.dart | 350 ------------------ 5 files changed, 407 insertions(+), 466 deletions(-) create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form.dart delete mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart 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 53fdb2e4..87876299 100644 --- a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart +++ b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart @@ -56,10 +56,8 @@ class ClientHubsModule extends Module { 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(), - ); + final Hub hub = data['hub'] as Hub; + return HubDetailsPage(hub: hub); }, ); r.child( @@ -67,21 +65,19 @@ class ClientHubsModule extends Module { transition: TransitionType.custom, customTransition: CustomTransition( opaque: false, - transitionBuilder: ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return FadeTransition(opacity: animation, child: child); - }, + transitionBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition(opacity: animation, child: child); + }, ), child: (_) { final Map data = r.args.data as Map; - return EditHubPage( - hub: data['hub'] as Hub?, - bloc: Modular.get(), - ); + return EditHubPage(hub: data['hub'] as Hub?); }, ); } 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 8bc8373e..12993f12 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 @@ -8,32 +8,21 @@ import 'package:krow_domain/krow_domain.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_form_dialog.dart'; +import '../widgets/hub_form.dart'; /// A wrapper page that shows the hub form in a modal-style layout. -class EditHubPage extends StatefulWidget { - const EditHubPage({this.hub, required this.bloc, super.key}); +class EditHubPage extends StatelessWidget { + const EditHubPage({this.hub, super.key}); final Hub? hub; - final EditHubBloc bloc; - - @override - State createState() => _EditHubPageState(); -} - -class _EditHubPageState extends State { - @override - void initState() { - super.initState(); - // Load available cost centers - widget.bloc.add(const EditHubCostCentersLoadRequested()); - } @override Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.bloc, - child: BlocListener( + return BlocProvider( + create: (_) => + Modular.get() + ..add(const EditHubCostCentersLoadRequested()), + child: BlocConsumer( listenWhen: (EditHubState prev, EditHubState curr) => prev.status != curr.status || prev.successKey != curr.successKey, listener: (BuildContext context, EditHubState state) { @@ -58,74 +47,70 @@ class _EditHubPageState extends State { ); } }, - child: BlocBuilder( - builder: (BuildContext context, EditHubState state) { - final bool isSaving = state.status == EditHubStatus.loading; + builder: (BuildContext context, EditHubState state) { + final bool isSaving = state.status == EditHubStatus.loading; + final bool isEditing = hub != null; + final String title = isEditing + ? t.client_hubs.edit_hub.title + : t.client_hubs.add_hub_dialog.title; - return Scaffold( - backgroundColor: UiColors.bgOverlay, - body: Stack( - children: [ - // Tap background to dismiss - GestureDetector( - onTap: () => Modular.to.pop(), - child: Container(color: Colors.transparent), - ), - - // Dialog-style content centered - Align( - alignment: Alignment.center, - child: HubFormDialog( - hub: widget.hub, - costCenters: state.costCenters, - onCancel: () => Modular.to.pop(), - onSave: ({ - required String name, - required String address, - String? costCenterId, - String? placeId, - double? latitude, - double? longitude, - }) { - if (widget.hub == null) { - widget.bloc.add( - EditHubAddRequested( - name: name, - address: address, - costCenterId: costCenterId, - placeId: placeId, - latitude: latitude, - longitude: longitude, - ), - ); - } else { - widget.bloc.add( - EditHubUpdateRequested( - id: widget.hub!.id, - name: name, - address: address, - costCenterId: costCenterId, - placeId: placeId, - latitude: latitude, - longitude: longitude, - ), - ); - } - }, - ), + return Scaffold( + appBar: UiAppBar(title: title, showBackButton: true), + body: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: HubForm( + hub: hub, + costCenters: state.costCenters, + onCancel: () => Modular.to.pop(), + onSave: + ({ + required String name, + required String address, + String? costCenterId, + String? placeId, + double? latitude, + double? longitude, + }) { + if (hub == null) { + BlocProvider.of(context).add( + EditHubAddRequested( + name: name, + address: address, + costCenterId: costCenterId, + placeId: placeId, + latitude: latitude, + longitude: longitude, + ), + ); + } else { + BlocProvider.of(context).add( + EditHubUpdateRequested( + id: hub!.id, + name: name, + address: address, + costCenterId: costCenterId, + placeId: placeId, + latitude: latitude, + longitude: longitude, + ), + ); + } + }, ), + ), - // Global loading overlay if saving - if (isSaving) - Container( - color: UiColors.black.withValues(alpha: 0.1), - child: const Center(child: CircularProgressIndicator()), - ), - ], - ), - ); - }, - ), + // Global loading overlay if saving + if (isSaving) + Container( + color: UiColors.black.withValues(alpha: 0.1), + child: const Center(child: CircularProgressIndicator()), + ), + ], + ), + ); + }, ), ); } 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 16861eb5..d8725551 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 @@ -10,28 +10,27 @@ import '../blocs/hub_details/hub_details_bloc.dart'; import '../blocs/hub_details/hub_details_event.dart'; import '../blocs/hub_details/hub_details_state.dart'; import '../widgets/hub_details/hub_details_bottom_actions.dart'; -import '../widgets/hub_details/hub_details_header.dart'; import '../widgets/hub_details/hub_details_item.dart'; /// A read-only details page for a single [Hub]. /// /// Shows hub name, address, and NFC tag assignment. class HubDetailsPage extends StatelessWidget { - const HubDetailsPage({required this.hub, required this.bloc, super.key}); + const HubDetailsPage({required this.hub, super.key}); final Hub hub; - final HubDetailsBloc bloc; @override Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, + return BlocProvider( + create: (_) => Modular.get(), child: BlocListener( listener: (BuildContext context, HubDetailsState state) { if (state.status == HubDetailsStatus.deleted) { final String message = state.successKey == 'deleted' ? t.client_hubs.hub_details.deleted_success - : (state.successMessage ?? t.client_hubs.hub_details.deleted_success); + : (state.successMessage ?? + t.client_hubs.hub_details.deleted_success); UiSnackbar.show( context, message: message, @@ -53,23 +52,22 @@ class HubDetailsPage extends StatelessWidget { final bool isLoading = state.status == HubDetailsStatus.loading; return Scaffold( - appBar: const UiAppBar(showBackButton: true), + appBar: UiAppBar( + title: hub.name, + subtitle: hub.address, + showBackButton: true, + ), bottomNavigationBar: HubDetailsBottomActions( isLoading: isLoading, onDelete: () => _confirmDeleteHub(context), onEdit: () => _navigateToEditPage(context), ), - backgroundColor: UiColors.bgMenu, body: Stack( children: [ SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // ── Header ────────────────────────────────────────── - HubDetailsHeader(hub: hub), - const Divider(height: 1, thickness: 0.5), - Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( @@ -85,11 +83,16 @@ class HubDetailsPage extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), HubDetailsItem( - label: t.client_hubs.hub_details.cost_center_label, + label: + t.client_hubs.hub_details.cost_center_label, value: hub.costCenter != null ? '${hub.costCenter!.name} (${hub.costCenter!.code})' - : t.client_hubs.hub_details.cost_center_none, - icon: UiIcons.bank, // Using bank icon for cost center + : t + .client_hubs + .hub_details + .cost_center_none, + icon: UiIcons + .bank, // Using bank icon for cost center isHighlight: hub.costCenter != null, ), ], @@ -140,7 +143,7 @@ class HubDetailsPage extends StatelessWidget { ); if (confirm == true) { - bloc.add(HubDetailsDeleteRequested(hub.id)); + Modular.get().add(HubDetailsDeleteRequested(hub.id)); } } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form.dart new file mode 100644 index 00000000..a945097f --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form.dart @@ -0,0 +1,307 @@ +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 'package:krow_domain/krow_domain.dart'; + +import 'hub_address_autocomplete.dart'; +import 'edit_hub/edit_hub_field_label.dart'; + +/// A bottom sheet dialog for adding or editing a hub. +class HubForm extends StatefulWidget { + const HubForm({ + required this.onSave, + required this.onCancel, + this.hub, + this.costCenters = const [], + super.key, + }); + + final Hub? hub; + final List costCenters; + final void Function({ + required String name, + required String address, + String? costCenterId, + String? placeId, + double? latitude, + double? longitude, + }) + onSave; + final VoidCallback onCancel; + + @override + State createState() => _HubFormState(); +} + +class _HubFormState extends State { + late final TextEditingController _nameController; + late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + String? _selectedCostCenterId; + Prediction? _selectedPrediction; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.hub?.name); + _addressController = TextEditingController(text: widget.hub?.address); + _addressFocusNode = FocusNode(); + _selectedCostCenterId = widget.hub?.costCenter?.id; + } + + @override + void dispose() { + _nameController.dispose(); + _addressController.dispose(); + _addressFocusNode.dispose(); + super.dispose(); + } + + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final bool isEditing = widget.hub != null; + final String buttonText = isEditing + ? t.client_hubs.edit_hub.save_button + : t.client_hubs.add_hub_dialog.create_button; + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Hub Name ──────────────────────────────── + EditHubFieldLabel(t.client_hubs.add_hub_dialog.name_label), + const SizedBox(height: UiConstants.space2), + TextFormField( + controller: _nameController, + textInputAction: TextInputAction.next, + validator: (String? value) { + if (value == null || value.trim().isEmpty) { + return t.client_hubs.add_hub_dialog.name_required; + } + return null; + }, + decoration: _buildInputDecoration( + t.client_hubs.add_hub_dialog.name_hint, + ), + ), + + const SizedBox(height: UiConstants.space4), + + // ── Cost Center ───────────────────────────── + EditHubFieldLabel(t.client_hubs.add_hub_dialog.cost_center_label), + const SizedBox(height: UiConstants.space2), + InkWell( + onTap: _showCostCenterSelector, + borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: 16, + ), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFD), + borderRadius: BorderRadius.circular( + UiConstants.radiusBase * 1.5, + ), + border: Border.all( + color: _selectedCostCenterId != null + ? UiColors.primary + : UiColors.primary.withValues(alpha: 0.1), + width: _selectedCostCenterId != null ? 2 : 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + _selectedCostCenterId != null + ? _getCostCenterName(_selectedCostCenterId!) + : t.client_hubs.add_hub_dialog.cost_center_hint, + style: _selectedCostCenterId != null + ? UiTypography.body1r.textPrimary + : UiTypography.body2r.textPlaceholder.copyWith( + color: UiColors.textSecondary.withValues( + alpha: 0.5, + ), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const Icon( + Icons.keyboard_arrow_down, + color: UiColors.iconSecondary, + ), + ], + ), + ), + ), + + const SizedBox(height: UiConstants.space4), + + // ── Address ───────────────────────────────── + EditHubFieldLabel(t.client_hubs.add_hub_dialog.address_label), + const SizedBox(height: UiConstants.space2), + HubAddressAutocomplete( + controller: _addressController, + hintText: t.client_hubs.add_hub_dialog.address_hint, + decoration: _buildInputDecoration( + t.client_hubs.add_hub_dialog.address_hint, + ), + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, + ), + + const SizedBox(height: UiConstants.space8), + + // ── Save Button ───────────────────────────── + Row( + children: [ + Expanded( + child: UiButton.primary( + onPressed: () { + if (_formKey.currentState!.validate()) { + if (_addressController.text.trim().isEmpty) { + UiSnackbar.show( + context, + message: + t.client_hubs.add_hub_dialog.address_required, + type: UiSnackbarType.error, + ); + return; + } + + widget.onSave( + name: _nameController.text.trim(), + address: _addressController.text.trim(), + costCenterId: _selectedCostCenterId, + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse( + _selectedPrediction?.lat ?? '', + ), + longitude: double.tryParse( + _selectedPrediction?.lng ?? '', + ), + ); + } + }, + text: buttonText, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: UiButton.secondary( + onPressed: widget.onCancel, + text: t.common.cancel, + ), + ), + ], + ), + ], + ), + ); + } + + InputDecoration _buildInputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: UiTypography.body2r.textPlaceholder.copyWith( + color: UiColors.textSecondary.withValues(alpha: 0.5), + ), + filled: true, + fillColor: const Color(0xFFF8FAFD), + contentPadding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), + borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), + borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), + borderSide: const BorderSide(color: UiColors.primary, width: 2), + ), + errorStyle: UiTypography.footnote2r.textError, + ); + } + + String _getCostCenterName(String id) { + try { + return widget.costCenters.firstWhere((CostCenter cc) => cc.id == id).name; + } catch (_) { + return id; + } + } + + Future _showCostCenterSelector() async { + final CostCenter? selected = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + title: Text( + t.client_hubs.add_hub_dialog.cost_center_label, + style: UiTypography.headline3m.textPrimary, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 16), + content: SizedBox( + width: double.maxFinite, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: widget.costCenters.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + t.client_hubs.add_hub_dialog.cost_centers_empty, + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: widget.costCenters.length, + itemBuilder: (BuildContext context, int index) { + final CostCenter cc = widget.costCenters[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + ), + title: Text( + cc.name, + style: UiTypography.body1m.textPrimary, + ), + subtitle: cc.code != null + ? Text( + cc.code!, + style: UiTypography.body2r.textSecondary, + ) + : null, + onTap: () => Navigator.of(context).pop(cc), + ); + }, + ), + ), + ), + ); + }, + ); + + if (selected != null) { + setState(() { + _selectedCostCenterId = selected.id; + }); + } + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart deleted file mode 100644 index 25d5f4b0..00000000 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart +++ /dev/null @@ -1,350 +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 'package:krow_domain/krow_domain.dart'; - -import 'hub_address_autocomplete.dart'; -import 'edit_hub/edit_hub_field_label.dart'; - -/// A bottom sheet dialog for adding or editing a hub. -class HubFormDialog extends StatefulWidget { - /// Creates a [HubFormDialog]. - const HubFormDialog({ - required this.onSave, - required this.onCancel, - this.hub, - this.costCenters = const [], - super.key, - }); - - /// The hub to edit. If null, a new hub is created. - final Hub? hub; - - /// Available cost centers for selection. - final List costCenters; - - /// Callback when the "Save" button is pressed. - final void Function({ - required String name, - required String address, - String? costCenterId, - String? placeId, - double? latitude, - double? longitude, - }) onSave; - - /// Callback when the dialog is cancelled. - final VoidCallback onCancel; - - @override - State createState() => _HubFormDialogState(); -} - -class _HubFormDialogState extends State { - late final TextEditingController _nameController; - late final TextEditingController _addressController; - late final FocusNode _addressFocusNode; - String? _selectedCostCenterId; - Prediction? _selectedPrediction; - - @override - void initState() { - super.initState(); - _nameController = TextEditingController(text: widget.hub?.name); - _addressController = TextEditingController(text: widget.hub?.address); - _addressFocusNode = FocusNode(); - _selectedCostCenterId = widget.hub?.costCenter?.id; - } - - @override - void dispose() { - _nameController.dispose(); - _addressController.dispose(); - _addressFocusNode.dispose(); - super.dispose(); - } - - final GlobalKey _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - final bool isEditing = widget.hub != null; - final String title = isEditing - ? t.client_hubs.edit_hub.title - : t.client_hubs.add_hub_dialog.title; - - final String buttonText = isEditing - ? t.client_hubs.edit_hub.save_button - : t.client_hubs.add_hub_dialog.create_button; - - return Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: UiConstants.space5), - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 3), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.15), - blurRadius: 30, - offset: const Offset(0, 10), - ), - ], - ), - padding: const EdgeInsets.all(UiConstants.space6), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - title, - style: UiTypography.headline3m.textPrimary.copyWith( - fontSize: 20, - ), - ), - const SizedBox(height: UiConstants.space5), - - // ── Hub Name ──────────────────────────────── - EditHubFieldLabel(t.client_hubs.add_hub_dialog.name_label), - const SizedBox(height: UiConstants.space2), - TextFormField( - controller: _nameController, - style: UiTypography.body1r.textPrimary, - textInputAction: TextInputAction.next, - validator: (String? value) { - if (value == null || value.trim().isEmpty) { - return t.client_hubs.add_hub_dialog.name_required; - } - return null; - }, - decoration: _buildInputDecoration( - t.client_hubs.add_hub_dialog.name_hint, - ), - ), - - const SizedBox(height: UiConstants.space4), - - // ── Cost Center ───────────────────────────── - EditHubFieldLabel(t.client_hubs.add_hub_dialog.cost_center_label), - const SizedBox(height: UiConstants.space2), - InkWell( - onTap: _showCostCenterSelector, - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: 16, - ), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFD), - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), - border: Border.all( - color: _selectedCostCenterId != null - ? UiColors.primary - : UiColors.primary.withValues(alpha: 0.1), - width: _selectedCostCenterId != null ? 2 : 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _selectedCostCenterId != null - ? _getCostCenterName(_selectedCostCenterId!) - : t.client_hubs.add_hub_dialog.cost_center_hint, - style: _selectedCostCenterId != null - ? UiTypography.body1r.textPrimary - : UiTypography.body2r.textPlaceholder.copyWith( - color: UiColors.textSecondary.withValues(alpha: 0.5), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const Icon( - Icons.keyboard_arrow_down, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ), - - const SizedBox(height: UiConstants.space4), - - // ── Address ───────────────────────────────── - EditHubFieldLabel(t.client_hubs.add_hub_dialog.address_label), - const SizedBox(height: UiConstants.space2), - HubAddressAutocomplete( - controller: _addressController, - hintText: t.client_hubs.add_hub_dialog.address_hint, - decoration: _buildInputDecoration( - t.client_hubs.add_hub_dialog.address_hint, - ), - focusNode: _addressFocusNode, - onSelected: (Prediction prediction) { - _selectedPrediction = prediction; - }, - ), - - const SizedBox(height: UiConstants.space8), - - // ── Buttons ───────────────────────────────── - Row( - children: [ - Expanded( - child: UiButton.secondary( - style: OutlinedButton.styleFrom( - side: BorderSide( - color: UiColors.primary.withValues(alpha: 0.1), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - UiConstants.radiusBase * 1.5, - ), - ), - ), - onPressed: widget.onCancel, - text: t.common.cancel, - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: UiButton.primary( - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.accent, - foregroundColor: UiColors.accentForeground, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - UiConstants.radiusBase * 1.5, - ), - ), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - if (_addressController.text.trim().isEmpty) { - UiSnackbar.show( - context, - message: t.client_hubs.add_hub_dialog.address_required, - type: UiSnackbarType.error, - ); - return; - } - - widget.onSave( - name: _nameController.text.trim(), - address: _addressController.text.trim(), - costCenterId: _selectedCostCenterId, - placeId: _selectedPrediction?.placeId, - latitude: double.tryParse( - _selectedPrediction?.lat ?? '', - ), - longitude: double.tryParse( - _selectedPrediction?.lng ?? '', - ), - ); - } - }, - text: buttonText, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } - - InputDecoration _buildInputDecoration(String hint) { - return InputDecoration( - hintText: hint, - hintStyle: UiTypography.body2r.textPlaceholder.copyWith( - color: UiColors.textSecondary.withValues(alpha: 0.5), - ), - filled: true, - fillColor: const Color(0xFFF8FAFD), - contentPadding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: 16, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), - borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), - borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5), - borderSide: const BorderSide(color: UiColors.primary, width: 2), - ), - errorStyle: UiTypography.footnote2r.textError, - ); - } - - String _getCostCenterName(String id) { - try { - return widget.costCenters.firstWhere((CostCenter cc) => cc.id == id).name; - } catch (_) { - return id; - } - } - - Future _showCostCenterSelector() async { - final CostCenter? selected = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - title: Text( - t.client_hubs.add_hub_dialog.cost_center_label, - style: UiTypography.headline3m.textPrimary, - ), - contentPadding: const EdgeInsets.symmetric(vertical: 16), - content: SizedBox( - width: double.maxFinite, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 400), - child: widget.costCenters.isEmpty - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text(t.client_hubs.add_hub_dialog.cost_centers_empty), - ) - : ListView.builder( - shrinkWrap: true, - itemCount: widget.costCenters.length, - itemBuilder: (BuildContext context, int index) { - final CostCenter cc = widget.costCenters[index]; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - title: Text(cc.name, style: UiTypography.body1m.textPrimary), - subtitle: cc.code != null ? Text(cc.code!, style: UiTypography.body2r.textSecondary) : null, - onTap: () => Navigator.of(context).pop(cc), - ); - }, - ), - ), - ), - ); - }, - ); - - if (selected != null) { - setState(() { - _selectedCostCenterId = selected.id; - }); - } - } -}