feat: Implement completion status tracking for personal info, emergency contacts, experience, and tax forms in profile management
This commit is contained in:
@@ -12,10 +12,20 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
with BlocErrorHandler<ProfileState> {
|
with BlocErrorHandler<ProfileState> {
|
||||||
final GetStaffProfileUseCase _getProfileUseCase;
|
final GetStaffProfileUseCase _getProfileUseCase;
|
||||||
final SignOutStaffUseCase _signOutUseCase;
|
final SignOutStaffUseCase _signOutUseCase;
|
||||||
|
final GetPersonalInfoCompletionUseCase _getPersonalInfoCompletionUseCase;
|
||||||
|
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
|
||||||
|
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
|
||||||
|
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
|
||||||
|
|
||||||
/// Creates a [ProfileCubit] with the required use cases.
|
/// Creates a [ProfileCubit] with the required use cases.
|
||||||
ProfileCubit(this._getProfileUseCase, this._signOutUseCase)
|
ProfileCubit(
|
||||||
: super(const ProfileState());
|
this._getProfileUseCase,
|
||||||
|
this._signOutUseCase,
|
||||||
|
this._getPersonalInfoCompletionUseCase,
|
||||||
|
this._getEmergencyContactsCompletionUseCase,
|
||||||
|
this._getExperienceCompletionUseCase,
|
||||||
|
this._getTaxFormsCompletionUseCase,
|
||||||
|
) : super(const ProfileState());
|
||||||
|
|
||||||
/// Loads the staff member's profile.
|
/// Loads the staff member's profile.
|
||||||
///
|
///
|
||||||
@@ -64,5 +74,61 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads personal information completion status.
|
||||||
|
Future<void> loadPersonalInfoCompletion() async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
final bool isComplete = await _getPersonalInfoCompletionUseCase();
|
||||||
|
emit(state.copyWith(personalInfoComplete: isComplete));
|
||||||
|
},
|
||||||
|
onError: (String _) {
|
||||||
|
return state.copyWith(personalInfoComplete: false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads emergency contacts completion status.
|
||||||
|
Future<void> loadEmergencyContactsCompletion() async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
final bool isComplete = await _getEmergencyContactsCompletionUseCase();
|
||||||
|
emit(state.copyWith(emergencyContactsComplete: isComplete));
|
||||||
|
},
|
||||||
|
onError: (String _) {
|
||||||
|
return state.copyWith(emergencyContactsComplete: false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads experience completion status.
|
||||||
|
Future<void> loadExperienceCompletion() async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
final bool isComplete = await _getExperienceCompletionUseCase();
|
||||||
|
emit(state.copyWith(experienceComplete: isComplete));
|
||||||
|
},
|
||||||
|
onError: (String _) {
|
||||||
|
return state.copyWith(experienceComplete: false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads tax forms completion status.
|
||||||
|
Future<void> loadTaxFormsCompletion() async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
final bool isComplete = await _getTaxFormsCompletionUseCase();
|
||||||
|
emit(state.copyWith(taxFormsComplete: isComplete));
|
||||||
|
},
|
||||||
|
onError: (String _) {
|
||||||
|
return state.copyWith(taxFormsComplete: false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,27 @@ class ProfileState extends Equatable {
|
|||||||
|
|
||||||
/// Error message if status is error
|
/// Error message if status is error
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Whether personal information is complete
|
||||||
|
final bool? personalInfoComplete;
|
||||||
|
|
||||||
|
/// Whether emergency contacts are complete
|
||||||
|
final bool? emergencyContactsComplete;
|
||||||
|
|
||||||
|
/// Whether experience information is complete
|
||||||
|
final bool? experienceComplete;
|
||||||
|
|
||||||
|
/// Whether tax forms are complete
|
||||||
|
final bool? taxFormsComplete;
|
||||||
|
|
||||||
const ProfileState({
|
const ProfileState({
|
||||||
this.status = ProfileStatus.initial,
|
this.status = ProfileStatus.initial,
|
||||||
this.profile,
|
this.profile,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.personalInfoComplete,
|
||||||
|
this.emergencyContactsComplete,
|
||||||
|
this.experienceComplete,
|
||||||
|
this.taxFormsComplete,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates a copy of this state with updated values.
|
/// Creates a copy of this state with updated values.
|
||||||
@@ -44,14 +60,30 @@ class ProfileState extends Equatable {
|
|||||||
ProfileStatus? status,
|
ProfileStatus? status,
|
||||||
Staff? profile,
|
Staff? profile,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
bool? personalInfoComplete,
|
||||||
|
bool? emergencyContactsComplete,
|
||||||
|
bool? experienceComplete,
|
||||||
|
bool? taxFormsComplete,
|
||||||
}) {
|
}) {
|
||||||
return ProfileState(
|
return ProfileState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
profile: profile ?? this.profile,
|
profile: profile ?? this.profile,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
personalInfoComplete: personalInfoComplete ?? this.personalInfoComplete,
|
||||||
|
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
||||||
|
experienceComplete: experienceComplete ?? this.experienceComplete,
|
||||||
|
taxFormsComplete: taxFormsComplete ?? this.taxFormsComplete,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [status, profile, errorMessage];
|
List<Object?> get props => <Object?>[
|
||||||
|
status,
|
||||||
|
profile,
|
||||||
|
errorMessage,
|
||||||
|
personalInfoComplete,
|
||||||
|
emergencyContactsComplete,
|
||||||
|
experienceComplete,
|
||||||
|
taxFormsComplete,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
value: cubit,
|
value: cubit,
|
||||||
child: BlocConsumer<ProfileCubit, ProfileState>(
|
child: BlocConsumer<ProfileCubit, ProfileState>(
|
||||||
listener: (BuildContext context, ProfileState state) {
|
listener: (BuildContext context, ProfileState state) {
|
||||||
|
// Load completion statuses when profile loads successfully
|
||||||
|
if (state.status == ProfileStatus.loaded &&
|
||||||
|
state.personalInfoComplete == null) {
|
||||||
|
cubit.loadPersonalInfoCompletion();
|
||||||
|
cubit.loadEmergencyContactsCompletion();
|
||||||
|
cubit.loadExperienceCompletion();
|
||||||
|
cubit.loadTaxFormsCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
if (state.status == ProfileStatus.signedOut) {
|
if (state.status == ProfileStatus.signedOut) {
|
||||||
Modular.to.toGetStartedPage();
|
Modular.to.toGetStartedPage();
|
||||||
} else if (state.status == ProfileStatus.error &&
|
} else if (state.status == ProfileStatus.error &&
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// An individual item within the profile menu grid.
|
/// An individual item within the profile menu grid.
|
||||||
///
|
///
|
||||||
/// Uses design system tokens for all colors, typography, spacing, and borders.
|
/// Uses design system tokens for all colors, typography, spacing, and borders.
|
||||||
class ProfileMenuItem extends StatelessWidget {
|
class ProfileMenuItem extends StatelessWidget {
|
||||||
final IconData icon;
|
|
||||||
final String label;
|
|
||||||
final bool? completed;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
const ProfileMenuItem({
|
const ProfileMenuItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
@@ -18,6 +13,11 @@ class ProfileMenuItem extends StatelessWidget {
|
|||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final bool? completed;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -32,12 +32,12 @@ class ProfileMenuItem extends StatelessWidget {
|
|||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1.0,
|
aspectRatio: 1.0,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: <Widget>[
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
@@ -73,21 +73,22 @@ class ProfileMenuItem extends StatelessWidget {
|
|||||||
height: 16,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: completed! ? UiColors.primary : UiColors.error,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
color: completed!
|
color: completed!
|
||||||
? UiColors.primary
|
? UiColors.primary.withValues(alpha: 0.1)
|
||||||
: UiColors.primary.withValues(alpha: 0.1),
|
: UiColors.error.withValues(alpha: 0.15),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: completed!
|
child: completed!
|
||||||
? const Icon(
|
? const Icon(
|
||||||
UiIcons.check,
|
UiIcons.check,
|
||||||
size: 10,
|
size: 10,
|
||||||
color: UiColors.primaryForeground,
|
color: UiColors.primary,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text("!", style: UiTypography.footnote2b.textError),
|
||||||
"!",
|
|
||||||
style: UiTypography.footnote2b.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
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 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import '../../blocs/profile_cubit.dart';
|
||||||
|
import '../../blocs/profile_state.dart';
|
||||||
import '../profile_menu_grid.dart';
|
import '../profile_menu_grid.dart';
|
||||||
import '../profile_menu_item.dart';
|
import '../profile_menu_item.dart';
|
||||||
import '../section_title.dart';
|
import '../section_title.dart';
|
||||||
@@ -11,6 +14,7 @@ import '../section_title.dart';
|
|||||||
/// Widget displaying the compliance section of the staff profile.
|
/// Widget displaying the compliance section of the staff profile.
|
||||||
///
|
///
|
||||||
/// This section contains menu items for tax forms and other compliance-related documents.
|
/// This section contains menu items for tax forms and other compliance-related documents.
|
||||||
|
/// Displays completion status for each item.
|
||||||
class ComplianceSection extends StatelessWidget {
|
class ComplianceSection extends StatelessWidget {
|
||||||
/// Creates a [ComplianceSection].
|
/// Creates a [ComplianceSection].
|
||||||
const ComplianceSection({super.key});
|
const ComplianceSection({super.key});
|
||||||
@@ -19,21 +23,26 @@ class ComplianceSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
||||||
|
|
||||||
return Column(
|
return BlocBuilder<ProfileCubit, ProfileState>(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (BuildContext context, ProfileState state) {
|
||||||
children: <Widget>[
|
return Column(
|
||||||
SectionTitle(i18n.sections.compliance),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
ProfileMenuGrid(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ProfileMenuItem(
|
SectionTitle(i18n.sections.compliance),
|
||||||
icon: UiIcons.file,
|
ProfileMenuGrid(
|
||||||
label: i18n.menu_items.tax_forms,
|
crossAxisCount: 3,
|
||||||
onTap: () => Modular.to.toTaxForms(),
|
children: <Widget>[
|
||||||
|
ProfileMenuItem(
|
||||||
|
icon: UiIcons.file,
|
||||||
|
label: i18n.menu_items.tax_forms,
|
||||||
|
completed: state.taxFormsComplete,
|
||||||
|
onTap: () => Modular.to.toTaxForms(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
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 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import '../../blocs/profile_cubit.dart';
|
||||||
|
import '../../blocs/profile_state.dart';
|
||||||
import '../profile_menu_grid.dart';
|
import '../profile_menu_grid.dart';
|
||||||
import '../profile_menu_item.dart';
|
import '../profile_menu_item.dart';
|
||||||
import '../section_title.dart';
|
import '../section_title.dart';
|
||||||
@@ -11,7 +14,7 @@ import '../section_title.dart';
|
|||||||
/// Widget displaying the onboarding section of the staff profile.
|
/// Widget displaying the onboarding section of the staff profile.
|
||||||
///
|
///
|
||||||
/// This section contains menu items for personal information, emergency contact,
|
/// This section contains menu items for personal information, emergency contact,
|
||||||
/// and work experience setup.
|
/// and work experience setup. Displays completion status for each item.
|
||||||
class OnboardingSection extends StatelessWidget {
|
class OnboardingSection extends StatelessWidget {
|
||||||
/// Creates an [OnboardingSection].
|
/// Creates an [OnboardingSection].
|
||||||
const OnboardingSection({super.key});
|
const OnboardingSection({super.key});
|
||||||
@@ -20,30 +23,37 @@ class OnboardingSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
|
||||||
|
|
||||||
return Column(
|
return BlocBuilder<ProfileCubit, ProfileState>(
|
||||||
children: <Widget>[
|
builder: (BuildContext context, ProfileState state) {
|
||||||
SectionTitle(i18n.sections.onboarding),
|
return Column(
|
||||||
ProfileMenuGrid(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ProfileMenuItem(
|
SectionTitle(i18n.sections.onboarding),
|
||||||
icon: UiIcons.user,
|
ProfileMenuGrid(
|
||||||
label: i18n.menu_items.personal_info,
|
crossAxisCount: 3,
|
||||||
onTap: () => Modular.to.toPersonalInfo(),
|
children: <Widget>[
|
||||||
),
|
ProfileMenuItem(
|
||||||
ProfileMenuItem(
|
icon: UiIcons.user,
|
||||||
icon: UiIcons.phone,
|
label: i18n.menu_items.personal_info,
|
||||||
label: i18n.menu_items.emergency_contact,
|
completed: state.personalInfoComplete,
|
||||||
onTap: () => Modular.to.toEmergencyContact(),
|
onTap: () => Modular.to.toPersonalInfo(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.briefcase,
|
icon: UiIcons.phone,
|
||||||
label: i18n.menu_items.experience,
|
label: i18n.menu_items.emergency_contact,
|
||||||
onTap: () => Modular.to.toExperience(),
|
completed: true,
|
||||||
|
onTap: () => Modular.to.toEmergencyContact(),
|
||||||
|
),
|
||||||
|
ProfileMenuItem(
|
||||||
|
icon: UiIcons.briefcase,
|
||||||
|
label: i18n.menu_items.experience,
|
||||||
|
completed: state.experienceComplete,
|
||||||
|
onTap: () => Modular.to.toExperience(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ class StaffProfileModule extends Module {
|
|||||||
i.addLazySingleton<SignOutStaffUseCase>(
|
i.addLazySingleton<SignOutStaffUseCase>(
|
||||||
() => SignOutStaffUseCase(repository: i.get<StaffConnectorRepository>()),
|
() => SignOutStaffUseCase(repository: i.get<StaffConnectorRepository>()),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton<GetPersonalInfoCompletionUseCase>(
|
||||||
|
() => GetPersonalInfoCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetEmergencyContactsCompletionUseCase>(
|
||||||
|
() => GetEmergencyContactsCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetExperienceCompletionUseCase>(
|
||||||
|
() => GetExperienceCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetTaxFormsCompletionUseCase>(
|
||||||
|
() => GetTaxFormsCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Presentation layer - Cubit as singleton to avoid recreation
|
// Presentation layer - Cubit as singleton to avoid recreation
|
||||||
// BlocProvider will use this same instance, preventing state emission after close
|
// BlocProvider will use this same instance, preventing state emission after close
|
||||||
@@ -37,6 +57,10 @@ class StaffProfileModule extends Module {
|
|||||||
() => ProfileCubit(
|
() => ProfileCubit(
|
||||||
i.get<GetStaffProfileUseCase>(),
|
i.get<GetStaffProfileUseCase>(),
|
||||||
i.get<SignOutStaffUseCase>(),
|
i.get<SignOutStaffUseCase>(),
|
||||||
|
i.get<GetPersonalInfoCompletionUseCase>(),
|
||||||
|
i.get<GetEmergencyContactsCompletionUseCase>(),
|
||||||
|
i.get<GetExperienceCompletionUseCase>(),
|
||||||
|
i.get<GetTaxFormsCompletionUseCase>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user