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 @override
Widget build(BuildContext context) { 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( return Scaffold(
body: BlocConsumer<ProfileCubit, ProfileState>( body: BlocProvider<ProfileCubit>(
bloc: cubit, create: (_) => Modular.get<ProfileCubit>()..loadProfile(),
listener: (BuildContext context, ProfileState state) { child: BlocConsumer<ProfileCubit, ProfileState>(
if (state.status == ProfileStatus.signedOut) { listener: (BuildContext context, ProfileState state) {
Modular.to.toGetStartedPage(); if (state.status == ProfileStatus.signedOut) {
} else if (state.status == ProfileStatus.error && Modular.to.toGetStartedPage();
state.errorMessage != null) { } else if (state.status == ProfileStatus.error &&
UiSnackbar.show( state.errorMessage != null) {
context, UiSnackbar.show(
message: translateErrorKey(state.errorMessage!), context,
type: UiSnackbarType.error, message: translateErrorKey(state.errorMessage!),
); type: UiSnackbarType.error,
} );
}, }
builder: (BuildContext context, ProfileState state) { },
// Show loading spinner if status is loading builder: (BuildContext context, ProfileState state) {
// Show loading spinner if status is loading
if (state.status == ProfileStatus.loading) { if (state.status == ProfileStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (state.status == ProfileStatus.error) { if (state.status == ProfileStatus.error) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space4),
child: Text( child: Text(
state.errorMessage != null state.errorMessage != null
? translateErrorKey(state.errorMessage!) ? translateErrorKey(state.errorMessage!)
: 'An error occurred', : 'An error occurred',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: UiTypography.body1r.copyWith( style: UiTypography.body1r.copyWith(
color: UiColors.textSecondary, 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: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 '../blocs/profile_cubit.dart';
import '../blocs/profile_state.dart';
/// The sign-out button widget. /// The sign-out button widget.
/// ///
/// Uses design system tokens for all colors, typography, spacing, and icons. /// 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 { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final i18n = t.staff.profile.header; final TranslationsStaffProfileHeaderEn i18n = t.staff.profile.header;
return Container( return BlocListener<ProfileCubit, ProfileState>(
width: double.infinity, listener: (BuildContext context, ProfileState state) {
decoration: BoxDecoration( if (state.status == ProfileStatus.signedOut) {
color: UiColors.bgPopup, // Navigate to get started page after successful sign-out
borderRadius: UiConstants.radiusLg, // This will be handled by the profile page listener
border: Border.all(color: UiColors.border), }
), },
child: Material( child: Container(
color: UiColors.transparent, width: double.infinity,
child: InkWell( decoration: BoxDecoration(
onTap: onTap, color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
child: Padding( border: Border.all(color: UiColors.border),
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), ),
child: Row( child: Material(
mainAxisAlignment: MainAxisAlignment.center, color: UiColors.transparent,
children: [ child: InkWell(
const Icon( onTap: () {
UiIcons.logOut, _handleSignOut(
color: UiColors.destructive, context,
size: 20, context.read<ProfileCubit>().state,
), );
const SizedBox(width: UiConstants.space2), },
Text( borderRadius: UiConstants.radiusLg,
i18n.sign_out, child: Padding(
style: UiTypography.body1m.textError, 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:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
/// The header section of the staff profile page, containing avatar, name, level, /// The header section of the staff profile page, containing avatar, name, and level.
/// and a sign-out button.
/// ///
/// Uses design system tokens for all colors, typography, and spacing. /// Uses design system tokens for all colors, typography, and spacing.
class ProfileHeader extends StatelessWidget { class ProfileHeader extends StatelessWidget {
@@ -15,9 +14,6 @@ class ProfileHeader extends StatelessWidget {
/// Optional photo URL for the avatar /// Optional photo URL for the avatar
final String? photoUrl; final String? photoUrl;
/// Callback when sign out is tapped
final VoidCallback onSignOutTap;
/// Creates a [ProfileHeader]. /// Creates a [ProfileHeader].
const ProfileHeader({ const ProfileHeader({
@@ -25,12 +21,11 @@ class ProfileHeader extends StatelessWidget {
required this.fullName, required this.fullName,
required this.level, required this.level,
this.photoUrl, this.photoUrl,
required this.onSignOutTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final i18n = t.staff.profile.header; final TranslationsStaffProfileHeaderEn i18n = t.staff.profile.header;
return Container( return Container(
width: double.infinity, width: double.infinity,
@@ -49,31 +44,22 @@ class ProfileHeader extends StatelessWidget {
child: SafeArea( child: SafeArea(
bottom: false, bottom: false,
child: Column( child: Column(
children: [ children: <Widget>[
// Top Bar // Top Bar
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: <Widget>[
Text( Text(
i18n.title, i18n.title,
style: UiTypography.headline4m.textSecondary, 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), const SizedBox(height: UiConstants.space8),
// Avatar Section // Avatar Section
Stack( Stack(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
children: [ children: <Widget>[
Container( Container(
width: 112, width: 112,
height: 112, height: 112,
@@ -83,13 +69,13 @@ class ProfileHeader extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: <Color>[
UiColors.accent, UiColors.accent,
UiColors.accent.withValues(alpha: 0.5), UiColors.accent.withValues(alpha: 0.5),
UiColors.primaryForeground, UiColors.primaryForeground,
], ],
), ),
boxShadow: [ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: UiColors.foreground.withValues(alpha: 0.2), color: UiColors.foreground.withValues(alpha: 0.2),
blurRadius: 10, blurRadius: 10,
@@ -119,7 +105,7 @@ class ProfileHeader extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: <Color>[
UiColors.accent, UiColors.accent,
UiColors.accent.withValues(alpha: 0.7), UiColors.accent.withValues(alpha: 0.7),
], ],
@@ -144,7 +130,7 @@ class ProfileHeader extends StatelessWidget {
color: UiColors.primaryForeground, color: UiColors.primaryForeground,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: UiColors.primary, width: 2), border: Border.all(color: UiColors.primary, width: 2),
boxShadow: [ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: UiColors.foreground.withValues(alpha: 0.1), color: UiColors.foreground.withValues(alpha: 0.1),
blurRadius: 4, blurRadius: 4,

View File

@@ -68,7 +68,7 @@ graph TD
- `data/`: Repository Implementations. - `data/`: Repository Implementations.
- `presentation/`: - `presentation/`:
- Pages, BLoCs, Widgets. - Pages, BLoCs, Widgets.
- For performance make the pages as `StatelessWidget` and move the state management to the BLoC or `StatefulWidget` to an external separate widget file. - For performance make the pages as `StatelessWidget` and move the state management to the BLoC (always use a BlocProvider when providing the BLoC to the widget tree) or `StatefulWidget` to an external separate widget file.
- **Responsibilities**: - **Responsibilities**:
- **Presentation**: UI Pages, Modular Routes. - **Presentation**: UI Pages, Modular Routes.
- **State Management**: BLoCs / Cubits. - **State Management**: BLoCs / Cubits.