feat: Refactor staff profile page and logout button for improved state management and navigation

This commit is contained in:
Achintha Isuru
2026-02-19 13:39:03 -05:00
parent 55344fad90
commit 7b9507b87f
4 changed files with 161 additions and 153 deletions

View File

@@ -38,116 +38,112 @@ class StaffProfilePage extends StatelessWidget {
}
}
void _onSignOut(ProfileCubit cubit, ProfileState state) {
if (state.status != ProfileStatus.loading) {
cubit.signOut();
}
}
@override
Widget build(BuildContext context) {
final ProfileCubit cubit = Modular.get<ProfileCubit>();
// Load profile data on first build
if (cubit.state.status == ProfileStatus.initial) {
cubit.loadProfile();
}
return Scaffold(
body: BlocConsumer<ProfileCubit, ProfileState>(
bloc: cubit,
listener: (BuildContext context, ProfileState state) {
if (state.status == ProfileStatus.signedOut) {
Modular.to.toGetStartedPage();
} else if (state.status == ProfileStatus.error &&
state.errorMessage != null) {
UiSnackbar.show(
context,
message: translateErrorKey(state.errorMessage!),
type: UiSnackbarType.error,
);
}
},
builder: (BuildContext context, ProfileState state) {
// Show loading spinner if status is loading
body: BlocProvider<ProfileCubit>(
create: (_) => Modular.get<ProfileCubit>()..loadProfile(),
child: BlocConsumer<ProfileCubit, ProfileState>(
listener: (BuildContext context, ProfileState state) {
if (state.status == ProfileStatus.signedOut) {
Modular.to.toGetStartedPage();
} else if (state.status == ProfileStatus.error &&
state.errorMessage != null) {
UiSnackbar.show(
context,
message: translateErrorKey(state.errorMessage!),
type: UiSnackbarType.error,
);
}
},
builder: (BuildContext context, ProfileState state) {
// Show loading spinner if status is loading
if (state.status == ProfileStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == ProfileStatus.error) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Text(
state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'An error occurred',
textAlign: TextAlign.center,
style: UiTypography.body1r.copyWith(
color: UiColors.textSecondary,
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Text(
state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'An error occurred',
textAlign: TextAlign.center,
style: UiTypography.body1r.copyWith(
color: UiColors.textSecondary,
),
),
),
);
}
final Staff? profile = state.profile;
if (profile == null) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: UiConstants.space16),
child: Column(
children: <Widget>[
ProfileHeader(
fullName: profile.name,
level: _mapStatusToLevel(profile.status),
photoUrl: profile.avatar,
),
Transform.translate(
offset: const Offset(0, -UiConstants.space6),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: Column(
spacing: UiConstants.space6,
children: <Widget>[
// Reliability Stats and Score
ReliabilityStatsCard(
totalShifts: profile.totalShifts,
averageRating: profile.averageRating,
onTimeRate: profile.onTimeRate,
noShowCount: profile.noShowCount,
cancellationCount: profile.cancellationCount,
),
// Reliability Score Bar
ReliabilityScoreBar(
reliabilityScore: profile.reliabilityScore,
),
// Ordered sections
const OnboardingSection(),
// Compliance section
const ComplianceSection(),
// Finance section
const FinanceSection(),
// Support section
const SupportSection(),
// Settings section
const SettingsSection(),
// Logout button at the bottom
const LogoutButton(),
const SizedBox(height: UiConstants.space6),
],
),
),
),
],
),
);
}
final Staff? profile = state.profile;
if (profile == null) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: UiConstants.space16),
child: Column(
children: <Widget>[
ProfileHeader(
fullName: profile.name,
level: _mapStatusToLevel(profile.status),
photoUrl: profile.avatar,
onSignOutTap: () => _onSignOut(cubit, state),
),
Transform.translate(
offset: const Offset(0, -UiConstants.space6),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: Column(
children: <Widget>[
ReliabilityStatsCard(
totalShifts: profile.totalShifts,
averageRating: profile.averageRating,
onTimeRate: profile.onTimeRate,
noShowCount: profile.noShowCount,
cancellationCount: profile.cancellationCount,
),
const SizedBox(height: UiConstants.space6),
ReliabilityScoreBar(
reliabilityScore: profile.reliabilityScore,
),
const SizedBox(height: UiConstants.space6),
const OnboardingSection(),
const SizedBox(height: UiConstants.space6),
const ComplianceSection(),
const SizedBox(height: UiConstants.space6),
const FinanceSection(),
const SizedBox(height: UiConstants.space6),
const SupportSection(),
const SizedBox(height: UiConstants.space6),
const SettingsSection(),
const SizedBox(height: UiConstants.space6),
LogoutButton(
onTap: () => _onSignOut(cubit, state),
),
const SizedBox(height: UiConstants.space12),
],
),
),
),
],
),
);
},
},
),
),
);
}

