From cd51e8488c638f1904ffb23b2de457db06e33fca Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 24 Feb 2026 14:22:34 -0500 Subject: [PATCH] refactor: Extract hub details UI components into dedicated widgets and introduce new edit hub form elements. --- .../src/presentation/pages/edit_hub_page.dart | 106 ++--------- .../presentation/pages/hub_details_page.dart | 168 ++---------------- .../edit_hub/edit_hub_field_label.dart | 17 ++ .../edit_hub/edit_hub_form_section.dart | 105 +++++++++++ .../hub_details_bottom_actions.dart | 55 ++++++ .../hub_details/hub_details_header.dart | 45 +++++ .../widgets/hub_details/hub_details_item.dart | 59 ++++++ .../widgets/sections/onboarding_section.dart | 9 +- 8 files changed, 312 insertions(+), 252 deletions(-) create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_field_label.dart create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_form_section.dart create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_bottom_actions.dart create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_header.dart create mode 100644 apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_item.dart 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 3e9a1f15..ea547ab2 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 @@ -9,7 +9,7 @@ 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_address_autocomplete.dart'; +import '../widgets/edit_hub/edit_hub_form_section.dart'; /// A dedicated full-screen page for adding or editing a hub. class EditHubPage extends StatefulWidget { @@ -36,7 +36,7 @@ class _EditHubPageState extends State { _addressController = TextEditingController(text: widget.hub?.address); _addressFocusNode = FocusNode(); - // Update header on change + // Update header on change (if header is added back) _nameController.addListener(() => setState(() {})); _addressController.addListener(() => setState(() {})); } @@ -136,59 +136,17 @@ class _EditHubPageState extends State { children: [ Padding( 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: widget.hub == null - ? t - .client_hubs - .add_hub_dialog - .create_button - : t.client_hubs.edit_hub.save_button, - ), - - const SizedBox(height: 40), - ], - ), + child: EditHubFormSection( + formKey: _formKey, + nameController: _nameController, + addressController: _addressController, + addressFocusNode: _addressFocusNode, + onAddressSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, + onSave: _onSave, + isSaving: isSaving, + isEdit: widget.hub != null, ), ), ], @@ -209,42 +167,4 @@ class _EditHubPageState extends State { ), ); } - - 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 2713d4ae..cbcf5d61 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 @@ -9,6 +9,9 @@ import 'package:krow_domain/krow_domain.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'; +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]. /// @@ -47,49 +50,11 @@ class HubDetailsPage extends StatelessWidget { final bool isLoading = state.status == HubDetailsStatus.loading; return Scaffold( - appBar: UiAppBar( - title: t.client_hubs.hub_details.title, - onLeadingPressed: () => Modular.to.pop(), - ), - bottomNavigationBar: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Divider(height: 1, thickness: 0.5), - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: UiButton.secondary( - onPressed: isLoading - ? null - : () => _confirmDeleteHub(context), - text: t.common.delete, - leadingIcon: UiIcons.delete, - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.destructive, - side: const BorderSide( - color: UiColors.destructive, - ), - ), - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: UiButton.secondary( - onPressed: isLoading - ? null - : () => _navigateToEditPage(context), - text: t.client_hubs.hub_details.edit_button, - leadingIcon: UiIcons.edit, - ), - ), - ], - ), - ), - ], - ), + appBar: const UiAppBar(showBackButton: true), + bottomNavigationBar: HubDetailsBottomActions( + isLoading: isLoading, + onDelete: () => _confirmDeleteHub(context), + onEdit: () => _navigateToEditPage(context), ), backgroundColor: UiColors.bgMenu, body: Stack( @@ -99,75 +64,7 @@ class HubDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Header ────────────────────────────────────────── - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - width: 114, - decoration: BoxDecoration( - color: UiColors.primary.withValues( - alpha: 0.08, - ), - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - border: Border.all(color: UiColors.primary), - ), - child: const Center( - child: Icon( - UiIcons.nfc, - color: UiColors.primary, - size: 32, - ), - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - hub.name, - style: - UiTypography.headline1b.textPrimary, - ), - const SizedBox( - height: UiConstants.space1, - ), - Row( - children: [ - const Icon( - UiIcons.mapPin, - size: 16, - color: UiColors.textSecondary, - ), - const SizedBox( - width: UiConstants.space1, - ), - Expanded( - child: Text( - hub.address, - style: UiTypography - .body2r - .textSecondary, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), + HubDetailsHeader(hub: hub), const Divider(height: 1, thickness: 0.5), Padding( @@ -175,7 +72,7 @@ class HubDetailsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildDetailItem( + HubDetailsItem( label: t.client_hubs.hub_details.nfc_label, value: hub.nfcTagId ?? @@ -203,51 +100,6 @@ class HubDetailsPage extends StatelessWidget { ); } - Widget _buildDetailItem({ - required String label, - required String value, - required IconData icon, - bool isHighlight = false, - }) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.border), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: isHighlight - ? UiColors.tagInProgress - : UiColors.bgInputField, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Icon( - icon, - color: isHighlight ? UiColors.iconSuccess : UiColors.iconThird, - size: 20, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote1r.textSecondary), - const SizedBox(height: UiConstants.space1), - Text(value, style: UiTypography.body1m.textPrimary), - ], - ), - ), - ], - ), - ); - } - Future _navigateToEditPage(BuildContext context) async { final bool? saved = await Modular.to.toEditHub(hub: hub); if (saved == true && context.mounted) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_field_label.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_field_label.dart new file mode 100644 index 00000000..7cd617a2 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_field_label.dart @@ -0,0 +1,17 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A simple field label widget for the edit hub page. +class EditHubFieldLabel extends StatelessWidget { + const EditHubFieldLabel(this.text, {super.key}); + + 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/widgets/edit_hub/edit_hub_form_section.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_form_section.dart new file mode 100644 index 00000000..b874dd3b --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/edit_hub/edit_hub_form_section.dart @@ -0,0 +1,105 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:google_places_flutter/model/prediction.dart'; + +import '../hub_address_autocomplete.dart'; +import 'edit_hub_field_label.dart'; + +/// The form section for adding or editing a hub. +class EditHubFormSection extends StatelessWidget { + const EditHubFormSection({ + required this.formKey, + required this.nameController, + required this.addressController, + required this.addressFocusNode, + required this.onAddressSelected, + required this.onSave, + this.isSaving = false, + this.isEdit = false, + super.key, + }); + + final GlobalKey formKey; + final TextEditingController nameController; + final TextEditingController addressController; + final FocusNode addressFocusNode; + final ValueChanged onAddressSelected; + final VoidCallback onSave; + final bool isSaving; + final bool isEdit; + + @override + Widget build(BuildContext context) { + return Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Name field ────────────────────────────────── + EditHubFieldLabel(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 ──────────────────────────────── + EditHubFieldLabel(t.client_hubs.edit_hub.address_label), + HubAddressAutocomplete( + controller: addressController, + hintText: t.client_hubs.edit_hub.address_hint, + focusNode: addressFocusNode, + onSelected: onAddressSelected, + ), + + const SizedBox(height: UiConstants.space8), + + // ── Save button ────────────────────────────────── + UiButton.primary( + onPressed: isSaving ? null : onSave, + text: isEdit + ? t.client_hubs.edit_hub.save_button + : t.client_hubs.add_hub_dialog.create_button, + ), + + const SizedBox(height: 40), + ], + ), + ); + } + + 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), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_bottom_actions.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_bottom_actions.dart new file mode 100644 index 00000000..d109c6bc --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_bottom_actions.dart @@ -0,0 +1,55 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Bottom action buttons for the hub details page. +class HubDetailsBottomActions extends StatelessWidget { + const HubDetailsBottomActions({ + required this.onDelete, + required this.onEdit, + this.isLoading = false, + super.key, + }); + + final VoidCallback onDelete; + final VoidCallback onEdit; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(height: 1, thickness: 0.5), + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: UiButton.secondary( + onPressed: isLoading ? null : onDelete, + text: t.common.delete, + leadingIcon: UiIcons.delete, + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.destructive, + side: const BorderSide(color: UiColors.destructive), + ), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: UiButton.secondary( + onPressed: isLoading ? null : onEdit, + text: t.client_hubs.hub_details.edit_button, + leadingIcon: UiIcons.edit, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_header.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_header.dart new file mode 100644 index 00000000..ccf670ed --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_header.dart @@ -0,0 +1,45 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Header widget for the hub details page. +class HubDetailsHeader extends StatelessWidget { + const HubDetailsHeader({required this.hub, super.key}); + + final Hub hub; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + spacing: UiConstants.space1, + children: [ + Text(hub.name, style: UiTypography.headline1b.textPrimary), + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 16, + color: UiColors.textSecondary, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + hub.address, + style: UiTypography.body2r.textSecondary, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_item.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_item.dart new file mode 100644 index 00000000..9a087669 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_details/hub_details_item.dart @@ -0,0 +1,59 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A reusable detail item for the hub details page. +class HubDetailsItem extends StatelessWidget { + const HubDetailsItem({ + required this.label, + required this.value, + required this.icon, + this.isHighlight = false, + super.key, + }); + + final String label; + final String value; + final IconData icon; + final bool isHighlight; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: isHighlight + ? UiColors.tagInProgress + : UiColors.bgInputField, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Icon( + icon, + color: isHighlight ? UiColors.iconSuccess : UiColors.iconThird, + size: 20, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote1r.textSecondary), + const SizedBox(height: UiConstants.space1), + Text(value, style: UiTypography.body1m.textPrimary), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart index ece3bc18..327e58ea 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart @@ -21,7 +21,9 @@ class OnboardingSection extends StatelessWidget { @override Widget build(BuildContext context) { - final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile; + final TranslationsStaffProfileEn i18n = Translations.of( + context, + ).staff.profile; return BlocBuilder( builder: (BuildContext context, ProfileState state) { @@ -49,6 +51,11 @@ class OnboardingSection extends StatelessWidget { completed: state.experienceComplete, onTap: () => Modular.to.toExperience(), ), + ProfileMenuItem( + icon: UiIcons.shirt, + label: i18n.menu_items.attire, + onTap: () => Modular.to.toAttire(), + ), ], ), ],