feat: Refactor staff profile page and logout button for improved state management and navigation
This commit is contained in:
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user