View File

@@ -1,47 +1,73 @@
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 '../blocs/profile_cubit.dart';
import '../blocs/profile_state.dart';
/// The sign-out button widget.
///
/// Uses design system tokens for all colors, typography, spacing, and icons.
/// Handles logout logic when tapped and navigates to onboarding on success.
class LogoutButton extends StatelessWidget {
final VoidCallback onTap;
const LogoutButton({super.key});
const LogoutButton({super.key, required this.onTap});
/// Handles the sign-out action.
///
/// Checks if the profile is not currently loading, then triggers the
/// sign-out process via the ProfileCubit.
void _handleSignOut(BuildContext context, ProfileState state) {
if (state.status != ProfileStatus.loading) {
context.read<ProfileCubit>().signOut();
}
}
@override
Widget build(BuildContext context) {
final i18n = t.staff.profile.header;
final TranslationsStaffProfileHeaderEn i18n = t.staff.profile.header;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Material(
color: UiColors.transparent,
child: InkWell(
onTap: onTap,
return BlocListener<ProfileCubit, ProfileState>(
listener: (BuildContext context, ProfileState state) {
if (state.status == ProfileStatus.signedOut) {
// Navigate to get started page after successful sign-out
// This will be handled by the profile page listener
}
},
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
UiIcons.logOut,
color: UiColors.destructive,
size: 20,
),
const SizedBox(width: UiConstants.space2),
Text(
i18n.sign_out,
style: UiTypography.body1m.textError,
),
],
border: Border.all(color: UiColors.border),
),
child: Material(
color: UiColors.transparent,
child: InkWell(
onTap: () {
_handleSignOut(
context,
context.read<ProfileCubit>().state,
);
},
borderRadius: UiConstants.radiusLg,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.logOut,
color: UiColors.destructive,
size: 20,
),
const SizedBox(width: UiConstants.space2),
Text(
i18n.sign_out,
style: UiTypography.body1m.textError,
),
],
),
),
),
),

View File

@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
/// The header section of the staff profile page, containing avatar, name, level,
/// and a sign-out button.
/// The header section of the staff profile page, containing avatar, name, and level.
///
/// Uses design system tokens for all colors, typography, and spacing.
class ProfileHeader extends StatelessWidget {
@@ -15,9 +14,6 @@ class ProfileHeader extends StatelessWidget {
/// Optional photo URL for the avatar
final String? photoUrl;
/// Callback when sign out is tapped
final VoidCallback onSignOutTap;
/// Creates a [ProfileHeader].
const ProfileHeader({
@@ -25,12 +21,11 @@ class ProfileHeader extends StatelessWidget {
required this.fullName,
required this.level,
this.photoUrl,
required this.onSignOutTap,
});
@override
Widget build(BuildContext context) {
final i18n = t.staff.profile.header;
final TranslationsStaffProfileHeaderEn i18n = t.staff.profile.header;
return Container(
width: double.infinity,
@@ -49,31 +44,22 @@ class ProfileHeader extends StatelessWidget {
child: SafeArea(
bottom: false,
child: Column(
children: [
children: <Widget>[
// Top Bar
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
i18n.title,
style: UiTypography.headline4m.textSecondary,
),
GestureDetector(
onTap: onSignOutTap,
child: Text(
i18n.sign_out,
style: UiTypography.body2m.copyWith(
color: UiColors.primaryForeground.withValues(alpha: 0.8),
),
),
),
],
),
const SizedBox(height: UiConstants.space8),
// Avatar Section
Stack(
alignment: Alignment.bottomRight,
children: [
children: <Widget>[
Container(
width: 112,
height: 112,
@@ -83,13 +69,13 @@ class ProfileHeader extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
UiColors.accent,
UiColors.accent.withValues(alpha: 0.5),
UiColors.primaryForeground,
],
),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.foreground.withValues(alpha: 0.2),
blurRadius: 10,
@@ -119,7 +105,7 @@ class ProfileHeader extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
UiColors.accent,
UiColors.accent.withValues(alpha: 0.7),
],
@@ -144,7 +130,7 @@ class ProfileHeader extends StatelessWidget {
color: UiColors.primaryForeground,
shape: BoxShape.circle,
border: Border.all(color: UiColors.primary, width: 2),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.foreground.withValues(alpha: 0.1),
blurRadius: 4,