feat: Add post-save navigation to staff profile for emergency contact and experience, remove a placeholder page, and refine bloc usage and UI rendering.

This commit is contained in:
Achintha Isuru
2026-02-22 03:01:44 -05:00
parent 214e0d1237
commit a9ead783e4
8 changed files with 149 additions and 126 deletions

View File

@@ -29,7 +29,7 @@ extension StaffNavigator on IModularNavigator {
// ========================================================================== // ==========================================================================
/// Navigates to the root get started/authentication screen. /// Navigates to the root get started/authentication screen.
/// ///
/// This effectively logs out the user by navigating to root. /// This effectively logs out the user by navigating to root.
/// Used when signing out or session expires. /// Used when signing out or session expires.
void toInitialPage() { void toInitialPage() {
@@ -37,7 +37,7 @@ extension StaffNavigator on IModularNavigator {
} }
/// Navigates to the get started page. /// Navigates to the get started page.
/// ///
/// This is the landing page for unauthenticated users, offering login/signup options. /// This is the landing page for unauthenticated users, offering login/signup options.
void toGetStartedPage() { void toGetStartedPage() {
navigate(StaffPaths.getStarted); navigate(StaffPaths.getStarted);
@@ -64,7 +64,7 @@ extension StaffNavigator on IModularNavigator {
/// This is typically called after successful phone verification for new /// This is typically called after successful phone verification for new
/// staff members. Uses pushReplacement to prevent going back to verification. /// staff members. Uses pushReplacement to prevent going back to verification.
void toProfileSetup() { 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. /// This is the main landing page for authenticated staff members.
/// Displays shift cards, quick actions, and notifications. /// Displays shift cards, quick actions, and notifications.
void toStaffHome() { void toStaffHome() {
pushNamed(StaffPaths.home); pushNamedAndRemoveUntil(StaffPaths.home, (_) => false);
} }
/// Navigates to the staff main shell. /// 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 /// This is the container with bottom navigation. Navigates to home tab
/// by default. Usually you'd navigate to a specific tab instead. /// by default. Usually you'd navigate to a specific tab instead.
void toStaffMain() { void toStaffMain() {
navigate('${StaffPaths.main}/home/'); pushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false);
} }
// ========================================================================== // ==========================================================================
@@ -113,8 +113,9 @@ extension StaffNavigator on IModularNavigator {
if (refreshAvailable == true) { if (refreshAvailable == true) {
args['refreshAvailable'] = true; args['refreshAvailable'] = true;
} }
navigate( pushNamedAndRemoveUntil(
StaffPaths.shifts, StaffPaths.shifts,
(_) => false,
arguments: args.isEmpty ? null : args, arguments: args.isEmpty ? null : args,
); );
} }
@@ -123,21 +124,21 @@ extension StaffNavigator on IModularNavigator {
/// ///
/// View payment history, earnings breakdown, and tax information. /// View payment history, earnings breakdown, and tax information.
void toPayments() { void toPayments() {
navigate(StaffPaths.payments); pushNamedAndRemoveUntil(StaffPaths.payments, (_) => false);
} }
/// Navigates to the Clock In tab. /// Navigates to the Clock In tab.
/// ///
/// Access time tracking interface for active shifts. /// Access time tracking interface for active shifts.
void toClockIn() { void toClockIn() {
navigate(StaffPaths.clockIn); pushNamedAndRemoveUntil(StaffPaths.clockIn, (_) => false);
} }
/// Navigates to the Profile tab. /// Navigates to the Profile tab.
/// ///
/// Manage personal information, documents, and preferences. /// Manage personal information, documents, and preferences.
void toProfile() { 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 /// The shift object is passed as an argument and can be retrieved
/// in the details page. /// in the details page.
void toShiftDetails(Shift shift) { void toShiftDetails(Shift shift) {
navigate( navigate(StaffPaths.shiftDetails(shift.id), arguments: shift);
StaffPaths.shiftDetails(shift.id),
arguments: shift,
);
} }
/// Pushes the shift details page (alternative method). /// 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 /// Use this when you want to add the details page to the stack rather
/// than replacing the current route. /// than replacing the current route.
void pushShiftDetails(Shift shift) { void pushShiftDetails(Shift shift) {
pushNamed( pushNamed(StaffPaths.shiftDetails(shift.id), arguments: shift);
StaffPaths.shiftDetails(shift.id),
arguments: shift,
);
} }
// ========================================================================== // ==========================================================================

View File

@@ -40,13 +40,13 @@ class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
); );
}, },
onError: (String errorKey) { 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); return state.copyWith(status: HomeStatus.error, errorMessage: errorKey);
}, },
); );
} }
void toggleAutoMatch(bool enabled) { void toggleAutoMatch(bool enabled) {
emit(state.copyWith(autoMatchEnabled: enabled)); emit(state.copyWith(autoMatchEnabled: enabled));
} }

