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> {
final GetStaffProfileUseCase _getProfileUseCase;
final SignOutStaffUseCase _signOutUseCase;
final GetPersonalInfoCompletionUseCase _getPersonalInfoCompletionUseCase;
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
/// Creates a [ProfileCubit] with the required use cases.
ProfileCubit(this._getProfileUseCase, this._signOutUseCase)
: super(const ProfileState());
ProfileCubit(
this._getProfileUseCase,
this._signOutUseCase,
this._getPersonalInfoCompletionUseCase,
this._getEmergencyContactsCompletionUseCase,
this._getExperienceCompletionUseCase,
this._getTaxFormsCompletionUseCase,
) : super(const ProfileState());
/// 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
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({
this.status = ProfileStatus.initial,
this.profile,
this.errorMessage,
this.personalInfoComplete,
this.emergencyContactsComplete,
this.experienceComplete,
this.taxFormsComplete,
});
/// Creates a copy of this state with updated values.
@@ -44,14 +60,30 @@ class ProfileState extends Equatable {
ProfileStatus? status,
Staff? profile,
String? errorMessage,
bool? personalInfoComplete,
bool? emergencyContactsComplete,
bool? experienceComplete,
bool? taxFormsComplete,
}) {
return ProfileState(
status: status ?? this.status,
profile: profile ?? this.profile,
errorMessage: errorMessage ?? this.errorMessage,
personalInfoComplete: personalInfoComplete ?? this.personalInfoComplete,
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
experienceComplete: experienceComplete ?? this.experienceComplete,
taxFormsComplete: taxFormsComplete ?? this.taxFormsComplete,
);
}
@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,
child: BlocConsumer<ProfileCubit, ProfileState>(
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) {
Modular.to.toGetStartedPage();
} 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:flutter/material.dart';
/// An individual item within the profile menu grid.
///
/// Uses design system tokens for all colors, typography, spacing, and borders.
class ProfileMenuItem extends StatelessWidget {
final IconData icon;
final String label;
final bool? completed;
final VoidCallback? onTap;
const ProfileMenuItem({
super.key,
required this.icon,
@@ -18,6 +13,11 @@ class ProfileMenuItem extends StatelessWidget {
this.onTap,
});
final IconData icon;
final String label;
final bool? completed;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -32,12 +32,12 @@ class ProfileMenuItem extends StatelessWidget {
child: AspectRatio(
aspectRatio: 1.0,
child: Stack(
children: [
children: <Widget>[
Align(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Container(
width: 36,
height: 36,
@@ -73,21 +73,22 @@ class ProfileMenuItem extends StatelessWidget {
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: completed! ? UiColors.primary : UiColors.error,
width: 0.5,
),
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,
child: completed!
? const Icon(
UiIcons.check,
size: 10,
color: UiColors.primaryForeground,
color: UiColors.primary,
)
: Text(
"!",
style: UiTypography.footnote2b.primary,
),
: Text("!", style: UiTypography.footnote2b.textError),
),
),
],

View File

@@ -1,9 +1,12 @@
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 'package:flutter_modular/flutter_modular.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_item.dart';
import '../section_title.dart';
@@ -11,6 +14,7 @@ import '../section_title.dart';
/// Widget displaying the compliance section of the staff profile.
///
/// This section contains menu items for tax forms and other compliance-related documents.
/// Displays completion status for each item.
class ComplianceSection extends StatelessWidget {
/// Creates a [ComplianceSection].
const ComplianceSection({super.key});
@@ -19,6 +23,8 @@ class ComplianceSection extends StatelessWidget {
Widget build(BuildContext context) {
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
return BlocBuilder<ProfileCubit, ProfileState>(
builder: (BuildContext context, ProfileState state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@@ -29,11 +35,14 @@ class ComplianceSection extends StatelessWidget {
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:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.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_item.dart';
import '../section_title.dart';
@@ -11,7 +14,7 @@ import '../section_title.dart';
/// Widget displaying the onboarding section of the staff profile.
///
/// 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 {
/// Creates an [OnboardingSection].
const OnboardingSection({super.key});
@@ -20,6 +23,8 @@ class OnboardingSection extends StatelessWidget {
Widget build(BuildContext context) {
final TranslationsStaffProfileEn i18n = Translations.of(context).staff.profile;
return BlocBuilder<ProfileCubit, ProfileState>(
builder: (BuildContext context, ProfileState state) {
return Column(
children: <Widget>[
SectionTitle(i18n.sections.onboarding),
@@ -29,21 +34,26 @@ class OnboardingSection extends StatelessWidget {
ProfileMenuItem(
icon: UiIcons.user,
label: i18n.menu_items.personal_info,
completed: state.personalInfoComplete,
onTap: () => Modular.to.toPersonalInfo(),
),
ProfileMenuItem(
icon: UiIcons.phone,
label: i18n.menu_items.emergency_contact,
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>(
() => 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
// BlocProvider will use this same instance, preventing state emission after close
@@ -37,6 +57,10 @@ class StaffProfileModule extends Module {
() => ProfileCubit(
i.get<GetStaffProfileUseCase>(),
i.get<SignOutStaffUseCase>(),
i.get<GetPersonalInfoCompletionUseCase>(),
i.get<GetEmergencyContactsCompletionUseCase>(),
i.get<GetExperienceCompletionUseCase>(),
i.get<GetTaxFormsCompletionUseCase>(),
),
);
}