diff --git a/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart b/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart index 4812be9b..2d3dd946 100644 --- a/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart +++ b/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart @@ -59,7 +59,7 @@ class CoreBlocObserver extends BlocObserver { super.onChange(bloc, change); if (logStateChanges) { developer.log( - 'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}', + 'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}', name: bloc.runtimeType.toString(), ); } @@ -109,7 +109,7 @@ class CoreBlocObserver extends BlocObserver { super.onTransition(bloc, transition); if (logStateChanges) { developer.log( - 'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}', + 'Transition: ${transition.event.runtimeType}’ ${transition.nextState.runtimeType}', name: bloc.runtimeType.toString(), ); } diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index 378eb395..7f6f5187 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -24,6 +24,9 @@ export 'src/connectors/staff/domain/usecases/get_personal_info_completion_usecas export 'src/connectors/staff/domain/usecases/get_emergency_contacts_completion_usecase.dart'; export 'src/connectors/staff/domain/usecases/get_experience_completion_usecase.dart'; export 'src/connectors/staff/domain/usecases/get_tax_forms_completion_usecase.dart'; +export 'src/connectors/staff/domain/usecases/get_attire_options_completion_usecase.dart'; +export 'src/connectors/staff/domain/usecases/get_staff_documents_completion_usecase.dart'; +export 'src/connectors/staff/domain/usecases/get_staff_certificates_completion_usecase.dart'; export 'src/connectors/staff/domain/usecases/get_staff_profile_usecase.dart'; export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart'; export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart'; diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart index e4ced00d..770f1d68 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; + import '../../domain/repositories/staff_connector_repository.dart'; /// Implementation of [StaffConnectorRepository]. @@ -33,10 +34,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { final dc.GetStaffProfileCompletionStaff? staff = response.data.staff; final List emergencyContacts = response.data.emergencyContacts; - final List taxForms = - response.data.taxForms; - - return _isProfileComplete(staff, emergencyContacts, taxForms); + return _isProfileComplete(staff, emergencyContacts); }); } @@ -107,7 +105,140 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { .getStaffTaxFormsProfileCompletion(id: staffId) .execute(); - return response.data.taxForms.isNotEmpty; + final List taxForms = + response.data.taxForms; + + // Return false if no tax forms exist + if (taxForms.isEmpty) return false; + + // Return true only if all tax forms have status == "SUBMITTED" + return taxForms.every( + (dc.GetStaffTaxFormsProfileCompletionTaxForms form) { + if (form.status is dc.Unknown) return false; + final dc.TaxFormStatus status = + (form.status as dc.Known).value; + return status == dc.TaxFormStatus.SUBMITTED; + }, + ); + }); + } + + @override + Future getAttireOptionsCompletion() async { + return _service.run(() async { + final String staffId = await _service.getStaffId(); + + final List> results = + await Future.wait>( + >>[ + _service.connector.listAttireOptions().execute(), + _service.connector.getStaffAttire(staffId: staffId).execute(), + ], + ); + + final QueryResult optionsRes = + results[0] as QueryResult; + final QueryResult + staffAttireRes = + results[1] + as QueryResult; + + final List attireOptions = + optionsRes.data.attireOptions; + final List staffAttire = + staffAttireRes.data.staffAttires; + + // Get only mandatory attire options + final List mandatoryOptions = + attireOptions + .where((dc.ListAttireOptionsAttireOptions opt) => + opt.isMandatory ?? false) + .toList(); + + // Return null if no mandatory attire options + if (mandatoryOptions.isEmpty) return null; + + // Return true only if all mandatory attire items are verified + return mandatoryOptions.every( + (dc.ListAttireOptionsAttireOptions mandatoryOpt) { + final dc.GetStaffAttireStaffAttires? currentAttire = staffAttire + .where( + (dc.GetStaffAttireStaffAttires a) => + a.attireOptionId == mandatoryOpt.id, + ) + .firstOrNull; + + if (currentAttire == null) return false; // Not uploaded + if (currentAttire.verificationStatus is dc.Unknown) return false; + final dc.AttireVerificationStatus status = + (currentAttire.verificationStatus + as dc.Known) + .value; + return status == dc.AttireVerificationStatus.APPROVED; + }, + ); + }); + } + + @override + Future getStaffDocumentsCompletion() async { + return _service.run(() async { + final String staffId = await _service.getStaffId(); + + final QueryResult< + dc.ListStaffDocumentsByStaffIdData, + dc.ListStaffDocumentsByStaffIdVariables + > + response = await _service.connector + .listStaffDocumentsByStaffId(staffId: staffId) + .execute(); + + final List staffDocs = + response.data.staffDocuments; + + // Return null if no documents + if (staffDocs.isEmpty) return null; + + // Return true only if all documents are verified + return staffDocs.every( + (dc.ListStaffDocumentsByStaffIdStaffDocuments doc) { + if (doc.status is dc.Unknown) return false; + final dc.DocumentStatus status = + (doc.status as dc.Known).value; + return status == dc.DocumentStatus.VERIFIED; + }, + ); + }); + } + + @override + Future getStaffCertificatesCompletion() async { + return _service.run(() async { + final String staffId = await _service.getStaffId(); + + final QueryResult< + dc.ListCertificatesByStaffIdData, + dc.ListCertificatesByStaffIdVariables + > + response = await _service.connector + .listCertificatesByStaffId(staffId: staffId) + .execute(); + + final List certificates = + response.data.certificates; + + // Return false if no certificates + if (certificates.isEmpty) return null; + + // Return true only if all certificates are fully validated + return certificates.every( + (dc.ListCertificatesByStaffIdCertificates cert) { + if (cert.validationStatus is dc.Unknown) return false; + final dc.ValidationStatus status = + (cert.validationStatus as dc.Known).value; + return status == dc.ValidationStatus.APPROVED; + }, + ); }); } @@ -134,7 +265,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { bool _isProfileComplete( dc.GetStaffProfileCompletionStaff? staff, List emergencyContacts, - List taxForms, ) { if (staff == null) return false; @@ -146,7 +276,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { return (staff.fullName.trim().isNotEmpty) && (staff.email?.trim().isNotEmpty ?? false) && emergencyContacts.isNotEmpty && - taxForms.isNotEmpty && hasExperience; } @@ -199,8 +328,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { return response.data.benefitsDatas.map(( dc.ListBenefitsDataByStaffIdBenefitsDatas e, ) { - final total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0; - final remaining = e.current.toDouble(); + final double total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0; + final double remaining = e.current.toDouble(); return domain.Benefit( title: e.vendorBenefitPlan.title, entitlementHours: total, @@ -346,8 +475,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { @override Future signOut() async { try { - await _service.auth.signOut(); - _service.clearCache(); + await _service.signOut(); } catch (e) { throw Exception('Error signing out: ${e.toString()}'); } diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/repositories/staff_connector_repository.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/repositories/staff_connector_repository.dart index 7a54aea0..f3ba6ac6 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/repositories/staff_connector_repository.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/repositories/staff_connector_repository.dart @@ -33,6 +33,21 @@ abstract interface class StaffConnectorRepository { /// Returns true if at least one tax form exists. Future getTaxFormsCompletion(); + /// Fetches attire options completion status. + /// + /// Returns true if all mandatory attire options are verified. + Future getAttireOptionsCompletion(); + + /// Fetches documents completion status. + /// + /// Returns true if all mandatory documents are verified. + Future getStaffDocumentsCompletion(); + + /// Fetches certificates completion status. + /// + /// Returns true if all certificates are validated. + Future getStaffCertificatesCompletion(); + /// Fetches the full staff profile for the current authenticated user. /// /// Returns a [Staff] entity containing all profile information. diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_attire_options_completion_usecase.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_attire_options_completion_usecase.dart new file mode 100644 index 00000000..acf51396 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_attire_options_completion_usecase.dart @@ -0,0 +1,27 @@ +import 'package:krow_core/core.dart'; + +import '../repositories/staff_connector_repository.dart'; + +/// Use case for retrieving attire options completion status. +/// +/// This use case encapsulates the business logic for determining whether +/// a staff member has fully uploaded and verified all mandatory attire options. +/// It delegates to the repository for data access. +class GetAttireOptionsCompletionUseCase extends NoInputUseCase { + /// Creates a [GetAttireOptionsCompletionUseCase]. + /// + /// Requires a [StaffConnectorRepository] for data access. + GetAttireOptionsCompletionUseCase({ + required StaffConnectorRepository repository, + }) : _repository = repository; + + final StaffConnectorRepository _repository; + + /// Executes the use case to get attire options completion status. + /// + /// Returns true if all mandatory attire options are verified, false otherwise. + /// + /// Throws an exception if the operation fails. + @override + Future call() => _repository.getAttireOptionsCompletion(); +} diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_certificates_completion_usecase.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_certificates_completion_usecase.dart new file mode 100644 index 00000000..a77238c7 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_certificates_completion_usecase.dart @@ -0,0 +1,27 @@ +import 'package:krow_core/core.dart'; + +import '../repositories/staff_connector_repository.dart'; + +/// Use case for retrieving certificates completion status. +/// +/// This use case encapsulates the business logic for determining whether +/// a staff member has fully validated all certificates. +/// It delegates to the repository for data access. +class GetStaffCertificatesCompletionUseCase extends NoInputUseCase { + /// Creates a [GetStaffCertificatesCompletionUseCase]. + /// + /// Requires a [StaffConnectorRepository] for data access. + GetStaffCertificatesCompletionUseCase({ + required StaffConnectorRepository repository, + }) : _repository = repository; + + final StaffConnectorRepository _repository; + + /// Executes the use case to get certificates completion status. + /// + /// Returns true if all certificates are validated, false otherwise. + /// + /// Throws an exception if the operation fails. + @override + Future call() => _repository.getStaffCertificatesCompletion(); +} diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_documents_completion_usecase.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_documents_completion_usecase.dart new file mode 100644 index 00000000..4bbe85db --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/domain/usecases/get_staff_documents_completion_usecase.dart @@ -0,0 +1,27 @@ +import 'package:krow_core/core.dart'; + +import '../repositories/staff_connector_repository.dart'; + +/// Use case for retrieving documents completion status. +/// +/// This use case encapsulates the business logic for determining whether +/// a staff member has fully uploaded and verified all mandatory documents. +/// It delegates to the repository for data access. +class GetStaffDocumentsCompletionUseCase extends NoInputUseCase { + /// Creates a [GetStaffDocumentsCompletionUseCase]. + /// + /// Requires a [StaffConnectorRepository] for data access. + GetStaffDocumentsCompletionUseCase({ + required StaffConnectorRepository repository, + }) : _repository = repository; + + final StaffConnectorRepository _repository; + + /// Executes the use case to get documents completion status. + /// + /// Returns true if all mandatory documents are verified, false otherwise. + /// + /// Throws an exception if the operation fails. + @override + Future call() => _repository.getStaffDocumentsCompletion(); +} diff --git a/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart index 8e79de80..4465a7cb 100644 --- a/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart +++ b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart @@ -224,8 +224,19 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin { } } + /// Signs out the current user from Firebase Auth and clears all session data. + Future signOut() async { + try { + await auth.signOut(); + _clearCache(); + } catch (e) { + debugPrint('DataConnectService: Error signing out: $e'); + rethrow; + } + } + /// Clears Cached Repositories and Session data. - void clearCache() { + void _clearCache() { _reportsRepository = null; _shiftsRepository = null; _hubsRepository = null; diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 511e6f15..3361b69d 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -338,8 +338,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { @override Future signOut() async { try { - await _service.auth.signOut(); - _service.clearCache(); + await _service.signOut(); } catch (e) { throw Exception('Error signing out: ${e.toString()}'); } @@ -371,9 +370,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { if (requireBusinessRole && user.userRole != 'BUSINESS' && user.userRole != 'BOTH') { - await _service.auth.signOut(); - dc.ClientSessionStore.instance.clear(); - _service.clearCache(); + await _service.signOut(); throw UnauthorizedAppException( technicalMessage: 'User role is ${user.userRole}, expected BUSINESS or BOTH', diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart index 299ea0ca..17410784 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart @@ -44,7 +44,6 @@ class _NoShowReportPageState extends State { return SingleChildScrollView( child: Column( children: [ - // ── Header ────────────────────────────────────────── Container( padding: const EdgeInsets.only( top: 60, @@ -151,7 +150,6 @@ class _NoShowReportPageState extends State { ), ), - // ── Content ───────────────────────────────────────── Transform.translate( offset: const Offset(0, -16), child: Padding( @@ -241,7 +239,7 @@ class _NoShowReportPageState extends State { } } -// ── Summary chip (top 3 stats) ─────────────────────────────────────────────── +// Summary chip (top 3 stats) class _SummaryChip extends StatelessWidget { const _SummaryChip({ @@ -305,7 +303,7 @@ class _SummaryChip extends StatelessWidget { } } -// ── Worker card with risk badge + latest incident ──────────────────────────── +// ” Worker card with risk badge + latest incident ”””””””””””””” class _WorkerCard extends StatelessWidget { const _WorkerCard({required this.worker}); @@ -448,5 +446,5 @@ class _WorkerCard extends StatelessWidget { } } -// ── Insight line ───────────────────────────────────────────────────────────── +// Insight line diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart index f43b3cd8..3593b5fa 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart @@ -40,11 +40,11 @@ class _PerformanceReportPageState extends State { if (state is PerformanceLoaded) { final PerformanceReport report = state.report; - // Compute overall score (0–100) from the 4 KPIs + // Compute overall score (0 - 100) from the 4 KPIs final double overallScore = ((report.fillRate * 0.3) + (report.completionRate * 0.3) + (report.onTimeRate * 0.25) + - // avg fill time: 3h target → invert to score + // avg fill time: 3h target invert to score ((report.avgFillTimeHours <= 3 ? 100 : (3 / report.avgFillTimeHours) * 100) * @@ -107,7 +107,7 @@ class _PerformanceReportPageState extends State { iconColor: const Color(0xFFF39C12), label: context.t.client_reports.performance_report.kpis.avg_fill_time, target: context.t.client_reports.performance_report.kpis.target_hours(hours: '3'), - // invert: lower is better — show as % of target met + // invert: lower is better show as % of target met value: report.avgFillTimeHours == 0 ? 100 : (3 / report.avgFillTimeHours * 100).clamp(0, 100), @@ -122,7 +122,7 @@ class _PerformanceReportPageState extends State { return SingleChildScrollView( child: Column( children: [ - // ── Header ─────────────────────────────────────────── + // Header Container( padding: const EdgeInsets.only( top: 60, @@ -225,14 +225,14 @@ class _PerformanceReportPageState extends State { ), ), - // ── Content ────────────────────────────────────────── + // ” Content ””””””””””””””””””””” Transform.translate( offset: const Offset(0, -16), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ - // ── Overall Score Hero Card ─────────────────── + // Overall Score Hero Card Container( width: double.infinity, padding: const EdgeInsets.symmetric( @@ -299,7 +299,7 @@ class _PerformanceReportPageState extends State { const SizedBox(height: 24), - // ── KPI List ───────────────────────────────── + // KPI List Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -349,7 +349,7 @@ class _PerformanceReportPageState extends State { } } -// ── KPI data model ──────────────────────────────────────────────────────────── +// ” KPI data model ”””””””””””””””””””””””””””””” class _KpiData { const _KpiData({ @@ -367,14 +367,14 @@ class _KpiData { final Color iconColor; final String label; final String target; - final double value; // 0–100 for bar + final double value; // 0-100 for bar final String displayValue; final Color barColor; final bool met; final bool close; } -// ── KPI row widget ──────────────────────────────────────────────────────────── +// ” KPI row widget ”””””””””””””””””””””””””””””” class _KpiRow extends StatelessWidget { const _KpiRow({required this.kpi}); diff --git a/apps/mobile/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart b/apps/mobile/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart index 2da4bc85..7acb21ad 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart @@ -15,7 +15,7 @@ class SettingsRepositoryImpl implements SettingsRepositoryInterface { @override Future signOut() async { return _service.run(() async { - await _service.auth.signOut(); + await _service.signOut(); }); } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 7b6bc1bc..e9e7f1c7 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -96,10 +96,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { /// Signs out the current user. @override - Future signOut() { - StaffSessionStore.instance.clear(); - _service.clearCache(); - return _service.auth.signOut(); + Future signOut() async { + return await _service.signOut(); } /// Verifies an OTP code and returns the authenticated user. @@ -163,7 +161,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { if (staffResponse.data.staffs.isNotEmpty) { // If profile exists, they should use Login mode. - await _service.auth.signOut(); + await _service.signOut(); throw const domain.AccountExistsException( technicalMessage: 'This user already has a staff profile. Please log in.', @@ -185,14 +183,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } } else { if (user == null) { - await _service.auth.signOut(); + await _service.signOut(); throw const domain.UserNotFoundException( technicalMessage: 'Authenticated user profile not found in database.', ); } // Allow STAFF or BOTH roles to log in to the Staff App if (user.userRole != 'STAFF' && user.userRole != 'BOTH') { - await _service.auth.signOut(); + await _service.signOut(); throw const domain.UnauthorizedAppException( technicalMessage: 'User is not authorized for this app.', ); @@ -206,7 +204,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { requiresAuthentication: false, ); if (staffResponse.data.staffs.isEmpty) { - await _service.auth.signOut(); + await _service.signOut(); throw const domain.UserNotFoundException( technicalMessage: 'Your account is not registered yet. Please register first.', diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart index 94f8a0b5..3f6fbadc 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart @@ -180,7 +180,7 @@ class _ClockInPageState extends State { style: UiTypography.body2b, ), Text( - "${shift.clientName} • ${shift.location}", + "${shift.clientName} ${shift.location}", style: UiTypography .body3r .textSecondary, diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart index 0fc38f98..b017c9f2 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart @@ -1,12 +1,12 @@ +import 'dart:math' as math; + +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 'package:krow_domain/krow_domain.dart'; -import 'package:core_localization/core_localization.dart'; import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; -import 'dart:math' as math; /// Page displaying a detailed overview of the worker's benefits. class BenefitsOverviewPage extends StatelessWidget { diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart index 141a9c2b..db64fa43 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart @@ -19,6 +19,9 @@ class ProfileCubit extends Cubit this._getEmergencyContactsCompletionUseCase, this._getExperienceCompletionUseCase, this._getTaxFormsCompletionUseCase, + this._getAttireOptionsCompletionUseCase, + this._getStaffDocumentsCompletionUseCase, + this._getStaffCertificatesCompletionUseCase, ) : super(const ProfileState()); final GetStaffProfileUseCase _getProfileUseCase; final SignOutStaffUseCase _signOutUseCase; @@ -26,6 +29,9 @@ class ProfileCubit extends Cubit final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase; final GetExperienceCompletionUseCase _getExperienceCompletionUseCase; final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase; + final GetAttireOptionsCompletionUseCase _getAttireOptionsCompletionUseCase; + final GetStaffDocumentsCompletionUseCase _getStaffDocumentsCompletionUseCase; + final GetStaffCertificatesCompletionUseCase _getStaffCertificatesCompletionUseCase; /// Loads the staff member's profile. /// @@ -130,5 +136,47 @@ class ProfileCubit extends Cubit }, ); } + + /// Loads attire options completion status. + Future loadAttireCompletion() async { + await handleError( + emit: emit, + action: () async { + final bool? isComplete = await _getAttireOptionsCompletionUseCase(); + emit(state.copyWith(attireComplete: isComplete)); + }, + onError: (String _) { + return state.copyWith(attireComplete: false); + }, + ); + } + + /// Loads documents completion status. + Future loadDocumentsCompletion() async { + await handleError( + emit: emit, + action: () async { + final bool? isComplete = await _getStaffDocumentsCompletionUseCase(); + emit(state.copyWith(documentsComplete: isComplete)); + }, + onError: (String _) { + return state.copyWith(documentsComplete: false); + }, + ); + } + + /// Loads certificates completion status. + Future loadCertificatesCompletion() async { + await handleError( + emit: emit, + action: () async { + final bool? isComplete = await _getStaffCertificatesCompletionUseCase(); + emit(state.copyWith(certificatesComplete: isComplete)); + }, + onError: (String _) { + return state.copyWith(certificatesComplete: false); + }, + ); + } } diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart index 39994b97..5c0b3903 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart @@ -33,6 +33,9 @@ class ProfileState extends Equatable { this.emergencyContactsComplete, this.experienceComplete, this.taxFormsComplete, + this.attireComplete, + this.documentsComplete, + this.certificatesComplete, }); /// Current status of the profile feature final ProfileStatus status; @@ -54,6 +57,15 @@ class ProfileState extends Equatable { /// Whether tax forms are complete final bool? taxFormsComplete; + + /// Whether attire options are complete + final bool? attireComplete; + + /// Whether documents are complete + final bool? documentsComplete; + + /// Whether certificates are complete + final bool? certificatesComplete; /// Creates a copy of this state with updated values. ProfileState copyWith({ @@ -64,6 +76,9 @@ class ProfileState extends Equatable { bool? emergencyContactsComplete, bool? experienceComplete, bool? taxFormsComplete, + bool? attireComplete, + bool? documentsComplete, + bool? certificatesComplete, }) { return ProfileState( status: status ?? this.status, @@ -73,6 +88,9 @@ class ProfileState extends Equatable { emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete, experienceComplete: experienceComplete ?? this.experienceComplete, taxFormsComplete: taxFormsComplete ?? this.taxFormsComplete, + attireComplete: attireComplete ?? this.attireComplete, + documentsComplete: documentsComplete ?? this.documentsComplete, + certificatesComplete: certificatesComplete ?? this.certificatesComplete, ); } @@ -85,5 +103,8 @@ class ProfileState extends Equatable { emergencyContactsComplete, experienceComplete, taxFormsComplete, + attireComplete, + documentsComplete, + certificatesComplete, ]; } 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 49767da9..8bec14f2 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 @@ -46,6 +46,9 @@ class StaffProfilePage extends StatelessWidget { cubit.loadEmergencyContactsCompletion(); cubit.loadExperienceCompletion(); cubit.loadTaxFormsCompletion(); + cubit.loadAttireCompletion(); + cubit.loadDocumentsCompletion(); + cubit.loadCertificatesCompletion(); } if (state.status == ProfileStatus.signedOut) { diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/compliance_section.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/compliance_section.dart index b3a700e4..8db16a18 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/compliance_section.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/compliance_section.dart @@ -43,11 +43,13 @@ class ComplianceSection extends StatelessWidget { ProfileMenuItem( icon: UiIcons.file, label: i18n.menu_items.documents, + completed: state.documentsComplete, onTap: () => Modular.to.toDocuments(), ), ProfileMenuItem( icon: UiIcons.certificate, label: i18n.menu_items.certificates, + completed: state.certificatesComplete, onTap: () => Modular.to.toCertificates(), ), ], diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart index 327e58ea..0cb1e574 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/onboarding_section.dart @@ -54,6 +54,7 @@ class OnboardingSection extends StatelessWidget { ProfileMenuItem( icon: UiIcons.shirt, label: i18n.menu_items.attire, + completed: state.attireComplete, onTap: () => Modular.to.toAttire(), ), ], diff --git a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart index 06b38c53..f9b720cb 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart @@ -50,6 +50,21 @@ class StaffProfileModule extends Module { repository: i.get(), ), ); + i.addLazySingleton( + () => GetAttireOptionsCompletionUseCase( + repository: i.get(), + ), + ); + i.addLazySingleton( + () => GetStaffDocumentsCompletionUseCase( + repository: i.get(), + ), + ); + i.addLazySingleton( + () => GetStaffCertificatesCompletionUseCase( + repository: i.get(), + ), + ); // Presentation layer - Cubit as singleton to avoid recreation // BlocProvider will use this same instance, preventing state emission after close @@ -61,6 +76,9 @@ class StaffProfileModule extends Module { i.get(), i.get(), i.get(), + i.get(), + i.get(), + i.get(), ), ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart index 015c1d14..6c53978e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart @@ -44,7 +44,14 @@ class TaxFormMapper { String subtitle = ''; String description = ''; - if (form.formType == dc.TaxFormType.I9) { + final dc.TaxFormType formType; + if (form.formType is dc.Known) { + formType = (form.formType as dc.Known).value; + } else { + formType = dc.TaxFormType.W4; + } + + if (formType == dc.TaxFormType.I9) { title = 'Form I-9'; subtitle = 'Employment Eligibility Verification'; description = 'Required for all new hires to verify identity.'; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart index 73de4e89..35aac207 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart @@ -7,8 +7,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../../domain/repositories/tax_forms_repository.dart'; import '../mappers/tax_form_mapper.dart'; -class TaxFormsRepositoryImpl - implements TaxFormsRepository { +class TaxFormsRepositoryImpl implements TaxFormsRepository { TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance; final dc.DataConnectService _service; @@ -17,16 +16,22 @@ class TaxFormsRepositoryImpl Future> getTaxForms() async { return _service.run(() async { final String staffId = await _service.getStaffId(); - final QueryResult response = await _service.connector - .getTaxFormsByStaffId(staffId: staffId) - .execute(); + final QueryResult< + dc.GetTaxFormsByStaffIdData, + dc.GetTaxFormsByStaffIdVariables + > + response = await _service.connector + .getTaxFormsByStaffId(staffId: staffId) + .execute(); - final List forms = - response.data.taxForms.map(TaxFormMapper.fromDataConnect).toList(); + final List forms = response.data.taxForms + .map(TaxFormMapper.fromDataConnect) + .toList(); // Check if required forms exist, create if not. - final Set typesPresent = - forms.map((TaxForm f) => f.type).toSet(); + final Set typesPresent = forms + .map((TaxForm f) => f.type) + .toSet(); bool createdNew = false; if (!typesPresent.contains(TaxFormType.i9)) { @@ -39,8 +44,13 @@ class TaxFormsRepositoryImpl } if (createdNew) { - final QueryResult response2 = - await _service.connector.getTaxFormsByStaffId(staffId: staffId).execute(); + final QueryResult< + dc.GetTaxFormsByStaffIdData, + dc.GetTaxFormsByStaffIdVariables + > + response2 = await _service.connector + .getTaxFormsByStaffId(staffId: staffId) + .execute(); return response2.data.taxForms .map(TaxFormMapper.fromDataConnect) .toList(); @@ -54,8 +64,9 @@ class TaxFormsRepositoryImpl await _service.connector .createTaxForm( staffId: staffId, - formType: - dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)), + formType: dc.TaxFormType.values.byName( + TaxFormAdapter.typeToString(type), + ), firstName: '', lastName: '', socialSN: 0, @@ -69,8 +80,8 @@ class TaxFormsRepositoryImpl Future updateI9Form(I9TaxForm form) async { return _service.run(() async { final Map data = form.formData; - final dc.UpdateTaxFormVariablesBuilder builder = - _service.connector.updateTaxForm(id: form.id); + final dc.UpdateTaxFormVariablesBuilder builder = _service.connector + .updateTaxForm(id: form.id); _mapCommonFields(builder, data); _mapI9Fields(builder, data); await builder.execute(); @@ -81,8 +92,8 @@ class TaxFormsRepositoryImpl Future submitI9Form(I9TaxForm form) async { return _service.run(() async { final Map data = form.formData; - final dc.UpdateTaxFormVariablesBuilder builder = - _service.connector.updateTaxForm(id: form.id); + final dc.UpdateTaxFormVariablesBuilder builder = _service.connector + .updateTaxForm(id: form.id); _mapCommonFields(builder, data); _mapI9Fields(builder, data); await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); @@ -93,8 +104,8 @@ class TaxFormsRepositoryImpl Future updateW4Form(W4TaxForm form) async { return _service.run(() async { final Map data = form.formData; - final dc.UpdateTaxFormVariablesBuilder builder = - _service.connector.updateTaxForm(id: form.id); + final dc.UpdateTaxFormVariablesBuilder builder = _service.connector + .updateTaxForm(id: form.id); _mapCommonFields(builder, data); _mapW4Fields(builder, data); await builder.execute(); @@ -105,8 +116,8 @@ class TaxFormsRepositoryImpl Future submitW4Form(W4TaxForm form) async { return _service.run(() async { final Map data = form.formData; - final dc.UpdateTaxFormVariablesBuilder builder = - _service.connector.updateTaxForm(id: form.id); + final dc.UpdateTaxFormVariablesBuilder builder = _service.connector + .updateTaxForm(id: form.id); _mapCommonFields(builder, data); _mapW4Fields(builder, data); await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); @@ -114,7 +125,9 @@ class TaxFormsRepositoryImpl } void _mapCommonFields( - dc.UpdateTaxFormVariablesBuilder builder, Map data) { + dc.UpdateTaxFormVariablesBuilder builder, + Map data, + ) { if (data.containsKey('firstName')) { builder.firstName(data['firstName'] as String?); } @@ -154,8 +167,8 @@ class TaxFormsRepositoryImpl } if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) { builder.socialSN( - int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? - 0); + int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0, + ); } if (data.containsKey('email')) builder.email(data['email'] as String?); if (data.containsKey('phone')) builder.phone(data['phone'] as String?); @@ -173,14 +186,17 @@ class TaxFormsRepositoryImpl } void _mapI9Fields( - dc.UpdateTaxFormVariablesBuilder builder, Map data) { + dc.UpdateTaxFormVariablesBuilder builder, + Map data, + ) { if (data.containsKey('citizenshipStatus')) { final String status = data['citizenshipStatus'] as String; // Map string to enum if possible, or handle otherwise. // Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED try { builder.citizen( - dc.CitizenshipStatus.values.byName(status.toUpperCase())); + dc.CitizenshipStatus.values.byName(status.toUpperCase()), + ); } catch (_) {} } if (data.containsKey('uscisNumber')) { @@ -202,7 +218,9 @@ class TaxFormsRepositoryImpl } void _mapW4Fields( - dc.UpdateTaxFormVariablesBuilder builder, Map data) { + dc.UpdateTaxFormVariablesBuilder builder, + Map data, + ) { if (data.containsKey('cityStateZip')) { final String csz = data['cityStateZip'] as String; // Extremely basic split: City, State Zip @@ -222,10 +240,11 @@ class TaxFormsRepositoryImpl // Simple mapping assumptions: if (status.contains('single')) { builder.marital(dc.MaritalStatus.SINGLE); - } else if (status.contains('married')) + } else if (status.contains('married')) { builder.marital(dc.MaritalStatus.MARRIED); - else if (status.contains('head')) + } else if (status.contains('head')) { builder.marital(dc.MaritalStatus.HEAD); + } } catch (_) {} } if (data.containsKey('multipleJobs')) { @@ -245,11 +264,11 @@ class TaxFormsRepositoryImpl } if (data.containsKey('extraWithholding')) { builder.extraWithholding( - double.tryParse(data['extraWithholding'].toString())); + double.tryParse(data['extraWithholding'].toString()), + ); } if (data.containsKey('signature')) { builder.signature(data['signature'] as String?); } } } - diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart index 2dd39496..bc350439 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart @@ -7,6 +7,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../blocs/tax_forms/tax_forms_cubit.dart'; import '../blocs/tax_forms/tax_forms_state.dart'; +import '../widgets/tax_forms_page/index.dart'; class TaxFormsPage extends StatelessWidget { const TaxFormsPage({super.key}); @@ -57,11 +58,16 @@ class TaxFormsPage extends StatelessWidget { child: Column( spacing: UiConstants.space4, children: [ - _buildProgressOverview(state.forms), - ...state.forms.map( - (TaxForm form) => _buildFormCard(context, form), + const TaxFormsInfoCard(), + TaxFormsProgressOverview(forms: state.forms), + Column( + spacing: UiConstants.space2, + children: [ + ...state.forms.map( + (TaxForm form) => _buildFormCard(context, form), + ), + ], ), - _buildInfoCard(), ], ), ); @@ -71,56 +77,9 @@ class TaxFormsPage extends StatelessWidget { ); } - Widget _buildProgressOverview(List forms) { - final int completedCount = forms - .where( - (TaxForm f) => - f.status == TaxFormStatus.submitted || - f.status == TaxFormStatus.approved, - ) - .length; - final int totalCount = forms.length; - final double progress = totalCount > 0 ? completedCount / totalCount : 0.0; - - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Document Progress', style: UiTypography.body2m.textPrimary), - Text( - '$completedCount/$totalCount', - style: UiTypography.body2m.textSecondary, - ), - ], - ), - const SizedBox(height: UiConstants.space3), - ClipRRect( - borderRadius: UiConstants.radiusSm, - child: LinearProgressIndicator( - value: progress, - minHeight: 8, - backgroundColor: UiColors.background, - valueColor: const AlwaysStoppedAnimation(UiColors.primary), - ), - ), - ], - ), - ); - } - Widget _buildFormCard(BuildContext context, TaxForm form) { - // Helper to get icon based on type (could be in entity or a mapper) - final String icon = form is I9TaxForm ? '🛂' : '📋'; - - return GestureDetector( + return TaxFormCard( + form: form, onTap: () async { if (form is I9TaxForm) { final Object? result = await Modular.to.pushNamed( @@ -140,161 +99,6 @@ class TaxFormsPage extends StatelessWidget { } } }, - child: Container( - margin: const EdgeInsets.only(bottom: UiConstants.space4), - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: UiColors.primary.withValues(alpha: 0.1), - borderRadius: UiConstants.radiusLg, - ), - child: Center(child: Text(icon, style: UiTypography.headline1m)), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - form.title, - style: UiTypography.headline4m.textPrimary, - ), - _buildStatusBadge(form.status), - ], - ), - const SizedBox(height: UiConstants.space1), - Text( - form.subtitle ?? '', - style: UiTypography.body2m.textSecondary.copyWith( - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: UiConstants.space1), - Text( - form.description ?? '', - style: UiTypography.body3r.textSecondary, - ), - ], - ), - ), - const SizedBox(width: UiConstants.space2), - const Icon( - UiIcons.chevronRight, - color: UiColors.textSecondary, - size: 20, - ), - ], - ), - ), - ); - } - - Widget _buildStatusBadge(TaxFormStatus status) { - switch (status) { - case TaxFormStatus.submitted: - case TaxFormStatus.approved: - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.tagSuccess, - borderRadius: UiConstants.radiusLg, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - UiIcons.success, - size: 12, - color: UiColors.textSuccess, - ), - const SizedBox(width: UiConstants.space1), - Text('Completed', style: UiTypography.footnote2b.textSuccess), - ], - ), - ); - case TaxFormStatus.inProgress: - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.tagPending, - borderRadius: UiConstants.radiusLg, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(UiIcons.clock, size: 12, color: UiColors.textWarning), - const SizedBox(width: UiConstants.space1), - Text('In Progress', style: UiTypography.footnote2b.textWarning), - ], - ), - ); - default: - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.tagValue, - borderRadius: UiConstants.radiusLg, - ), - child: Text( - 'Not Started', - style: UiTypography.footnote2b.textSecondary, - ), - ); - } - } - - Widget _buildInfoCard() { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusLg, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(UiIcons.file, color: UiColors.primary, size: 20), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Why are these needed?', - style: UiTypography.headline4m.textPrimary, - ), - const SizedBox(height: UiConstants.space1), - Text( - 'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.', - style: UiTypography.body3r.textSecondary, - ), - ], - ), - ), - ], - ), ); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/index.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/index.dart new file mode 100644 index 00000000..7c893fe6 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/index.dart @@ -0,0 +1,4 @@ +export 'progress_overview.dart'; +export 'tax_form_card.dart'; +export 'tax_form_status_badge.dart'; +export 'tax_forms_info_card.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/progress_overview.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/progress_overview.dart new file mode 100644 index 00000000..86efd133 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/progress_overview.dart @@ -0,0 +1,56 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Widget displaying the overall progress of tax form completion. +class TaxFormsProgressOverview extends StatelessWidget { + const TaxFormsProgressOverview({required this.forms}); + + final List forms; + + @override + Widget build(BuildContext context) { + final int completedCount = forms + .where( + (TaxForm f) => + f.status == TaxFormStatus.submitted || + f.status == TaxFormStatus.approved, + ) + .length; + final int totalCount = forms.length; + final double progress = totalCount > 0 ? completedCount / totalCount : 0.0; + + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Document Progress', style: UiTypography.body2m.textPrimary), + Text( + '$completedCount/$totalCount', + style: UiTypography.body2m.textSecondary, + ), + ], + ), + const SizedBox(height: UiConstants.space3), + ClipRRect( + borderRadius: UiConstants.radiusSm, + child: LinearProgressIndicator( + value: progress, + minHeight: 8, + backgroundColor: UiColors.background, + valueColor: const AlwaysStoppedAnimation(UiColors.primary), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_card.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_card.dart new file mode 100644 index 00000000..8f214404 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_card.dart @@ -0,0 +1,78 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +import 'tax_form_status_badge.dart'; + +/// Widget displaying a single tax form card with information and navigation. +class TaxFormCard extends StatelessWidget { + const TaxFormCard({super.key, required this.form, required this.onTap}); + + final TaxForm form; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + // Helper to get icon based on type + final String icon = form is I9TaxForm ? '🛂' : '📋'; + + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border, width: 0.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: UiColors.primary.withValues(alpha: 0.1), + borderRadius: UiConstants.radiusLg, + ), + child: Center(child: Text(icon, style: UiTypography.headline1m)), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + form.title, + style: UiTypography.headline4m.textPrimary, + ), + TaxFormStatusBadge(status: form.status), + ], + ), + const SizedBox(height: UiConstants.space1), + Text( + form.subtitle ?? '', + style: UiTypography.body3r.textSecondary, + ), + Text( + form.description ?? '', + style: UiTypography.body3r.textSecondary, + ), + ], + ), + ), + const SizedBox(width: UiConstants.space2), + const Icon( + UiIcons.chevronRight, + color: UiColors.textSecondary, + size: 20, + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_status_badge.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_status_badge.dart new file mode 100644 index 00000000..930229df --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_form_status_badge.dart @@ -0,0 +1,74 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Widget displaying status badge for a tax form. +class TaxFormStatusBadge extends StatelessWidget { + const TaxFormStatusBadge({required this.status}); + + final TaxFormStatus status; + + @override + Widget build(BuildContext context) { + switch (status) { + case TaxFormStatus.submitted: + case TaxFormStatus.approved: + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.tagSuccess, + borderRadius: UiConstants.radiusLg, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + UiIcons.success, + size: 12, + color: UiColors.textSuccess, + ), + const SizedBox(width: UiConstants.space1), + Text('Completed', style: UiTypography.footnote2b.textSuccess), + ], + ), + ); + case TaxFormStatus.inProgress: + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.tagPending, + borderRadius: UiConstants.radiusLg, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(UiIcons.clock, size: 12, color: UiColors.textWarning), + const SizedBox(width: UiConstants.space1), + Text('In Progress', style: UiTypography.footnote2b.textWarning), + ], + ), + ); + default: + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.tagValue, + borderRadius: UiConstants.radiusLg, + ), + child: Text( + 'Not Started', + style: UiTypography.footnote2b.textSecondary, + ), + ); + } + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_forms_info_card.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_forms_info_card.dart new file mode 100644 index 00000000..203f6f52 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_page/tax_forms_info_card.dart @@ -0,0 +1,17 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Information card explaining why tax forms are required. +class TaxFormsInfoCard extends StatelessWidget { + const TaxFormsInfoCard({super.key}); + + @override + Widget build(BuildContext context) { + return const UiNoticeBanner( + title: 'Why are these needed?', + description: + 'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.', + icon: UiIcons.file, + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart index 243c2b65..2cc52470 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart @@ -298,17 +298,11 @@ class _AttireCapturePageState extends State { currentPhotoUrl: currentPhotoUrl, referenceImageUrl: widget.item.imageUrl, ), - const SizedBox(height: UiConstants.space1), InfoSection( description: widget.item.description, statusText: _getStatusText(hasUploadedPhoto), statusColor: _getStatusColor(hasUploadedPhoto), isPending: _isPending, - showCheckbox: !_hasVerificationStatus, - isAttested: state.isAttested, - onAttestationChanged: (bool? val) { - cubit.toggleAttestation(val ?? false); - }, ), ], ), @@ -321,6 +315,11 @@ class _AttireCapturePageState extends State { hasVerificationStatus: _hasVerificationStatus, hasUploadedPhoto: hasUploadedPhoto, updatedItem: state.updatedItem, + showCheckbox: !_hasVerificationStatus, + isAttested: state.isAttested, + onAttestationChanged: (bool? val) { + cubit.toggleAttestation(val ?? false); + }, onGallery: () => _onGallery(context), onCamera: () => _onCamera(context), onSubmit: () => _onSubmit(context), diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attestation_checkbox.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attestation_checkbox.dart index 1594b993..421599bd 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attestation_checkbox.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attestation_checkbox.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; class AttestationCheckbox extends StatelessWidget { - const AttestationCheckbox({ super.key, required this.isChecked, @@ -14,37 +13,22 @@ class AttestationCheckbox extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.border), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 24, - height: 24, - child: Checkbox( - value: isChecked, - onChanged: onChanged, - activeColor: UiColors.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, + children: [ + SizedBox( + width: 24, + height: 24, + child: Checkbox(value: isChecked, onChanged: onChanged), + ), + Expanded( + child: Text( + t.staff_profile_attire.attestation, + style: UiTypography.body2r, ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Text( - t.staff_profile_attire.attestation, - style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), - ), - ), - ], - ), + ), + ], ); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/footer_section.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/footer_section.dart index 895a803f..ba73830d 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/footer_section.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/footer_section.dart @@ -4,6 +4,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:krow_core/core.dart'; +import '../attestation_checkbox.dart'; import 'attire_upload_buttons.dart'; /// Handles the primary actions at the bottom of the page. @@ -16,6 +17,9 @@ class FooterSection extends StatelessWidget { required this.hasVerificationStatus, required this.hasUploadedPhoto, this.updatedItem, + required this.showCheckbox, + required this.isAttested, + required this.onAttestationChanged, required this.onGallery, required this.onCamera, required this.onSubmit, @@ -37,6 +41,15 @@ class FooterSection extends StatelessWidget { /// The updated attire item, if any. final AttireItem? updatedItem; + /// Whether to show the attestation checkbox. + final bool showCheckbox; + + /// Whether the user has attested to owning the item. + final bool isAttested; + + /// Callback when the attestation status changes. + final ValueChanged onAttestationChanged; + /// Callback to open the gallery. final VoidCallback onGallery; @@ -57,6 +70,13 @@ class FooterSection extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (showCheckbox) ...[ + AttestationCheckbox( + isChecked: isAttested, + onChanged: onAttestationChanged, + ), + const SizedBox(height: UiConstants.space8), + ], if (isUploading) const Center( child: Padding( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/info_section.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/info_section.dart index be5995f2..f20639d0 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/info_section.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/info_section.dart @@ -1,10 +1,9 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import '../attestation_checkbox.dart'; import 'attire_verification_status_card.dart'; -/// Displays the item details, verification status, and attestation checkbox. +/// Displays the item details and verification status. class InfoSection extends StatelessWidget { /// Creates an [InfoSection]. const InfoSection({ @@ -13,9 +12,6 @@ class InfoSection extends StatelessWidget { required this.statusText, required this.statusColor, required this.isPending, - required this.showCheckbox, - required this.isAttested, - required this.onAttestationChanged, }); /// The description of the attire item. @@ -30,15 +26,6 @@ class InfoSection extends StatelessWidget { /// Whether the item is currently pending verification. final bool isPending; - /// Whether to show the attestation checkbox. - final bool showCheckbox; - - /// Whether the user has attested to owning the item. - final bool isAttested; - - /// Callback when the attestation status changes. - final ValueChanged onAttestationChanged; - @override Widget build(BuildContext context) { return Column( @@ -74,15 +61,6 @@ class InfoSection extends StatelessWidget { statusText: statusText, statusColor: statusColor, ), - const SizedBox(height: UiConstants.space6), - - if (showCheckbox) ...[ - AttestationCheckbox( - isChecked: isAttested, - onChanged: onAttestationChanged, - ), - const SizedBox(height: UiConstants.space6), - ], ], ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart index 52b942b3..6daa1b57 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart @@ -136,7 +136,7 @@ class PersonalInfoBloc extends Bloc PersonalInfoAddressSelected event, Emitter emit, ) { - // Legacy address selected – no-op; use PersonalInfoLocationAdded instead. + // Legacy address selected no-op; use PersonalInfoLocationAdded instead. } /// Adds a location to the preferredLocations list (max 5, no duplicates). diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart index 29024f42..3eaa9a0b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart @@ -109,7 +109,7 @@ class _PreferredLocationsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ── Description + // ” Description Padding( padding: const EdgeInsets.fromLTRB( UiConstants.space5, @@ -123,7 +123,7 @@ class _PreferredLocationsPageState extends State { ), ), - // ── Search autocomplete field + // ” Search autocomplete field Padding( padding: const EdgeInsets.symmetric( horizontal: UiConstants.space5, @@ -137,7 +137,7 @@ class _PreferredLocationsPageState extends State { ), ), - // ── "Max reached" banner + // ” "Max reached" banner if (atMax) Padding( padding: const EdgeInsets.fromLTRB( @@ -164,7 +164,7 @@ class _PreferredLocationsPageState extends State { const SizedBox(height: UiConstants.space5), - // ── Section label + // ” Section label Padding( padding: const EdgeInsets.symmetric( horizontal: UiConstants.space5, diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/legal/legal_section_widget.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/legal/legal_section_widget.dart index d50540a3..ed0cef19 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/legal/legal_section_widget.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/legal/legal_section_widget.dart @@ -28,9 +28,9 @@ class LegalSectionWidget extends StatelessWidget { Container( decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.border), + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border, width: 0.5), ), child: Column( children: [ diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/privacy/privacy_section_widget.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/privacy/privacy_section_widget.dart index c8a54a63..215a103c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/privacy/privacy_section_widget.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/privacy/privacy_section_widget.dart @@ -40,10 +40,11 @@ class PrivacySectionWidget extends StatelessWidget { const SizedBox(height: 12.0), Container( decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, border: Border.all( color: UiColors.border, + width: 0.5, ), ), child: Column( diff --git a/backend/dataconnect/connector/staff/queries/profile_completion.gql b/backend/dataconnect/connector/staff/queries/profile_completion.gql index a80e0b10..bd6e655d 100644 --- a/backend/dataconnect/connector/staff/queries/profile_completion.gql +++ b/backend/dataconnect/connector/staff/queries/profile_completion.gql @@ -15,11 +15,6 @@ query getStaffProfileCompletion($id: UUID!) @auth(level: USER) { emergencyContacts(where: { staffId: { eq: $id } }) { id } - taxForms(where: { staffId: { eq: $id } }) { - id - formType - status - } } query getStaffPersonalInfoCompletion($id: UUID!) @auth(level: USER) { diff --git a/docs/MOBILE/04-use-case-completion-audit.md b/docs/MOBILE/04-use-case-completion-audit.md index 8f7d4e98..d215b556 100644 --- a/docs/MOBILE/04-use-case-completion-audit.md +++ b/docs/MOBILE/04-use-case-completion-audit.md @@ -1,9 +1,9 @@ # 📊 Use Case Completion Audit -**Generated:** 2026-02-23 +**Generated:** 2026-03-02 **Auditor Role:** System Analyst / Flutter Architect -**Source of Truth:** `docs/ARCHITECTURE/client-mobile-application/use-case.md`, `docs/ARCHITECTURE/staff-mobile-application/use-case.md`, `docs/ARCHITECTURE/system-bible.md`, `docs/ARCHITECTURE/architecture.md` -**Codebase Checked:** `apps/mobile/packages/features/` (real app) vs `apps/mobile/prototypes/` (prototypes) +**Source of Truth:** `docs/ARCHITECTURE/client-mobile-application/use-case.md`, `docs/ARCHITECTURE/staff-mobile-application/use-case.md` +**Codebase Checked:** `apps/mobile/packages/features/` and `apps/mobile/apps/` (actual production apps) --- @@ -11,11 +11,10 @@ | Symbol | Meaning | |:---:|:--- | -| ✅ | Fully implemented in the real app | -| 🟡 | Partially implemented — UI or domain exists but logic is incomplete | -| ❌ | Defined in docs but entirely missing in the real app | -| ⚠️ | Exists in prototype but has **not** been migrated to the real app | -| 🚫 | Exists in real app code but is **not** documented in use cases | +| ✅ | Fully implemented with all 4 architecture layers | +| 🟡 | Partially implemented — Some layers missing or functionality incomplete | +| ❌ | Defined in use case docs but entirely missing in production app | +| 🚫 | Exists in production app but **not** documented in use cases (extra feature) | --- @@ -23,204 +22,222 @@ ### Feature Module: `authentication` -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 1.1 Initial Startup & Auth Check | System checks session on launch | ✅ | ✅ | ✅ Completed | `client_get_started_page.dart` handles auth routing via Modular. | -| 1.1 Initial Startup & Auth Check | Route to Home if authenticated | ✅ | ✅ | ✅ Completed | Navigation guard implemented in auth module. | -| 1.1 Initial Startup & Auth Check | Route to Get Started if unauthenticated | ✅ | ✅ | ✅ Completed | `client_intro_page.dart` + `client_get_started_page.dart` both exist. | -| 1.2 Register Business Account | Enter company name & industry | ✅ | ✅ | ✅ Completed | `client_sign_up_page.dart` fully implemented. | -| 1.2 Register Business Account | Enter contact info & password | ✅ | ✅ | ✅ Completed | Real app BLoC-backed form with validation. | -| 1.2 Register Business Account | Registration success → Main App | ✅ | ✅ | ✅ Completed | Post-registration redirection intact. | -| 1.3 Business Sign In | Enter email & password | ✅ | ✅ | ✅ Completed | `client_sign_in_page.dart` fully implemented. | -| 1.3 Business Sign In | System validates credentials | ✅ | ✅ | ✅ Completed | Auth BLoC with error states present. | -| 1.3 Business Sign In | Grant access to dashboard | ✅ | ✅ | ✅ Completed | Redirects to `client_main` shell on success. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 1.1 Initial Startup & Auth Check | System checks session on launch | ✅ | ✅ Completed | Auth BLoC + navigation guards handle routing. | +| 1.1 Initial Startup & Auth Check | Route to Home if authenticated | ✅ | ✅ Completed | Modular routing configured with auth state checks. | +| 1.1 Initial Startup & Auth Check | Route to Get Started if unauthenticated | ✅ | ✅ Completed | `get_started_page.dart` + `intro_page.dart` implemented. | +| 1.2 Register Business Account | Enter company name & industry | ✅ | ✅ Completed | `client_sign_up_page.dart` with full BLoC implementation. | +| 1.2 Register Business Account | Enter contact info & password | ✅ | ✅ Completed | Form validation + use cases properly wired. | +| 1.2 Register Business Account | Registration success → Main App | ✅ | ✅ Completed | Post-registration navigation functional. | +| 1.3 Business Sign In | Enter email & password | ✅ | ✅ Completed | `client_sign_in_page.dart` with AuthBLoC. | +| 1.3 Business Sign In | System validates credentials | ✅ | ✅ Completed | Use cases: `sign_in_with_email`, `sign_in_with_social`. | +| 1.3 Business Sign In | Grant access to dashboard | ✅ | ✅ Completed | Navigation to `client_main` on success. | --- ### Feature Module: `orders` (Order Management) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 2.1 Rapid Order | Tap RAPID → Select Role → Set Qty → Post | ✅ | ✅ | 🟡 Partial | `rapid_order_page.dart` & `RapidOrderBloc` exist with full view. Voice recognition is **simulated** (UI only, no actual voice API). | -| 2.2 Scheduled Orders — One-Time | Create single shift (date, time, role, location) | ✅ | ✅ | ✅ Completed | `one_time_order_page.dart` fully implemented with BLoC. | -| 2.2 Scheduled Orders — Recurring | Create recurring shifts (e.g., every Monday) | ✅ | ✅ | ✅ Completed | `recurring_order_page.dart` fully implemented. | -| 2.2 Scheduled Orders — Permanent | Long-term staffing placement | ✅ | ✅ | ✅ Completed | `permanent_order_page.dart` fully implemented. | -| 2.2 Scheduled Orders | Review cost before posting | ✅ | ✅ | 🟡 Partial | Order summary shown, but real-time cost calculation depends on backend. | -| View & Browse Active Orders | Search & toggle between weeks to view orders | ✅ | ✅ | 🚫 Completed | `view_orders_page.dart` exists with `ViewOrderCard`. Added `eventName` visibility. | -| Modify Posted Orders | Refine staffing needs post-publish | ✅ | ✅ | 🚫 Completed | `OrderEditSheet` handles position updates and entire order cancellation flow. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 2.1 Rapid Order | Tap RAPID → Select Role → Set Qty → Post | ✅ | ✅ Completed | `rapid_order_page.dart` + RapidOrderBloc + 3 use cases (create, parse, transcribe). Voice recognition integrated. | +| 2.2 Scheduled Orders — One-Time | Create single shift (date, time, role, location) | ✅ | ✅ Completed | `one_time_order_page.dart` + OneTimeOrderBloc + use case. | +| 2.2 Scheduled Orders — Recurring | Create recurring shifts (e.g., every Monday) | ✅ | ✅ Completed | `recurring_order_page.dart` + RecurringOrderBloc + use case. | +| 2.2 Scheduled Orders — Permanent | Long-term staffing placement | ✅ | ✅ Completed | `permanent_order_page.dart` + PermanentOrderBloc + use case. | +| 2.2 Scheduled Orders | Review cost before posting | ✅ | ✅ Completed | Cost calculation integrated in order creation flows. | +| View & Browse Active Orders | Search & toggle between views | ✅ | ✅ Completed | `view_orders_page.dart` + ViewOrdersCubit with filters. | +| Modify Posted Orders | Edit or cancel orders | ✅ | ✅ Completed | `order_edit_sheet.dart` with hub updates + cancel flow. | +| Reorder Functionality | Quickly recreate past orders | ✅ | 🚫 Completed | `ReorderUseCase` + recent reorders widget on home. | --- ### Feature Module: `client_coverage` (Operations & Workforce Management) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 3.1 Monitor Today's Coverage | View coverage tab | ✅ | ✅ | ✅ Completed | `coverage_page.dart` exists with coverage header and shift list. | -| 3.1 Monitor Today's Coverage | View percentage filled | ✅ | ✅ | ✅ Completed | `coverage_header.dart` shows fill rate. | -| 3.1 Monitor Today's Coverage | Identify open gaps | ✅ | ✅ | ✅ Completed | Open/filled shift list in `coverage_shift_list.dart`. | -| 3.1 Monitor Today's Coverage | Re-post unfilled shifts | ✅ | ✅ | 🚫 Completed | Action added to shift header on Coverage page. | -| 3.2 Live Activity Tracking | Real-time feed of worker clock-ins | ✅ | ✅ | ✅ Completed | `live_activity_widget.dart` wired to Data Connect. | -| 3.3 Verify Worker Attire | Select active shift → Select worker → Check attire | ✅ | ✅ | ✅ Completed | Action added to coverage view; workers can be verified in real-time. | -| 3.4 Review & Approve Timesheets | Navigate to Timesheets section | ✅ | ✅ | ✅ Completed | Implemented `TimesheetsPage` in billing module for approval workflow. | -| 3.4 Review & Approve Timesheets | Review actual vs. scheduled hours | ✅ | ✅ | ✅ Completed | Viewable in the timesheet approval card. | -| 3.4 Review & Approve Timesheets | Tap Approve / Dispute | ✅ | ✅ | ✅ Completed | Approve/Decline actions implemented in `TimesheetsPage`. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 3.1 Monitor Today's Coverage | View coverage tab | ✅ | ✅ Completed | `coverage_page.dart` + CoverageCubit. | +| 3.1 Monitor Today's Coverage | View percentage filled | ✅ | ✅ Completed | `coverage_header.dart` displays fill rate stats. | +| 3.1 Monitor Today's Coverage | Identify open gaps | ✅ | ✅ Completed | `coverage_shift_list.dart` shows unfilled shifts. | +| 3.1 Monitor Today's Coverage | Re-post unfilled shifts | ✅ | 🟡 Partial | Re-post event exists but mutation noted as stub (needs backend wiring). | +| 3.2 Live Activity Tracking | Real-time feed of worker clock-ins | ✅ | ✅ Completed | `live_activity_widget.dart` in home module, wired to Data Connect. | +| 3.3 Verify Worker Attire | Select shift → Select worker → Check attire | ✅ | 🟡 Partial | Verify attire button exists in coverage but full flow needs verification. | +| 3.4 Review & Approve Timesheets | Navigate to Timesheets section | ✅ | ✅ Completed | Integrated in billing module with `shift_completion_review_bloc`. | +| 3.4 Review & Approve Timesheets | Review actual vs. scheduled hours | ✅ | ✅ Completed | `completion_review_page.dart` displays timesheet data. | +| 3.4 Review & Approve Timesheets | Tap Approve / Dispute | ✅ | ✅ Completed | Approve/dispute use cases implemented. | --- ### Feature Module: `reports` (Reports & Analytics) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 4.1 Business Intelligence Reporting | Daily Ops Report | ✅ | ✅ | ✅ Completed | `daily_ops_report_page.dart` fully implemented. | -| 4.1 Business Intelligence Reporting | Spend Report | ✅ | ✅ | ✅ Completed | `spend_report_page.dart` fully implemented. | -| 4.1 Business Intelligence Reporting | Forecast Report | ✅ | ✅ | ✅ Completed | `forecast_report_page.dart` fully implemented. | -| 4.1 Business Intelligence Reporting | Performance Report | ✅ | ✅ | ✅ Completed | `performance_report_page.dart` fully implemented. | -| 4.1 Business Intelligence Reporting | No-Show Report | ✅ | ✅ | ✅ Completed | `no_show_report_page.dart` fully implemented. | -| 4.1 Business Intelligence Reporting | Coverage Report | ✅ | ✅ | ✅ Completed | `coverage_report_page.dart` fully implemented. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 4.1 Business Intelligence Reporting | Daily Ops Report | ✅ | ✅ Completed | `daily_ops_report_page.dart` + DailyOpsReportBloc. | +| 4.1 Business Intelligence Reporting | Spend Report | ✅ | ✅ Completed | `spend_report_page.dart` + SpendReportBloc. | +| 4.1 Business Intelligence Reporting | Forecast Report | ✅ | ✅ Completed | `forecast_report_page.dart` + ForecastReportBloc. | +| 4.1 Business Intelligence Reporting | Performance Report | ✅ | ✅ Completed | `performance_report_page.dart` + PerformanceReportBloc. | +| 4.1 Business Intelligence Reporting | No-Show Report | ✅ | ✅ Completed | `no_show_report_page.dart` + NoShowReportBloc. | +| 4.1 Business Intelligence Reporting | Coverage Report | ✅ | ✅ Completed | `coverage_report_page.dart` + CoverageReportBloc. | --- ### Feature Module: `billing` (Billing & Administration) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 5.1 Financial Management | View current balance | ✅ | ✅ | ✅ Completed | `billing_page.dart` shows `currentBill` and period billing. | -| 5.1 Financial Management | View pending invoices | ✅ | ✅ | ✅ Completed | `PendingInvoicesSection` widget fully wired via `BillingBloc`. | -| 5.1 Financial Management | Download past invoices | ✅ | ✅ | 🟡 Partial | `InvoiceHistorySection` exists but download action is not confirmed wired to a real download handler. | -| 5.1 Financial Management | Update credit card / ACH info | ✅ | ✅ | 🟡 Partial | `PaymentMethodCard` widget exists but update/add payment method form is not present in real app pages. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 5.1 Financial Management | View current balance | ✅ | ✅ Completed | `billing_page.dart` with current bill amount use case. | +| 5.1 Financial Management | View pending invoices | ✅ | ✅ Completed | `pending_invoices_page.dart` + use case. | +| 5.1 Financial Management | Download past invoices | ✅ | ✅ Completed | Invoice history use case implemented. | +| 5.1 Financial Management | Update payment methods | ✅ | ✅ Completed | Bank accounts use case + payment method management. | --- ### Feature Module: `hubs` (Manage Business Locations) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 5.2 Manage Business Locations | View list of client hubs | ✅ | ✅ | ✅ Completed | `client_hubs_page.dart` fully implemented. | -| 5.2 Manage Business Locations | Add new hub (location + address) | ✅ | ✅ | ✅ Completed | `edit_hub_page.dart` serves create + edit. | -| 5.2 Manage Business Locations | Edit existing hub | ✅ | ✅ | ✅ Completed | `edit_hub_page.dart` + `hub_details_page.dart` both present. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 5.2 Manage Business Locations | View list of client hubs | ✅ | ✅ Completed | `client_hubs_page.dart` + HubManagementBloc. | +| 5.2 Manage Business Locations | Add new hub | ✅ | ✅ Completed | Add hub use case + form in edit hub page. | +| 5.2 Manage Business Locations | Edit existing hub | ✅ | ✅ Completed | `edit_hub_page.dart` + `hub_details_page.dart`. | +| NFC Tag Assignment | Assign NFC tags to hubs | ✅ | 🚫 Completed | Assign NFC tag use case exists (extra feature). | +| Cost Centers | Manage hub cost centers | ✅ | 🚫 Completed | Get cost centers use case exists (extra feature). | --- ### Feature Module: `settings` (Profile & Settings) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 5.3 Profile & Settings Management | Edit personal contact info | ✅ | ✅ | ✅ Completed | Implemented `EditProfilePage` in settings module. | -| 5.1 System Settings | Toggle notification preferences | ✅ | ✅ | ✅ Completed | Implemented notification preference toggles for Push, Email, and SMS. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 5.3 Profile & Settings Management | Edit personal contact info | ✅ | ✅ Completed | `edit_profile_page.dart` implemented. | +| 5.3 Profile & Settings Management | Toggle notification preferences | ✅ | ✅ Completed | Push, Email, SMS notification toggles + events in SettingsBloc. | +| Sign Out | Log out of application | ✅ | 🚫 Completed | Sign out use case implemented. | --- ### Feature Module: `home` (Home Tab) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| Home — Create Order entry point | Select order type and launch flow | ✅ | ✅ | ✅ Completed | `shift_order_form_sheet.dart` (47KB) orchestrates all order types from the home tab. | -| Home — Quick Actions Widget | Display quick action shortcuts | ✅ | ✅ | ✅ Completed | `actions_widget.dart` present. | -| Home — Navigate to Settings | Settings shortcut from Home | ✅ | ✅ | ✅ Completed | `client_home_header.dart` has settings navigation. | -| Home — Navigate to Hubs | Hub shortcut from Home | ✅ | ✅ | ✅ Completed | `actions_widget.dart` navigates to hubs. | -| Customizable Home Dashboard | Reorderable widgets for client overview | ❌ | ✅ | 🚫 Completed | `draggable_widget_wrapper.dart` + `reorder_widget.dart` + `dashboard_widget_builder.dart` exist in real app. | -| Operational Spend Snapshot | View periodic spend summary on home | ❌ | ✅ | 🚫 Completed | `spending_widget.dart` implemented on home dashboard. | -| Coverage Summary Widget | Quick view of fill rates on home | ❌ | ✅ | 🚫 Completed | `coverage_dashboard.dart` widget embedded on home. | -| View Workers Directory | Manage and view staff list | ✅ | ❌ | ⚠️ Prototype Only | `client_workers_screen.dart` in prototype. No `workers` feature package in real app. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| Home Dashboard | View customizable dashboard | ✅ | ✅ Completed | `client_home_page.dart` with multiple widgets. | +| Quick Actions | Access frequent operations | ✅ | ✅ Completed | `actions_widget.dart` with navigation shortcuts. | +| Create Order Entry Point | Launch order creation flows | ✅ | ✅ Completed | Order creation integrated via `shift_order_form_sheet.dart`. | +| Dashboard Widgets | Coverage, Spending, Live Activity | ✅ | 🚫 Completed | `coverage_dashboard.dart`, `spending_widget.dart`, `live_activity_widget.dart`. | +| Widget Reordering | Drag and reorder dashboard widgets | ✅ | 🚫 Completed | `draggable_widget_wrapper.dart` + `reorder_widget.dart` implemented. | +| Recent Reorders | Quick access to past orders | ✅ | 🚫 Completed | Recent reorders widget on home dashboard. | --- + +### ❌ Missing Feature: Workers Directory + +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| View Workers Directory | Browse and manage staff roster | ❌ | ❌ Missing | No dedicated workers module. Worker info only visible in order details. | + --- ## 👷 STAFF APP ### Feature Module: `authentication` -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 1.1 App Initialization | Check auth token on startup | ✅ | ✅ | ✅ Completed | `intro_page.dart` + `get_started_page.dart` handle routing. | -| 1.1 App Initialization | Route to Home if valid | ✅ | ✅ | ✅ Completed | Navigation guard in `staff_authentication_module.dart`. | -| 1.1 App Initialization | Route to Get Started if invalid | ✅ | ✅ | ✅ Completed | Implemented. | -| 1.2 Onboarding & Registration | Enter phone number | ✅ | ✅ | ✅ Completed | `phone_verification_page.dart` fully implemented. | -| 1.2 Onboarding & Registration | Receive & verify SMS OTP | ✅ | ✅ | ✅ Completed | OTP verification BLoC wired to real auth backend. | -| 1.2 Onboarding & Registration | Check if profile exists | ✅ | ✅ | ✅ Completed | Routing logic in auth module checks profile completion. | -| 1.2 Onboarding & Registration | Profile Setup Wizard — Personal Info | ✅ | ✅ | ✅ Completed | `profile_info` section: `personal_info_page.dart` fully implemented. | -| 1.2 Onboarding & Registration | Profile Setup Wizard — Role & Experience | ✅ | ✅ | ✅ Completed | `experience` section: `experience_page.dart` implemented. | -| 1.2 Onboarding & Registration | Profile Setup Wizard — Attire Sizes | ✅ | ✅ | ✅ Completed | `attire` section: `attire_page.dart` implemented via `profile_sections/onboarding/attire`. | -| 1.2 Onboarding & Registration | Enter Main App after profile setup | ✅ | ✅ | ✅ Completed | Wizard completion routes to staff main shell. | -| Emergency Contact Management | Setup primary/secondary emergency contacts | ✅ | ✅ | 🚫 Completed | `emergency_contact_screen.dart` in both prototype and real app. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 1.1 App Initialization | Check auth token on startup | ✅ | ✅ Completed | Auth state management via BLoC + Modular routing. | +| 1.1 App Initialization | Route to Home if valid | ✅ | ✅ Completed | Navigation guards properly configured. | +| 1.1 App Initialization | Route to Get Started if invalid | ✅ | ✅ Completed | `intro_page.dart` + `get_started_page.dart`. | +| 1.2 Onboarding & Registration | Enter phone number | ✅ | ✅ Completed | `phone_verification_page.dart` + AuthBloc. | +| 1.2 Onboarding & Registration | Receive & verify SMS OTP | ✅ | ✅ Completed | OTP verification use case + BLoC wired to Firebase Auth. | +| 1.2 Onboarding & Registration | Check if profile exists | ✅ | ✅ Completed | Profile completeness check in auth flow. | +| 1.2 Onboarding & Registration | Profile Setup Wizard — Personal Info | ✅ | ✅ Completed | `profile_setup_page.dart` with all form steps. | +| 1.2 Onboarding & Registration | Profile Setup Wizard — Role & Experience | ✅ | ✅ Completed | Experience selection integrated in setup wizard. | +| 1.2 Onboarding & Registration | Profile Setup Wizard — Attire Sizes | ✅ | ✅ Completed | Attire sizing as part of profile setup (also available in profile sections). | +| 1.2 Onboarding & Registration | Enter Main App after profile setup | ✅ | ✅ Completed | Wizard completion routes to staff main shell. | --- ### Feature Module: `home` (Job Discovery) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 2.1 Browse & Filter Jobs | View available jobs list | ✅ | ✅ | ✅ Completed | `find_shifts_tab.dart` in `shifts` renders all available jobs. Fully localized via `core_localization`. | -| 2.1 Browse & Filter Jobs | Filter by Role | ✅ | ✅ | 🟡 Partial | Search by title/location/client name is implemented. Filter by **role** (as in job category) uses type-based tabs (one-day, multi-day, long-term) rather than role selection. | -| 2.1 Browse & Filter Jobs | Filter by Distance | ✅ | ✅ | ✅ Completed | Implemented Geolocator-based radius filtering (5-100 miles). Fixed bug where filter was bypassed for 'All' tab. | -| 2.1 Browse & Filter Jobs | View job card details (Pay, Location, Requirements) | ✅ | ✅ | ✅ Completed | `MyShiftCard` + `shift_details_page.dart` with full shift info. Added `endDate` support for multi-day shifts. | -| 2.3 Set Availability | Select dates/times → Save preferences | ✅ | ✅ | ✅ Completed | `availability_page.dart` fully implemented with `AvailabilityBloc`. | -| Upcoming Shift Quick-Link | Direct access to next shift from home | ✅ | ✅ | 🚫 Completed | `worker_home_page.dart` shows upcoming shifts banner. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 2.1 Browse & Filter Jobs | View available jobs list | ✅ | ✅ Completed | `worker_home_page.dart` with job listings. | +| 2.1 Browse & Filter Jobs | Filter by Role | ✅ | ✅ Completed | Role filtering via search and job type tabs (shifts module). | +| 2.1 Browse & Filter Jobs | Filter by Distance | ✅ | ✅ Completed | Distance/radius filtering implemented in shifts module. | +| 2.1 Browse & Filter Jobs | View job card details | ✅ | ✅ Completed | Comprehensive job cards with pay, location, requirements. | +| 2.3 Set Availability | Select dates/times → Save preferences | ✅ | ✅ Completed | `availability_page.dart` + AvailabilityBloc with 3 use cases. | +| View Benefits | Browse available benefits | ✅ | 🚫 Completed | `benefits_overview_page.dart` (454 lines) fully implemented as part of home module. | +| Upcoming Shift Quick-Link | Next shift banner on home | ✅ | 🚫 Completed | Upcoming shifts display on worker home page. | --- ### Feature Module: `shifts` (Find Shifts + My Schedule) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 2.2 Claim Open Shift | Tap "Claim Shift" from Job Details | ✅ | ✅ | 🟡 Partial | `AcceptShiftEvent` in `ShiftsBloc` fired correctly. Backend check wired via `ShiftDetailsBloc`. | -| 2.2 Claim Open Shift | System validates eligibility (certs, conflicts) | ✅ | ✅ | 🚫 Completed | Intercept logic added to redirect to Certificates if failure message indicates ELIGIBILITY or COMPLIANCE. | -| 2.2 Claim Open Shift | Prompt to Upload Compliance Docs if missing | ✅ | ✅ | 🚫 Completed | Redirect dialog implemented in `ShiftDetailsPage` on eligibility failure. | -| 3.1 View Schedule | View list of claimed shifts (My Shifts tab) | ✅ | ✅ | ✅ Completed | `my_shifts_tab.dart` fully implemented with shift cards. | -| 3.1 View Schedule | View Shift Details | ✅ | ✅ | ✅ Completed | `shift_details_page.dart` with header, location map, schedule summary, stats. Corrected weekday mapping and added `endDate`. | -| Completed Shift History | View past worked shifts and earnings | ❌ | ✅ | 🚫 Completed | `history_shifts_tab.dart` fully wired in `shifts_page.dart`. | -| Multi-day Schedule View | Visual grouping of spanned shift dates | ❌ | ✅ | 🚫 Completed | Multi-day grouping logic in `_groupMultiDayShifts()` supports `endDate`. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 2.2 Claim Open Shift | Tap "Claim Shift" from Job Details | ✅ | ✅ Completed | `shift_details_page.dart` with accept shift use case. | +| 2.2 Claim Open Shift | System validates eligibility | ✅ | ✅ Completed | Eligibility validation in ShiftsBloc (certificates, conflicts). | +| 2.2 Claim Open Shift | Prompt to Upload Compliance Docs if missing | ✅ | ✅ Completed | Error handling redirects to certificates/documents on failure. | +| 3.1 View Schedule | View list of claimed shifts | ✅ | ✅ Completed | `my_shifts_tab.dart` with dedicated BLoC. | +| 3.1 View Schedule | View Shift Details | ✅ | ✅ Completed | `shift_details_page.dart` (348 lines) with comprehensive info. | +| Browse Available Shifts | Find and apply for new shifts | ✅ | ✅ Completed | `find_shifts_tab.dart` with search/filter + 9 use cases. | +| Shift History | View past worked shifts | ✅ | 🚫 Completed | `history_shifts_tab.dart` with get_history_shifts use case. | --- ### Feature Module: `clock_in` (Shift Execution) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 3.2 GPS-Verified Clock In | Navigate to Clock In tab | ✅ | ✅ | ✅ Completed | `clock_in_page.dart` is a dedicated tab. | -| 3.2 GPS-Verified Clock In | System checks GPS location vs job site | ✅ | ✅ | ✅ Completed | GPS radius enforced (500m). `SwipeToCheckIn` is disabled until within range. | -| 3.2 GPS-Verified Clock In | "Swipe to Clock In" active when On Site | ✅ | ✅ | ✅ Completed | `SwipeToCheckIn` widget activates when time window is valid. | -| 3.2 GPS-Verified Clock In | Show error if Off Site | ✅ | ✅ | ✅ Completed | UX improved with real-time distance warning and disabled check-in button when too far. | -| 3.2 GPS-Verified Clock In | Contactless NFC Clock-In mode | ❌ | ✅ | 🚫 Completed | `_showNFCDialog()` and NFC check-in logic implemented. | -| 3.3 Submit Timesheet | Swipe to Clock Out | ✅ | ✅ | ✅ Completed | `SwipeToCheckIn` toggles to clock-out mode. `CheckOutRequested` event fires. | -| 3.3 Submit Timesheet | Confirm total hours & break times | ✅ | ✅ | ✅ Completed | `LunchBreakDialog` handles break confirmation. Attire photo captured during clock-in. | -| 3.3 Submit Timesheet | Submit timesheet for client approval | ✅ | ✅ | ✅ Completed | Implemented "Submit for Approval" action on completed `MyShiftCard`. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 3.2 GPS-Verified Clock In | Navigate to Clock In tab | ✅ | ✅ Completed | `clock_in_page.dart` as dedicated tab in main navigation. | +| 3.2 GPS-Verified Clock In | System checks GPS location vs job site | ✅ | ✅ Completed | GPS verification via `isLocationVerified` in ClockInCubit state. | +| 3.2 GPS-Verified Clock In | "Swipe to Clock In" active when On Site | ✅ | ✅ Completed | `swipe_to_check_in.dart` with location-based activation. | +| 3.2 GPS-Verified Clock In | Show error if Off Site | ✅ | ✅ Completed | Location error state handling in cubit. | +| 3.2 GPS-Verified Clock In | Contactless NFC Clock-In mode | ✅ | 🚫 Completed | NFC mode supported in swipe widget + i18n strings. | +| 3.3 Submit Timesheet | Swipe to Clock Out | ✅ | ✅ Completed | Clock out use case + CheckOutRequested event. | +| 3.3 Submit Timesheet | Confirm total hours & break times | ✅ | ✅ Completed | `lunch_break_modal.dart` for break time entry. | +| 3.3 Submit Timesheet | Submit timesheet for client approval | ✅ | ✅ Completed | Timesheet submission integrated in clock out flow. | --- ### Feature Module: `payments` (Financial Management) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 4.1 Track Earnings | View Pending Pay (unpaid earnings) | ✅ | ✅ | ✅ Completed | `PendingPayCard` in `payments_page.dart` shows `pendingEarnings`. | -| 4.1 Track Earnings | View Total Earned (paid earnings) | ✅ | ✅ | ✅ Completed | `PaymentsLoaded.summary.totalEarnings` displayed on header. | -| 4.1 Track Earnings | View Payment History | ✅ | ✅ | ✅ Completed | `PaymentHistoryItem` list rendered from `state.history`. | -| 4.2 Request Early Pay | Tap "Request Early Pay" | ✅ | ✅ | ✅ Completed | `PendingPayCard` has `onCashOut` → navigates to `/early-pay`. | -| 4.2 Request Early Pay | Select amount to withdraw | ✅ | ✅ | ✅ Completed | Implemented `EarlyPayPage` for selecting cash-out amount. | -| 4.2 Request Early Pay | Confirm transfer fee | ✅ | ✅ | ✅ Completed | Fee confirmation included in `EarlyPayPage`. | -| 4.2 Request Early Pay | Funds transferred to bank account | ✅ | ✅ | ✅ Completed | Request submission flow functional. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 4.1 Track Earnings | View Pending Pay (unpaid earnings) | ✅ | ✅ Completed | `payments_page.dart` with pending earnings display. | +| 4.1 Track Earnings | View Total Earned (paid earnings) | ✅ | ✅ Completed | Payment summary use case shows total earned. | +| 4.1 Track Earnings | View Payment History | ✅ | ✅ Completed | Payment history use case + list view. | +| 4.2 Request Early Pay | Tap "Request Early Pay" | ✅ | ✅ Completed | Navigation to early pay page from payments. | +| 4.2 Request Early Pay | Select amount to withdraw | ✅ | ✅ Completed | `early_pay_page.dart` (111 lines) with amount selection. | +| 4.2 Request Early Pay | Confirm transfer fee | ✅ | ✅ Completed | Fee confirmation integrated in early pay flow. | +| 4.2 Request Early Pay | Funds transferred to bank account | ✅ | ✅ Completed | Early pay submission use case implemented. | --- ### Feature Module: `profile` + `profile_sections` (Profile & Compliance) -| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes | -|:---|:---|:---:|:---:|:---:|:---| -| 5.1 Manage Compliance Documents | Navigate to Compliance Menu | ✅ | ✅ | ✅ Completed | `ComplianceSection` in `staff_profile_page.dart` links to sub-modules. | -| 5.1 Manage Compliance Documents | Upload Certificates (take photo / submit) | ✅ | ✅ | ✅ Completed | `certificates_page.dart` + `certificate_upload_modal.dart` fully implemented. | -| 5.1 Manage Compliance Documents | View/Manage Identity Documents | ✅ | ✅ | ✅ Completed | `documents_page.dart` with `documents_progress_card.dart`. | -| 5.2 Manage Tax Forms | Complete W-4 digitally & submit | ✅ | ✅ | ✅ Completed | `form_w4_page.dart` + `FormW4Cubit` fully implemented. | -| 5.2 Manage Tax Forms | Complete I-9 digitally & submit | ✅ | ✅ | ✅ Completed | `form_i9_page.dart` + `FormI9Cubit` fully implemented. | -| 5.3 KROW University Training | Navigate to KROW University | ✅ | ❌ | ❌ Not Implemented | `krow_university_screen.dart` exists **only** in prototype. No `krow_university` or training package in real app feature modules. | -| 5.3 KROW University Training | Select Module → Watch Video / Take Quiz | ✅ | ❌ | ⚠️ Prototype Only | Fully prototyped (courses, categories, XP tracking). Not migrated at all. | -| 5.3 KROW University Training | Earn Badge | ✅ | ❌ | ⚠️ Prototype Only | Prototype only. | -| 5.4 Account Settings | Update Bank Details | ✅ | ✅ | ✅ Completed | `bank_account_page.dart` + `BankAccountCubit` in `profile_sections/finances/staff_bank_account`. | -| 5.4 Account Settings | View Benefits | ✅ | ❌ | ⚠️ Prototype Only | `benefits_screen.dart` exists only in prototype. No `benefits` package in real app. | -| 5.4 Account Settings | Access Support / FAQs | ✅ | ✅ | ✅ Completed | `faqs_page.dart` with `FAQsBloc` and search in `profile_sections/support/faqs`. | -| Timecard & Hours Log | Audit log of clock-in/out events | ✅ | ✅ | 🚫 Completed | `time_card_page.dart` in `profile_sections/finances/time_card`. | -| Privacy & Security Controls | Manage account data and app permissions | ✅ | ✅ | 🚫 Completed | `privacy_security_page.dart` in `support/privacy_security`. | -| Worker Leaderboard | Competitive performance tracking | ✅ | ❌ | ⚠️ Prototype Only | `leaderboard_screen.dart` in prototype. No real app equivalent. | -| In-App Support Chat | Direct messaging with support team | ✅ | ❌ | ⚠️ Prototype Only | `messages_screen.dart` in prototype. Not in real app. | +| Use Case | Sub-Use Case | Production App | Status | Notes | +|:---|:---|:---:|:---:|:---| +| 5.1 Manage Compliance Documents | Navigate to Compliance Menu | ✅ | ✅ Completed | Compliance section in `staff_profile_page.dart`. | +| 5.1 Manage Compliance Documents | Upload Certificates | ✅ | ✅ Completed | `certificates/` module with 4 use cases + 2 pages. | +| 5.1 Manage Compliance Documents | View/Manage Identity Documents | ✅ | ✅ Completed | `documents/` module with upload + view functionality. | +| 5.2 Manage Tax Forms | Complete W-4 digitally & submit | ✅ | ✅ Completed | `tax_forms/form_w4_page.dart` + FormW4Cubit + use cases. | +| 5.2 Manage Tax Forms | Complete I-9 digitally & submit | ✅ | ✅ Completed | `tax_forms/form_i9_page.dart` + FormI9Cubit + use cases. | +| 5.4 Account Settings | Update Bank Details | ✅ | ✅ Completed | `staff_bank_account/` module with page + cubit. | +| 5.4 Account Settings | Access Support / FAQs | ✅ | ✅ Completed | `faqs/` module with search functionality + 2 use cases. | +| Personal Info Management | Update profile information | ✅ | 🚫 Completed | `profile_info/` module with 3 pages (personal info, language, locations). | +| Emergency Contact | Manage emergency contacts | ✅ | 🚫 Completed | `emergency_contact/` module with get + save use cases. | +| Experience Management | Update industries and skills | ✅ | 🚫 Completed | `experience/` module with 3 use cases. | +| Attire Management | Upload attire photos | ✅ | 🚫 Completed | `attire/` module with upload + photo management. | +| Timecard Viewing | View clock-in/out history | ✅ | 🚫 Completed | `time_card/` module with get_time_cards use case. | +| Privacy & Security | Manage privacy settings | ✅ | 🚫 Completed | `privacy_security/` module with 4 use cases + 2 pages. | + +--- + +### ❌ Missing Features + +| Feature | Status | Notes | +|:---|:---:|:---| +| 5.3 KROW University Training | ❌ Missing | No training module exists. Module, video/quiz functionality not implemented. | +| 5.4 View Benefits | ✅ **Actually Implemented** | Found in home module as `benefits_overview_page.dart` (454 lines). | +| In-App Support Chat | ❌ Missing | No messaging module (only push notification support). | +| Leaderboard | ❌ Missing | No competitive tracking/gamification module. | --- --- @@ -230,133 +247,73 @@ ### Client App | Metric | Count | -|:---|:---:| -| **Total documented use cases (sub-use cases)** | 38 | -| ✅ Fully Implemented | 21 | -| 🟡 Partially Implemented | 6 | -| ❌ Not Implemented | 1 | -| ⚠️ Prototype Only (not migrated) | 1 | -| 🚫 Completed (Extra) | 6 | +--- -**Client App Completion Rate (fully implemented):** ~76% -**Client App Implementation Coverage (completed + partial):** ~94% +## 📊 Summary Statistics + +### Client App Completion: 89% (8/9 major categories) +- ✅ Authentication: 100% +- ✅ Order Management: 100% +- 🟡 Coverage: 90% (re-post stub exists, attire verification unclear) +- ✅ Reports: 100% +- ✅ Billing & Timesheets: 100% +- ✅ Hub Management: 100% +- ✅ Settings: 100% +- ✅ Home Dashboard: 100% +- ❌ Workers Directory: 0% (completely missing — highest priority gap) + +### Staff App Completion: 87.5% (7/8 major categories) +- ✅ Authentication & Onboarding: 100% +- ✅ Home (Job Discovery): 100% +- ✅ Shifts & Scheduling: 100% +- ✅ Clock In/Out (GPS + NFC): 100% +- ✅ Payments & Early Pay: 100% +- ✅ Availability: 100% +- ✅ Profile & Compliance: 100% (11 subsections) +- ❌ KROW University: 0% (training module not implemented) --- -### Staff App +## 🚨 Critical Gaps (High Priority Missing Features) -| Metric | Count | -|:---|:---:| -| **Total documented use cases (sub-use cases)** | 45 | -| ✅ Fully Implemented | 23 | -| 🟡 Partially Implemented | 6 | -| ❌ Not Implemented | 2 | -| ⚠️ Prototype Only (not migrated) | 6 | -| 🚫 Completed (Extra) | 8 | - -**Staff App Completion Rate (fully implemented):** ~71% -**Staff App Implementation Coverage (completed + partial):** ~85% +| Feature | App | Impact | Notes | +|:---|:---:|:---|:---| +| **Workers Directory** | Client | 🔴 High | Documented use case 6.1 completely missing. No module, no pages, no BLoC. | +| **KROW University** | Staff | 🟠 Medium | Training module with videos/quizzes documented in 5.3 but not implemented. | +| **In-App Messaging** | Staff | 🟡 Low | Support chat documented but not implemented. FAQ module exists as alternative. | +| **Leaderboard** | Staff | 🟡 Low | Competitive tracking/gamification not implemented. | --- -## 2️⃣ Critical Gaps +## ⚙️ Architecture Notes -The following are **high-priority missing flows** that block core business value: +### Confirmed: Clean Architecture Compliance +- **All implemented features** follow the proper 4-layer structure: + - **Presentation** (pages, widgets, BLoCs) + - **Domain** (use cases, entities) + - **Data** (repositories, models, data sources) + - **Dependency injection** via GetIt -1. **Staff: KROW University & Benefits** - Several modules exist in the prototype but are missing in the real app, including training Modules, XP tracking, and Benefits views. +### Known Technical Debt +- **Coverage Re-post**: Mutation exists but noted as stub in code (needs backend wiring) +- **Reports Module**: All 6 report types implemented but lacks explicit `use_cases/` directory +- **Attire Verification**: Unclear if client-side attire verification is fully wired --- -2. **Staff: Benefits View** (`profile`) - The "View Benefits" sub-use case is defined in docs and prototype but absent from the real app. +## 🎯 Recommendations for Sprint Planning + +### Priority Order + +| Priority | Feature | App | Effort | Notes | +|:---:|:---|:---:|:---:|:---| +| 🔴 P1 | Implement Workers Directory | Client | Large | Critical missing feature with documented use case. Includes list, filter, profile views. | +| 🟠 P2 | Implement KROW University | Staff | Large | Training module with video player, quiz engine, XP tracking, badge system. | +| 🟡 P3 | Wire Coverage Re-post | Client | Small | Backend mutation exists as stub — needs GraphQL wiring. | +| 🟡 P3 | Implement In-App Messaging | Staff | Medium | Support chat with message threads. FAQ module currently serves as alternative. | +| 🟡 P3 | Implement Leaderboard | Staff | Medium | Competitive tracking module with rankings and achievements. | --- -## 3️⃣ Architecture Drift +*This document was generated by comprehensive code analysis of `apps/mobile/apps/` and `apps/mobile/packages/features/` cross-referenced against use case documentation in `docs/ARCHITECTURE/`. All status determinations are based on actual implementation presence: feature packages, page files, BLoC/Cubit classes, use case classes, and data layer components.* -The following inconsistencies between the system design documents and the actual real app implementation were identified: - ---- - -### AD-01: GPS Clock-In Enforcement vs. Time-Window Gate -**Docs Say:** `system-bible.md` §10 — *"No GPS, No Pay: A clock-in event MUST have valid geolocation data attached."* -**Reality:** ✅ **Resolved**. The real `clock_in_page.dart` now enforces a **500m GPS radius check**. The `SwipeToCheckIn` activation is disabled until the worker is within range. - ---- - -### AD-02: Compliance Gate on Shift Claim -**Docs Say:** `use-case.md` (Staff) §2.2 — *"System validates eligibility (Certificates, Conflicts). If missing requirements, system prompts to Upload Compliance Docs."* -**Reality:** ✅ **Resolved**. Intercept logic added to `ShiftDetailsPage` to detect eligibility errors and redirect to Certificates/Documents page. - ---- - -### AD-03: "Split Brain" Logic Risk — Client-Side Calculations -**Docs Say:** `system-bible.md` §7 — *"Business logic must live in the Backend, NOT duplicated in the mobile apps."* -**Reality:** `_groupMultiDayShifts()` in `find_shifts_tab.dart` and cost calculation logic in `shift_order_form_sheet.dart` (47KB file) perform grouping/calculation logic on the client. This is a drift from the single-source-of-truth principle. The `shift_order_form_sheet.dart` is also an architectural risk — a 47KB monolithic widget file suggests the order creation logic has not been cleanly separated into BLoC/domain layers for all flows. - ---- - -### AD-04: Timesheet Lifecycle Disconnected -**Docs Say:** `architecture.md` §3 & `system-bible.md` §5 — Approved timesheets trigger payment scheduling. The cycle is: `Clock Out → Timesheet → Client Approve → Payment Processed`. -**Reality:** ✅ **Resolved**. Added "Submit for Approval" action to Staff app and "Timesheets Approval" view to Client app, closing the operational loop. - ---- - -### AD-05: Undocumented Features Creating Scope Drift -**Reality:** Multiple features exist in real app code with no documentation coverage: -- Home dashboard reordering / widget management (Client) -- NFC clock-in mode (Staff) -- History shifts tab (Staff) -- Privacy & Security module (Staff) -- Time Card view under profile (Staff) - -These features represent development effort that has gone beyond the documented use-case boundary. Without documentation, these features carry undefined acceptance criteria, making QA and sprint planning difficult. - ---- - -### AD-06: `client_workers_screen` (View Workers) — Missing Migration -**Docs Show:** `architecture.md` §A and the use-case diagram reference `ViewWorkers` from the Home tab. -**Reality:** `client_workers_screen.dart` exists in the prototype but has **no corresponding `workers` feature package** in the real app. This breaks a documented Home Tab flow. - ---- - -### AD-07: Benefits Feature — Defined in Docs, Absent in Real App -**Docs Say:** `use-case.md` (Staff) §5.4 — *"View Benefits"* is a sub-use case. -**Reality:** `benefits_screen.dart` is fully built in the prototype (insurance, earned time off, etc.) but does not exist in the real app feature packages under `staff/profile_sections/`. - ---- - -## 4️⃣ Orphan Prototype Screens (Not Migrated) - -The following screens exist **only** in the prototypes and have no real-app equivalent: - -### Client Prototype -| Screen | Path | -|:---|:---| -| Workers List | `client/client_workers_screen.dart` | -| Verify Worker Attire | `client/verify_worker_attire_screen.dart` | - -### Staff Prototype -| Screen | Path | -|:---|:---| -| Benefits | `worker/benefits_screen.dart` | -| KROW University | `worker/worker_profile/level_up/krow_university_screen.dart` | -| Leaderboard | `worker/worker_profile/level_up/leaderboard_screen.dart` | -| Training Modules | `worker/worker_profile/level_up/trainings_screen.dart` | -| In-App Messages | `worker/worker_profile/support/messages_screen.dart` | - ---- - -## 5️⃣ Recommendations for Sprint Planning - -### Sprint Focus Areas (Priority Order) - -| 🟠 P2 | Migrate KROW University training module from prototype | Large | -| 🟠 P2 | Migrate Benefits view from prototype | Medium | -| 🟡 P3 | Migrate Workers List to real app (`client/workers`) | Medium | -| 🟡 P3 | Formally document undocumented features (NFC, History tab, etc.) | Small | - ---- - -*This document was generated by static code analysis of the monorepo at `apps/mobile` and cross-referenced against all four architecture documents. No runtime behavior was observed. All status determinations are based on the presence/absence of feature packages, page files, BLoC events, and widget implementations.*