Merge pull request #562 from Oloodi/493-implement-rapid-order-creation-voice-text-in-client-mobile-app
Session clearance issue fixed
This commit is contained in:
@@ -59,7 +59,7 @@ class CoreBlocObserver extends BlocObserver {
|
|||||||
super.onChange(bloc, change);
|
super.onChange(bloc, change);
|
||||||
if (logStateChanges) {
|
if (logStateChanges) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}',
|
'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}',
|
||||||
name: bloc.runtimeType.toString(),
|
name: bloc.runtimeType.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ class CoreBlocObserver extends BlocObserver {
|
|||||||
super.onTransition(bloc, transition);
|
super.onTransition(bloc, transition);
|
||||||
if (logStateChanges) {
|
if (logStateChanges) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}',
|
'Transition: ${transition.event.runtimeType}’ ${transition.nextState.runtimeType}',
|
||||||
name: bloc.runtimeType.toString(),
|
name: bloc.runtimeType.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_emergency_contacts_completion_usecase.dart';
|
||||||
export 'src/connectors/staff/domain/usecases/get_experience_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_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/get_staff_profile_usecase.dart';
|
||||||
export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart';
|
export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart';
|
||||||
export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart';
|
export '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_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
|
|
||||||
import '../../domain/repositories/staff_connector_repository.dart';
|
import '../../domain/repositories/staff_connector_repository.dart';
|
||||||
|
|
||||||
/// Implementation of [StaffConnectorRepository].
|
/// Implementation of [StaffConnectorRepository].
|
||||||
@@ -33,10 +34,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
final dc.GetStaffProfileCompletionStaff? staff = response.data.staff;
|
final dc.GetStaffProfileCompletionStaff? staff = response.data.staff;
|
||||||
final List<dc.GetStaffProfileCompletionEmergencyContacts>
|
final List<dc.GetStaffProfileCompletionEmergencyContacts>
|
||||||
emergencyContacts = response.data.emergencyContacts;
|
emergencyContacts = response.data.emergencyContacts;
|
||||||
final List<dc.GetStaffProfileCompletionTaxForms> taxForms =
|
return _isProfileComplete(staff, emergencyContacts);
|
||||||
response.data.taxForms;
|
|
||||||
|
|
||||||
return _isProfileComplete(staff, emergencyContacts, taxForms);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +105,140 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
.getStaffTaxFormsProfileCompletion(id: staffId)
|
.getStaffTaxFormsProfileCompletion(id: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return response.data.taxForms.isNotEmpty;
|
final List<dc.GetStaffTaxFormsProfileCompletionTaxForms> 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<dc.TaxFormStatus>).value;
|
||||||
|
return status == dc.TaxFormStatus.SUBMITTED;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool?> getAttireOptionsCompletion() async {
|
||||||
|
return _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
|
final List<QueryResult<Object, Object?>> results =
|
||||||
|
await Future.wait<QueryResult<Object, Object?>>(
|
||||||
|
<Future<QueryResult<Object, Object?>>>[
|
||||||
|
_service.connector.listAttireOptions().execute(),
|
||||||
|
_service.connector.getStaffAttire(staffId: staffId).execute(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final QueryResult<dc.ListAttireOptionsData, void> optionsRes =
|
||||||
|
results[0] as QueryResult<dc.ListAttireOptionsData, void>;
|
||||||
|
final QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>
|
||||||
|
staffAttireRes =
|
||||||
|
results[1]
|
||||||
|
as QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>;
|
||||||
|
|
||||||
|
final List<dc.ListAttireOptionsAttireOptions> attireOptions =
|
||||||
|
optionsRes.data.attireOptions;
|
||||||
|
final List<dc.GetStaffAttireStaffAttires> staffAttire =
|
||||||
|
staffAttireRes.data.staffAttires;
|
||||||
|
|
||||||
|
// Get only mandatory attire options
|
||||||
|
final List<dc.ListAttireOptionsAttireOptions> 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<dc.AttireVerificationStatus>)
|
||||||
|
.value;
|
||||||
|
return status == dc.AttireVerificationStatus.APPROVED;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool?> 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<dc.ListStaffDocumentsByStaffIdStaffDocuments> 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<dc.DocumentStatus>).value;
|
||||||
|
return status == dc.DocumentStatus.VERIFIED;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool?> 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<dc.ListCertificatesByStaffIdCertificates> 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<dc.ValidationStatus>).value;
|
||||||
|
return status == dc.ValidationStatus.APPROVED;
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +265,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
bool _isProfileComplete(
|
bool _isProfileComplete(
|
||||||
dc.GetStaffProfileCompletionStaff? staff,
|
dc.GetStaffProfileCompletionStaff? staff,
|
||||||
List<dc.GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
|
List<dc.GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
|
||||||
List<dc.GetStaffProfileCompletionTaxForms> taxForms,
|
|
||||||
) {
|
) {
|
||||||
if (staff == null) return false;
|
if (staff == null) return false;
|
||||||
|
|
||||||
@@ -146,7 +276,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return (staff.fullName.trim().isNotEmpty) &&
|
return (staff.fullName.trim().isNotEmpty) &&
|
||||||
(staff.email?.trim().isNotEmpty ?? false) &&
|
(staff.email?.trim().isNotEmpty ?? false) &&
|
||||||
emergencyContacts.isNotEmpty &&
|
emergencyContacts.isNotEmpty &&
|
||||||
taxForms.isNotEmpty &&
|
|
||||||
hasExperience;
|
hasExperience;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +328,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return response.data.benefitsDatas.map((
|
return response.data.benefitsDatas.map((
|
||||||
dc.ListBenefitsDataByStaffIdBenefitsDatas e,
|
dc.ListBenefitsDataByStaffIdBenefitsDatas e,
|
||||||
) {
|
) {
|
||||||
final total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0;
|
final double total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0;
|
||||||
final remaining = e.current.toDouble();
|
final double remaining = e.current.toDouble();
|
||||||
return domain.Benefit(
|
return domain.Benefit(
|
||||||
title: e.vendorBenefitPlan.title,
|
title: e.vendorBenefitPlan.title,
|
||||||
entitlementHours: total,
|
entitlementHours: total,
|
||||||
@@ -346,8 +475,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
_service.clearCache();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Error signing out: ${e.toString()}');
|
throw Exception('Error signing out: ${e.toString()}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,21 @@ abstract interface class StaffConnectorRepository {
|
|||||||
/// Returns true if at least one tax form exists.
|
/// Returns true if at least one tax form exists.
|
||||||
Future<bool> getTaxFormsCompletion();
|
Future<bool> getTaxFormsCompletion();
|
||||||
|
|
||||||
|
/// Fetches attire options completion status.
|
||||||
|
///
|
||||||
|
/// Returns true if all mandatory attire options are verified.
|
||||||
|
Future<bool?> getAttireOptionsCompletion();
|
||||||
|
|
||||||
|
/// Fetches documents completion status.
|
||||||
|
///
|
||||||
|
/// Returns true if all mandatory documents are verified.
|
||||||
|
Future<bool?> getStaffDocumentsCompletion();
|
||||||
|
|
||||||
|
/// Fetches certificates completion status.
|
||||||
|
///
|
||||||
|
/// Returns true if all certificates are validated.
|
||||||
|
Future<bool?> getStaffCertificatesCompletion();
|
||||||
|
|
||||||
/// Fetches the full staff profile for the current authenticated user.
|
/// Fetches the full staff profile for the current authenticated user.
|
||||||
///
|
///
|
||||||
/// Returns a [Staff] entity containing all profile information.
|
/// Returns a [Staff] entity containing all profile information.
|
||||||
|
|||||||
@@ -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<bool?> {
|
||||||
|
/// 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<bool?> call() => _repository.getAttireOptionsCompletion();
|
||||||
|
}
|
||||||
@@ -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<bool?> {
|
||||||
|
/// 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<bool?> call() => _repository.getStaffCertificatesCompletion();
|
||||||
|
}
|
||||||
@@ -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<bool?> {
|
||||||
|
/// 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<bool?> call() => _repository.getStaffDocumentsCompletion();
|
||||||
|
}
|
||||||
@@ -224,8 +224,19 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signs out the current user from Firebase Auth and clears all session data.
|
||||||
|
Future<void> signOut() async {
|
||||||
|
try {
|
||||||
|
await auth.signOut();
|
||||||
|
_clearCache();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('DataConnectService: Error signing out: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Clears Cached Repositories and Session data.
|
/// Clears Cached Repositories and Session data.
|
||||||
void clearCache() {
|
void _clearCache() {
|
||||||
_reportsRepository = null;
|
_reportsRepository = null;
|
||||||
_shiftsRepository = null;
|
_shiftsRepository = null;
|
||||||
_hubsRepository = null;
|
_hubsRepository = null;
|
||||||
|
|||||||
@@ -338,8 +338,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
_service.clearCache();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Error signing out: ${e.toString()}');
|
throw Exception('Error signing out: ${e.toString()}');
|
||||||
}
|
}
|
||||||
@@ -371,9 +370,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
if (requireBusinessRole &&
|
if (requireBusinessRole &&
|
||||||
user.userRole != 'BUSINESS' &&
|
user.userRole != 'BUSINESS' &&
|
||||||
user.userRole != 'BOTH') {
|
user.userRole != 'BOTH') {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
dc.ClientSessionStore.instance.clear();
|
|
||||||
_service.clearCache();
|
|
||||||
throw UnauthorizedAppException(
|
throw UnauthorizedAppException(
|
||||||
technicalMessage:
|
technicalMessage:
|
||||||
'User role is ${user.userRole}, expected BUSINESS or BOTH',
|
'User role is ${user.userRole}, expected BUSINESS or BOTH',
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// ── Header ──────────────────────────────────────────
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 60,
|
top: 60,
|
||||||
@@ -151,7 +150,6 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── Content ─────────────────────────────────────────
|
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: const Offset(0, -16),
|
offset: const Offset(0, -16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -241,7 +239,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Summary chip (top 3 stats) ───────────────────────────────────────────────
|
// Summary chip (top 3 stats)
|
||||||
class _SummaryChip extends StatelessWidget {
|
class _SummaryChip extends StatelessWidget {
|
||||||
|
|
||||||
const _SummaryChip({
|
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 {
|
class _WorkerCard extends StatelessWidget {
|
||||||
|
|
||||||
const _WorkerCard({required this.worker});
|
const _WorkerCard({required this.worker});
|
||||||
@@ -448,5 +446,5 @@ class _WorkerCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Insight line ─────────────────────────────────────────────────────────────
|
// Insight line
|
||||||
|
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
if (state is PerformanceLoaded) {
|
if (state is PerformanceLoaded) {
|
||||||
final PerformanceReport report = state.report;
|
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) +
|
final double overallScore = ((report.fillRate * 0.3) +
|
||||||
(report.completionRate * 0.3) +
|
(report.completionRate * 0.3) +
|
||||||
(report.onTimeRate * 0.25) +
|
(report.onTimeRate * 0.25) +
|
||||||
// avg fill time: 3h target → invert to score
|
// avg fill time: 3h target invert to score
|
||||||
((report.avgFillTimeHours <= 3
|
((report.avgFillTimeHours <= 3
|
||||||
? 100
|
? 100
|
||||||
: (3 / report.avgFillTimeHours) * 100) *
|
: (3 / report.avgFillTimeHours) * 100) *
|
||||||
@@ -107,7 +107,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
iconColor: const Color(0xFFF39C12),
|
iconColor: const Color(0xFFF39C12),
|
||||||
label: context.t.client_reports.performance_report.kpis.avg_fill_time,
|
label: context.t.client_reports.performance_report.kpis.avg_fill_time,
|
||||||
target: context.t.client_reports.performance_report.kpis.target_hours(hours: '3'),
|
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
|
value: report.avgFillTimeHours == 0
|
||||||
? 100
|
? 100
|
||||||
: (3 / report.avgFillTimeHours * 100).clamp(0, 100),
|
: (3 / report.avgFillTimeHours * 100).clamp(0, 100),
|
||||||
@@ -122,7 +122,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// ── Header ───────────────────────────────────────────
|
// Header
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 60,
|
top: 60,
|
||||||
@@ -225,14 +225,14 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── Content ──────────────────────────────────────────
|
// ” Content ”””””””””””””””””””””
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: const Offset(0, -16),
|
offset: const Offset(0, -16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// ── Overall Score Hero Card ───────────────────
|
// Overall Score Hero Card
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -299,7 +299,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// ── KPI List ─────────────────────────────────
|
// KPI List
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -349,7 +349,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── KPI data model ────────────────────────────────────────────────────────────
|
// ” KPI data model ””””””””””””””””””””””””””””””
|
||||||
class _KpiData {
|
class _KpiData {
|
||||||
|
|
||||||
const _KpiData({
|
const _KpiData({
|
||||||
@@ -367,14 +367,14 @@ class _KpiData {
|
|||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
final String label;
|
final String label;
|
||||||
final String target;
|
final String target;
|
||||||
final double value; // 0–100 for bar
|
final double value; // 0-100 for bar
|
||||||
final String displayValue;
|
final String displayValue;
|
||||||
final Color barColor;
|
final Color barColor;
|
||||||
final bool met;
|
final bool met;
|
||||||
final bool close;
|
final bool close;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── KPI row widget ────────────────────────────────────────────────────────────
|
// ” KPI row widget ””””””””””””””””””””””””””””””
|
||||||
class _KpiRow extends StatelessWidget {
|
class _KpiRow extends StatelessWidget {
|
||||||
|
|
||||||
const _KpiRow({required this.kpi});
|
const _KpiRow({required this.kpi});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SettingsRepositoryImpl implements SettingsRepositoryInterface {
|
|||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,10 +96,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() {
|
Future<void> signOut() async {
|
||||||
StaffSessionStore.instance.clear();
|
return await _service.signOut();
|
||||||
_service.clearCache();
|
|
||||||
return _service.auth.signOut();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies an OTP code and returns the authenticated user.
|
/// Verifies an OTP code and returns the authenticated user.
|
||||||
@@ -163,7 +161,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
|
|
||||||
if (staffResponse.data.staffs.isNotEmpty) {
|
if (staffResponse.data.staffs.isNotEmpty) {
|
||||||
// If profile exists, they should use Login mode.
|
// If profile exists, they should use Login mode.
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
throw const domain.AccountExistsException(
|
throw const domain.AccountExistsException(
|
||||||
technicalMessage:
|
technicalMessage:
|
||||||
'This user already has a staff profile. Please log in.',
|
'This user already has a staff profile. Please log in.',
|
||||||
@@ -185,14 +183,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
throw const domain.UserNotFoundException(
|
throw const domain.UserNotFoundException(
|
||||||
technicalMessage: 'Authenticated user profile not found in database.',
|
technicalMessage: 'Authenticated user profile not found in database.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Allow STAFF or BOTH roles to log in to the Staff App
|
// Allow STAFF or BOTH roles to log in to the Staff App
|
||||||
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
|
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
throw const domain.UnauthorizedAppException(
|
throw const domain.UnauthorizedAppException(
|
||||||
technicalMessage: 'User is not authorized for this app.',
|
technicalMessage: 'User is not authorized for this app.',
|
||||||
);
|
);
|
||||||
@@ -206,7 +204,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
requiresAuthentication: false,
|
requiresAuthentication: false,
|
||||||
);
|
);
|
||||||
if (staffResponse.data.staffs.isEmpty) {
|
if (staffResponse.data.staffs.isEmpty) {
|
||||||
await _service.auth.signOut();
|
await _service.signOut();
|
||||||
throw const domain.UserNotFoundException(
|
throw const domain.UserNotFoundException(
|
||||||
technicalMessage:
|
technicalMessage:
|
||||||
'Your account is not registered yet. Please register first.',
|
'Your account is not registered yet. Please register first.',
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
style: UiTypography.body2b,
|
style: UiTypography.body2b,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${shift.clientName} • ${shift.location}",
|
"${shift.clientName} ${shift.location}",
|
||||||
style: UiTypography
|
style: UiTypography
|
||||||
.body3r
|
.body3r
|
||||||
.textSecondary,
|
.textSecondary,
|
||||||
|
|||||||
@@ -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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
|
||||||
import 'package:krow_domain/krow_domain.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 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Page displaying a detailed overview of the worker's benefits.
|
/// Page displaying a detailed overview of the worker's benefits.
|
||||||
class BenefitsOverviewPage extends StatelessWidget {
|
class BenefitsOverviewPage extends StatelessWidget {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
this._getEmergencyContactsCompletionUseCase,
|
this._getEmergencyContactsCompletionUseCase,
|
||||||
this._getExperienceCompletionUseCase,
|
this._getExperienceCompletionUseCase,
|
||||||
this._getTaxFormsCompletionUseCase,
|
this._getTaxFormsCompletionUseCase,
|
||||||
|
this._getAttireOptionsCompletionUseCase,
|
||||||
|
this._getStaffDocumentsCompletionUseCase,
|
||||||
|
this._getStaffCertificatesCompletionUseCase,
|
||||||
) : super(const ProfileState());
|
) : super(const ProfileState());
|
||||||
final GetStaffProfileUseCase _getProfileUseCase;
|
final GetStaffProfileUseCase _getProfileUseCase;
|
||||||
final SignOutStaffUseCase _signOutUseCase;
|
final SignOutStaffUseCase _signOutUseCase;
|
||||||
@@ -26,6 +29,9 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
|
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
|
||||||
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
|
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
|
||||||
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
|
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
|
||||||
|
final GetAttireOptionsCompletionUseCase _getAttireOptionsCompletionUseCase;
|
||||||
|
final GetStaffDocumentsCompletionUseCase _getStaffDocumentsCompletionUseCase;
|
||||||
|
final GetStaffCertificatesCompletionUseCase _getStaffCertificatesCompletionUseCase;
|
||||||
|
|
||||||
/// Loads the staff member's profile.
|
/// Loads the staff member's profile.
|
||||||
///
|
///
|
||||||
@@ -130,5 +136,47 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads attire options completion status.
|
||||||
|
Future<void> 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<void> 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<void> 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class ProfileState extends Equatable {
|
|||||||
this.emergencyContactsComplete,
|
this.emergencyContactsComplete,
|
||||||
this.experienceComplete,
|
this.experienceComplete,
|
||||||
this.taxFormsComplete,
|
this.taxFormsComplete,
|
||||||
|
this.attireComplete,
|
||||||
|
this.documentsComplete,
|
||||||
|
this.certificatesComplete,
|
||||||
});
|
});
|
||||||
/// Current status of the profile feature
|
/// Current status of the profile feature
|
||||||
final ProfileStatus status;
|
final ProfileStatus status;
|
||||||
@@ -55,6 +58,15 @@ class ProfileState extends Equatable {
|
|||||||
/// Whether tax forms are complete
|
/// Whether tax forms are complete
|
||||||
final bool? taxFormsComplete;
|
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.
|
/// Creates a copy of this state with updated values.
|
||||||
ProfileState copyWith({
|
ProfileState copyWith({
|
||||||
ProfileStatus? status,
|
ProfileStatus? status,
|
||||||
@@ -64,6 +76,9 @@ class ProfileState extends Equatable {
|
|||||||
bool? emergencyContactsComplete,
|
bool? emergencyContactsComplete,
|
||||||
bool? experienceComplete,
|
bool? experienceComplete,
|
||||||
bool? taxFormsComplete,
|
bool? taxFormsComplete,
|
||||||
|
bool? attireComplete,
|
||||||
|
bool? documentsComplete,
|
||||||
|
bool? certificatesComplete,
|
||||||
}) {
|
}) {
|
||||||
return ProfileState(
|
return ProfileState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -73,6 +88,9 @@ class ProfileState extends Equatable {
|
|||||||
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
||||||
experienceComplete: experienceComplete ?? this.experienceComplete,
|
experienceComplete: experienceComplete ?? this.experienceComplete,
|
||||||
taxFormsComplete: taxFormsComplete ?? this.taxFormsComplete,
|
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,
|
emergencyContactsComplete,
|
||||||
experienceComplete,
|
experienceComplete,
|
||||||
taxFormsComplete,
|
taxFormsComplete,
|
||||||
|
attireComplete,
|
||||||
|
documentsComplete,
|
||||||
|
certificatesComplete,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
cubit.loadEmergencyContactsCompletion();
|
cubit.loadEmergencyContactsCompletion();
|
||||||
cubit.loadExperienceCompletion();
|
cubit.loadExperienceCompletion();
|
||||||
cubit.loadTaxFormsCompletion();
|
cubit.loadTaxFormsCompletion();
|
||||||
|
cubit.loadAttireCompletion();
|
||||||
|
cubit.loadDocumentsCompletion();
|
||||||
|
cubit.loadCertificatesCompletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == ProfileStatus.signedOut) {
|
if (state.status == ProfileStatus.signedOut) {
|
||||||
|
|||||||
@@ -43,11 +43,13 @@ class ComplianceSection extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.file,
|
icon: UiIcons.file,
|
||||||
label: i18n.menu_items.documents,
|
label: i18n.menu_items.documents,
|
||||||
|
completed: state.documentsComplete,
|
||||||
onTap: () => Modular.to.toDocuments(),
|
onTap: () => Modular.to.toDocuments(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.certificate,
|
icon: UiIcons.certificate,
|
||||||
label: i18n.menu_items.certificates,
|
label: i18n.menu_items.certificates,
|
||||||
|
completed: state.certificatesComplete,
|
||||||
onTap: () => Modular.to.toCertificates(),
|
onTap: () => Modular.to.toCertificates(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class OnboardingSection extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.shirt,
|
icon: UiIcons.shirt,
|
||||||
label: i18n.menu_items.attire,
|
label: i18n.menu_items.attire,
|
||||||
|
completed: state.attireComplete,
|
||||||
onTap: () => Modular.to.toAttire(),
|
onTap: () => Modular.to.toAttire(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -50,6 +50,21 @@ class StaffProfileModule extends Module {
|
|||||||
repository: i.get<StaffConnectorRepository>(),
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton<GetAttireOptionsCompletionUseCase>(
|
||||||
|
() => GetAttireOptionsCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetStaffDocumentsCompletionUseCase>(
|
||||||
|
() => GetStaffDocumentsCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetStaffCertificatesCompletionUseCase>(
|
||||||
|
() => GetStaffCertificatesCompletionUseCase(
|
||||||
|
repository: i.get<StaffConnectorRepository>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Presentation layer - Cubit as singleton to avoid recreation
|
// Presentation layer - Cubit as singleton to avoid recreation
|
||||||
// BlocProvider will use this same instance, preventing state emission after close
|
// BlocProvider will use this same instance, preventing state emission after close
|
||||||
@@ -61,6 +76,9 @@ class StaffProfileModule extends Module {
|
|||||||
i.get<GetEmergencyContactsCompletionUseCase>(),
|
i.get<GetEmergencyContactsCompletionUseCase>(),
|
||||||
i.get<GetExperienceCompletionUseCase>(),
|
i.get<GetExperienceCompletionUseCase>(),
|
||||||
i.get<GetTaxFormsCompletionUseCase>(),
|
i.get<GetTaxFormsCompletionUseCase>(),
|
||||||
|
i.get<GetAttireOptionsCompletionUseCase>(),
|
||||||
|
i.get<GetStaffDocumentsCompletionUseCase>(),
|
||||||
|
i.get<GetStaffCertificatesCompletionUseCase>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,14 @@ class TaxFormMapper {
|
|||||||
String subtitle = '';
|
String subtitle = '';
|
||||||
String description = '';
|
String description = '';
|
||||||
|
|
||||||
if (form.formType == dc.TaxFormType.I9) {
|
final dc.TaxFormType formType;
|
||||||
|
if (form.formType is dc.Known<dc.TaxFormType>) {
|
||||||
|
formType = (form.formType as dc.Known<dc.TaxFormType>).value;
|
||||||
|
} else {
|
||||||
|
formType = dc.TaxFormType.W4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formType == dc.TaxFormType.I9) {
|
||||||
title = 'Form I-9';
|
title = 'Form I-9';
|
||||||
subtitle = 'Employment Eligibility Verification';
|
subtitle = 'Employment Eligibility Verification';
|
||||||
description = 'Required for all new hires to verify identity.';
|
description = 'Required for all new hires to verify identity.';
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../../domain/repositories/tax_forms_repository.dart';
|
import '../../domain/repositories/tax_forms_repository.dart';
|
||||||
import '../mappers/tax_form_mapper.dart';
|
import '../mappers/tax_form_mapper.dart';
|
||||||
|
|
||||||
class TaxFormsRepositoryImpl
|
class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||||
implements TaxFormsRepository {
|
|
||||||
TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance;
|
TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance;
|
||||||
|
|
||||||
final dc.DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
@@ -17,16 +16,22 @@ class TaxFormsRepositoryImpl
|
|||||||
Future<List<TaxForm>> getTaxForms() async {
|
Future<List<TaxForm>> getTaxForms() async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables> response = await _service.connector
|
final QueryResult<
|
||||||
|
dc.GetTaxFormsByStaffIdData,
|
||||||
|
dc.GetTaxFormsByStaffIdVariables
|
||||||
|
>
|
||||||
|
response = await _service.connector
|
||||||
.getTaxFormsByStaffId(staffId: staffId)
|
.getTaxFormsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final List<TaxForm> forms =
|
final List<TaxForm> forms = response.data.taxForms
|
||||||
response.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
|
.map(TaxFormMapper.fromDataConnect)
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Check if required forms exist, create if not.
|
// Check if required forms exist, create if not.
|
||||||
final Set<TaxFormType> typesPresent =
|
final Set<TaxFormType> typesPresent = forms
|
||||||
forms.map((TaxForm f) => f.type).toSet();
|
.map((TaxForm f) => f.type)
|
||||||
|
.toSet();
|
||||||
bool createdNew = false;
|
bool createdNew = false;
|
||||||
|
|
||||||
if (!typesPresent.contains(TaxFormType.i9)) {
|
if (!typesPresent.contains(TaxFormType.i9)) {
|
||||||
@@ -39,8 +44,13 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (createdNew) {
|
if (createdNew) {
|
||||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables> response2 =
|
final QueryResult<
|
||||||
await _service.connector.getTaxFormsByStaffId(staffId: staffId).execute();
|
dc.GetTaxFormsByStaffIdData,
|
||||||
|
dc.GetTaxFormsByStaffIdVariables
|
||||||
|
>
|
||||||
|
response2 = await _service.connector
|
||||||
|
.getTaxFormsByStaffId(staffId: staffId)
|
||||||
|
.execute();
|
||||||
return response2.data.taxForms
|
return response2.data.taxForms
|
||||||
.map(TaxFormMapper.fromDataConnect)
|
.map(TaxFormMapper.fromDataConnect)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -54,8 +64,9 @@ class TaxFormsRepositoryImpl
|
|||||||
await _service.connector
|
await _service.connector
|
||||||
.createTaxForm(
|
.createTaxForm(
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
formType:
|
formType: dc.TaxFormType.values.byName(
|
||||||
dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)),
|
TaxFormAdapter.typeToString(type),
|
||||||
|
),
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
socialSN: 0,
|
socialSN: 0,
|
||||||
@@ -69,8 +80,8 @@ class TaxFormsRepositoryImpl
|
|||||||
Future<void> updateI9Form(I9TaxForm form) async {
|
Future<void> updateI9Form(I9TaxForm form) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final Map<String, dynamic> data = form.formData;
|
final Map<String, dynamic> data = form.formData;
|
||||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||||
_service.connector.updateTaxForm(id: form.id);
|
.updateTaxForm(id: form.id);
|
||||||
_mapCommonFields(builder, data);
|
_mapCommonFields(builder, data);
|
||||||
_mapI9Fields(builder, data);
|
_mapI9Fields(builder, data);
|
||||||
await builder.execute();
|
await builder.execute();
|
||||||
@@ -81,8 +92,8 @@ class TaxFormsRepositoryImpl
|
|||||||
Future<void> submitI9Form(I9TaxForm form) async {
|
Future<void> submitI9Form(I9TaxForm form) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final Map<String, dynamic> data = form.formData;
|
final Map<String, dynamic> data = form.formData;
|
||||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||||
_service.connector.updateTaxForm(id: form.id);
|
.updateTaxForm(id: form.id);
|
||||||
_mapCommonFields(builder, data);
|
_mapCommonFields(builder, data);
|
||||||
_mapI9Fields(builder, data);
|
_mapI9Fields(builder, data);
|
||||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||||
@@ -93,8 +104,8 @@ class TaxFormsRepositoryImpl
|
|||||||
Future<void> updateW4Form(W4TaxForm form) async {
|
Future<void> updateW4Form(W4TaxForm form) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final Map<String, dynamic> data = form.formData;
|
final Map<String, dynamic> data = form.formData;
|
||||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||||
_service.connector.updateTaxForm(id: form.id);
|
.updateTaxForm(id: form.id);
|
||||||
_mapCommonFields(builder, data);
|
_mapCommonFields(builder, data);
|
||||||
_mapW4Fields(builder, data);
|
_mapW4Fields(builder, data);
|
||||||
await builder.execute();
|
await builder.execute();
|
||||||
@@ -105,8 +116,8 @@ class TaxFormsRepositoryImpl
|
|||||||
Future<void> submitW4Form(W4TaxForm form) async {
|
Future<void> submitW4Form(W4TaxForm form) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final Map<String, dynamic> data = form.formData;
|
final Map<String, dynamic> data = form.formData;
|
||||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||||
_service.connector.updateTaxForm(id: form.id);
|
.updateTaxForm(id: form.id);
|
||||||
_mapCommonFields(builder, data);
|
_mapCommonFields(builder, data);
|
||||||
_mapW4Fields(builder, data);
|
_mapW4Fields(builder, data);
|
||||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||||
@@ -114,7 +125,9 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _mapCommonFields(
|
void _mapCommonFields(
|
||||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
dc.UpdateTaxFormVariablesBuilder builder,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
if (data.containsKey('firstName')) {
|
if (data.containsKey('firstName')) {
|
||||||
builder.firstName(data['firstName'] as String?);
|
builder.firstName(data['firstName'] as String?);
|
||||||
}
|
}
|
||||||
@@ -154,8 +167,8 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
|
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
|
||||||
builder.socialSN(
|
builder.socialSN(
|
||||||
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ??
|
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0,
|
||||||
0);
|
);
|
||||||
}
|
}
|
||||||
if (data.containsKey('email')) builder.email(data['email'] as String?);
|
if (data.containsKey('email')) builder.email(data['email'] as String?);
|
||||||
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
|
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
|
||||||
@@ -173,14 +186,17 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _mapI9Fields(
|
void _mapI9Fields(
|
||||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
dc.UpdateTaxFormVariablesBuilder builder,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
if (data.containsKey('citizenshipStatus')) {
|
if (data.containsKey('citizenshipStatus')) {
|
||||||
final String status = data['citizenshipStatus'] as String;
|
final String status = data['citizenshipStatus'] as String;
|
||||||
// Map string to enum if possible, or handle otherwise.
|
// Map string to enum if possible, or handle otherwise.
|
||||||
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
|
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
|
||||||
try {
|
try {
|
||||||
builder.citizen(
|
builder.citizen(
|
||||||
dc.CitizenshipStatus.values.byName(status.toUpperCase()));
|
dc.CitizenshipStatus.values.byName(status.toUpperCase()),
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (data.containsKey('uscisNumber')) {
|
if (data.containsKey('uscisNumber')) {
|
||||||
@@ -202,7 +218,9 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _mapW4Fields(
|
void _mapW4Fields(
|
||||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
dc.UpdateTaxFormVariablesBuilder builder,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
if (data.containsKey('cityStateZip')) {
|
if (data.containsKey('cityStateZip')) {
|
||||||
final String csz = data['cityStateZip'] as String;
|
final String csz = data['cityStateZip'] as String;
|
||||||
// Extremely basic split: City, State Zip
|
// Extremely basic split: City, State Zip
|
||||||
@@ -222,10 +240,11 @@ class TaxFormsRepositoryImpl
|
|||||||
// Simple mapping assumptions:
|
// Simple mapping assumptions:
|
||||||
if (status.contains('single')) {
|
if (status.contains('single')) {
|
||||||
builder.marital(dc.MaritalStatus.SINGLE);
|
builder.marital(dc.MaritalStatus.SINGLE);
|
||||||
} else if (status.contains('married'))
|
} else if (status.contains('married')) {
|
||||||
builder.marital(dc.MaritalStatus.MARRIED);
|
builder.marital(dc.MaritalStatus.MARRIED);
|
||||||
else if (status.contains('head'))
|
} else if (status.contains('head')) {
|
||||||
builder.marital(dc.MaritalStatus.HEAD);
|
builder.marital(dc.MaritalStatus.HEAD);
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (data.containsKey('multipleJobs')) {
|
if (data.containsKey('multipleJobs')) {
|
||||||
@@ -245,11 +264,11 @@ class TaxFormsRepositoryImpl
|
|||||||
}
|
}
|
||||||
if (data.containsKey('extraWithholding')) {
|
if (data.containsKey('extraWithholding')) {
|
||||||
builder.extraWithholding(
|
builder.extraWithholding(
|
||||||
double.tryParse(data['extraWithholding'].toString()));
|
double.tryParse(data['extraWithholding'].toString()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (data.containsKey('signature')) {
|
if (data.containsKey('signature')) {
|
||||||
builder.signature(data['signature'] as String?);
|
builder.signature(data['signature'] as String?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_cubit.dart';
|
||||||
import '../blocs/tax_forms/tax_forms_state.dart';
|
import '../blocs/tax_forms/tax_forms_state.dart';
|
||||||
|
import '../widgets/tax_forms_page/index.dart';
|
||||||
|
|
||||||
class TaxFormsPage extends StatelessWidget {
|
class TaxFormsPage extends StatelessWidget {
|
||||||
const TaxFormsPage({super.key});
|
const TaxFormsPage({super.key});
|
||||||
@@ -57,11 +58,16 @@ class TaxFormsPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
spacing: UiConstants.space4,
|
spacing: UiConstants.space4,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildProgressOverview(state.forms),
|
const TaxFormsInfoCard(),
|
||||||
|
TaxFormsProgressOverview(forms: state.forms),
|
||||||
|
Column(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
children: <Widget>[
|
||||||
...state.forms.map(
|
...state.forms.map(
|
||||||
(TaxForm form) => _buildFormCard(context, form),
|
(TaxForm form) => _buildFormCard(context, form),
|
||||||
),
|
),
|
||||||
_buildInfoCard(),
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -71,56 +77,9 @@ class TaxFormsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProgressOverview(List<TaxForm> 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: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
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<Color>(UiColors.primary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFormCard(BuildContext context, TaxForm form) {
|
Widget _buildFormCard(BuildContext context, TaxForm form) {
|
||||||
// Helper to get icon based on type (could be in entity or a mapper)
|
return TaxFormCard(
|
||||||
final String icon = form is I9TaxForm ? '🛂' : '📋';
|
form: form,
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (form is I9TaxForm) {
|
if (form is I9TaxForm) {
|
||||||
final Object? result = await Modular.to.pushNamed(
|
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
const Icon(UiIcons.file, color: UiColors.primary, size: 20),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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<TaxForm> 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: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
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<Color>(UiColors.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -298,17 +298,11 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
currentPhotoUrl: currentPhotoUrl,
|
currentPhotoUrl: currentPhotoUrl,
|
||||||
referenceImageUrl: widget.item.imageUrl,
|
referenceImageUrl: widget.item.imageUrl,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
InfoSection(
|
InfoSection(
|
||||||
description: widget.item.description,
|
description: widget.item.description,
|
||||||
statusText: _getStatusText(hasUploadedPhoto),
|
statusText: _getStatusText(hasUploadedPhoto),
|
||||||
statusColor: _getStatusColor(hasUploadedPhoto),
|
statusColor: _getStatusColor(hasUploadedPhoto),
|
||||||
isPending: _isPending,
|
isPending: _isPending,
|
||||||
showCheckbox: !_hasVerificationStatus,
|
|
||||||
isAttested: state.isAttested,
|
|
||||||
onAttestationChanged: (bool? val) {
|
|
||||||
cubit.toggleAttestation(val ?? false);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -321,6 +315,11 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
hasVerificationStatus: _hasVerificationStatus,
|
hasVerificationStatus: _hasVerificationStatus,
|
||||||
hasUploadedPhoto: hasUploadedPhoto,
|
hasUploadedPhoto: hasUploadedPhoto,
|
||||||
updatedItem: state.updatedItem,
|
updatedItem: state.updatedItem,
|
||||||
|
showCheckbox: !_hasVerificationStatus,
|
||||||
|
isAttested: state.isAttested,
|
||||||
|
onAttestationChanged: (bool? val) {
|
||||||
|
cubit.toggleAttestation(val ?? false);
|
||||||
|
},
|
||||||
onGallery: () => _onGallery(context),
|
onGallery: () => _onGallery(context),
|
||||||
onCamera: () => _onCamera(context),
|
onCamera: () => _onCamera(context),
|
||||||
onSubmit: () => _onSubmit(context),
|
onSubmit: () => _onSubmit(context),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
class AttestationCheckbox extends StatelessWidget {
|
class AttestationCheckbox extends StatelessWidget {
|
||||||
|
|
||||||
const AttestationCheckbox({
|
const AttestationCheckbox({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isChecked,
|
required this.isChecked,
|
||||||
@@ -14,37 +13,22 @@ class AttestationCheckbox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Row(
|
||||||
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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
child: Checkbox(
|
child: Checkbox(value: isChecked, onChanged: onChanged),
|
||||||
value: isChecked,
|
|
||||||
onChanged: onChanged,
|
|
||||||
activeColor: UiColors.primary,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
t.staff_profile_attire.attestation,
|
t.staff_profile_attire.attestation,
|
||||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
style: UiTypography.body2r,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import '../attestation_checkbox.dart';
|
||||||
import 'attire_upload_buttons.dart';
|
import 'attire_upload_buttons.dart';
|
||||||
|
|
||||||
/// Handles the primary actions at the bottom of the page.
|
/// Handles the primary actions at the bottom of the page.
|
||||||
@@ -16,6 +17,9 @@ class FooterSection extends StatelessWidget {
|
|||||||
required this.hasVerificationStatus,
|
required this.hasVerificationStatus,
|
||||||
required this.hasUploadedPhoto,
|
required this.hasUploadedPhoto,
|
||||||
this.updatedItem,
|
this.updatedItem,
|
||||||
|
required this.showCheckbox,
|
||||||
|
required this.isAttested,
|
||||||
|
required this.onAttestationChanged,
|
||||||
required this.onGallery,
|
required this.onGallery,
|
||||||
required this.onCamera,
|
required this.onCamera,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
@@ -37,6 +41,15 @@ class FooterSection extends StatelessWidget {
|
|||||||
/// The updated attire item, if any.
|
/// The updated attire item, if any.
|
||||||
final AttireItem? updatedItem;
|
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<bool?> onAttestationChanged;
|
||||||
|
|
||||||
/// Callback to open the gallery.
|
/// Callback to open the gallery.
|
||||||
final VoidCallback onGallery;
|
final VoidCallback onGallery;
|
||||||
|
|
||||||
@@ -57,6 +70,13 @@ class FooterSection extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (showCheckbox) ...<Widget>[
|
||||||
|
AttestationCheckbox(
|
||||||
|
isChecked: isAttested,
|
||||||
|
onChanged: onAttestationChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
],
|
||||||
if (isUploading)
|
if (isUploading)
|
||||||
const Center(
|
const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../attestation_checkbox.dart';
|
|
||||||
import 'attire_verification_status_card.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 {
|
class InfoSection extends StatelessWidget {
|
||||||
/// Creates an [InfoSection].
|
/// Creates an [InfoSection].
|
||||||
const InfoSection({
|
const InfoSection({
|
||||||
@@ -13,9 +12,6 @@ class InfoSection extends StatelessWidget {
|
|||||||
required this.statusText,
|
required this.statusText,
|
||||||
required this.statusColor,
|
required this.statusColor,
|
||||||
required this.isPending,
|
required this.isPending,
|
||||||
required this.showCheckbox,
|
|
||||||
required this.isAttested,
|
|
||||||
required this.onAttestationChanged,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The description of the attire item.
|
/// The description of the attire item.
|
||||||
@@ -30,15 +26,6 @@ class InfoSection extends StatelessWidget {
|
|||||||
/// Whether the item is currently pending verification.
|
/// Whether the item is currently pending verification.
|
||||||
final bool isPending;
|
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<bool?> onAttestationChanged;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -74,15 +61,6 @@ class InfoSection extends StatelessWidget {
|
|||||||
statusText: statusText,
|
statusText: statusText,
|
||||||
statusColor: statusColor,
|
statusColor: statusColor,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
if (showCheckbox) ...<Widget>[
|
|
||||||
AttestationCheckbox(
|
|
||||||
isChecked: isAttested,
|
|
||||||
onChanged: onAttestationChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
|||||||
PersonalInfoAddressSelected event,
|
PersonalInfoAddressSelected event,
|
||||||
Emitter<PersonalInfoState> emit,
|
Emitter<PersonalInfoState> 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).
|
/// Adds a location to the preferredLocations list (max 5, no duplicates).
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// ── Description
|
// ” Description
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
UiConstants.space5,
|
UiConstants.space5,
|
||||||
@@ -123,7 +123,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── Search autocomplete field
|
// ” Search autocomplete field
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: UiConstants.space5,
|
horizontal: UiConstants.space5,
|
||||||
@@ -137,7 +137,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── "Max reached" banner
|
// ” "Max reached" banner
|
||||||
if (atMax)
|
if (atMax)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
@@ -164,7 +164,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
|||||||
|
|
||||||
const SizedBox(height: UiConstants.space5),
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
// ── Section label
|
// ” Section label
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: UiConstants.space5,
|
horizontal: UiConstants.space5,
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ class LegalSectionWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: UiColors.bgPopup,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border, width: 0.5),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|||||||
@@ -40,10 +40,11 @@ class PrivacySectionWidget extends StatelessWidget {
|
|||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: UiColors.bgPopup,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: UiColors.border,
|
color: UiColors.border,
|
||||||
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ query getStaffProfileCompletion($id: UUID!) @auth(level: USER) {
|
|||||||
emergencyContacts(where: { staffId: { eq: $id } }) {
|
emergencyContacts(where: { staffId: { eq: $id } }) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
taxForms(where: { staffId: { eq: $id } }) {
|
|
||||||
id
|
|
||||||
formType
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query getStaffPersonalInfoCompletion($id: UUID!) @auth(level: USER) {
|
query getStaffPersonalInfoCompletion($id: UUID!) @auth(level: USER) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# 📊 Use Case Completion Audit
|
# 📊 Use Case Completion Audit
|
||||||
|
|
||||||
**Generated:** 2026-02-23
|
**Generated:** 2026-03-02
|
||||||
**Auditor Role:** System Analyst / Flutter Architect
|
**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`
|
**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/` (real app) vs `apps/mobile/prototypes/` (prototypes)
|
**Codebase Checked:** `apps/mobile/packages/features/` and `apps/mobile/apps/` (actual production apps)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,11 +11,10 @@
|
|||||||
|
|
||||||
| Symbol | Meaning |
|
| Symbol | Meaning |
|
||||||
|:---:|:--- |
|
|:---:|:--- |
|
||||||
| ✅ | Fully implemented in the real app |
|
| ✅ | Fully implemented with all 4 architecture layers |
|
||||||
| 🟡 | Partially implemented — UI or domain exists but logic is incomplete |
|
| 🟡 | Partially implemented — Some layers missing or functionality incomplete |
|
||||||
| ❌ | Defined in docs but entirely missing in the real app |
|
| ❌ | Defined in use case docs but entirely missing in production app |
|
||||||
| ⚠️ | Exists in prototype but has **not** been migrated to the real app |
|
| 🚫 | Exists in production app but **not** documented in use cases (extra feature) |
|
||||||
| 🚫 | Exists in real app code but is **not** documented in use cases |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,204 +22,222 @@
|
|||||||
|
|
||||||
### Feature Module: `authentication`
|
### Feature Module: `authentication`
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | System checks session on launch | ✅ | ✅ Completed | Auth BLoC + navigation guards handle routing. |
|
||||||
| 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 Home if authenticated | ✅ | ✅ Completed | Modular routing configured with auth state checks. |
|
||||||
| 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.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` fully 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 | Real app BLoC-backed form with validation. |
|
| 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 redirection intact. |
|
| 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` fully implemented. |
|
| 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 | Auth BLoC with error states present. |
|
| 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 | Redirects to `client_main` shell on success. |
|
| 1.3 Business Sign In | Grant access to dashboard | ✅ | ✅ Completed | Navigation to `client_main` on success. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `orders` (Order Management)
|
### Feature Module: `orders` (Order Management)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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.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` fully implemented with BLoC. |
|
| 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` fully implemented. |
|
| 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` fully implemented. |
|
| 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 | ✅ | ✅ | 🟡 Partial | Order summary shown, but real-time cost calculation depends on backend. |
|
| 2.2 Scheduled Orders | Review cost before posting | ✅ | ✅ Completed | Cost calculation integrated in order creation flows. |
|
||||||
| View & Browse Active Orders | Search & toggle between weeks to view orders | ✅ | ✅ | 🚫 Completed | `view_orders_page.dart` exists with `ViewOrderCard`. Added `eventName` visibility. |
|
| View & Browse Active Orders | Search & toggle between views | ✅ | ✅ Completed | `view_orders_page.dart` + ViewOrdersCubit with filters. |
|
||||||
| Modify Posted Orders | Refine staffing needs post-publish | ✅ | ✅ | 🚫 Completed | `OrderEditSheet` handles position updates and entire order cancellation flow. |
|
| 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)
|
### Feature Module: `client_coverage` (Operations & Workforce Management)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 coverage tab | ✅ | ✅ Completed | `coverage_page.dart` + CoverageCubit. |
|
||||||
| 3.1 Monitor Today's Coverage | View percentage filled | ✅ | ✅ | ✅ Completed | `coverage_header.dart` shows fill rate. |
|
| 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 | Open/filled shift list in `coverage_shift_list.dart`. |
|
| 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 | ✅ | ✅ | 🚫 Completed | Action added to shift header on Coverage page. |
|
| 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` wired to Data Connect. |
|
| 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 active shift → Select worker → Check attire | ✅ | ✅ | ✅ Completed | Action added to coverage view; workers can be verified in real-time. |
|
| 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 | Implemented `TimesheetsPage` in billing module for approval workflow. |
|
| 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 | Viewable in the timesheet approval card. |
|
| 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/Decline actions implemented in `TimesheetsPage`. |
|
| 3.4 Review & Approve Timesheets | Tap Approve / Dispute | ✅ | ✅ Completed | Approve/dispute use cases implemented. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `reports` (Reports & Analytics)
|
### Feature Module: `reports` (Reports & Analytics)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production App | Status | Notes |
|
||||||
|:---|:---|:---:|:---:|:---:|:---|
|
|:---|:---|:---:|:---:|:---|
|
||||||
| 4.1 Business Intelligence Reporting | Daily Ops Report | ✅ | ✅ | ✅ Completed | `daily_ops_report_page.dart` fully implemented. |
|
| 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` fully implemented. |
|
| 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` fully implemented. |
|
| 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` fully implemented. |
|
| 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` fully implemented. |
|
| 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` fully implemented. |
|
| 4.1 Business Intelligence Reporting | Coverage Report | ✅ | ✅ Completed | `coverage_report_page.dart` + CoverageReportBloc. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `billing` (Billing & Administration)
|
### Feature Module: `billing` (Billing & Administration)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production App | Status | Notes |
|
||||||
|:---|:---|:---:|:---:|:---:|:---|
|
|:---|:---|:---:|:---:|:---|
|
||||||
| 5.1 Financial Management | View current balance | ✅ | ✅ | ✅ Completed | `billing_page.dart` shows `currentBill` and period billing. |
|
| 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 | `PendingInvoicesSection` widget fully wired via `BillingBloc`. |
|
| 5.1 Financial Management | View pending invoices | ✅ | ✅ Completed | `pending_invoices_page.dart` + use case. |
|
||||||
| 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 | Download past invoices | ✅ | ✅ Completed | Invoice history use case implemented. |
|
||||||
| 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. |
|
| 5.1 Financial Management | Update payment methods | ✅ | ✅ Completed | Bank accounts use case + payment method management. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `hubs` (Manage Business Locations)
|
### Feature Module: `hubs` (Manage Business Locations)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | View list of client hubs | ✅ | ✅ Completed | `client_hubs_page.dart` + HubManagementBloc. |
|
||||||
| 5.2 Manage Business Locations | Add new hub (location + address) | ✅ | ✅ | ✅ Completed | `edit_hub_page.dart` serves create + edit. |
|
| 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` both present. |
|
| 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)
|
### Feature Module: `settings` (Profile & Settings)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production App | Status | Notes |
|
||||||
|:---|:---|:---:|:---:|:---:|:---|
|
|:---|:---|:---:|:---:|:---|
|
||||||
| 5.3 Profile & Settings Management | Edit personal contact info | ✅ | ✅ | ✅ Completed | Implemented `EditProfilePage` in settings module. |
|
| 5.3 Profile & Settings Management | Edit personal contact info | ✅ | ✅ Completed | `edit_profile_page.dart` implemented. |
|
||||||
| 5.1 System Settings | Toggle notification preferences | ✅ | ✅ | ✅ Completed | Implemented notification preference toggles for Push, Email, and SMS. |
|
| 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)
|
### Feature Module: `home` (Home Tab)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 Dashboard | View customizable dashboard | ✅ | ✅ Completed | `client_home_page.dart` with multiple widgets. |
|
||||||
| Home — Quick Actions Widget | Display quick action shortcuts | ✅ | ✅ | ✅ Completed | `actions_widget.dart` present. |
|
| Quick Actions | Access frequent operations | ✅ | ✅ Completed | `actions_widget.dart` with navigation shortcuts. |
|
||||||
| Home — Navigate to Settings | Settings shortcut from Home | ✅ | ✅ | ✅ Completed | `client_home_header.dart` has settings navigation. |
|
| Create Order Entry Point | Launch order creation flows | ✅ | ✅ Completed | Order creation integrated via `shift_order_form_sheet.dart`. |
|
||||||
| Home — Navigate to Hubs | Hub shortcut from Home | ✅ | ✅ | ✅ Completed | `actions_widget.dart` navigates to hubs. |
|
| Dashboard Widgets | Coverage, Spending, Live Activity | ✅ | 🚫 Completed | `coverage_dashboard.dart`, `spending_widget.dart`, `live_activity_widget.dart`. |
|
||||||
| Customizable Home Dashboard | Reorderable widgets for client overview | ❌ | ✅ | 🚫 Completed | `draggable_widget_wrapper.dart` + `reorder_widget.dart` + `dashboard_widget_builder.dart` exist in real app. |
|
| Widget Reordering | Drag and reorder dashboard widgets | ✅ | 🚫 Completed | `draggable_widget_wrapper.dart` + `reorder_widget.dart` implemented. |
|
||||||
| Operational Spend Snapshot | View periodic spend summary on home | ❌ | ✅ | 🚫 Completed | `spending_widget.dart` implemented on home dashboard. |
|
| Recent Reorders | Quick access to past orders | ✅ | 🚫 Completed | Recent reorders widget 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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### ❌ 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
|
## 👷 STAFF APP
|
||||||
|
|
||||||
### Feature Module: `authentication`
|
### Feature Module: `authentication`
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | Check auth token on startup | ✅ | ✅ Completed | Auth state management via BLoC + Modular routing. |
|
||||||
| 1.1 App Initialization | Route to Home if valid | ✅ | ✅ | ✅ Completed | Navigation guard in `staff_authentication_module.dart`. |
|
| 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 | Implemented. |
|
| 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` fully implemented. |
|
| 1.2 Onboarding & Registration | Enter phone number | ✅ | ✅ Completed | `phone_verification_page.dart` + AuthBloc. |
|
||||||
| 1.2 Onboarding & Registration | Receive & verify SMS OTP | ✅ | ✅ | ✅ Completed | OTP verification BLoC wired to real auth backend. |
|
| 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 | Routing logic in auth module checks profile completion. |
|
| 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_info` section: `personal_info_page.dart` fully implemented. |
|
| 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` section: `experience_page.dart` implemented. |
|
| 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` section: `attire_page.dart` implemented via `profile_sections/onboarding/attire`. |
|
| 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. |
|
| 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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `home` (Job Discovery)
|
### Feature Module: `home` (Job Discovery)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | View available jobs list | ✅ | ✅ Completed | `worker_home_page.dart` with job listings. |
|
||||||
| 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 Role | ✅ | ✅ Completed | Role filtering via search and job type tabs (shifts module). |
|
||||||
| 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 | Filter by Distance | ✅ | ✅ Completed | Distance/radius filtering implemented in shifts module. |
|
||||||
| 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.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` fully implemented with `AvailabilityBloc`. |
|
| 2.3 Set Availability | Select dates/times → Save preferences | ✅ | ✅ Completed | `availability_page.dart` + AvailabilityBloc with 3 use cases. |
|
||||||
| Upcoming Shift Quick-Link | Direct access to next shift from home | ✅ | ✅ | 🚫 Completed | `worker_home_page.dart` shows upcoming shifts banner. |
|
| 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)
|
### Feature Module: `shifts` (Find Shifts + My Schedule)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | Tap "Claim Shift" from Job Details | ✅ | ✅ Completed | `shift_details_page.dart` with accept shift use case. |
|
||||||
| 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 | System validates eligibility | ✅ | ✅ Completed | Eligibility validation in ShiftsBloc (certificates, conflicts). |
|
||||||
| 2.2 Claim Open Shift | Prompt to Upload Compliance Docs if missing | ✅ | ✅ | 🚫 Completed | Redirect dialog implemented in `ShiftDetailsPage` on eligibility failure. |
|
| 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 (My Shifts tab) | ✅ | ✅ | ✅ Completed | `my_shifts_tab.dart` fully implemented with shift cards. |
|
| 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` with header, location map, schedule summary, stats. Corrected weekday mapping and added `endDate`. |
|
| 3.1 View Schedule | View Shift Details | ✅ | ✅ Completed | `shift_details_page.dart` (348 lines) with comprehensive info. |
|
||||||
| Completed Shift History | View past worked shifts and earnings | ❌ | ✅ | 🚫 Completed | `history_shifts_tab.dart` fully wired in `shifts_page.dart`. |
|
| Browse Available Shifts | Find and apply for new shifts | ✅ | ✅ Completed | `find_shifts_tab.dart` with search/filter + 9 use cases. |
|
||||||
| Multi-day Schedule View | Visual grouping of spanned shift dates | ❌ | ✅ | 🚫 Completed | Multi-day grouping logic in `_groupMultiDayShifts()` supports `endDate`. |
|
| Shift History | View past worked shifts | ✅ | 🚫 Completed | `history_shifts_tab.dart` with get_history_shifts use case. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `clock_in` (Shift Execution)
|
### Feature Module: `clock_in` (Shift Execution)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| 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` is a dedicated tab. |
|
| 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 radius enforced (500m). `SwipeToCheckIn` is disabled until within range. |
|
| 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 | `SwipeToCheckIn` widget activates when time window is valid. |
|
| 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 | UX improved with real-time distance warning and disabled check-in button when too far. |
|
| 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 | `_showNFCDialog()` and NFC check-in logic implemented. |
|
| 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 | `SwipeToCheckIn` toggles to clock-out mode. `CheckOutRequested` event fires. |
|
| 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 | `LunchBreakDialog` handles break confirmation. Attire photo captured during clock-in. |
|
| 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 | Implemented "Submit for Approval" action on completed `MyShiftCard`. |
|
| 3.3 Submit Timesheet | Submit timesheet for client approval | ✅ | ✅ Completed | Timesheet submission integrated in clock out flow. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `payments` (Financial Management)
|
### Feature Module: `payments` (Financial Management)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 Pending Pay (unpaid earnings) | ✅ | ✅ Completed | `payments_page.dart` with pending earnings display. |
|
||||||
| 4.1 Track Earnings | View Total Earned (paid earnings) | ✅ | ✅ | ✅ Completed | `PaymentsLoaded.summary.totalEarnings` displayed on header. |
|
| 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 | `PaymentHistoryItem` list rendered from `state.history`. |
|
| 4.1 Track Earnings | View Payment History | ✅ | ✅ Completed | Payment history use case + list view. |
|
||||||
| 4.2 Request Early Pay | Tap "Request Early Pay" | ✅ | ✅ | ✅ Completed | `PendingPayCard` has `onCashOut` → navigates to `/early-pay`. |
|
| 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 | Implemented `EarlyPayPage` for selecting cash-out amount. |
|
| 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 included in `EarlyPayPage`. |
|
| 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 | Request submission flow functional. |
|
| 4.2 Request Early Pay | Funds transferred to bank account | ✅ | ✅ Completed | Early pay submission use case implemented. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Module: `profile` + `profile_sections` (Profile & Compliance)
|
### Feature Module: `profile` + `profile_sections` (Profile & Compliance)
|
||||||
|
|
||||||
| Use Case | Sub-Use Case | Prototype | Real App | Status | Notes |
|
| Use Case | Sub-Use Case | Production 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 | Navigate to Compliance Menu | ✅ | ✅ Completed | Compliance section in `staff_profile_page.dart`. |
|
||||||
| 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 | Upload Certificates | ✅ | ✅ Completed | `certificates/` module with 4 use cases + 2 pages. |
|
||||||
| 5.1 Manage Compliance Documents | View/Manage Identity Documents | ✅ | ✅ | ✅ Completed | `documents_page.dart` with `documents_progress_card.dart`. |
|
| 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 | `form_w4_page.dart` + `FormW4Cubit` fully implemented. |
|
| 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 | `form_i9_page.dart` + `FormI9Cubit` fully implemented. |
|
| 5.2 Manage Tax Forms | Complete I-9 digitally & submit | ✅ | ✅ Completed | `tax_forms/form_i9_page.dart` + FormI9Cubit + use cases. |
|
||||||
| 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.4 Account Settings | Update Bank Details | ✅ | ✅ Completed | `staff_bank_account/` module with page + cubit. |
|
||||||
| 5.3 KROW University Training | Select Module → Watch Video / Take Quiz | ✅ | ❌ | ⚠️ Prototype Only | Fully prototyped (courses, categories, XP tracking). Not migrated at all. |
|
| 5.4 Account Settings | Access Support / FAQs | ✅ | ✅ Completed | `faqs/` module with search functionality + 2 use cases. |
|
||||||
| 5.3 KROW University Training | Earn Badge | ✅ | ❌ | ⚠️ Prototype Only | Prototype only. |
|
| Personal Info Management | Update profile information | ✅ | 🚫 Completed | `profile_info/` module with 3 pages (personal info, language, locations). |
|
||||||
| 5.4 Account Settings | Update Bank Details | ✅ | ✅ | ✅ Completed | `bank_account_page.dart` + `BankAccountCubit` in `profile_sections/finances/staff_bank_account`. |
|
| Emergency Contact | Manage emergency contacts | ✅ | 🚫 Completed | `emergency_contact/` module with get + save use cases. |
|
||||||
| 5.4 Account Settings | View Benefits | ✅ | ❌ | ⚠️ Prototype Only | `benefits_screen.dart` exists only in prototype. No `benefits` package in real app. |
|
| Experience Management | Update industries and skills | ✅ | 🚫 Completed | `experience/` module with 3 use cases. |
|
||||||
| 5.4 Account Settings | Access Support / FAQs | ✅ | ✅ | ✅ Completed | `faqs_page.dart` with `FAQsBloc` and search in `profile_sections/support/faqs`. |
|
| Attire Management | Upload attire photos | ✅ | 🚫 Completed | `attire/` module with upload + photo management. |
|
||||||
| Timecard & Hours Log | Audit log of clock-in/out events | ✅ | ✅ | 🚫 Completed | `time_card_page.dart` in `profile_sections/finances/time_card`. |
|
| Timecard Viewing | View clock-in/out history | ✅ | 🚫 Completed | `time_card/` module with get_time_cards use case. |
|
||||||
| Privacy & Security Controls | Manage account data and app permissions | ✅ | ✅ | 🚫 Completed | `privacy_security_page.dart` in `support/privacy_security`. |
|
| Privacy & Security | Manage privacy settings | ✅ | 🚫 Completed | `privacy_security/` module with 4 use cases + 2 pages. |
|
||||||
| 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. |
|
---
|
||||||
|
|
||||||
|
### ❌ 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
|
### Client App
|
||||||
|
|
||||||
| Metric | Count |
|
| 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%
|
## 📊 Summary Statistics
|
||||||
**Client App Implementation Coverage (completed + partial):** ~94%
|
|
||||||
|
### 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 |
|
| Feature | App | Impact | Notes |
|
||||||
|:---|:---:|
|
|:---|:---:|:---|:---|
|
||||||
| **Total documented use cases (sub-use cases)** | 45 |
|
| **Workers Directory** | Client | 🔴 High | Documented use case 6.1 completely missing. No module, no pages, no BLoC. |
|
||||||
| ✅ Fully Implemented | 23 |
|
| **KROW University** | Staff | 🟠 Medium | Training module with videos/quizzes documented in 5.3 but not implemented. |
|
||||||
| 🟡 Partially Implemented | 6 |
|
| **In-App Messaging** | Staff | 🟡 Low | Support chat documented but not implemented. FAQ module exists as alternative. |
|
||||||
| ❌ Not Implemented | 2 |
|
| **Leaderboard** | Staff | 🟡 Low | Competitive tracking/gamification not implemented. |
|
||||||
| ⚠️ Prototype Only (not migrated) | 6 |
|
|
||||||
| 🚫 Completed (Extra) | 8 |
|
|
||||||
|
|
||||||
**Staff App Completion Rate (fully implemented):** ~71%
|
|
||||||
**Staff App Implementation Coverage (completed + partial):** ~85%
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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**
|
### Known Technical Debt
|
||||||
Several modules exist in the prototype but are missing in the real app, including training Modules, XP tracking, and Benefits views.
|
- **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`)
|
## 🎯 Recommendations for Sprint Planning
|
||||||
The "View Benefits" sub-use case is defined in docs and prototype but absent from the real app.
|
|
||||||
|
### 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.*
|
|
||||||
|
|||||||
Reference in New Issue
Block a user