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

@@ -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);
}
// ==========================================================================

View File

@@ -40,13 +40,13 @@ class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
);
},
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));
}

View File

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

View File

@@ -39,7 +39,6 @@ class EmergencyContactScreen extends StatelessWidget {
body: BlocProvider(
create: (context) => Modular.get<EmergencyContactBloc>(),
child: BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
listener: (context, state) {
if (state.status == EmergencyContactStatus.failure) {
UiSnackbar.show(

View File

@@ -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<EmergencyContactBloc>().add(EmergencyContactsSaved());
BlocProvider.of<EmergencyContactBloc>(
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,
),
),
),
);

View File

@@ -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,7 +75,7 @@ class ExperiencePage extends StatelessWidget {
return Scaffold(
appBar: UiAppBar(
title: i18n.title,
onLeadingPressed: () => Modular.to.pop(),
onLeadingPressed: () => Modular.to.toProfile(),
),
body: BlocProvider<ExperienceBloc>(
create: (context) => Modular.get<ExperienceBloc>(),
@@ -68,7 +92,6 @@ class ExperiencePage extends StatelessWidget {
right: UiConstants.space4,
),
);
Modular.to.pop();
} else if (state.status == ExperienceStatus.failure) {
UiSnackbar.show(
context,
@@ -106,13 +129,13 @@ class ExperiencePage extends StatelessWidget {
.map(
(i) => UiChip(
label: _getIndustryLabel(i18n.industries, i),
isSelected:
state.selectedIndustries.contains(i),
onTap: () =>
BlocProvider.of<ExperienceBloc>(context)
.add(ExperienceIndustryToggled(i)),
variant:
state.selectedIndustries.contains(i)
isSelected: state.selectedIndustries.contains(
i,
),
onTap: () => BlocProvider.of<ExperienceBloc>(
context,
).add(ExperienceIndustryToggled(i)),
variant: state.selectedIndustries.contains(i)
? UiChipVariant.primary
: UiChipVariant.secondary,
),
@@ -133,11 +156,12 @@ class ExperiencePage extends StatelessWidget {
.map(
(s) => UiChip(
label: _getSkillLabel(i18n.skills, s),
isSelected:
state.selectedSkills.contains(s.value),
onTap: () =>
BlocProvider.of<ExperienceBloc>(context)
.add(ExperienceSkillToggled(s.value)),
isSelected: state.selectedSkills.contains(
s.value,
),
onTap: () => BlocProvider.of<ExperienceBloc>(
context,
).add(ExperienceSkillToggled(s.value)),
variant:
state.selectedSkills.contains(s.value)
? UiChipVariant.primary
@@ -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<ExperienceBloc>(context)
.add(ExperienceSubmitted()),
: () => BlocProvider.of<ExperienceBloc>(
context,
).add(ExperienceSubmitted()),
fullWidth: true,
text: state.status == ExperienceStatus.loading
? null

View File

@@ -12,13 +12,18 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
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<StaffMainState> implements Disposable {
}
/// Loads the profile completion status.
Future<void> _loadProfileCompletion() async {
Future<void> refreshProfileCompletion() async {
if (_isLoadingCompletion || isClosed) return;
_isLoadingCompletion = true;
try {
final isComplete = await _getProfileCompletionUsecase();
if (!isClosed) {
@@ -53,6 +61,8 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
if (!isClosed) {
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')),
);
}
}