From caaf972349f2669c5e7515bf877b27a086aa0800 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 24 Jan 2026 19:57:10 -0500 Subject: [PATCH] feat: Update navigation paths and enhance personal info page with design system compliance --- .../navigation/profile_navigator.dart | 2 +- .../navigation/onboarding_navigator.dart | 2 +- .../navigation/profile_info_navigator.dart | 2 +- .../pages/personal_info_page.dart | 141 +++++++++--------- .../widgets/personal_info_content.dart | 45 +++--- .../widgets/personal_info_form.dart | 47 +++--- .../widgets/profile_photo_widget.dart | 14 +- .../src/presentation/widgets/save_button.dart | 14 +- .../lib/src/staff_profile_info_module.dart | 5 + 9 files changed, 139 insertions(+), 133 deletions(-) diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart index aba723c5..adde9c58 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart @@ -8,7 +8,7 @@ import 'package:flutter_modular/flutter_modular.dart'; extension ProfileNavigator on IModularNavigator { /// Navigates to the personal info page. void pushPersonalInfo() { - pushNamed('/profile/onboarding/personal-info'); + pushNamed('./onboarding/personal-info'); } /// Navigates to the emergency contact page. diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/onboarding_navigator.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/onboarding_navigator.dart index 57ba732b..4686f340 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/onboarding_navigator.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/onboarding_navigator.dart @@ -10,7 +10,7 @@ extension ProfileInfoNavigator on IModularNavigator { /// This page allows staff members to edit their personal information /// including phone, bio, languages, and preferred locations. Future pushPersonalInfo() { - return pushNamed('/profile/onboarding/personal-info'); + return pushNamed('./personal-info'); } /// Navigates to the Emergency Contact page. diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/profile_info_navigator.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/profile_info_navigator.dart index 57ba732b..4686f340 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/profile_info_navigator.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/navigation/profile_info_navigator.dart @@ -10,7 +10,7 @@ extension ProfileInfoNavigator on IModularNavigator { /// This page allows staff members to edit their personal information /// including phone, bio, languages, and preferred locations. Future pushPersonalInfo() { - return pushNamed('/profile/onboarding/personal-info'); + return pushNamed('./personal-info'); } /// Navigates to the Emergency Contact page. diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart index 8b3ec989..01971c2a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; @@ -9,99 +10,91 @@ import '../blocs/personal_info_event.dart'; import '../blocs/personal_info_state.dart'; import '../widgets/personal_info_content.dart'; + /// The Personal Info page for staff onboarding. /// /// This page allows staff members to view and edit their personal information -/// including phone number, bio, languages, and preferred locations. -/// Full name and email are read-only as they come from authentication. +/// including phone number and address. Full name and email are read-only as they come from authentication. /// /// This page is a StatelessWidget that uses BLoC for state management, -/// following Clean Architecture principles. +/// following Clean Architecture and the design system guidelines. class PersonalInfoPage extends StatelessWidget { /// Creates a [PersonalInfoPage]. const PersonalInfoPage({super.key}); @override Widget build(BuildContext context) { + final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; return BlocProvider( - create: (context) => Modular.get() + create: (BuildContext context) => Modular.get() ..add(const PersonalInfoLoadRequested()), - child: const _PersonalInfoPageContent(), - ); - } -} - -/// Internal content widget that reacts to BLoC state changes. -class _PersonalInfoPageContent extends StatelessWidget { - const _PersonalInfoPageContent(); - - @override - Widget build(BuildContext context) { - final i18n = t.staff.onboarding.personal_info; - - return BlocListener( - listener: (context, state) { - if (state.status == PersonalInfoStatus.saved) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(i18n.save_success), - duration: const Duration(seconds: 2), + child: BlocListener( + listener: (BuildContext context, PersonalInfoState state) { + if (state.status == PersonalInfoStatus.saved) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(i18n.save_success), + duration: const Duration(seconds: 2), + ), + ); + Modular.to.pop(); + } else if (state.status == PersonalInfoStatus.error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'An error occurred'), + backgroundColor: UiColors.destructive, + duration: const Duration(seconds: 3), + ), + ); + } + }, + child: Scaffold( + backgroundColor: UiColors.background, + appBar: AppBar( + backgroundColor: UiColors.bgPopup, + elevation: 0, + leading: IconButton( + icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), + onPressed: () => Modular.to.pop(), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, ), - ); - Modular.to.pop(); - } else if (state.status == PersonalInfoStatus.error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'An error occurred'), - backgroundColor: UiColors.destructive, - duration: const Duration(seconds: 3), + title: Text( + i18n.title, + style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), ), - ); - } - }, - child: Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - backgroundColor: UiColors.bgPopup, - elevation: 0, - leading: IconButton( - icon: Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), - onPressed: () => Modular.to.pop(), - ), - title: Text( - i18n.title, - style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container( - color: UiColors.border, - height: 1.0, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: UiColors.border, + height: 1.0, + ), ), ), - ), - body: BlocBuilder( - builder: (context, state) { - if (state.status == PersonalInfoStatus.loading || - state.status == PersonalInfoStatus.initial) { - return const Center( - child: CircularProgressIndicator(), - ); - } + body: SafeArea( + child: BlocBuilder( + builder: (BuildContext context, PersonalInfoState state) { + if (state.status == PersonalInfoStatus.loading || + state.status == PersonalInfoStatus.initial) { + return const Center( + child: CircularProgressIndicator(), + ); + } - if (state.staff == null) { - return Center( - child: Text( - 'Failed to load personal information', - style: UiTypography.body1r.copyWith( - color: UiColors.textSecondary, - ), - ), - ); - } + if (state.staff == null) { + return Center( + child: Text( + 'Failed to load personal information', + style: UiTypography.body1r.copyWith( + color: UiColors.textSecondary, + ), + ), + ); + } - return PersonalInfoContent(staff: state.staff!); - }, + return PersonalInfoContent(staff: state.staff!); + }, + ), + ), ), ), ); diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart index 14734347..8f9fe8c8 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart @@ -11,10 +11,11 @@ import 'profile_photo_widget.dart'; import 'personal_info_form.dart'; import 'save_button.dart'; + /// Content widget that displays and manages the staff profile form. /// /// This widget is extracted from the page to handle form state separately, -/// following Clean Architecture's separation of concerns principle. +/// following Clean Architecture's separation of concerns principle and the design system guidelines. /// Works with the shared [Staff] entity from the domain layer. class PersonalInfoContent extends StatefulWidget { /// The staff profile to display and edit. @@ -52,22 +53,23 @@ class _PersonalInfoContentState extends State { super.dispose(); } + void _onPhoneChanged() { context.read().add( - PersonalInfoFieldUpdated( - field: 'phone', - value: _phoneController.text, - ), - ); + PersonalInfoFieldUpdated( + field: 'phone', + value: _phoneController.text, + ), + ); } void _onAddressChanged() { context.read().add( - PersonalInfoFieldUpdated( - field: 'address', - value: _addressController.text, - ), - ); + PersonalInfoFieldUpdated( + field: 'address', + value: _addressController.text, + ), + ); } void _handleSave() { @@ -83,23 +85,24 @@ class _PersonalInfoContentState extends State { @override Widget build(BuildContext context) { + final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; return BlocBuilder( - builder: (context, state) { - final isSaving = state.status == PersonalInfoStatus.saving; - + builder: (BuildContext context, PersonalInfoState state) { + final bool isSaving = state.status == PersonalInfoStatus.saving; return Column( - children: [ + children: [ Expanded( child: SingleChildScrollView( - padding: EdgeInsets.all(UiConstants.space5), + padding: const EdgeInsets.all(UiConstants.space6), child: Column( - children: [ + crossAxisAlignment: CrossAxisAlignment.center, + children: [ ProfilePhotoWidget( photoUrl: widget.staff.avatar, fullName: widget.staff.name, onTap: isSaving ? null : _handlePhotoTap, ), - SizedBox(height: UiConstants.space6), + const SizedBox(height: UiConstants.space6), PersonalInfoForm( fullName: widget.staff.name, email: widget.staff.email, @@ -107,16 +110,14 @@ class _PersonalInfoContentState extends State { addressController: _addressController, enabled: !isSaving, ), - SizedBox( - height: UiConstants.space16, - ), // Space for bottom button + const SizedBox(height: UiConstants.space16), // Space for bottom button ], ), ), ), SaveButton( onPressed: isSaving ? null : _handleSave, - label: t.staff.onboarding.personal_info.save_button, + label: i18n.save_button, isLoading: isSaving, ), ], diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart index b175a645..7b919362 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; + /// A form widget containing all personal information fields. /// /// Includes read-only fields for full name and email, /// and editable fields for phone and address. +/// Uses only design system tokens for colors, typography, and spacing. class PersonalInfoForm extends StatelessWidget { /// The staff member's full name (read-only). final String fullName; @@ -34,28 +36,32 @@ class PersonalInfoForm extends StatelessWidget { @override Widget build(BuildContext context) { - final i18n = t.staff.onboarding.personal_info; + final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ _FieldLabel(text: i18n.full_name_label), + const SizedBox(height: UiConstants.space2), _ReadOnlyField(value: fullName), - SizedBox(height: UiConstants.space4), - + const SizedBox(height: UiConstants.space4), + _FieldLabel(text: i18n.email_label), + const SizedBox(height: UiConstants.space2), _ReadOnlyField(value: email), - SizedBox(height: UiConstants.space4), - + const SizedBox(height: UiConstants.space4), + _FieldLabel(text: i18n.phone_label), + const SizedBox(height: UiConstants.space2), _EditableField( controller: phoneController, hint: i18n.phone_hint, enabled: enabled, ), - SizedBox(height: UiConstants.space4), - + const SizedBox(height: UiConstants.space4), + _FieldLabel(text: i18n.locations_label), + const SizedBox(height: UiConstants.space2), _EditableField( controller: addressController, hint: i18n.locations_hint, @@ -66,6 +72,7 @@ class PersonalInfoForm extends StatelessWidget { } } +/// A label widget for form fields. /// A label widget for form fields. class _FieldLabel extends StatelessWidget { final String text; @@ -74,16 +81,14 @@ class _FieldLabel extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(bottom: UiConstants.space2), - child: Text( - text, - style: UiTypography.body2m.copyWith(color: UiColors.textPrimary), - ), + return Text( + text, + style: UiTypography.body2m.copyWith(color: UiColors.textPrimary), ); } } +/// A read-only field widget for displaying non-editable information. /// A read-only field widget for displaying non-editable information. class _ReadOnlyField extends StatelessWidget { final String value; @@ -94,7 +99,7 @@ class _ReadOnlyField extends StatelessWidget { Widget build(BuildContext context) { return Container( width: double.infinity, - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( horizontal: UiConstants.space3, vertical: UiConstants.space3, ), @@ -111,17 +116,16 @@ class _ReadOnlyField extends StatelessWidget { } } +/// An editable text field widget. /// An editable text field widget. class _EditableField extends StatelessWidget { final TextEditingController controller; final String hint; - final int maxLines; final bool enabled; const _EditableField({ required this.controller, required this.hint, - this.maxLines = 1, this.enabled = true, }); @@ -129,27 +133,26 @@ class _EditableField extends StatelessWidget { Widget build(BuildContext context) { return TextField( controller: controller, - maxLines: maxLines, enabled: enabled, style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), decoration: InputDecoration( hintText: hint, hintStyle: UiTypography.body2r.copyWith(color: UiColors.textSecondary), - contentPadding: EdgeInsets.symmetric( + contentPadding: const EdgeInsets.symmetric( horizontal: UiConstants.space3, vertical: UiConstants.space3, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - borderSide: BorderSide(color: UiColors.border), + borderSide: const BorderSide(color: UiColors.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - borderSide: BorderSide(color: UiColors.border), + borderSide: const BorderSide(color: UiColors.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - borderSide: BorderSide(color: UiColors.primary), + borderSide: const BorderSide(color: UiColors.primary), ), fillColor: UiColors.bgPopup, filled: true, diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart index f625f9c7..528e7e4d 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; + /// A widget displaying the staff member's profile photo with an edit option. /// /// Shows either the photo URL or an initial avatar if no photo is available. /// Includes a camera icon button for changing the photo. +/// Uses only design system tokens for colors, typography, and spacing. class ProfilePhotoWidget extends StatelessWidget { /// The URL of the staff member's photo. final String? photoUrl; @@ -26,14 +28,14 @@ class ProfilePhotoWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final i18n = t.staff.onboarding.personal_info; + final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; return Column( - children: [ + children: [ GestureDetector( onTap: onTap, child: Stack( - children: [ + children: [ Container( width: 96, height: 96, @@ -67,7 +69,7 @@ class ProfilePhotoWidget extends StatelessWidget { color: UiColors.bgPopup, shape: BoxShape.circle, border: Border.all(color: UiColors.border), - boxShadow: [ + boxShadow: [ BoxShadow( color: UiColors.textPrimary.withOpacity(0.1), blurRadius: UiConstants.space1, @@ -75,7 +77,7 @@ class ProfilePhotoWidget extends StatelessWidget { ), ], ), - child: Center( + child: const Center( child: Icon( UiIcons.camera, size: 16, @@ -87,7 +89,7 @@ class ProfilePhotoWidget extends StatelessWidget { ], ), ), - SizedBox(height: UiConstants.space3), + const SizedBox(height: UiConstants.space3), Text( i18n.change_photo_hint, style: UiTypography.body2r.copyWith(color: UiColors.textSecondary), diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart index e6e8a074..44b4d5c6 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; + /// A save button widget for the bottom of the personal info page. /// /// Displays a full-width button with a save icon and customizable label. +/// Uses only design system tokens for colors, typography, and spacing. class SaveButton extends StatelessWidget { /// Callback when the button is pressed. final VoidCallback? onPressed; @@ -25,8 +27,8 @@ class SaveButton extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: const BoxDecoration( color: UiColors.bgPopup, border: Border( top: BorderSide(color: UiColors.border), @@ -46,7 +48,7 @@ class SaveButton extends StatelessWidget { elevation: 0, ), child: isLoading - ? SizedBox( + ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( @@ -58,9 +60,9 @@ class SaveButton extends StatelessWidget { ) : Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(UiIcons.check, color: UiColors.bgPopup, size: 20), - SizedBox(width: UiConstants.space2), + children: [ + const Icon(UiIcons.check, color: UiColors.bgPopup, size: 20), + const SizedBox(width: UiConstants.space2), Text( label, style: UiTypography.body1m.copyWith( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart index 5b41a59d..59c31ba7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart @@ -52,6 +52,11 @@ class StaffProfileInfoModule extends Module { '/personal-info', child: (BuildContext context) => const PersonalInfoPage(), ); + // Alias with trailing slash to be tolerant of external deep links + r.child( + '/personal-info/', + child: (BuildContext context) => const PersonalInfoPage(), + ); // Additional routes will be added as more onboarding pages are implemented } }