From 7b9507b87f41affee9c0ba86ac98b28c0b6b5024 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 19 Feb 2026 13:39:03 -0500 Subject: [PATCH] feat: Refactor staff profile page and logout button for improved state management and navigation --- .../pages/staff_profile_page.dart | 192 +++++++++--------- .../presentation/widgets/logout_button.dart | 86 +++++--- .../presentation/widgets/profile_header.dart | 34 +--- docs/MOBILE/01-architecture-principles.md | 2 +- 4 files changed, 161 insertions(+), 153 deletions(-) diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index 96b98016..0ee25694 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -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(); - - // Load profile data on first build - if (cubit.state.status == ProfileStatus.initial) { - cubit.loadProfile(); - } - return Scaffold( - body: BlocConsumer( - 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( + create: (_) => Modular.get()..loadProfile(), + child: BlocConsumer( + 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: [ + 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: [ + // 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: [ - 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: [ - 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), - ], - ), - ), - ), - ], - ), - ); - }, + }, + ), ), ); } diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/logout_button.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/logout_button.dart index 3a2499c6..d74e9655 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/logout_button.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/logout_button.dart @@ -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().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( + 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().state, + ); + }, + 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, + ), + ], + ), ), ), ), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_header.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_header.dart index bee90690..04991ba1 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_header.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_header.dart @@ -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: [ // Top Bar Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + mainAxisAlignment: MainAxisAlignment.start, + children: [ 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: [ Container( width: 112, height: 112, @@ -83,13 +69,13 @@ class ProfileHeader extends StatelessWidget { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ + colors: [ UiColors.accent, UiColors.accent.withValues(alpha: 0.5), UiColors.primaryForeground, ], ), - 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: [ 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( color: UiColors.foreground.withValues(alpha: 0.1), blurRadius: 4, diff --git a/docs/MOBILE/01-architecture-principles.md b/docs/MOBILE/01-architecture-principles.md index b8c6f460..f151673a 100644 --- a/docs/MOBILE/01-architecture-principles.md +++ b/docs/MOBILE/01-architecture-principles.md @@ -68,7 +68,7 @@ graph TD - `data/`: Repository Implementations. - `presentation/`: - 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**: - **Presentation**: UI Pages, Modular Routes. - **State Management**: BLoCs / Cubits.