From a9ead783e469a736b795bb32ca4e591355115abc Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 22 Feb 2026 03:01:44 -0500 Subject: [PATCH] feat: Add post-save navigation to staff profile for emergency contact and experience, remove a placeholder page, and refine bloc usage and UI rendering. --- .../core/lib/src/routing/staff/navigator.dart | 29 ++- .../src/presentation/blocs/home_cubit.dart | 4 +- .../presentation/pages/worker_home_page.dart | 3 - .../pages/emergency_contact_screen.dart | 13 +- .../emergency_contact_save_button.dart | 25 ++- .../presentation/pages/experience_page.dart | 168 ++++++++++-------- .../presentation/blocs/staff_main_cubit.dart | 18 +- .../presentation/pages/placeholder_page.dart | 15 -- 8 files changed, 149 insertions(+), 126 deletions(-) delete mode 100644 apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart diff --git a/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart b/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart index 5da83e16..aa6288fe 100644 --- a/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart +++ b/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart @@ -29,7 +29,7 @@ extension StaffNavigator on IModularNavigator { // ========================================================================== /// Navigates to the root get started/authentication screen. - /// + /// /// This effectively logs out the user by navigating to root. /// Used when signing out or session expires. void toInitialPage() { @@ -37,7 +37,7 @@ extension StaffNavigator on IModularNavigator { } /// Navigates to the get started page. - /// + /// /// This is the landing page for unauthenticated users, offering login/signup options. void toGetStartedPage() { navigate(StaffPaths.getStarted); @@ -64,7 +64,7 @@ extension StaffNavigator on IModularNavigator { /// This is typically called after successful phone verification for new /// staff members. Uses pushReplacement to prevent going back to verification. void toProfileSetup() { - pushReplacementNamed(StaffPaths.profileSetup); + pushNamed(StaffPaths.profileSetup); } // ========================================================================== @@ -76,7 +76,7 @@ extension StaffNavigator on IModularNavigator { /// This is the main landing page for authenticated staff members. /// Displays shift cards, quick actions, and notifications. void toStaffHome() { - pushNamed(StaffPaths.home); + pushNamedAndRemoveUntil(StaffPaths.home, (_) => false); } /// Navigates to the staff main shell. @@ -84,7 +84,7 @@ extension StaffNavigator on IModularNavigator { /// This is the container with bottom navigation. Navigates to home tab /// by default. Usually you'd navigate to a specific tab instead. void toStaffMain() { - navigate('${StaffPaths.main}/home/'); + pushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false); } // ========================================================================== @@ -113,8 +113,9 @@ extension StaffNavigator on IModularNavigator { if (refreshAvailable == true) { args['refreshAvailable'] = true; } - navigate( + pushNamedAndRemoveUntil( StaffPaths.shifts, + (_) => false, arguments: args.isEmpty ? null : args, ); } @@ -123,21 +124,21 @@ extension StaffNavigator on IModularNavigator { /// /// View payment history, earnings breakdown, and tax information. void toPayments() { - navigate(StaffPaths.payments); + pushNamedAndRemoveUntil(StaffPaths.payments, (_) => false); } /// Navigates to the Clock In tab. /// /// Access time tracking interface for active shifts. void toClockIn() { - navigate(StaffPaths.clockIn); + pushNamedAndRemoveUntil(StaffPaths.clockIn, (_) => false); } /// Navigates to the Profile tab. /// /// Manage personal information, documents, and preferences. void toProfile() { - navigate(StaffPaths.profile); + pushNamedAndRemoveUntil(StaffPaths.profile, (_) => false); } // ========================================================================== @@ -155,10 +156,7 @@ extension StaffNavigator on IModularNavigator { /// The shift object is passed as an argument and can be retrieved /// in the details page. void toShiftDetails(Shift shift) { - navigate( - StaffPaths.shiftDetails(shift.id), - arguments: shift, - ); + navigate(StaffPaths.shiftDetails(shift.id), arguments: shift); } /// Pushes the shift details page (alternative method). @@ -167,10 +165,7 @@ extension StaffNavigator on IModularNavigator { /// Use this when you want to add the details page to the stack rather /// than replacing the current route. void pushShiftDetails(Shift shift) { - pushNamed( - StaffPaths.shiftDetails(shift.id), - arguments: shift, - ); + pushNamed(StaffPaths.shiftDetails(shift.id), arguments: shift); } // ========================================================================== diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart index a0e158ee..c6b06a7b 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart @@ -40,13 +40,13 @@ class HomeCubit extends Cubit with BlocErrorHandler { ); }, onError: (String errorKey) { - if (isClosed) return state; // Avoid state emission if closed, though emit handles it gracefully usually + if (isClosed) + return state; // Avoid state emission if closed, though emit handles it gracefully usually return state.copyWith(status: HomeStatus.error, errorMessage: errorKey); }, ); } - void toggleAutoMatch(bool enabled) { emit(state.copyWith(autoMatchEnabled: enabled)); } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index 906d45f1..d383c75c 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -63,9 +63,6 @@ class WorkerHomePage extends StatelessWidget { child: Column( children: [ BlocBuilder( - buildWhen: (previous, current) => - previous.isProfileComplete != - current.isProfileComplete, builder: (context, state) { if (state.isProfileComplete) return const SizedBox(); return PlaceholderBanner( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart index c8aab7be..7a00374c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart @@ -39,7 +39,6 @@ class EmergencyContactScreen extends StatelessWidget { body: BlocProvider( create: (context) => Modular.get(), child: BlocConsumer( - listener: (context, state) { if (state.status == EmergencyContactStatus.failure) { UiSnackbar.show( @@ -66,12 +65,12 @@ class EmergencyContactScreen extends StatelessWidget { const EmergencyContactInfoBanner(), const SizedBox(height: UiConstants.space6), ...state.contacts.asMap().entries.map( - (entry) => EmergencyContactFormItem( - index: entry.key, - contact: entry.value, - totalContacts: state.contacts.length, - ), - ), + (entry) => EmergencyContactFormItem( + index: entry.key, + contact: entry.value, + totalContacts: state.contacts.length, + ), + ), const EmergencyContactAddButton(), const SizedBox(height: UiConstants.space16), ], diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart index c332ac74..2097d866 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart @@ -2,13 +2,17 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; import '../blocs/emergency_contact_bloc.dart'; class EmergencyContactSaveButton extends StatelessWidget { const EmergencyContactSaveButton({super.key}); void _onSave(BuildContext context) { - context.read().add(EmergencyContactsSaved()); + BlocProvider.of( + context, + ).add(EmergencyContactsSaved()); } @override @@ -19,10 +23,13 @@ class EmergencyContactSaveButton extends StatelessWidget { if (state.status == EmergencyContactStatus.saved) { UiSnackbar.show( context, - message: t.staff.profile.menu_items.emergency_contact_page.save_success, + message: + t.staff.profile.menu_items.emergency_contact_page.save_success, type: UiSnackbarType.success, margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16), ); + + Modular.to.toProfile(); } }, builder: (context, state) { @@ -36,8 +43,9 @@ class EmergencyContactSaveButton extends StatelessWidget { child: SafeArea( child: UiButton.primary( fullWidth: true, - onPressed: - state.isValid && !isLoading ? () => _onSave(context) : null, + onPressed: state.isValid && !isLoading + ? () => _onSave(context) + : null, child: isLoading ? const SizedBox( height: 20.0, @@ -49,7 +57,14 @@ class EmergencyContactSaveButton extends StatelessWidget { ), ), ) - : Text(t.staff.profile.menu_items.emergency_contact_page.save_continue), + : Text( + t + .staff + .profile + .menu_items + .emergency_contact_page + .save_continue, + ), ), ), ); diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart index d7a77c28..7b42e3d0 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart @@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import '../blocs/experience_bloc.dart'; @@ -13,34 +14,57 @@ class ExperiencePage extends StatelessWidget { String _getIndustryLabel(dynamic node, Industry industry) { switch (industry) { - case Industry.hospitality: return node.hospitality; - case Industry.foodService: return node.food_service; - case Industry.warehouse: return node.warehouse; - case Industry.events: return node.events; - case Industry.retail: return node.retail; - case Industry.healthcare: return node.healthcare; - case Industry.other: return node.other; + case Industry.hospitality: + return node.hospitality; + case Industry.foodService: + return node.food_service; + case Industry.warehouse: + return node.warehouse; + case Industry.events: + return node.events; + case Industry.retail: + return node.retail; + case Industry.healthcare: + return node.healthcare; + case Industry.other: + return node.other; } } String _getSkillLabel(dynamic node, ExperienceSkill skill) { switch (skill) { - case ExperienceSkill.foodService: return node.food_service; - case ExperienceSkill.bartending: return node.bartending; - case ExperienceSkill.eventSetup: return node.event_setup; - case ExperienceSkill.hospitality: return node.hospitality; - case ExperienceSkill.warehouse: return node.warehouse; - case ExperienceSkill.customerService: return node.customer_service; - case ExperienceSkill.cleaning: return node.cleaning; - case ExperienceSkill.security: return node.security; - case ExperienceSkill.retail: return node.retail; - case ExperienceSkill.driving: return node.driving; - case ExperienceSkill.cooking: return node.cooking; - case ExperienceSkill.cashier: return node.cashier; - case ExperienceSkill.server: return node.server; - case ExperienceSkill.barista: return node.barista; - case ExperienceSkill.hostHostess: return node.host_hostess; - case ExperienceSkill.busser: return node.busser; + case ExperienceSkill.foodService: + return node.food_service; + case ExperienceSkill.bartending: + return node.bartending; + case ExperienceSkill.eventSetup: + return node.event_setup; + case ExperienceSkill.hospitality: + return node.hospitality; + case ExperienceSkill.warehouse: + return node.warehouse; + case ExperienceSkill.customerService: + return node.customer_service; + case ExperienceSkill.cleaning: + return node.cleaning; + case ExperienceSkill.security: + return node.security; + case ExperienceSkill.retail: + return node.retail; + case ExperienceSkill.driving: + return node.driving; + case ExperienceSkill.cooking: + return node.cooking; + case ExperienceSkill.cashier: + return node.cashier; + case ExperienceSkill.server: + return node.server; + case ExperienceSkill.barista: + return node.barista; + case ExperienceSkill.hostHostess: + return node.host_hostess; + case ExperienceSkill.busser: + return node.busser; } } @@ -51,39 +75,38 @@ class ExperiencePage extends StatelessWidget { return Scaffold( appBar: UiAppBar( title: i18n.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.toProfile(), ), body: BlocProvider( create: (context) => Modular.get(), child: BlocConsumer( listener: (context, state) { - if (state.status == ExperienceStatus.success) { - UiSnackbar.show( - context, - message: 'Experience saved successfully', - type: UiSnackbarType.success, - margin: const EdgeInsets.only( - bottom: 120, - left: UiConstants.space4, - right: UiConstants.space4, - ), - ); - Modular.to.pop(); - } else if (state.status == ExperienceStatus.failure) { - UiSnackbar.show( - context, - message: state.errorMessage != null - ? translateErrorKey(state.errorMessage!) - : 'An error occurred', - type: UiSnackbarType.error, - margin: const EdgeInsets.only( - bottom: 120, - left: UiConstants.space4, - right: UiConstants.space4, - ), - ); - } - }, + if (state.status == ExperienceStatus.success) { + UiSnackbar.show( + context, + message: 'Experience saved successfully', + type: UiSnackbarType.success, + margin: const EdgeInsets.only( + bottom: 120, + left: UiConstants.space4, + right: UiConstants.space4, + ), + ); + } else if (state.status == ExperienceStatus.failure) { + UiSnackbar.show( + context, + message: state.errorMessage != null + ? translateErrorKey(state.errorMessage!) + : 'An error occurred', + type: UiSnackbarType.error, + margin: const EdgeInsets.only( + bottom: 120, + left: UiConstants.space4, + right: UiConstants.space4, + ), + ); + } + }, builder: (context, state) { return Column( children: [ @@ -106,15 +129,15 @@ class ExperiencePage extends StatelessWidget { .map( (i) => UiChip( label: _getIndustryLabel(i18n.industries, i), - isSelected: - state.selectedIndustries.contains(i), - onTap: () => - BlocProvider.of(context) - .add(ExperienceIndustryToggled(i)), - variant: - state.selectedIndustries.contains(i) - ? UiChipVariant.primary - : UiChipVariant.secondary, + isSelected: state.selectedIndustries.contains( + i, + ), + onTap: () => BlocProvider.of( + context, + ).add(ExperienceIndustryToggled(i)), + variant: state.selectedIndustries.contains(i) + ? UiChipVariant.primary + : UiChipVariant.secondary, ), ) .toList(), @@ -133,15 +156,16 @@ class ExperiencePage extends StatelessWidget { .map( (s) => UiChip( label: _getSkillLabel(i18n.skills, s), - isSelected: - state.selectedSkills.contains(s.value), - onTap: () => - BlocProvider.of(context) - .add(ExperienceSkillToggled(s.value)), + isSelected: state.selectedSkills.contains( + s.value, + ), + onTap: () => BlocProvider.of( + context, + ).add(ExperienceSkillToggled(s.value)), variant: state.selectedSkills.contains(s.value) - ? UiChipVariant.primary - : UiChipVariant.secondary, + ? UiChipVariant.primary + : UiChipVariant.secondary, ), ) .toList(), @@ -177,10 +201,7 @@ class ExperiencePage extends StatelessWidget { spacing: UiConstants.space2, runSpacing: UiConstants.space2, children: customSkills.map((skill) { - return UiChip( - label: skill, - variant: UiChipVariant.accent, - ); + return UiChip(label: skill, variant: UiChipVariant.accent); }).toList(), ), ], @@ -202,8 +223,9 @@ class ExperiencePage extends StatelessWidget { child: UiButton.primary( onPressed: state.status == ExperienceStatus.loading ? null - : () => BlocProvider.of(context) - .add(ExperienceSubmitted()), + : () => BlocProvider.of( + context, + ).add(ExperienceSubmitted()), fullWidth: true, text: state.status == ExperienceStatus.loading ? null diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart index b868c7ca..814b5932 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart @@ -8,17 +8,22 @@ import 'package:staff_main/src/presentation/blocs/staff_main_state.dart'; class StaffMainCubit extends Cubit implements Disposable { StaffMainCubit({ required GetProfileCompletionUseCase getProfileCompletionUsecase, - }) : _getProfileCompletionUsecase = getProfileCompletionUsecase, - super(const StaffMainState()) { + }) : _getProfileCompletionUsecase = getProfileCompletionUsecase, + super(const StaffMainState()) { Modular.to.addListener(_onRouteChanged); _onRouteChanged(); - _loadProfileCompletion(); } final GetProfileCompletionUseCase _getProfileCompletionUsecase; + bool _isLoadingCompletion = false; void _onRouteChanged() { if (isClosed) return; + + // Refresh completion status whenever route changes to catch profile updates + // only if it's not already complete. + refreshProfileCompletion(); + final String path = Modular.to.path; int newIndex = state.currentIndex; @@ -41,7 +46,10 @@ class StaffMainCubit extends Cubit implements Disposable { } /// Loads the profile completion status. - Future _loadProfileCompletion() async { + Future refreshProfileCompletion() async { + if (_isLoadingCompletion || isClosed) return; + + _isLoadingCompletion = true; try { final isComplete = await _getProfileCompletionUsecase(); if (!isClosed) { @@ -53,6 +61,8 @@ class StaffMainCubit extends Cubit implements Disposable { if (!isClosed) { emit(state.copyWith(isProfileComplete: true)); } + } finally { + _isLoadingCompletion = false; } } diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart deleted file mode 100644 index b9d993d6..00000000 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class PlaceholderPage extends StatelessWidget { - const PlaceholderPage({required this.title, super.key}); - - final String title; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(title)), - body: Center(child: Text('$title Page')), - ); - } -}