View File

@@ -63,9 +63,6 @@ class WorkerHomePage extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
BlocBuilder<HomeCubit, HomeState>( BlocBuilder<HomeCubit, HomeState>(
buildWhen: (previous, current) =>
previous.isProfileComplete !=
current.isProfileComplete,
builder: (context, state) { builder: (context, state) {
if (state.isProfileComplete) return const SizedBox(); if (state.isProfileComplete) return const SizedBox();
return PlaceholderBanner( return PlaceholderBanner(

View File

@@ -39,7 +39,6 @@ class EmergencyContactScreen extends StatelessWidget {
body: BlocProvider( body: BlocProvider(
create: (context) => Modular.get<EmergencyContactBloc>(), create: (context) => Modular.get<EmergencyContactBloc>(),
child: BlocConsumer<EmergencyContactBloc, EmergencyContactState>( child: BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
listener: (context, state) { listener: (context, state) {
if (state.status == EmergencyContactStatus.failure) { if (state.status == EmergencyContactStatus.failure) {
UiSnackbar.show( UiSnackbar.show(
@@ -66,12 +65,12 @@ class EmergencyContactScreen extends StatelessWidget {
const EmergencyContactInfoBanner(), const EmergencyContactInfoBanner(),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
...state.contacts.asMap().entries.map( ...state.contacts.asMap().entries.map(
(entry) => EmergencyContactFormItem( (entry) => EmergencyContactFormItem(
index: entry.key, index: entry.key,
contact: entry.value, contact: entry.value,
totalContacts: state.contacts.length, totalContacts: state.contacts.length,
), ),
), ),
const EmergencyContactAddButton(), const EmergencyContactAddButton(),
const SizedBox(height: UiConstants.space16), const SizedBox(height: UiConstants.space16),
], ],

View File

@@ -2,13 +2,17 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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'; import '../blocs/emergency_contact_bloc.dart';
class EmergencyContactSaveButton extends StatelessWidget { class EmergencyContactSaveButton extends StatelessWidget {
const EmergencyContactSaveButton({super.key}); const EmergencyContactSaveButton({super.key});
void _onSave(BuildContext context) { void _onSave(BuildContext context) {
context.read<EmergencyContactBloc>().add(EmergencyContactsSaved()); BlocProvider.of<EmergencyContactBloc>(
context,
).add(EmergencyContactsSaved());
} }
@override @override
@@ -19,10 +23,13 @@ class EmergencyContactSaveButton extends StatelessWidget {
if (state.status == EmergencyContactStatus.saved) { if (state.status == EmergencyContactStatus.saved) {
UiSnackbar.show( UiSnackbar.show(
context, 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, type: UiSnackbarType.success,
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16), margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
); );
Modular.to.toProfile();
} }
}, },
builder: (context, state) { builder: (context, state) {
@@ -36,8 +43,9 @@ class EmergencyContactSaveButton extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: UiButton.primary( child: UiButton.primary(
fullWidth: true, fullWidth: true,
onPressed: onPressed: state.isValid && !isLoading
state.isValid && !isLoading ? () => _onSave(context) : null, ? () => _onSave(context)
: null,
child: isLoading child: isLoading
? const SizedBox( ? const SizedBox(
height: 20.0, 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,
),
), ),
), ),
); );

View File

@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../blocs/experience_bloc.dart'; import '../blocs/experience_bloc.dart';
@@ -13,34 +14,57 @@ class ExperiencePage extends StatelessWidget {
String _getIndustryLabel(dynamic node, Industry industry) { String _getIndustryLabel(dynamic node, Industry industry) {
switch (industry) { switch (industry) {
case Industry.hospitality: return node.hospitality; case Industry.hospitality:
case Industry.foodService: return node.food_service; return node.hospitality;
case Industry.warehouse: return node.warehouse; case Industry.foodService:
case Industry.events: return node.events; return node.food_service;
case Industry.retail: return node.retail; case Industry.warehouse:
case Industry.healthcare: return node.healthcare; return node.warehouse;
case Industry.other: return node.other; 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) { String _getSkillLabel(dynamic node, ExperienceSkill skill) {
switch (skill) { switch (skill) {
case ExperienceSkill.foodService: return node.food_service; case ExperienceSkill.foodService:
case ExperienceSkill.bartending: return node.bartending; return node.food_service;
case ExperienceSkill.eventSetup: return node.event_setup; case ExperienceSkill.bartending:
case ExperienceSkill.hospitality: return node.hospitality; return node.bartending;
case ExperienceSkill.warehouse: return node.warehouse; case ExperienceSkill.eventSetup:
case ExperienceSkill.customerService: return node.customer_service; return node.event_setup;
case ExperienceSkill.cleaning: return node.cleaning; case ExperienceSkill.hospitality:
case ExperienceSkill.security: return node.security; return node.hospitality;
case ExperienceSkill.retail: return node.retail; case ExperienceSkill.warehouse:
case ExperienceSkill.driving: return node.driving; return node.warehouse;
case ExperienceSkill.cooking: return node.cooking; case ExperienceSkill.customerService:
case ExperienceSkill.cashier: return node.cashier; return node.customer_service;
case ExperienceSkill.server: return node.server; case ExperienceSkill.cleaning:
case ExperienceSkill.barista: return node.barista; return node.cleaning;
case ExperienceSkill.hostHostess: return node.host_hostess; case ExperienceSkill.security:
case ExperienceSkill.busser: return node.busser; 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( return Scaffold(
appBar: UiAppBar( appBar: UiAppBar(
title: i18n.title, title: i18n.title,
onLeadingPressed: () => Modular.to.pop(), onLeadingPressed: () => Modular.to.toProfile(),
), ),
body: BlocProvider<ExperienceBloc>( body: BlocProvider<ExperienceBloc>(
create: (context) => Modular.get<ExperienceBloc>(), create: (context) => Modular.get<ExperienceBloc>(),
child: BlocConsumer<ExperienceBloc, ExperienceState>( child: BlocConsumer<ExperienceBloc, ExperienceState>(
listener: (context, state) { listener: (context, state) {
if (state.status == ExperienceStatus.success) { if (state.status == ExperienceStatus.success) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Experience saved successfully', message: 'Experience saved successfully',
type: UiSnackbarType.success, type: UiSnackbarType.success,
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
bottom: 120, bottom: 120,
left: UiConstants.space4, left: UiConstants.space4,
right: UiConstants.space4, right: UiConstants.space4,
), ),
); );
Modular.to.pop(); } else if (state.status == ExperienceStatus.failure) {
} else if (state.status == ExperienceStatus.failure) { UiSnackbar.show(
UiSnackbar.show( context,
context, message: state.errorMessage != null
message: state.errorMessage != null ? translateErrorKey(state.errorMessage!)
? translateErrorKey(state.errorMessage!) : 'An error occurred',
: 'An error occurred', type: UiSnackbarType.error,
type: UiSnackbarType.error, margin: const EdgeInsets.only(
margin: const EdgeInsets.only( bottom: 120,
bottom: 120, left: UiConstants.space4,
left: UiConstants.space4, right: UiConstants.space4,
right: UiConstants.space4, ),
), );
); }
} },
},
builder: (context, state) { builder: (context, state) {
return Column( return Column(
children: [ children: [
@@ -106,15 +129,15 @@ class ExperiencePage extends StatelessWidget {
.map( .map(
(i) => UiChip( (i) => UiChip(
label: _getIndustryLabel(i18n.industries, i), label: _getIndustryLabel(i18n.industries, i),
isSelected: isSelected: state.selectedIndustries.contains(
state.selectedIndustries.contains(i), i,
onTap: () => ),
BlocProvider.of<ExperienceBloc>(context) onTap: () => BlocProvider.of<ExperienceBloc>(
.add(ExperienceIndustryToggled(i)), context,
variant: ).add(ExperienceIndustryToggled(i)),
state.selectedIndustries.contains(i) variant: state.selectedIndustries.contains(i)
? UiChipVariant.primary ? UiChipVariant.primary
: UiChipVariant.secondary, : UiChipVariant.secondary,
), ),
) )
.toList(), .toList(),
@@ -133,15 +156,16 @@ class ExperiencePage extends StatelessWidget {
.map( .map(
(s) => UiChip( (s) => UiChip(
label: _getSkillLabel(i18n.skills, s), label: _getSkillLabel(i18n.skills, s),
isSelected: isSelected: state.selectedSkills.contains(
state.selectedSkills.contains(s.value), s.value,
onTap: () => ),
BlocProvider.of<ExperienceBloc>(context) onTap: () => BlocProvider.of<ExperienceBloc>(
.add(ExperienceSkillToggled(s.value)), context,
).add(ExperienceSkillToggled(s.value)),
variant: variant:
state.selectedSkills.contains(s.value) state.selectedSkills.contains(s.value)
? UiChipVariant.primary ? UiChipVariant.primary
: UiChipVariant.secondary, : UiChipVariant.secondary,
), ),
) )
.toList(), .toList(),
@@ -177,10 +201,7 @@ class ExperiencePage extends StatelessWidget {
spacing: UiConstants.space2, spacing: UiConstants.space2,
runSpacing: UiConstants.space2, runSpacing: UiConstants.space2,
children: customSkills.map((skill) { children: customSkills.map((skill) {
return UiChip( return UiChip(label: skill, variant: UiChipVariant.accent);
label: skill,
variant: UiChipVariant.accent,
);
}).toList(), }).toList(),
), ),
], ],
@@ -202,8 +223,9 @@ class ExperiencePage extends StatelessWidget {
child: UiButton.primary( child: UiButton.primary(
onPressed: state.status == ExperienceStatus.loading onPressed: state.status == ExperienceStatus.loading
? null ? null
: () => BlocProvider.of<ExperienceBloc>(context) : () => BlocProvider.of<ExperienceBloc>(
.add(ExperienceSubmitted()), context,
).add(ExperienceSubmitted()),
fullWidth: true, fullWidth: true,
text: state.status == ExperienceStatus.loading text: state.status == ExperienceStatus.loading
? null ? null

View File

@@ -8,17 +8,22 @@ import 'package:staff_main/src/presentation/blocs/staff_main_state.dart';
class StaffMainCubit extends Cubit<StaffMainState> implements Disposable { class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
StaffMainCubit({ StaffMainCubit({
required GetProfileCompletionUseCase getProfileCompletionUsecase, required GetProfileCompletionUseCase getProfileCompletionUsecase,
}) : _getProfileCompletionUsecase = getProfileCompletionUsecase, }) : _getProfileCompletionUsecase = getProfileCompletionUsecase,
super(const StaffMainState()) { super(const StaffMainState()) {
Modular.to.addListener(_onRouteChanged); Modular.to.addListener(_onRouteChanged);
_onRouteChanged(); _onRouteChanged();
_loadProfileCompletion();
} }
final GetProfileCompletionUseCase _getProfileCompletionUsecase; final GetProfileCompletionUseCase _getProfileCompletionUsecase;
bool _isLoadingCompletion = false;
void _onRouteChanged() { void _onRouteChanged() {
if (isClosed) return; 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; final String path = Modular.to.path;
int newIndex = state.currentIndex; int newIndex = state.currentIndex;
@@ -41,7 +46,10 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
} }
/// Loads the profile completion status. /// Loads the profile completion status.
Future<void> _loadProfileCompletion() async { Future<void> refreshProfileCompletion() async {
if (_isLoadingCompletion || isClosed) return;
_isLoadingCompletion = true;
try { try {
final isComplete = await _getProfileCompletionUsecase(); final isComplete = await _getProfileCompletionUsecase();
if (!isClosed) { if (!isClosed) {
@@ -53,6 +61,8 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
if (!isClosed) { if (!isClosed) {
emit(state.copyWith(isProfileComplete: true)); emit(state.copyWith(isProfileComplete: true));
} }
} finally {
_isLoadingCompletion = false;
} }
} }

View File

@@ -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')),
);
}
}