feat: Implement comprehensive staff document management with verification status tracking and complete document listing.
This commit is contained in:
@@ -87,75 +87,51 @@ To ensure a consistent experience across all compliance uploads (documents, cert
|
||||
|
||||
---
|
||||
|
||||
## 4. Pending — Data Layer (Real Repository Implementation)
|
||||
|
||||
> The `DocumentsRepositoryImpl.uploadDocument` method is currently a **mock**. Replace it with the following sequence:
|
||||
|
||||
### Step-by-step Repository Implementation
|
||||
|
||||
```dart
|
||||
@override
|
||||
Future<domain.StaffDocument> uploadDocument(
|
||||
String documentId,
|
||||
String filePath,
|
||||
) async {
|
||||
return _service.execute(() async {
|
||||
// 1. Upload file to cloud storage
|
||||
final String fileUri = await _fileUploadService.upload(filePath);
|
||||
|
||||
// 2. Generate a signed URL for viewing
|
||||
final String documentUrl =
|
||||
await _signedUrlService.createSignedUrl(fileUri);
|
||||
|
||||
// 3. Initiate verification job
|
||||
final String verificationId = await _verificationService
|
||||
.createVerification(fileUri: fileUri, documentType: documentId);
|
||||
|
||||
// 4. (Optional) Poll for immediate feedback (~10s)
|
||||
// final VerificationStatus status =
|
||||
// await _verificationService.poll(verificationId, maxSeconds: 10);
|
||||
|
||||
// 5. Persist to Data Connect
|
||||
final String staffId = await _service.getStaffId();
|
||||
await _service.connector.upsertStaffDocument(
|
||||
staffId: staffId,
|
||||
documentId: documentId,
|
||||
documentUrl: documentUrl,
|
||||
verificationId: verificationId,
|
||||
status: DocumentStatus.UPLOADED,
|
||||
);
|
||||
|
||||
return domain.StaffDocument(
|
||||
id: documentId,
|
||||
staffId: staffId,
|
||||
documentId: documentId,
|
||||
name: '', // populated from local cache or refetch
|
||||
status: domain.DocumentStatus.pending,
|
||||
documentUrl: documentUrl,
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
### ✅ Completed — Data Layer
|
||||
|
||||
#### Data Connect Integration
|
||||
- `StaffConnectorRepository` interface updated with `getStaffDocuments()` and `upsertStaffDocument()`.
|
||||
- `upsertStaffDocument` mutation in `backend/dataconnect/connector/staffDocument/mutations.gql` updated to accept `verificationId`.
|
||||
- `getStaffDocumentByKey` and `listStaffDocumentsByStaffId` queries updated to include `verificationId`.
|
||||
- SDK regenerated: `make dataconnect-generate-sdk ENV=dev`.
|
||||
|
||||
#### Repository Implementation — `StaffConnectorRepositoryImpl`
|
||||
- **`getStaffDocuments()`**:
|
||||
- Uses `Future.wait` to simultaneously fetch the full list of available `Document` types and the current staff's `StaffDocument` records.
|
||||
- Maps the master list of `Document` entities, joining them with any existing `StaffDocument` entry.
|
||||
- This ensures the UI always shows all required documents, even if they haven't been uploaded yet (status: `missing`).
|
||||
- Populates `verificationId` and `verificationStatus` for presentation layer mapping.
|
||||
- **`upsertStaffDocument()`**:
|
||||
- Handles the upsert (create or update) of a staff document record.
|
||||
- Explicitly passes `documentUrl` and `verificationId`, ensuring metadata is persisted alongside the file reference.
|
||||
- **Status Mapping**:
|
||||
- `_mapDocumentStatus`: Collapses backend statuses into domain `verified | pending | rejected | missing`.
|
||||
- `_mapFromDCDocumentVerificationStatus`: Preserves the full granularity of the backend status for the UI/presentation layer.
|
||||
|
||||
#### Feature Repository — `DocumentsRepositoryImpl`
|
||||
- Fixed to ensure all cross-cutting services (`FileUploadService`, `VerificationService`) are properly orchestrated.
|
||||
- **Verification Integration**: When a document is uploaded, a verification job is triggered, and its `verificationId` is saved to Data Connect immediately. This allows the UI to show a "Processing" state while the background job runs.
|
||||
|
||||
---
|
||||
|
||||
## 5. DocumentStatus Mapping
|
||||
## 5. DocumentStatus Mapping Reference
|
||||
|
||||
The backend uses a richer enum than the domain layer. The mapping in `_mapDocumentStatus` should be finalized as follows:
|
||||
The backend uses a richer enum than the domain layer. The mapping is standardized as follows:
|
||||
|
||||
| Backend `DocumentStatus` | Domain `DocumentStatus` | Notes |
|
||||
|--------------------------|-------------------------|-------|
|
||||
| `VERIFIED` | `verified` | Fully approved |
|
||||
| `AUTO_PASS` | `verified` | AI approved |
|
||||
| `APPROVED` | `verified` | Manually approved |
|
||||
| `UPLOADED` | `pending` | File received, not yet processed |
|
||||
| `PENDING` | `pending` | Queued for verification |
|
||||
| `PROCESSING` | `pending` | AI analysis in progress |
|
||||
| `AUTO_PASS` | `verified` | AI approved |
|
||||
| `APPROVED` | `verified` | Manually approved |
|
||||
| `NEEDS_REVIEW` | `pending` | AI unsure, human review needed |
|
||||
| `EXPIRING` | `pending` | Approaching expiry (treated as pending for renewal) |
|
||||
| `MISSING` | `missing` | Document not yet uploaded |
|
||||
| `AUTO_FAIL` | `rejected` | AI rejected |
|
||||
| `REJECTED` | `rejected` | Manually rejected; check `rejectionReason` |
|
||||
| `REJECTED` | `rejected` | Manually rejected |
|
||||
| `ERROR` | `rejected` | System error during verification |
|
||||
| `EXPIRING` | `verified` | Valid but approaching expiry |
|
||||
|
||||
---
|
||||
|
||||
@@ -172,3 +148,13 @@ DocumentUploadStatus
|
||||
**Cubit guards:**
|
||||
- Upload is blocked unless `state.isAttested == true`
|
||||
- Button is only enabled when both a file is selected AND attestation is checked
|
||||
|
||||
---
|
||||
|
||||
## 7. Future Considerations: Certificates
|
||||
|
||||
The implementation of the **Certificates** module should follow this exact pattern with a few key differences:
|
||||
1. **Repository**: Use `StaffConnectorRepository.getStaffCertificates()` and `upsertStaffCertificate()`.
|
||||
2. **Metadata**: Certificates often require an `expiryDate` and `issueingBody` which should be captured during the upload step if not already present in the schema.
|
||||
3. **Verification**: If using the same `VerificationService`, ensure the `category` is set to `CERTIFICATE` instead of `DOCUMENT` to trigger appropriate verification logic.
|
||||
4. **UI**: Mirror the `DocumentUploadPage` design but update the instructions and translation keys to reference certificates.
|
||||
|
||||
@@ -7,40 +7,23 @@ import '../../domain/repositories/documents_repository.dart';
|
||||
|
||||
/// Implementation of [DocumentsRepository] using Data Connect.
|
||||
class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
DocumentsRepositoryImpl() : _service = DataConnectService.instance;
|
||||
DocumentsRepositoryImpl({
|
||||
required FileUploadService uploadService,
|
||||
required SignedUrlService signedUrlService,
|
||||
required VerificationService verificationService,
|
||||
}) : _service = DataConnectService.instance,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService,
|
||||
_verificationService = verificationService;
|
||||
|
||||
final DataConnectService _service;
|
||||
final FileUploadService _uploadService;
|
||||
final SignedUrlService _signedUrlService;
|
||||
final VerificationService _verificationService;
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffDocument>> getDocuments() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
/// MOCK IMPLEMENTATION
|
||||
/// To be replaced with real data connect query when available
|
||||
return <domain.StaffDocument>[
|
||||
domain.StaffDocument(
|
||||
id: 'doc1',
|
||||
staffId: staffId,
|
||||
documentId: 'd1',
|
||||
name: 'Work Permit',
|
||||
description: 'Valid work permit document',
|
||||
status: domain.DocumentStatus.verified,
|
||||
documentUrl: 'https://example.com/documents/work_permit.pdf',
|
||||
expiryDate: DateTime.now().add(const Duration(days: 365)),
|
||||
),
|
||||
domain.StaffDocument(
|
||||
id: 'doc2',
|
||||
staffId: staffId,
|
||||
documentId: 'd2',
|
||||
name: 'Health and Safety Training',
|
||||
description:
|
||||
'Certificate of completion for health and safety training',
|
||||
status: domain.DocumentStatus.pending,
|
||||
documentUrl: 'https://example.com/documents/health_safety.pdf',
|
||||
expiryDate: DateTime.now().add(const Duration(days: 180)),
|
||||
),
|
||||
];
|
||||
});
|
||||
return _service.getStaffRepository().getStaffDocuments();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,70 +31,63 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
String documentId,
|
||||
String filePath,
|
||||
) async {
|
||||
// Mock upload delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
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,
|
||||
);
|
||||
|
||||
final String staffId = await _service.getStaffId();
|
||||
return domain.StaffDocument(
|
||||
id: 'mock_uploaded_${DateTime.now().millisecondsSinceEpoch}',
|
||||
staffId: staffId,
|
||||
documentId: documentId,
|
||||
name: 'Uploaded Document',
|
||||
status: domain.DocumentStatus.pending,
|
||||
documentUrl: 'https://example.com/mock.pdf',
|
||||
);
|
||||
}
|
||||
// 2. Generate a signed URL for verification service to access the file
|
||||
final SignedUrlResponse signedUrlRes = await _signedUrlService
|
||||
.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
|
||||
domain.StaffDocument _mapToDomain(
|
||||
ListStaffDocumentsByStaffIdStaffDocuments doc,
|
||||
) {
|
||||
return domain.StaffDocument(
|
||||
id: doc.id,
|
||||
staffId: doc.staffId,
|
||||
documentId: doc.documentId,
|
||||
name: doc.document.name,
|
||||
description: null, // Description not available in data source
|
||||
status: _mapStatus(doc.status),
|
||||
documentUrl: doc.documentUrl,
|
||||
expiryDate: doc.expiryDate == null
|
||||
? null
|
||||
: DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()),
|
||||
);
|
||||
// 3. Initiate verification
|
||||
final String staffId = await _service.getStaffId();
|
||||
final VerificationResponse verificationRes = await _verificationService
|
||||
.createVerification(
|
||||
fileUri: uploadRes.fileUri,
|
||||
type: documentId, // Assuming documentId aligns with type
|
||||
subjectType: 'STAFF',
|
||||
subjectId: staffId,
|
||||
);
|
||||
|
||||
// 4. Update/Create StaffDocument in Data Connect
|
||||
await _service.getStaffRepository().upsertStaffDocument(
|
||||
documentId: documentId,
|
||||
documentUrl: uploadRes.fileUri,
|
||||
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.UPLOADED:
|
||||
case DocumentStatus.EXPIRING:
|
||||
return domain.DocumentStatus.pending;
|
||||
case DocumentStatus.PROCESSING:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.AUTO_PASS:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.AUTO_FAIL:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.NEEDS_REVIEW:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.APPROVED:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.REJECTED:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
case DocumentStatus.ERROR:
|
||||
// TODO: Handle this case.
|
||||
throw UnimplementedError();
|
||||
return domain.DocumentStatus.rejected;
|
||||
}
|
||||
}
|
||||
// Default to pending for Unknown or unhandled cases
|
||||
|
||||
@@ -13,7 +13,13 @@ import 'presentation/pages/document_upload_page.dart';
|
||||
class StaffDocumentsModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<DocumentsRepository>(DocumentsRepositoryImpl.new);
|
||||
i.addLazySingleton<DocumentsRepository>(
|
||||
() => DocumentsRepositoryImpl(
|
||||
uploadService: i.get<FileUploadService>(),
|
||||
signedUrlService: i.get<SignedUrlService>(),
|
||||
verificationService: i.get<VerificationService>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton(GetDocumentsUseCase.new);
|
||||
i.addLazySingleton(UploadDocumentUseCase.new);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user