feat: Migrate staff profile features from Data Connect to V2 REST API
- Removed data_connect package from mobile pubspec.yaml. - Added documentation for V2 profile migration status and QA findings. - Implemented new session management with ClientSessionStore and StaffSessionStore. - Created V2SessionService for handling user sessions via the V2 API. - Developed use cases for cancelling late worker assignments and submitting worker reviews. - Added arguments and use cases for payment chart retrieval and profile completion checks. - Implemented repository interfaces and their implementations for staff main and profile features. - Ensured proper error handling and validation in use cases.
This commit is contained in:
@@ -1,137 +1,101 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/certificates_repository.dart';
|
||||
import 'package:staff_certificates/src/domain/repositories/certificates_repository.dart';
|
||||
|
||||
/// Implementation of [CertificatesRepository] using Data Connect.
|
||||
/// Implementation of [CertificatesRepository] using the V2 API for reads
|
||||
/// and core services for uploads/verification.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
/// Creates a [CertificatesRepositoryImpl].
|
||||
CertificatesRepositoryImpl({
|
||||
required BaseApiService apiService,
|
||||
required FileUploadService uploadService,
|
||||
required SignedUrlService signedUrlService,
|
||||
required VerificationService verificationService,
|
||||
}) : _service = DataConnectService.instance,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService,
|
||||
_verificationService = verificationService;
|
||||
}) : _api = apiService,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService,
|
||||
_verificationService = verificationService;
|
||||
|
||||
final DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
final FileUploadService _uploadService;
|
||||
final SignedUrlService _signedUrlService;
|
||||
final VerificationService _verificationService;
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffCertificate>> getCertificates() async {
|
||||
return _service.getStaffRepository().getStaffCertificates();
|
||||
Future<List<StaffCertificate>> getCertificates() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffCertificates);
|
||||
final List<dynamic> items =
|
||||
response.data['certificates'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
StaffCertificate.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.StaffCertificate> uploadCertificate({
|
||||
required domain.ComplianceType certificationType,
|
||||
Future<StaffCertificate> uploadCertificate({
|
||||
required String certificateType,
|
||||
required String name,
|
||||
required String filePath,
|
||||
DateTime? expiryDate,
|
||||
String? issuer,
|
||||
String? certificateNumber,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
// Get existing certificate to check if file has changed
|
||||
final List<domain.StaffCertificate> existingCerts = await getCertificates();
|
||||
domain.StaffCertificate? existingCert;
|
||||
try {
|
||||
existingCert = existingCerts.firstWhere(
|
||||
(domain.StaffCertificate c) => c.certificationType == certificationType,
|
||||
);
|
||||
} catch (e) {
|
||||
// Certificate doesn't exist yet
|
||||
}
|
||||
// 1. Upload the file to cloud storage
|
||||
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName:
|
||||
'staff_cert_${certificateType}_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
visibility: FileVisibility.private,
|
||||
);
|
||||
|
||||
String? signedUrl = existingCert?.certificateUrl;
|
||||
String? verificationId = existingCert?.verificationId;
|
||||
final bool fileChanged = existingCert == null || existingCert.certificateUrl != filePath;
|
||||
// 2. Generate a signed URL
|
||||
final SignedUrlResponse signedUrlRes =
|
||||
await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
|
||||
// Only upload and verify if file path has changed
|
||||
if (fileChanged) {
|
||||
// 1. Upload the file to cloud storage
|
||||
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName:
|
||||
'staff_cert_${certificationType.name}_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
visibility: domain.FileVisibility.private,
|
||||
);
|
||||
// 3. Initiate verification
|
||||
final VerificationResponse verificationRes =
|
||||
await _verificationService.createVerification(
|
||||
fileUri: uploadRes.fileUri,
|
||||
type: 'certification',
|
||||
subjectType: 'worker',
|
||||
subjectId: certificateType,
|
||||
rules: <String, dynamic>{
|
||||
'certificateName': name,
|
||||
'certificateIssuer': issuer,
|
||||
'certificateNumber': certificateNumber,
|
||||
},
|
||||
);
|
||||
|
||||
// 2. Generate a signed URL for verification service to access the file
|
||||
final SignedUrlResponse signedUrlRes = await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
signedUrl = signedUrlRes.signedUrl;
|
||||
// 4. Save certificate via V2 API
|
||||
await _api.post(
|
||||
V2ApiEndpoints.staffCertificates,
|
||||
data: <String, dynamic>{
|
||||
'certificateType': certificateType,
|
||||
'name': name,
|
||||
'fileUri': signedUrlRes.signedUrl,
|
||||
'expiresAt': expiryDate?.toIso8601String(),
|
||||
'issuer': issuer,
|
||||
'certificateNumber': certificateNumber,
|
||||
'verificationId': verificationRes.verificationId,
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Initiate verification
|
||||
final String staffId = await _service.getStaffId();
|
||||
final VerificationResponse verificationRes = await _verificationService
|
||||
.createVerification(
|
||||
fileUri: uploadRes.fileUri,
|
||||
type: 'certification',
|
||||
subjectType: 'worker',
|
||||
subjectId: staffId,
|
||||
rules: <String, dynamic>{
|
||||
'certificateName': name,
|
||||
'certificateIssuer': issuer,
|
||||
'certificateNumber': certificateNumber,
|
||||
},
|
||||
);
|
||||
verificationId = verificationRes.verificationId;
|
||||
}
|
||||
|
||||
// 4. Update/Create Certificate in Data Connect
|
||||
await _service.getStaffRepository().upsertStaffCertificate(
|
||||
certificationType: certificationType,
|
||||
name: name,
|
||||
status: existingCert?.status ?? domain.StaffCertificateStatus.pending,
|
||||
fileUrl: signedUrl,
|
||||
expiry: expiryDate,
|
||||
issuer: issuer,
|
||||
certificateNumber: certificateNumber,
|
||||
validationStatus: existingCert?.validationStatus ?? domain.StaffCertificateValidationStatus.pendingExpertReview,
|
||||
verificationId: verificationId,
|
||||
);
|
||||
|
||||
// 5. Return updated list or the specific certificate
|
||||
final List<domain.StaffCertificate> certificates =
|
||||
await getCertificates();
|
||||
return certificates.firstWhere(
|
||||
(domain.StaffCertificate c) => c.certificationType == certificationType,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upsertCertificate({
|
||||
required domain.ComplianceType certificationType,
|
||||
required String name,
|
||||
required domain.StaffCertificateStatus status,
|
||||
String? fileUrl,
|
||||
DateTime? expiry,
|
||||
String? issuer,
|
||||
String? certificateNumber,
|
||||
domain.StaffCertificateValidationStatus? validationStatus,
|
||||
}) async {
|
||||
await _service.getStaffRepository().upsertStaffCertificate(
|
||||
certificationType: certificationType,
|
||||
name: name,
|
||||
status: status,
|
||||
fileUrl: fileUrl,
|
||||
expiry: expiry,
|
||||
issuer: issuer,
|
||||
certificateNumber: certificateNumber,
|
||||
validationStatus: validationStatus,
|
||||
// 5. Return updated list
|
||||
final List<StaffCertificate> certificates = await getCertificates();
|
||||
return certificates.firstWhere(
|
||||
(StaffCertificate c) => c.certificateType == certificateType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteCertificate({
|
||||
required domain.ComplianceType certificationType,
|
||||
}) async {
|
||||
return _service.getStaffRepository().deleteStaffCertificate(
|
||||
certificationType: certificationType,
|
||||
Future<void> deleteCertificate({required String certificateId}) async {
|
||||
await _api.delete(
|
||||
V2ApiEndpoints.staffCertificateDelete(certificateId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the certificates repository.
|
||||
///
|
||||
/// Responsible for fetching staff compliance certificates.
|
||||
/// Implementations must reside in the data layer.
|
||||
/// Responsible for fetching, uploading, and deleting staff certificates
|
||||
/// via the V2 API. Uses [StaffCertificate] from the V2 domain.
|
||||
abstract interface class CertificatesRepository {
|
||||
/// Fetches the list of compliance certificates for the current staff member.
|
||||
///
|
||||
/// Returns a list of [StaffCertificate] entities.
|
||||
/// Fetches the list of certificates for the current staff member.
|
||||
Future<List<StaffCertificate>> getCertificates();
|
||||
|
||||
/// Uploads a certificate file and saves the record.
|
||||
Future<StaffCertificate> uploadCertificate({
|
||||
required ComplianceType certificationType,
|
||||
required String certificateType,
|
||||
required String name,
|
||||
required String filePath,
|
||||
DateTime? expiryDate,
|
||||
@@ -20,18 +18,6 @@ abstract interface class CertificatesRepository {
|
||||
String? certificateNumber,
|
||||
});
|
||||
|
||||
/// Deletes a staff certificate.
|
||||
Future<void> deleteCertificate({required ComplianceType certificationType});
|
||||
|
||||
/// Upserts a certificate record (metadata only).
|
||||
Future<void> upsertCertificate({
|
||||
required ComplianceType certificationType,
|
||||
required String name,
|
||||
required StaffCertificateStatus status,
|
||||
String? fileUrl,
|
||||
DateTime? expiry,
|
||||
String? issuer,
|
||||
String? certificateNumber,
|
||||
StaffCertificateValidationStatus? validationStatus,
|
||||
});
|
||||
/// Deletes a staff certificate by its [certificateId].
|
||||
Future<void> deleteCertificate({required String certificateId});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/certificates_repository.dart';
|
||||
|
||||
/// Use case for deleting a staff compliance certificate.
|
||||
class DeleteCertificateUseCase extends UseCase<ComplianceType, void> {
|
||||
class DeleteCertificateUseCase extends UseCase<String, void> {
|
||||
/// Creates a [DeleteCertificateUseCase].
|
||||
DeleteCertificateUseCase(this._repository);
|
||||
final CertificatesRepository _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(ComplianceType certificationType) {
|
||||
return _repository.deleteCertificate(certificationType: certificationType);
|
||||
Future<void> call(String certificateId) {
|
||||
return _repository.deleteCertificate(certificateId: certificateId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class UploadCertificateUseCase
|
||||
@override
|
||||
Future<StaffCertificate> call(UploadCertificateParams params) {
|
||||
return _repository.uploadCertificate(
|
||||
certificationType: params.certificationType,
|
||||
certificateType: params.certificateType,
|
||||
name: params.name,
|
||||
filePath: params.filePath,
|
||||
expiryDate: params.expiryDate,
|
||||
@@ -26,7 +26,7 @@ class UploadCertificateUseCase
|
||||
class UploadCertificateParams {
|
||||
/// Creates [UploadCertificateParams].
|
||||
UploadCertificateParams({
|
||||
required this.certificationType,
|
||||
required this.certificateType,
|
||||
required this.name,
|
||||
required this.filePath,
|
||||
this.expiryDate,
|
||||
@@ -34,8 +34,8 @@ class UploadCertificateParams {
|
||||
this.certificateNumber,
|
||||
});
|
||||
|
||||
/// The type of certification.
|
||||
final ComplianceType certificationType;
|
||||
/// The type of certification (e.g. "FOOD_HYGIENE", "SIA_BADGE").
|
||||
final String certificateType;
|
||||
|
||||
/// The name of the certificate.
|
||||
final String name;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/certificates_repository.dart';
|
||||
|
||||
/// Use case for upserting a staff compliance certificate.
|
||||
class UpsertCertificateUseCase extends UseCase<UpsertCertificateParams, void> {
|
||||
/// Creates an [UpsertCertificateUseCase].
|
||||
UpsertCertificateUseCase(this._repository);
|
||||
final CertificatesRepository _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(UpsertCertificateParams params) {
|
||||
return _repository.upsertCertificate(
|
||||
certificationType: params.certificationType,
|
||||
name: params.name,
|
||||
status: params.status,
|
||||
fileUrl: params.fileUrl,
|
||||
expiry: params.expiry,
|
||||
issuer: params.issuer,
|
||||
certificateNumber: params.certificateNumber,
|
||||
validationStatus: params.validationStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for [UpsertCertificateUseCase].
|
||||
class UpsertCertificateParams {
|
||||
/// Creates [UpsertCertificateParams].
|
||||
UpsertCertificateParams({
|
||||
required this.certificationType,
|
||||
required this.name,
|
||||
required this.status,
|
||||
this.fileUrl,
|
||||
this.expiry,
|
||||
this.issuer,
|
||||
this.certificateNumber,
|
||||
this.validationStatus,
|
||||
});
|
||||
|
||||
/// The type of certification.
|
||||
final ComplianceType certificationType;
|
||||
|
||||
/// The name of the certificate.
|
||||
final String name;
|
||||
|
||||
/// The status of the certificate.
|
||||
final StaffCertificateStatus status;
|
||||
|
||||
/// The URL of the certificate file.
|
||||
final String? fileUrl;
|
||||
|
||||
/// The expiry date of the certificate.
|
||||
final DateTime? expiry;
|
||||
|
||||
/// The issuer of the certificate.
|
||||
final String? issuer;
|
||||
|
||||
/// The certificate number.
|
||||
final String? certificateNumber;
|
||||
|
||||
/// The validation status of the certificate.
|
||||
final StaffCertificateValidationStatus? validationStatus;
|
||||
}
|
||||
@@ -23,12 +23,12 @@ class CertificateUploadCubit extends Cubit<CertificateUploadState>
|
||||
emit(state.copyWith(selectedFilePath: filePath));
|
||||
}
|
||||
|
||||
Future<void> deleteCertificate(ComplianceType type) async {
|
||||
Future<void> deleteCertificate(String certificateId) async {
|
||||
emit(state.copyWith(status: CertificateUploadStatus.uploading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _deleteCertificateUseCase(type);
|
||||
await _deleteCertificateUseCase(certificateId);
|
||||
emit(state.copyWith(status: CertificateUploadStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
|
||||
@@ -38,12 +38,12 @@ class CertificatesCubit extends Cubit<CertificatesState>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteCertificate(ComplianceType type) async {
|
||||
Future<void> deleteCertificate(String certificateId) async {
|
||||
emit(state.copyWith(status: CertificatesStatus.loading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _deleteCertificateUseCase(type);
|
||||
await _deleteCertificateUseCase(certificateId);
|
||||
await loadCertificates();
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
|
||||
@@ -33,7 +33,7 @@ class CertificatesState extends Equatable {
|
||||
int get completedCount => certificates
|
||||
.where(
|
||||
(StaffCertificate cert) =>
|
||||
cert.validationStatus == StaffCertificateValidationStatus.approved,
|
||||
cert.status == CertificateStatus.verified,
|
||||
)
|
||||
.length;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
final TextEditingController _numberController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
ComplianceType? _selectedType;
|
||||
String _selectedType = '';
|
||||
|
||||
final FilePickerService _filePicker = Modular.get<FilePickerService>();
|
||||
|
||||
@@ -44,13 +44,13 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
_cubit = Modular.get<CertificateUploadCubit>();
|
||||
|
||||
if (widget.certificate != null) {
|
||||
_selectedExpiryDate = widget.certificate!.expiryDate;
|
||||
_selectedExpiryDate = widget.certificate!.expiresAt;
|
||||
_issuerController.text = widget.certificate!.issuer ?? '';
|
||||
_numberController.text = widget.certificate!.certificateNumber ?? '';
|
||||
_nameController.text = widget.certificate!.name;
|
||||
_selectedType = widget.certificate!.certificationType;
|
||||
_selectedType = widget.certificate!.certificateType;
|
||||
} else {
|
||||
_selectedType = ComplianceType.other;
|
||||
_selectedType = 'OTHER';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
);
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
await cubit.deleteCertificate(widget.certificate!.certificationType);
|
||||
await cubit.deleteCertificate(widget.certificate!.certificateId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CertificateUploadCubit>.value(
|
||||
value: _cubit..setSelectedFilePath(
|
||||
widget.certificate?.certificateUrl,
|
||||
widget.certificate?.fileUri,
|
||||
),
|
||||
child: BlocConsumer<CertificateUploadCubit, CertificateUploadState>(
|
||||
listener: (BuildContext context, CertificateUploadState state) {
|
||||
@@ -231,7 +231,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
BlocProvider.of<CertificateUploadCubit>(context)
|
||||
.uploadCertificate(
|
||||
UploadCertificateParams(
|
||||
certificationType: _selectedType!,
|
||||
certificateType: _selectedType,
|
||||
name: _nameController.text,
|
||||
filePath: state.selectedFilePath!,
|
||||
expiryDate: _selectedExpiryDate,
|
||||
|
||||
@@ -19,28 +19,19 @@ class CertificateCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Determine UI state from certificate
|
||||
final bool isComplete =
|
||||
certificate.validationStatus ==
|
||||
StaffCertificateValidationStatus.approved;
|
||||
final bool isExpiring =
|
||||
certificate.status == StaffCertificateStatus.expiring ||
|
||||
certificate.status == StaffCertificateStatus.expiringSoon;
|
||||
final bool isExpired = certificate.status == StaffCertificateStatus.expired;
|
||||
final bool isVerified = certificate.status == CertificateStatus.verified;
|
||||
final bool isExpired = certificate.status == CertificateStatus.expired ||
|
||||
certificate.isExpired;
|
||||
final bool isPending = certificate.status == CertificateStatus.pending;
|
||||
final bool isNotStarted = certificate.fileUri == null ||
|
||||
certificate.status == CertificateStatus.rejected;
|
||||
|
||||
// Override isComplete if expiring or expired
|
||||
final bool showComplete = isComplete && !isExpired && !isExpiring;
|
||||
|
||||
final bool isPending =
|
||||
certificate.validationStatus ==
|
||||
StaffCertificateValidationStatus.pendingExpertReview;
|
||||
final bool isNotStarted =
|
||||
certificate.status == StaffCertificateStatus.notStarted ||
|
||||
certificate.validationStatus ==
|
||||
StaffCertificateValidationStatus.rejected;
|
||||
// Show verified badge only if not expired
|
||||
final bool showComplete = isVerified && !isExpired;
|
||||
|
||||
// UI Properties helper
|
||||
final _CertificateUiProps uiProps = _getUiProps(
|
||||
certificate.certificationType,
|
||||
certificate.certificateType,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
@@ -55,7 +46,7 @@ class CertificateCard extends StatelessWidget {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (isExpiring || isExpired)
|
||||
if (isExpired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
@@ -78,11 +69,7 @@ class CertificateCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expires_in_days(
|
||||
days: _daysUntilExpiry(certificate.expiryDate),
|
||||
),
|
||||
t.staff_certificates.card.expired,
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
],
|
||||
@@ -151,7 +138,7 @@ class CertificateCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
certificate.description ?? '',
|
||||
certificate.certificateType,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
if (showComplete) ...<Widget>[
|
||||
@@ -159,17 +146,15 @@ class CertificateCard extends StatelessWidget {
|
||||
_buildMiniStatus(
|
||||
t.staff_certificates.card.verified,
|
||||
UiColors.primary,
|
||||
certificate.expiryDate,
|
||||
certificate.expiresAt,
|
||||
),
|
||||
],
|
||||
if (isExpiring || isExpired) ...<Widget>[
|
||||
if (isExpired) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_buildMiniStatus(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expiring_soon,
|
||||
isExpired ? UiColors.destructive : UiColors.primary,
|
||||
certificate.expiryDate,
|
||||
t.staff_certificates.card.expired,
|
||||
UiColors.destructive,
|
||||
certificate.expiresAt,
|
||||
),
|
||||
],
|
||||
if (isNotStarted) ...<Widget>[
|
||||
@@ -220,18 +205,14 @@ class CertificateCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
int _daysUntilExpiry(DateTime? expiry) {
|
||||
if (expiry == null) return 0;
|
||||
return expiry.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
_CertificateUiProps _getUiProps(ComplianceType type) {
|
||||
switch (type) {
|
||||
case ComplianceType.backgroundCheck:
|
||||
_CertificateUiProps _getUiProps(String type) {
|
||||
switch (type.toUpperCase()) {
|
||||
case 'BACKGROUND_CHECK':
|
||||
return _CertificateUiProps(UiIcons.fileCheck, UiColors.primary);
|
||||
case ComplianceType.foodHandler:
|
||||
case 'FOOD_HYGIENE':
|
||||
case 'FOOD_HANDLER':
|
||||
return _CertificateUiProps(UiIcons.utensils, UiColors.primary);
|
||||
case ComplianceType.rbs:
|
||||
case 'RBS':
|
||||
return _CertificateUiProps(UiIcons.wine, UiColors.foreground);
|
||||
default:
|
||||
return _CertificateUiProps(UiIcons.award, UiColors.primary);
|
||||
|
||||
@@ -3,28 +3,38 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'data/repositories_impl/certificates_repository_impl.dart';
|
||||
import 'domain/repositories/certificates_repository.dart';
|
||||
import 'domain/usecases/get_certificates_usecase.dart';
|
||||
import 'domain/usecases/delete_certificate_usecase.dart';
|
||||
import 'domain/usecases/upsert_certificate_usecase.dart';
|
||||
import 'domain/usecases/upload_certificate_usecase.dart';
|
||||
import 'presentation/blocs/certificates/certificates_cubit.dart';
|
||||
import 'presentation/blocs/certificate_upload/certificate_upload_cubit.dart';
|
||||
import 'presentation/pages/certificate_upload_page.dart';
|
||||
import 'presentation/pages/certificates_page.dart';
|
||||
import 'package:staff_certificates/src/data/repositories_impl/certificates_repository_impl.dart';
|
||||
import 'package:staff_certificates/src/domain/repositories/certificates_repository.dart';
|
||||
import 'package:staff_certificates/src/domain/usecases/get_certificates_usecase.dart';
|
||||
import 'package:staff_certificates/src/domain/usecases/delete_certificate_usecase.dart';
|
||||
import 'package:staff_certificates/src/domain/usecases/upload_certificate_usecase.dart';
|
||||
import 'package:staff_certificates/src/presentation/blocs/certificates/certificates_cubit.dart';
|
||||
import 'package:staff_certificates/src/presentation/blocs/certificate_upload/certificate_upload_cubit.dart';
|
||||
import 'package:staff_certificates/src/presentation/pages/certificate_upload_page.dart';
|
||||
import 'package:staff_certificates/src/presentation/pages/certificates_page.dart';
|
||||
|
||||
/// Module for the Staff Certificates feature.
|
||||
///
|
||||
/// Uses the V2 REST API via [BaseApiService] for backend access.
|
||||
class StaffCertificatesModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<CertificatesRepository>(CertificatesRepositoryImpl.new);
|
||||
i.addLazySingleton<CertificatesRepository>(
|
||||
() => CertificatesRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
uploadService: i.get<FileUploadService>(),
|
||||
signedUrlService: i.get<SignedUrlService>(),
|
||||
verificationService: i.get<VerificationService>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetCertificatesUseCase>(GetCertificatesUseCase.new);
|
||||
i.addLazySingleton<DeleteCertificateUseCase>(DeleteCertificateUseCase.new);
|
||||
i.addLazySingleton<UpsertCertificateUseCase>(UpsertCertificateUseCase.new);
|
||||
i.addLazySingleton<UploadCertificateUseCase>(UploadCertificateUseCase.new);
|
||||
i.addLazySingleton<DeleteCertificateUseCase>(
|
||||
DeleteCertificateUseCase.new);
|
||||
i.addLazySingleton<UploadCertificateUseCase>(
|
||||
UploadCertificateUseCase.new);
|
||||
i.addLazySingleton<CertificatesCubit>(CertificatesCubit.new);
|
||||
i.add<CertificateUploadCubit>(CertificateUploadCubit.new);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ dependencies:
|
||||
flutter_bloc: ^8.1.0
|
||||
equatable: ^2.0.5
|
||||
intl: ^0.20.0
|
||||
get_it: ^7.6.0
|
||||
flutter_modular: ^6.3.0
|
||||
|
||||
|
||||
# KROW Dependencies
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
@@ -25,10 +24,6 @@ dependencies:
|
||||
path: ../../../../../domain
|
||||
krow_core:
|
||||
path: ../../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
firebase_auth: ^6.1.2
|
||||
firebase_data_connect: ^0.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,105 +1,80 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/documents_repository.dart';
|
||||
import 'package:staff_documents/src/domain/repositories/documents_repository.dart';
|
||||
|
||||
/// Implementation of [DocumentsRepository] using Data Connect.
|
||||
/// Implementation of [DocumentsRepository] using the V2 API for reads
|
||||
/// and core services for uploads/verification.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
/// Creates a [DocumentsRepositoryImpl].
|
||||
DocumentsRepositoryImpl({
|
||||
required BaseApiService apiService,
|
||||
required FileUploadService uploadService,
|
||||
required SignedUrlService signedUrlService,
|
||||
required VerificationService verificationService,
|
||||
}) : _service = DataConnectService.instance,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService,
|
||||
_verificationService = verificationService;
|
||||
}) : _api = apiService,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService,
|
||||
_verificationService = verificationService;
|
||||
|
||||
final DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
final FileUploadService _uploadService;
|
||||
final SignedUrlService _signedUrlService;
|
||||
final VerificationService _verificationService;
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffDocument>> getDocuments() async {
|
||||
return _service.getStaffRepository().getStaffDocuments();
|
||||
Future<List<ProfileDocument>> getDocuments() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffDocuments);
|
||||
final List<dynamic> items = response.data['documents'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
ProfileDocument.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.StaffDocument> uploadDocument(
|
||||
Future<ProfileDocument> uploadDocument(
|
||||
String documentId,
|
||||
String filePath,
|
||||
) async {
|
||||
return _service.run(() async {
|
||||
// 1. Upload the file to cloud storage
|
||||
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName: 'staff_document_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
visibility: domain.FileVisibility.private,
|
||||
);
|
||||
// 1. Upload the file to cloud storage
|
||||
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName:
|
||||
'staff_document_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
visibility: FileVisibility.private,
|
||||
);
|
||||
|
||||
// 2. Generate a signed URL for verification service to access the file
|
||||
final SignedUrlResponse signedUrlRes = await _signedUrlService
|
||||
.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
// 2. Generate a signed URL
|
||||
final SignedUrlResponse signedUrlRes =
|
||||
await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
|
||||
// 3. Initiate verification
|
||||
final List<domain.StaffDocument> allDocs = await getDocuments();
|
||||
final domain.StaffDocument currentDoc = allDocs.firstWhere(
|
||||
(domain.StaffDocument d) => d.documentId == documentId,
|
||||
);
|
||||
final String description = (currentDoc.description ?? '').toLowerCase();
|
||||
// 3. Initiate verification
|
||||
final VerificationResponse verificationRes =
|
||||
await _verificationService.createVerification(
|
||||
fileUri: uploadRes.fileUri,
|
||||
type: 'government_id',
|
||||
subjectType: 'worker',
|
||||
subjectId: documentId,
|
||||
rules: <String, dynamic>{'documentId': documentId},
|
||||
);
|
||||
|
||||
final String staffId = await _service.getStaffId();
|
||||
final VerificationResponse verificationRes = await _verificationService
|
||||
.createVerification(
|
||||
fileUri: uploadRes.fileUri,
|
||||
type: 'government_id',
|
||||
subjectType: 'worker',
|
||||
subjectId: staffId,
|
||||
rules: <String, dynamic>{
|
||||
'documentDescription': currentDoc.description,
|
||||
},
|
||||
);
|
||||
// 4. Submit upload result to V2 API
|
||||
await _api.put(
|
||||
V2ApiEndpoints.staffDocumentUpload(documentId),
|
||||
data: <String, dynamic>{
|
||||
'fileUri': signedUrlRes.signedUrl,
|
||||
'verificationId': verificationRes.verificationId,
|
||||
},
|
||||
);
|
||||
|
||||
// 4. Update/Create StaffDocument in Data Connect
|
||||
await _service.getStaffRepository().upsertStaffDocument(
|
||||
documentId: documentId,
|
||||
documentUrl: signedUrlRes.signedUrl,
|
||||
status: domain.DocumentStatus.pending,
|
||||
verificationId: verificationRes.verificationId,
|
||||
);
|
||||
|
||||
// 5. Return the updated document state
|
||||
final List<domain.StaffDocument> documents = await getDocuments();
|
||||
return documents.firstWhere(
|
||||
(domain.StaffDocument d) => d.documentId == documentId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
domain.DocumentStatus _mapStatus(EnumValue<DocumentStatus> status) {
|
||||
if (status is Known<DocumentStatus>) {
|
||||
switch (status.value) {
|
||||
case DocumentStatus.VERIFIED:
|
||||
case DocumentStatus.AUTO_PASS:
|
||||
case DocumentStatus.APPROVED:
|
||||
return domain.DocumentStatus.verified;
|
||||
case DocumentStatus.PENDING:
|
||||
case DocumentStatus.UPLOADED:
|
||||
case DocumentStatus.PROCESSING:
|
||||
case DocumentStatus.NEEDS_REVIEW:
|
||||
case DocumentStatus.EXPIRING:
|
||||
return domain.DocumentStatus.pending;
|
||||
case DocumentStatus.MISSING:
|
||||
return domain.DocumentStatus.missing;
|
||||
case DocumentStatus.AUTO_FAIL:
|
||||
case DocumentStatus.REJECTED:
|
||||
case DocumentStatus.ERROR:
|
||||
return domain.DocumentStatus.rejected;
|
||||
}
|
||||
}
|
||||
// Default to pending for Unknown or unhandled cases
|
||||
return domain.DocumentStatus.pending;
|
||||
// 5. Return the updated document
|
||||
final List<ProfileDocument> documents = await getDocuments();
|
||||
return documents.firstWhere(
|
||||
(ProfileDocument d) => d.documentId == documentId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the documents repository.
|
||||
///
|
||||
/// Responsible for fetching staff compliance documents.
|
||||
/// Responsible for fetching and uploading staff compliance documents
|
||||
/// via the V2 API. Uses [ProfileDocument] from the V2 domain.
|
||||
abstract interface class DocumentsRepository {
|
||||
/// Fetches the list of compliance documents for the current staff member.
|
||||
Future<List<StaffDocument>> getDocuments();
|
||||
Future<List<ProfileDocument>> getDocuments();
|
||||
|
||||
/// Uploads a document for the current staff member.
|
||||
Future<StaffDocument> uploadDocument(String documentId, String filePath);
|
||||
/// Uploads a document file for the given [documentId].
|
||||
Future<ProfileDocument> uploadDocument(String documentId, String filePath);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import '../repositories/documents_repository.dart';
|
||||
/// Use case for fetching staff compliance documents.
|
||||
///
|
||||
/// Delegates to [DocumentsRepository].
|
||||
class GetDocumentsUseCase implements NoInputUseCase<List<StaffDocument>> {
|
||||
class GetDocumentsUseCase implements NoInputUseCase<List<ProfileDocument>> {
|
||||
|
||||
GetDocumentsUseCase(this._repository);
|
||||
final DocumentsRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<StaffDocument>> call() {
|
||||
Future<List<ProfileDocument>> call() {
|
||||
return _repository.getDocuments();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/documents_repository.dart';
|
||||
|
||||
class UploadDocumentUseCase
|
||||
extends UseCase<UploadDocumentArguments, StaffDocument> {
|
||||
extends UseCase<UploadDocumentArguments, ProfileDocument> {
|
||||
UploadDocumentUseCase(this._repository);
|
||||
final DocumentsRepository _repository;
|
||||
|
||||
@override
|
||||
Future<StaffDocument> call(UploadDocumentArguments arguments) {
|
||||
Future<ProfileDocument> call(UploadDocumentArguments arguments) {
|
||||
return _repository.uploadDocument(arguments.documentId, arguments.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
emit(state.copyWith(status: DocumentUploadStatus.uploading));
|
||||
|
||||
try {
|
||||
final StaffDocument updatedDoc = await _uploadDocumentUseCase(
|
||||
final ProfileDocument updatedDoc = await _uploadDocumentUseCase(
|
||||
UploadDocumentArguments(documentId: documentId, filePath: filePath),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class DocumentUploadState extends Equatable {
|
||||
final bool isAttested;
|
||||
final String? selectedFilePath;
|
||||
final String? documentUrl;
|
||||
final StaffDocument? updatedDocument;
|
||||
final ProfileDocument? updatedDocument;
|
||||
final String? errorMessage;
|
||||
|
||||
DocumentUploadState copyWith({
|
||||
@@ -25,7 +25,7 @@ class DocumentUploadState extends Equatable {
|
||||
bool? isAttested,
|
||||
String? selectedFilePath,
|
||||
String? documentUrl,
|
||||
StaffDocument? updatedDocument,
|
||||
ProfileDocument? updatedDocument,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return DocumentUploadState(
|
||||
|
||||
@@ -15,7 +15,7 @@ class DocumentsCubit extends Cubit<DocumentsState>
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final List<StaffDocument> documents = await _getDocumentsUseCase();
|
||||
final List<ProfileDocument> documents = await _getDocumentsUseCase();
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: DocumentsStatus.success,
|
||||
|
||||
@@ -7,16 +7,16 @@ class DocumentsState extends Equatable {
|
||||
|
||||
const DocumentsState({
|
||||
this.status = DocumentsStatus.initial,
|
||||
List<StaffDocument>? documents,
|
||||
List<ProfileDocument>? documents,
|
||||
this.errorMessage,
|
||||
}) : documents = documents ?? const <StaffDocument>[];
|
||||
}) : documents = documents ?? const <ProfileDocument>[];
|
||||
final DocumentsStatus status;
|
||||
final List<StaffDocument> documents;
|
||||
final List<ProfileDocument> documents;
|
||||
final String? errorMessage;
|
||||
|
||||
DocumentsState copyWith({
|
||||
DocumentsStatus? status,
|
||||
List<StaffDocument>? documents,
|
||||
List<ProfileDocument>? documents,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return DocumentsState(
|
||||
@@ -27,7 +27,7 @@ class DocumentsState extends Equatable {
|
||||
}
|
||||
|
||||
int get completedCount =>
|
||||
documents.where((StaffDocument d) => d.status == DocumentStatus.verified).length;
|
||||
documents.where((ProfileDocument d) => d.status == ProfileDocumentStatus.verified).length;
|
||||
|
||||
int get totalCount => documents.length;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class DocumentUploadPage extends StatelessWidget {
|
||||
});
|
||||
|
||||
/// The staff document descriptor for the item being uploaded.
|
||||
final StaffDocument document;
|
||||
final ProfileDocument document;
|
||||
|
||||
/// Optional URL of an already-uploaded document.
|
||||
final String? initialUrl;
|
||||
@@ -62,7 +62,6 @@ class DocumentUploadPage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: document.name,
|
||||
subtitle: document.description,
|
||||
onLeadingPressed: () => Modular.to.toDocuments(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
|
||||
@@ -77,11 +77,11 @@ class DocumentsPage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
...state.documents.map(
|
||||
(StaffDocument doc) => DocumentCard(
|
||||
(ProfileDocument doc) => DocumentCard(
|
||||
document: doc,
|
||||
onTap: () => Modular.to.toDocumentUpload(
|
||||
document: doc,
|
||||
initialUrl: doc.documentUrl,
|
||||
initialUrl: doc.fileUri,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ class DocumentCard extends StatelessWidget {
|
||||
required this.document,
|
||||
this.onTap,
|
||||
});
|
||||
final StaffDocument document;
|
||||
final ProfileDocument document;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
@@ -57,12 +57,6 @@ class DocumentCard extends StatelessWidget {
|
||||
_getStatusIcon(document.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1 / 2),
|
||||
if (document.description != null)
|
||||
Text(
|
||||
document.description!,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@@ -79,15 +73,15 @@ class DocumentCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getStatusIcon(DocumentStatus status) {
|
||||
Widget _getStatusIcon(ProfileDocumentStatus status) {
|
||||
switch (status) {
|
||||
case DocumentStatus.verified:
|
||||
case ProfileDocumentStatus.verified:
|
||||
return const Icon(
|
||||
UiIcons.check,
|
||||
color: UiColors.iconSuccess,
|
||||
size: 20,
|
||||
);
|
||||
case DocumentStatus.pending:
|
||||
case ProfileDocumentStatus.pending:
|
||||
return const Icon(
|
||||
UiIcons.clock,
|
||||
color: UiColors.textWarning,
|
||||
@@ -102,37 +96,32 @@ class DocumentCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(DocumentStatus status) {
|
||||
Widget _buildStatusBadge(ProfileDocumentStatus status) {
|
||||
Color bg;
|
||||
Color text;
|
||||
String label;
|
||||
|
||||
switch (status) {
|
||||
case DocumentStatus.verified:
|
||||
case ProfileDocumentStatus.verified:
|
||||
bg = UiColors.tagSuccess;
|
||||
text = UiColors.textSuccess;
|
||||
label = t.staff_documents.card.verified;
|
||||
break;
|
||||
case DocumentStatus.pending:
|
||||
case ProfileDocumentStatus.pending:
|
||||
bg = UiColors.tagPending;
|
||||
text = UiColors.textWarning;
|
||||
label = t.staff_documents.card.pending;
|
||||
break;
|
||||
case DocumentStatus.missing:
|
||||
case ProfileDocumentStatus.notUploaded:
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.missing;
|
||||
break;
|
||||
case DocumentStatus.rejected:
|
||||
case ProfileDocumentStatus.rejected:
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.rejected;
|
||||
break;
|
||||
case DocumentStatus.expired:
|
||||
case ProfileDocumentStatus.expired:
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.rejected; // Or define "Expired" string
|
||||
break;
|
||||
label = t.staff_documents.card.rejected;
|
||||
}
|
||||
|
||||
return Container(
|
||||
@@ -150,8 +139,8 @@ class DocumentCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(DocumentStatus status) {
|
||||
final bool isVerified = status == DocumentStatus.verified;
|
||||
Widget _buildActionButton(ProfileDocumentStatus status) {
|
||||
final bool isVerified = status == ProfileDocumentStatus.verified;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'data/repositories_impl/documents_repository_impl.dart';
|
||||
import 'domain/repositories/documents_repository.dart';
|
||||
import 'domain/usecases/get_documents_usecase.dart';
|
||||
import 'domain/usecases/upload_document_usecase.dart';
|
||||
import 'presentation/blocs/documents/documents_cubit.dart';
|
||||
import 'presentation/blocs/document_upload/document_upload_cubit.dart';
|
||||
import 'presentation/pages/documents_page.dart';
|
||||
import 'presentation/pages/document_upload_page.dart';
|
||||
|
||||
import 'package:staff_documents/src/data/repositories_impl/documents_repository_impl.dart';
|
||||
import 'package:staff_documents/src/domain/repositories/documents_repository.dart';
|
||||
import 'package:staff_documents/src/domain/usecases/get_documents_usecase.dart';
|
||||
import 'package:staff_documents/src/domain/usecases/upload_document_usecase.dart';
|
||||
import 'package:staff_documents/src/presentation/blocs/documents/documents_cubit.dart';
|
||||
import 'package:staff_documents/src/presentation/blocs/document_upload/document_upload_cubit.dart';
|
||||
import 'package:staff_documents/src/presentation/pages/documents_page.dart';
|
||||
import 'package:staff_documents/src/presentation/pages/document_upload_page.dart';
|
||||
|
||||
/// Module for the Staff Documents feature.
|
||||
///
|
||||
/// Uses the V2 REST API via [BaseApiService] for backend access.
|
||||
class StaffDocumentsModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
@@ -18,6 +22,7 @@ class StaffDocumentsModule extends Module {
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<DocumentsRepository>(
|
||||
() => DocumentsRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
uploadService: i.get<FileUploadService>(),
|
||||
signedUrlService: i.get<SignedUrlService>(),
|
||||
verificationService: i.get<VerificationService>(),
|
||||
@@ -39,7 +44,7 @@ class StaffDocumentsModule extends Module {
|
||||
r.child(
|
||||
StaffPaths.childRoute(StaffPaths.documents, StaffPaths.documentUpload),
|
||||
child: (_) => DocumentUploadPage(
|
||||
document: r.args.data['document'] as StaffDocument,
|
||||
document: r.args.data['document'] as ProfileDocument,
|
||||
initialUrl: r.args.data['initialUrl'] as String?,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,8 +15,7 @@ dependencies:
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
firebase_auth: ^6.1.4
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
|
||||
# Architecture Packages
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
@@ -26,5 +25,3 @@ dependencies:
|
||||
path: ../../../../../core_localization
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
class TaxFormMapper {
|
||||
static TaxForm fromDataConnect(dc.GetTaxFormsByStaffIdTaxForms form) {
|
||||
// Construct the legacy map for the entity
|
||||
final Map<String, dynamic> formData = <String, dynamic>{
|
||||
'firstName': form.firstName,
|
||||
'lastName': form.lastName,
|
||||
'middleInitial': form.mInitial,
|
||||
'otherLastNames': form.oLastName,
|
||||
'dob': _formatDate(form.dob),
|
||||
'ssn': form.socialSN.toString(),
|
||||
'email': form.email,
|
||||
'phone': form.phone,
|
||||
'address': form.address,
|
||||
'aptNumber': form.apt,
|
||||
'city': form.city,
|
||||
'state': form.state,
|
||||
'zipCode': form.zipCode,
|
||||
|
||||
// I-9 Fields
|
||||
'citizenshipStatus': form.citizen?.stringValue,
|
||||
'uscisNumber': form.uscis,
|
||||
'passportNumber': form.passportNumber,
|
||||
'countryIssuance': form.countryIssue,
|
||||
'preparerUsed': form.prepartorOrTranslator,
|
||||
|
||||
// W-4 Fields
|
||||
'filingStatus': form.marital?.stringValue,
|
||||
'multipleJobs': form.multipleJob,
|
||||
'qualifyingChildren': form.childrens,
|
||||
'otherDependents': form.otherDeps,
|
||||
'otherIncome': form.otherInconme?.toString(),
|
||||
'deductions': form.deductions?.toString(),
|
||||
'extraWithholding': form.extraWithholding?.toString(),
|
||||
|
||||
'signature': form.signature,
|
||||
};
|
||||
|
||||
String title = '';
|
||||
String subtitle = '';
|
||||
String description = '';
|
||||
|
||||
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';
|
||||
subtitle = 'Employment Eligibility Verification';
|
||||
description = 'Required for all new hires to verify identity.';
|
||||
} else {
|
||||
title = 'Form W-4';
|
||||
subtitle = 'Employee\'s Withholding Certificate';
|
||||
description = 'Determines federal income tax withholding.';
|
||||
}
|
||||
|
||||
return TaxFormAdapter.fromPrimitives(
|
||||
id: form.id,
|
||||
type: form.formType.stringValue,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
status: form.status.stringValue,
|
||||
staffId: form.staffId,
|
||||
formData: formData,
|
||||
updatedAt: form.updatedAt == null
|
||||
? null
|
||||
: DateTimeUtils.toDeviceTime(form.updatedAt!.toDateTime()),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _formatDate(Timestamp? timestamp) {
|
||||
if (timestamp == null) return null;
|
||||
|
||||
final DateTime date =
|
||||
DateTimeUtils.toDeviceTime(timestamp.toDateTime());
|
||||
|
||||
return '${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}/${date.year}';
|
||||
}
|
||||
}
|
||||
@@ -1,274 +1,44 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/tax_forms_repository.dart';
|
||||
import '../mappers/tax_form_mapper.dart';
|
||||
import 'package:staff_tax_forms/src/domain/repositories/tax_forms_repository.dart';
|
||||
|
||||
/// Implementation of [TaxFormsRepository] using the V2 API.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance;
|
||||
/// Creates a [TaxFormsRepositoryImpl].
|
||||
TaxFormsRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<TaxForm>> getTaxForms() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
final QueryResult<
|
||||
dc.GetTaxFormsByStaffIdData,
|
||||
dc.GetTaxFormsByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<TaxForm> forms = response.data.taxForms
|
||||
.map(TaxFormMapper.fromDataConnect)
|
||||
.toList();
|
||||
|
||||
// Check if required forms exist, create if not.
|
||||
final Set<TaxFormType> typesPresent = forms
|
||||
.map((TaxForm f) => f.type)
|
||||
.toSet();
|
||||
bool createdNew = false;
|
||||
|
||||
if (!typesPresent.contains(TaxFormType.i9)) {
|
||||
await _createInitialForm(staffId, TaxFormType.i9);
|
||||
createdNew = true;
|
||||
}
|
||||
if (!typesPresent.contains(TaxFormType.w4)) {
|
||||
await _createInitialForm(staffId, TaxFormType.w4);
|
||||
createdNew = true;
|
||||
}
|
||||
|
||||
if (createdNew) {
|
||||
final QueryResult<
|
||||
dc.GetTaxFormsByStaffIdData,
|
||||
dc.GetTaxFormsByStaffIdVariables
|
||||
>
|
||||
response2 = await _service.connector
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
return response2.data.taxForms
|
||||
.map(TaxFormMapper.fromDataConnect)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return forms;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _createInitialForm(String staffId, TaxFormType type) async {
|
||||
await _service.connector
|
||||
.createTaxForm(
|
||||
staffId: staffId,
|
||||
formType: dc.TaxFormType.values.byName(
|
||||
TaxFormAdapter.typeToString(type),
|
||||
),
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
socialSN: 0,
|
||||
address: '',
|
||||
status: dc.TaxFormStatus.NOT_STARTED,
|
||||
)
|
||||
.execute();
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffTaxForms);
|
||||
final List<dynamic> items = response.data['taxForms'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
TaxForm.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateI9Form(I9TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.execute();
|
||||
});
|
||||
Future<void> updateTaxForm(TaxForm form) async {
|
||||
await _api.put(
|
||||
V2ApiEndpoints.staffTaxFormUpdate(form.formType),
|
||||
data: form.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitI9Form(I9TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateW4Form(W4TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitW4Form(W4TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
});
|
||||
}
|
||||
|
||||
void _mapCommonFields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('firstName')) {
|
||||
builder.firstName(data['firstName'] as String?);
|
||||
}
|
||||
if (data.containsKey('lastName')) {
|
||||
builder.lastName(data['lastName'] as String?);
|
||||
}
|
||||
if (data.containsKey('middleInitial')) {
|
||||
builder.mInitial(data['middleInitial'] as String?);
|
||||
}
|
||||
if (data.containsKey('otherLastNames')) {
|
||||
builder.oLastName(data['otherLastNames'] as String?);
|
||||
}
|
||||
if (data.containsKey('dob')) {
|
||||
final String dob = data['dob'] as String;
|
||||
// Handle both ISO string and MM/dd/yyyy manual entry
|
||||
DateTime? date;
|
||||
try {
|
||||
date = DateTime.parse(dob);
|
||||
} catch (_) {
|
||||
try {
|
||||
// Fallback minimal parse for mm/dd/yyyy
|
||||
final List<String> parts = dob.split('/');
|
||||
if (parts.length == 3) {
|
||||
date = DateTime(
|
||||
int.parse(parts[2]),
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (date != null) {
|
||||
final int ms = date.millisecondsSinceEpoch;
|
||||
final int seconds = (ms / 1000).floor();
|
||||
builder.dob(Timestamp(0, seconds));
|
||||
}
|
||||
}
|
||||
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
|
||||
builder.socialSN(
|
||||
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0,
|
||||
);
|
||||
}
|
||||
if (data.containsKey('email')) builder.email(data['email'] as String?);
|
||||
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
|
||||
if (data.containsKey('address')) {
|
||||
builder.address(data['address'] as String?);
|
||||
}
|
||||
if (data.containsKey('aptNumber')) {
|
||||
builder.apt(data['aptNumber'] as String?);
|
||||
}
|
||||
if (data.containsKey('city')) builder.city(data['city'] as String?);
|
||||
if (data.containsKey('state')) builder.state(data['state'] as String?);
|
||||
if (data.containsKey('zipCode')) {
|
||||
builder.zipCode(data['zipCode'] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
void _mapI9Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('citizenshipStatus')) {
|
||||
final String status = data['citizenshipStatus'] as String;
|
||||
// Map string to enum if possible, or handle otherwise.
|
||||
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
|
||||
try {
|
||||
builder.citizen(
|
||||
dc.CitizenshipStatus.values.byName(status.toUpperCase()),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('uscisNumber')) {
|
||||
builder.uscis(data['uscisNumber'] as String?);
|
||||
}
|
||||
if (data.containsKey('passportNumber')) {
|
||||
builder.passportNumber(data['passportNumber'] as String?);
|
||||
}
|
||||
if (data.containsKey('countryIssuance')) {
|
||||
builder.countryIssue(data['countryIssuance'] as String?);
|
||||
}
|
||||
if (data.containsKey('preparerUsed')) {
|
||||
builder.prepartorOrTranslator(data['preparerUsed'] as bool?);
|
||||
}
|
||||
if (data.containsKey('signature')) {
|
||||
builder.signature(data['signature'] as String?);
|
||||
}
|
||||
// Note: admissionNumber not in builder based on file read
|
||||
}
|
||||
|
||||
void _mapW4Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('cityStateZip')) {
|
||||
final String csz = data['cityStateZip'] as String;
|
||||
// Extremely basic split: City, State Zip
|
||||
final List<String> parts = csz.split(',');
|
||||
if (parts.length >= 2) {
|
||||
builder.city(parts[0].trim());
|
||||
final String stateZip = parts[1].trim();
|
||||
final List<String> szParts = stateZip.split(' ');
|
||||
if (szParts.isNotEmpty) builder.state(szParts[0]);
|
||||
if (szParts.length > 1) builder.zipCode(szParts.last);
|
||||
}
|
||||
}
|
||||
if (data.containsKey('filingStatus')) {
|
||||
// MARITIAL_STATUS_SINGLE, MARITIAL_STATUS_MARRIED, MARITIAL_STATUS_HEAD
|
||||
try {
|
||||
final String status = data['filingStatus'] as String;
|
||||
// Simple mapping assumptions:
|
||||
if (status.contains('single')) {
|
||||
builder.marital(dc.MaritalStatus.SINGLE);
|
||||
} else if (status.contains('married')) {
|
||||
builder.marital(dc.MaritalStatus.MARRIED);
|
||||
} else if (status.contains('head')) {
|
||||
builder.marital(dc.MaritalStatus.HEAD);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('multipleJobs')) {
|
||||
builder.multipleJob(data['multipleJobs'] as bool?);
|
||||
}
|
||||
if (data.containsKey('qualifyingChildren')) {
|
||||
builder.childrens(data['qualifyingChildren'] as int?);
|
||||
}
|
||||
if (data.containsKey('otherDependents')) {
|
||||
builder.otherDeps(data['otherDependents'] as int?);
|
||||
}
|
||||
if (data.containsKey('otherIncome')) {
|
||||
builder.otherInconme(double.tryParse(data['otherIncome'].toString()));
|
||||
}
|
||||
if (data.containsKey('deductions')) {
|
||||
builder.deductions(double.tryParse(data['deductions'].toString()));
|
||||
}
|
||||
if (data.containsKey('extraWithholding')) {
|
||||
builder.extraWithholding(
|
||||
double.tryParse(data['extraWithholding'].toString()),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('signature')) {
|
||||
builder.signature(data['signature'] as String?);
|
||||
}
|
||||
Future<void> submitTaxForm(TaxForm form) async {
|
||||
await _api.post(
|
||||
V2ApiEndpoints.staffTaxFormSubmit(form.formType),
|
||||
data: form.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for tax form operations.
|
||||
///
|
||||
/// Uses [TaxForm] from the V2 domain layer.
|
||||
abstract class TaxFormsRepository {
|
||||
/// Fetches the list of tax forms for the current staff member.
|
||||
Future<List<TaxForm>> getTaxForms();
|
||||
Future<void> updateI9Form(I9TaxForm form);
|
||||
Future<void> submitI9Form(I9TaxForm form);
|
||||
Future<void> updateW4Form(W4TaxForm form);
|
||||
Future<void> submitW4Form(W4TaxForm form);
|
||||
|
||||
/// Updates a tax form's fields (partial save).
|
||||
Future<void> updateTaxForm(TaxForm form);
|
||||
|
||||
/// Submits a tax form for review.
|
||||
Future<void> submitTaxForm(TaxForm form);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class SaveI9FormUseCase {
|
||||
SaveI9FormUseCase(this._repository);
|
||||
final TaxFormsRepository _repository;
|
||||
|
||||
Future<void> call(I9TaxForm form) async {
|
||||
return _repository.updateI9Form(form);
|
||||
Future<void> call(TaxForm form) async {
|
||||
return _repository.updateTaxForm(form);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class SaveW4FormUseCase {
|
||||
SaveW4FormUseCase(this._repository);
|
||||
final TaxFormsRepository _repository;
|
||||
|
||||
Future<void> call(W4TaxForm form) async {
|
||||
return _repository.updateW4Form(form);
|
||||
Future<void> call(TaxForm form) async {
|
||||
return _repository.updateTaxForm(form);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class SubmitI9FormUseCase {
|
||||
SubmitI9FormUseCase(this._repository);
|
||||
final TaxFormsRepository _repository;
|
||||
|
||||
Future<void> call(I9TaxForm form) async {
|
||||
return _repository.submitI9Form(form);
|
||||
Future<void> call(TaxForm form) async {
|
||||
return _repository.submitTaxForm(form);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class SubmitW4FormUseCase {
|
||||
SubmitW4FormUseCase(this._repository);
|
||||
final TaxFormsRepository _repository;
|
||||
|
||||
Future<void> call(W4TaxForm form) async {
|
||||
return _repository.submitW4Form(form);
|
||||
Future<void> call(TaxForm form) async {
|
||||
return _repository.submitTaxForm(form);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../domain/usecases/submit_i9_form_usecase.dart';
|
||||
import 'form_i9_state.dart';
|
||||
@@ -10,16 +9,16 @@ class FormI9Cubit extends Cubit<FormI9State> with BlocErrorHandler<FormI9State>
|
||||
|
||||
FormI9Cubit(this._submitI9FormUseCase) : super(const FormI9State());
|
||||
final SubmitI9FormUseCase _submitI9FormUseCase;
|
||||
String _formId = '';
|
||||
String _documentId = '';
|
||||
|
||||
void initialize(TaxForm? form) {
|
||||
if (form == null || form.formData.isEmpty) {
|
||||
if (form == null || form.fields.isEmpty) {
|
||||
emit(const FormI9State()); // Reset to empty if no form
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
_formId = form.id;
|
||||
final Map<String, dynamic> data = form.fields;
|
||||
_documentId = form.documentId;
|
||||
emit(
|
||||
FormI9State(
|
||||
firstName: data['firstName'] as String? ?? '',
|
||||
@@ -122,10 +121,11 @@ class FormI9Cubit extends Cubit<FormI9State> with BlocErrorHandler<FormI9State>
|
||||
'signature': state.signature,
|
||||
};
|
||||
|
||||
final I9TaxForm form = I9TaxForm(
|
||||
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
|
||||
title: 'Form I-9',
|
||||
formData: formData,
|
||||
final TaxForm form = TaxForm(
|
||||
documentId: _documentId,
|
||||
formType: 'I-9',
|
||||
status: TaxFormStatus.submitted,
|
||||
fields: formData,
|
||||
);
|
||||
|
||||
await _submitI9FormUseCase(form);
|
||||
@@ -139,4 +139,3 @@ class FormI9Cubit extends Cubit<FormI9State> with BlocErrorHandler<FormI9State>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../domain/usecases/submit_w4_form_usecase.dart';
|
||||
import 'form_w4_state.dart';
|
||||
@@ -10,16 +9,16 @@ class FormW4Cubit extends Cubit<FormW4State> with BlocErrorHandler<FormW4State>
|
||||
|
||||
FormW4Cubit(this._submitW4FormUseCase) : super(const FormW4State());
|
||||
final SubmitW4FormUseCase _submitW4FormUseCase;
|
||||
String _formId = '';
|
||||
String _documentId = '';
|
||||
|
||||
void initialize(TaxForm? form) {
|
||||
if (form == null || form.formData.isEmpty) {
|
||||
if (form == null || form.fields.isEmpty) {
|
||||
emit(const FormW4State()); // Reset
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
_formId = form.id;
|
||||
final Map<String, dynamic> data = form.fields;
|
||||
_documentId = form.documentId;
|
||||
|
||||
// Combine address parts if needed, or take existing
|
||||
final String city = data['city'] as String? ?? '';
|
||||
@@ -98,7 +97,7 @@ class FormW4Cubit extends Cubit<FormW4State> with BlocErrorHandler<FormW4State>
|
||||
'ssn': state.ssn,
|
||||
'address': state.address,
|
||||
'cityStateZip':
|
||||
state.cityStateZip, // Note: Repository should split this if needed.
|
||||
state.cityStateZip,
|
||||
'filingStatus': state.filingStatus,
|
||||
'multipleJobs': state.multipleJobs,
|
||||
'qualifyingChildren': state.qualifyingChildren,
|
||||
@@ -109,10 +108,11 @@ class FormW4Cubit extends Cubit<FormW4State> with BlocErrorHandler<FormW4State>
|
||||
'signature': state.signature,
|
||||
};
|
||||
|
||||
final W4TaxForm form = W4TaxForm(
|
||||
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
|
||||
title: 'Form W-4',
|
||||
formData: formData,
|
||||
final TaxForm form = TaxForm(
|
||||
documentId: _documentId,
|
||||
formType: 'W-4',
|
||||
status: TaxFormStatus.submitted,
|
||||
fields: formData,
|
||||
);
|
||||
|
||||
await _submitW4FormUseCase(form);
|
||||
@@ -126,4 +126,3 @@ class FormW4Cubit extends Cubit<FormW4State> with BlocErrorHandler<FormW4State>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,22 +82,15 @@ class TaxFormsPage extends StatelessWidget {
|
||||
return TaxFormCard(
|
||||
form: form,
|
||||
onTap: () async {
|
||||
if (form is I9TaxForm) {
|
||||
final Object? result = await Modular.to.pushNamed(
|
||||
'i9',
|
||||
arguments: form,
|
||||
);
|
||||
if (result == true && context.mounted) {
|
||||
await BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
|
||||
}
|
||||
} else if (form is W4TaxForm) {
|
||||
final Object? result = await Modular.to.pushNamed(
|
||||
'w4',
|
||||
arguments: form,
|
||||
);
|
||||
if (result == true && context.mounted) {
|
||||
await BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
|
||||
}
|
||||
final bool isI9 = form.formType.toUpperCase().contains('I-9') ||
|
||||
form.formType.toUpperCase().contains('I9');
|
||||
final String route = isI9 ? 'i9' : 'w4';
|
||||
final Object? result = await Modular.to.pushNamed(
|
||||
route,
|
||||
arguments: form,
|
||||
);
|
||||
if (result == true && context.mounted) {
|
||||
await BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,7 +14,8 @@ class TaxFormCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Helper to get icon based on type
|
||||
final String icon = form is I9TaxForm ? '🛂' : '📋';
|
||||
final bool isI9 = form.formType.toUpperCase().contains('I-9') ||
|
||||
form.formType.toUpperCase().contains('I9');
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
@@ -35,7 +36,13 @@ class TaxFormCard extends StatelessWidget {
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(child: Text(icon, style: UiTypography.headline1m)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
isI9 ? UiIcons.fileCheck : UiIcons.file,
|
||||
color: UiColors.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
@@ -46,7 +53,7 @@ class TaxFormCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
form.title,
|
||||
'Form ${form.formType}',
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
TaxFormStatusBadge(status: form.status),
|
||||
@@ -54,11 +61,9 @@ class TaxFormCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.subtitle ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
form.description ?? '',
|
||||
isI9
|
||||
? 'Employment Eligibility Verification'
|
||||
: 'Employee Withholding Certificate',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'data/repositories/tax_forms_repository_impl.dart';
|
||||
import 'domain/repositories/tax_forms_repository.dart';
|
||||
import 'domain/usecases/get_tax_forms_usecase.dart';
|
||||
import 'domain/usecases/submit_i9_form_usecase.dart';
|
||||
import 'domain/usecases/submit_w4_form_usecase.dart';
|
||||
import 'presentation/blocs/i9/form_i9_cubit.dart';
|
||||
import 'presentation/blocs/tax_forms/tax_forms_cubit.dart';
|
||||
import 'presentation/blocs/w4/form_w4_cubit.dart';
|
||||
import 'presentation/pages/form_i9_page.dart';
|
||||
import 'presentation/pages/form_w4_page.dart';
|
||||
import 'presentation/pages/tax_forms_page.dart';
|
||||
|
||||
import 'package:staff_tax_forms/src/data/repositories/tax_forms_repository_impl.dart';
|
||||
import 'package:staff_tax_forms/src/domain/repositories/tax_forms_repository.dart';
|
||||
import 'package:staff_tax_forms/src/domain/usecases/get_tax_forms_usecase.dart';
|
||||
import 'package:staff_tax_forms/src/domain/usecases/submit_i9_form_usecase.dart';
|
||||
import 'package:staff_tax_forms/src/domain/usecases/submit_w4_form_usecase.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/blocs/tax_forms/tax_forms_cubit.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/blocs/i9/form_i9_cubit.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/blocs/w4/form_w4_cubit.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/pages/form_i9_page.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/pages/form_w4_page.dart';
|
||||
import 'package:staff_tax_forms/src/presentation/pages/tax_forms_page.dart';
|
||||
|
||||
/// Module for the Staff Tax Forms feature.
|
||||
///
|
||||
/// Uses the V2 REST API via [BaseApiService] for backend access.
|
||||
class StaffTaxFormsModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<TaxFormsRepository>(TaxFormsRepositoryImpl.new);
|
||||
i.addLazySingleton<TaxFormsRepository>(
|
||||
() => TaxFormsRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addLazySingleton(GetTaxFormsUseCase.new);
|
||||
|
||||
@@ -15,9 +15,7 @@ dependencies:
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
firebase_auth: ^6.1.4
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
|
||||
|
||||
# Architecture Packages
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
@@ -27,5 +25,3 @@ dependencies:
|
||||
path: ../../../../../core_localization
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
Reference in New Issue
Block a user