feat: Implement completion status tracking for personal info, emergency contacts, experience, and tax forms in profile management

This commit is contained in:
Achintha Isuru
2026-02-19 14:41:44 -05:00
parent d50e09b67a
commit 3640bfafa3
7 changed files with 203 additions and 52 deletions

View File

@@ -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);
},
);
}
} }

View File

@@ -33,10 +33,26 @@ 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,
];
} }

View File

@@ -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 &&

View File

@@ -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,
),
), ),
), ),
], ],

View File

@@ -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(),
),
],
), ),
], ],
), );
], },
); );
} }
} }

View File

@@ -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(),
),
],
), ),
], ],
), );
], },
); );
} }
} }

View File

@@ -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>(),
), ),
); );
} }