feat: Implement comprehensive staff document management with verification status tracking and complete document listing.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
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_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';
|
||||||
@@ -349,4 +350,178 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
throw Exception('Error signing out: ${e.toString()}');
|
throw Exception('Error signing out: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.StaffDocument>> getStaffDocuments() 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.listDocuments().execute(),
|
||||||
|
_service.connector
|
||||||
|
.listStaffDocumentsByStaffId(staffId: staffId)
|
||||||
|
.execute(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final QueryResult<dc.ListDocumentsData, void> documentsRes =
|
||||||
|
results[0] as QueryResult<dc.ListDocumentsData, void>;
|
||||||
|
final QueryResult<
|
||||||
|
dc.ListStaffDocumentsByStaffIdData,
|
||||||
|
dc.ListStaffDocumentsByStaffIdVariables
|
||||||
|
>
|
||||||
|
staffDocsRes =
|
||||||
|
results[1]
|
||||||
|
as QueryResult<
|
||||||
|
dc.ListStaffDocumentsByStaffIdData,
|
||||||
|
dc.ListStaffDocumentsByStaffIdVariables
|
||||||
|
>;
|
||||||
|
|
||||||
|
final List<dc.ListStaffDocumentsByStaffIdStaffDocuments> staffDocs =
|
||||||
|
staffDocsRes.data.staffDocuments;
|
||||||
|
|
||||||
|
return documentsRes.data.documents.map((dc.ListDocumentsDocuments doc) {
|
||||||
|
// Find if this staff member has already uploaded this document
|
||||||
|
final dc.ListStaffDocumentsByStaffIdStaffDocuments? currentDoc =
|
||||||
|
staffDocs
|
||||||
|
.where(
|
||||||
|
(dc.ListStaffDocumentsByStaffIdStaffDocuments d) =>
|
||||||
|
d.documentId == doc.id,
|
||||||
|
)
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
return domain.StaffDocument(
|
||||||
|
id: currentDoc?.id ?? '',
|
||||||
|
staffId: staffId,
|
||||||
|
documentId: doc.id,
|
||||||
|
name: doc.name,
|
||||||
|
description: doc.description,
|
||||||
|
status: currentDoc != null
|
||||||
|
? _mapDocumentStatus(currentDoc.status)
|
||||||
|
: domain.DocumentStatus.missing,
|
||||||
|
documentUrl: currentDoc?.documentUrl,
|
||||||
|
expiryDate: currentDoc?.expiryDate == null
|
||||||
|
? null
|
||||||
|
: DateTimeUtils.toDeviceTime(
|
||||||
|
currentDoc!.expiryDate!.toDateTime(),
|
||||||
|
),
|
||||||
|
verificationId: currentDoc?.verificationId,
|
||||||
|
verificationStatus: currentDoc != null
|
||||||
|
? _mapFromDCDocumentVerificationStatus(currentDoc.status)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> upsertStaffDocument({
|
||||||
|
required String documentId,
|
||||||
|
required String documentUrl,
|
||||||
|
domain.DocumentStatus? status,
|
||||||
|
String? verificationId,
|
||||||
|
}) async {
|
||||||
|
await _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
final domain.Staff staff = await getStaffProfile();
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.upsertStaffDocument(
|
||||||
|
staffId: staffId,
|
||||||
|
staffName: staff.name,
|
||||||
|
documentId: documentId,
|
||||||
|
status: _mapToDCDocumentStatus(status),
|
||||||
|
)
|
||||||
|
.documentUrl(documentUrl)
|
||||||
|
.verificationId(verificationId)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.DocumentStatus _mapDocumentStatus(
|
||||||
|
dc.EnumValue<dc.DocumentStatus> status,
|
||||||
|
) {
|
||||||
|
if (status is dc.Unknown) {
|
||||||
|
return domain.DocumentStatus.pending;
|
||||||
|
}
|
||||||
|
final dc.DocumentStatus value =
|
||||||
|
(status as dc.Known<dc.DocumentStatus>).value;
|
||||||
|
switch (value) {
|
||||||
|
case dc.DocumentStatus.VERIFIED:
|
||||||
|
return domain.DocumentStatus.verified;
|
||||||
|
case dc.DocumentStatus.PENDING:
|
||||||
|
return domain.DocumentStatus.pending;
|
||||||
|
case dc.DocumentStatus.MISSING:
|
||||||
|
return domain.DocumentStatus.missing;
|
||||||
|
case dc.DocumentStatus.UPLOADED:
|
||||||
|
case dc.DocumentStatus.EXPIRING:
|
||||||
|
return domain.DocumentStatus.pending;
|
||||||
|
case dc.DocumentStatus.PROCESSING:
|
||||||
|
case dc.DocumentStatus.AUTO_PASS:
|
||||||
|
case dc.DocumentStatus.AUTO_FAIL:
|
||||||
|
case dc.DocumentStatus.NEEDS_REVIEW:
|
||||||
|
case dc.DocumentStatus.APPROVED:
|
||||||
|
case dc.DocumentStatus.REJECTED:
|
||||||
|
case dc.DocumentStatus.ERROR:
|
||||||
|
if (value == dc.DocumentStatus.AUTO_PASS ||
|
||||||
|
value == dc.DocumentStatus.APPROVED) {
|
||||||
|
return domain.DocumentStatus.verified;
|
||||||
|
}
|
||||||
|
if (value == dc.DocumentStatus.AUTO_FAIL ||
|
||||||
|
value == dc.DocumentStatus.REJECTED ||
|
||||||
|
value == dc.DocumentStatus.ERROR) {
|
||||||
|
return domain.DocumentStatus.rejected;
|
||||||
|
}
|
||||||
|
return domain.DocumentStatus.pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.DocumentVerificationStatus _mapFromDCDocumentVerificationStatus(
|
||||||
|
dc.EnumValue<dc.DocumentStatus> status,
|
||||||
|
) {
|
||||||
|
if (status is dc.Unknown) {
|
||||||
|
return domain.DocumentVerificationStatus.error;
|
||||||
|
}
|
||||||
|
final String name = (status as dc.Known<dc.DocumentStatus>).value.name;
|
||||||
|
switch (name) {
|
||||||
|
case 'PENDING':
|
||||||
|
return domain.DocumentVerificationStatus.pending;
|
||||||
|
case 'PROCESSING':
|
||||||
|
return domain.DocumentVerificationStatus.processing;
|
||||||
|
case 'AUTO_PASS':
|
||||||
|
return domain.DocumentVerificationStatus.autoPass;
|
||||||
|
case 'AUTO_FAIL':
|
||||||
|
return domain.DocumentVerificationStatus.autoFail;
|
||||||
|
case 'NEEDS_REVIEW':
|
||||||
|
return domain.DocumentVerificationStatus.needsReview;
|
||||||
|
case 'APPROVED':
|
||||||
|
return domain.DocumentVerificationStatus.approved;
|
||||||
|
case 'REJECTED':
|
||||||
|
return domain.DocumentVerificationStatus.rejected;
|
||||||
|
case 'VERIFIED':
|
||||||
|
return domain.DocumentVerificationStatus.approved;
|
||||||
|
case 'ERROR':
|
||||||
|
return domain.DocumentVerificationStatus.error;
|
||||||
|
default:
|
||||||
|
return domain.DocumentVerificationStatus.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.DocumentStatus _mapToDCDocumentStatus(domain.DocumentStatus? status) {
|
||||||
|
if (status == null) return dc.DocumentStatus.PENDING;
|
||||||
|
switch (status) {
|
||||||
|
case domain.DocumentStatus.verified:
|
||||||
|
return dc.DocumentStatus.VERIFIED;
|
||||||
|
case domain.DocumentStatus.pending:
|
||||||
|
return dc.DocumentStatus.PENDING;
|
||||||
|
case domain.DocumentStatus.missing:
|
||||||
|
return dc.DocumentStatus.MISSING;
|
||||||
|
case domain.DocumentStatus.rejected:
|
||||||
|
return dc.DocumentStatus.REJECTED;
|
||||||
|
case domain.DocumentStatus.expired:
|
||||||
|
return dc.DocumentStatus.EXPIRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,4 +72,15 @@ abstract interface class StaffConnectorRepository {
|
|||||||
String? bio,
|
String? bio,
|
||||||
String? profilePictureUrl,
|
String? profilePictureUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Fetches the staff documents for the current authenticated user.
|
||||||
|
Future<List<StaffDocument>> getStaffDocuments();
|
||||||
|
|
||||||
|
/// Upserts staff document information.
|
||||||
|
Future<void> upsertStaffDocument({
|
||||||
|
required String documentId,
|
||||||
|
required String documentUrl,
|
||||||
|
DocumentStatus? status,
|
||||||
|
String? verificationId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export 'src/adapters/financial/bank_account/bank_account_adapter.dart';
|
|||||||
|
|
||||||
// Profile
|
// Profile
|
||||||
export 'src/entities/profile/staff_document.dart';
|
export 'src/entities/profile/staff_document.dart';
|
||||||
|
export 'src/entities/profile/document_verification_status.dart';
|
||||||
export 'src/entities/profile/attire_item.dart';
|
export 'src/entities/profile/attire_item.dart';
|
||||||
export 'src/entities/profile/attire_verification_status.dart';
|
export 'src/entities/profile/attire_verification_status.dart';
|
||||||
export 'src/entities/profile/relationship_type.dart';
|
export 'src/entities/profile/relationship_type.dart';
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/// Represents the verification status of a compliance document.
|
||||||
|
enum DocumentVerificationStatus {
|
||||||
|
/// Job is created and waiting to be processed.
|
||||||
|
pending('PENDING'),
|
||||||
|
|
||||||
|
/// Job is currently being processed by machine or human.
|
||||||
|
processing('PROCESSING'),
|
||||||
|
|
||||||
|
/// Machine verification passed automatically.
|
||||||
|
autoPass('AUTO_PASS'),
|
||||||
|
|
||||||
|
/// Machine verification failed automatically.
|
||||||
|
autoFail('AUTO_FAIL'),
|
||||||
|
|
||||||
|
/// Machine results are inconclusive and require human review.
|
||||||
|
needsReview('NEEDS_REVIEW'),
|
||||||
|
|
||||||
|
/// Human reviewer approved the verification.
|
||||||
|
approved('APPROVED'),
|
||||||
|
|
||||||
|
/// Human reviewer rejected the verification.
|
||||||
|
rejected('REJECTED'),
|
||||||
|
|
||||||
|
/// An error occurred during processing.
|
||||||
|
error('ERROR');
|
||||||
|
|
||||||
|
const DocumentVerificationStatus(this.value);
|
||||||
|
|
||||||
|
/// The string value expected by the Core API.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Creates a [DocumentVerificationStatus] from a string.
|
||||||
|
static DocumentVerificationStatus fromString(String value) {
|
||||||
|
return DocumentVerificationStatus.values.firstWhere(
|
||||||
|
(DocumentVerificationStatus e) => e.value == value,
|
||||||
|
orElse: () => DocumentVerificationStatus.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import 'document_verification_status.dart';
|
||||||
|
|
||||||
/// Status of a compliance document.
|
/// Status of a compliance document.
|
||||||
enum DocumentStatus {
|
enum DocumentStatus { verified, pending, missing, rejected, expired }
|
||||||
verified,
|
|
||||||
pending,
|
|
||||||
missing,
|
|
||||||
rejected,
|
|
||||||
expired
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a staff compliance document.
|
/// Represents a staff compliance document.
|
||||||
class StaffDocument extends Equatable {
|
class StaffDocument extends Equatable {
|
||||||
|
|
||||||
const StaffDocument({
|
const StaffDocument({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.staffId,
|
required this.staffId,
|
||||||
@@ -21,7 +16,10 @@ class StaffDocument extends Equatable {
|
|||||||
required this.status,
|
required this.status,
|
||||||
this.documentUrl,
|
this.documentUrl,
|
||||||
this.expiryDate,
|
this.expiryDate,
|
||||||
|
this.verificationId,
|
||||||
|
this.verificationStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The unique identifier of the staff document record.
|
/// The unique identifier of the staff document record.
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@@ -46,15 +44,23 @@ class StaffDocument extends Equatable {
|
|||||||
/// The expiry date of the document.
|
/// The expiry date of the document.
|
||||||
final DateTime? expiryDate;
|
final DateTime? expiryDate;
|
||||||
|
|
||||||
|
/// The ID of the verification record.
|
||||||
|
final String? verificationId;
|
||||||
|
|
||||||
|
/// The detailed verification status.
|
||||||
|
final DocumentVerificationStatus? verificationStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
id,
|
id,
|
||||||
staffId,
|
staffId,
|
||||||
documentId,
|
documentId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
documentUrl,
|
documentUrl,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
];
|
verificationId,
|
||||||
|
verificationStatus,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,75 +87,51 @@ To ensure a consistent experience across all compliance uploads (documents, cert
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Pending — Data Layer (Real Repository Implementation)
|
### ✅ Completed — Data Layer
|
||||||
|
|
||||||
> The `DocumentsRepositoryImpl.uploadDocument` method is currently a **mock**. Replace it with the following sequence:
|
#### Data Connect Integration
|
||||||
|
- `StaffConnectorRepository` interface updated with `getStaffDocuments()` and `upsertStaffDocument()`.
|
||||||
### Step-by-step Repository Implementation
|
- `upsertStaffDocument` mutation in `backend/dataconnect/connector/staffDocument/mutations.gql` updated to accept `verificationId`.
|
||||||
|
- `getStaffDocumentByKey` and `listStaffDocumentsByStaffId` queries updated to include `verificationId`.
|
||||||
```dart
|
- SDK regenerated: `make dataconnect-generate-sdk ENV=dev`.
|
||||||
@override
|
|
||||||
Future<domain.StaffDocument> uploadDocument(
|
#### Repository Implementation — `StaffConnectorRepositoryImpl`
|
||||||
String documentId,
|
- **`getStaffDocuments()`**:
|
||||||
String filePath,
|
- Uses `Future.wait` to simultaneously fetch the full list of available `Document` types and the current staff's `StaffDocument` records.
|
||||||
) async {
|
- Maps the master list of `Document` entities, joining them with any existing `StaffDocument` entry.
|
||||||
return _service.execute(() async {
|
- This ensures the UI always shows all required documents, even if they haven't been uploaded yet (status: `missing`).
|
||||||
// 1. Upload file to cloud storage
|
- Populates `verificationId` and `verificationStatus` for presentation layer mapping.
|
||||||
final String fileUri = await _fileUploadService.upload(filePath);
|
- **`upsertStaffDocument()`**:
|
||||||
|
- Handles the upsert (create or update) of a staff document record.
|
||||||
// 2. Generate a signed URL for viewing
|
- Explicitly passes `documentUrl` and `verificationId`, ensuring metadata is persisted alongside the file reference.
|
||||||
final String documentUrl =
|
- **Status Mapping**:
|
||||||
await _signedUrlService.createSignedUrl(fileUri);
|
- `_mapDocumentStatus`: Collapses backend statuses into domain `verified | pending | rejected | missing`.
|
||||||
|
- `_mapFromDCDocumentVerificationStatus`: Preserves the full granularity of the backend status for the UI/presentation layer.
|
||||||
// 3. Initiate verification job
|
|
||||||
final String verificationId = await _verificationService
|
#### Feature Repository — `DocumentsRepositoryImpl`
|
||||||
.createVerification(fileUri: fileUri, documentType: documentId);
|
- 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.
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 |
|
| Backend `DocumentStatus` | Domain `DocumentStatus` | Notes |
|
||||||
|--------------------------|-------------------------|-------|
|
|--------------------------|-------------------------|-------|
|
||||||
| `VERIFIED` | `verified` | Fully approved |
|
| `VERIFIED` | `verified` | Fully approved |
|
||||||
|
| `AUTO_PASS` | `verified` | AI approved |
|
||||||
|
| `APPROVED` | `verified` | Manually approved |
|
||||||
| `UPLOADED` | `pending` | File received, not yet processed |
|
| `UPLOADED` | `pending` | File received, not yet processed |
|
||||||
| `PENDING` | `pending` | Queued for verification |
|
| `PENDING` | `pending` | Queued for verification |
|
||||||
| `PROCESSING` | `pending` | AI analysis in progress |
|
| `PROCESSING` | `pending` | AI analysis in progress |
|
||||||
| `AUTO_PASS` | `verified` | AI approved |
|
|
||||||
| `APPROVED` | `verified` | Manually approved |
|
|
||||||
| `NEEDS_REVIEW` | `pending` | AI unsure, human review needed |
|
| `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 |
|
| `AUTO_FAIL` | `rejected` | AI rejected |
|
||||||
| `REJECTED` | `rejected` | Manually rejected; check `rejectionReason` |
|
| `REJECTED` | `rejected` | Manually rejected |
|
||||||
| `ERROR` | `rejected` | System error during verification |
|
| `ERROR` | `rejected` | System error during verification |
|
||||||
| `EXPIRING` | `verified` | Valid but approaching expiry |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -172,3 +148,13 @@ DocumentUploadStatus
|
|||||||
**Cubit guards:**
|
**Cubit guards:**
|
||||||
- Upload is blocked unless `state.isAttested == true`
|
- Upload is blocked unless `state.isAttested == true`
|
||||||
- Button is only enabled when both a file is selected AND attestation is checked
|
- 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.
|
/// Implementation of [DocumentsRepository] using Data Connect.
|
||||||
class DocumentsRepositoryImpl implements DocumentsRepository {
|
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 DataConnectService _service;
|
||||||
|
final FileUploadService _uploadService;
|
||||||
|
final SignedUrlService _signedUrlService;
|
||||||
|
final VerificationService _verificationService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<domain.StaffDocument>> getDocuments() async {
|
Future<List<domain.StaffDocument>> getDocuments() async {
|
||||||
return _service.run(() async {
|
return _service.getStaffRepository().getStaffDocuments();
|
||||||
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)),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -48,70 +31,63 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
|
|||||||
String documentId,
|
String documentId,
|
||||||
String filePath,
|
String filePath,
|
||||||
) async {
|
) async {
|
||||||
// Mock upload delay
|
return _service.run(() async {
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
// 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();
|
// 2. Generate a signed URL for verification service to access the file
|
||||||
return domain.StaffDocument(
|
final SignedUrlResponse signedUrlRes = await _signedUrlService
|
||||||
id: 'mock_uploaded_${DateTime.now().millisecondsSinceEpoch}',
|
.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||||
staffId: staffId,
|
|
||||||
documentId: documentId,
|
|
||||||
name: 'Uploaded Document',
|
|
||||||
status: domain.DocumentStatus.pending,
|
|
||||||
documentUrl: 'https://example.com/mock.pdf',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
domain.StaffDocument _mapToDomain(
|
// 3. Initiate verification
|
||||||
ListStaffDocumentsByStaffIdStaffDocuments doc,
|
final String staffId = await _service.getStaffId();
|
||||||
) {
|
final VerificationResponse verificationRes = await _verificationService
|
||||||
return domain.StaffDocument(
|
.createVerification(
|
||||||
id: doc.id,
|
fileUri: uploadRes.fileUri,
|
||||||
staffId: doc.staffId,
|
type: documentId, // Assuming documentId aligns with type
|
||||||
documentId: doc.documentId,
|
subjectType: 'STAFF',
|
||||||
name: doc.document.name,
|
subjectId: staffId,
|
||||||
description: null, // Description not available in data source
|
);
|
||||||
status: _mapStatus(doc.status),
|
|
||||||
documentUrl: doc.documentUrl,
|
// 4. Update/Create StaffDocument in Data Connect
|
||||||
expiryDate: doc.expiryDate == null
|
await _service.getStaffRepository().upsertStaffDocument(
|
||||||
? null
|
documentId: documentId,
|
||||||
: DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()),
|
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) {
|
domain.DocumentStatus _mapStatus(EnumValue<DocumentStatus> status) {
|
||||||
if (status is Known<DocumentStatus>) {
|
if (status is Known<DocumentStatus>) {
|
||||||
switch (status.value) {
|
switch (status.value) {
|
||||||
case DocumentStatus.VERIFIED:
|
case DocumentStatus.VERIFIED:
|
||||||
|
case DocumentStatus.AUTO_PASS:
|
||||||
|
case DocumentStatus.APPROVED:
|
||||||
return domain.DocumentStatus.verified;
|
return domain.DocumentStatus.verified;
|
||||||
case DocumentStatus.PENDING:
|
case DocumentStatus.PENDING:
|
||||||
|
case DocumentStatus.UPLOADED:
|
||||||
|
case DocumentStatus.PROCESSING:
|
||||||
|
case DocumentStatus.NEEDS_REVIEW:
|
||||||
|
case DocumentStatus.EXPIRING:
|
||||||
return domain.DocumentStatus.pending;
|
return domain.DocumentStatus.pending;
|
||||||
case DocumentStatus.MISSING:
|
case DocumentStatus.MISSING:
|
||||||
return domain.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:
|
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:
|
case DocumentStatus.REJECTED:
|
||||||
// TODO: Handle this case.
|
|
||||||
throw UnimplementedError();
|
|
||||||
case DocumentStatus.ERROR:
|
case DocumentStatus.ERROR:
|
||||||
// TODO: Handle this case.
|
return domain.DocumentStatus.rejected;
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Default to pending for Unknown or unhandled cases
|
// Default to pending for Unknown or unhandled cases
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ import 'presentation/pages/document_upload_page.dart';
|
|||||||
class StaffDocumentsModule extends Module {
|
class StaffDocumentsModule extends Module {
|
||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
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(GetDocumentsUseCase.new);
|
||||||
i.addLazySingleton(UploadDocumentUseCase.new);
|
i.addLazySingleton(UploadDocumentUseCase.new);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ mutation upsertStaffDocument(
|
|||||||
$status: DocumentStatus!
|
$status: DocumentStatus!
|
||||||
$documentUrl: String
|
$documentUrl: String
|
||||||
$expiryDate: Timestamp
|
$expiryDate: Timestamp
|
||||||
|
$verificationId: String
|
||||||
) @auth(level: USER) {
|
) @auth(level: USER) {
|
||||||
staffDocument_upsert(
|
staffDocument_upsert(
|
||||||
data: {
|
data: {
|
||||||
@@ -67,6 +68,7 @@ mutation upsertStaffDocument(
|
|||||||
status: $status
|
status: $status
|
||||||
documentUrl: $documentUrl
|
documentUrl: $documentUrl
|
||||||
expiryDate: $expiryDate
|
expiryDate: $expiryDate
|
||||||
|
verificationId: $verificationId
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ query getStaffDocumentByKey(
|
|||||||
status
|
status
|
||||||
documentUrl
|
documentUrl
|
||||||
expiryDate
|
expiryDate
|
||||||
|
verificationId
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
document {
|
document {
|
||||||
@@ -39,6 +40,7 @@ query listStaffDocumentsByStaffId(
|
|||||||
status
|
status
|
||||||
documentUrl
|
documentUrl
|
||||||
expiryDate
|
expiryDate
|
||||||
|
verificationId
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
document {
|
document {
|
||||||
|
|||||||
Reference in New Issue
Block a user