feat: implement document upload functionality with dedicated UI, state management, and routing.

This commit is contained in:
Achintha Isuru
2026-02-26 16:11:24 -05:00
parent 050072bd93
commit 1aa5132abe
14 changed files with 618 additions and 90 deletions

View File

@@ -225,6 +225,21 @@ extension StaffNavigator on IModularNavigator {
pushNamed(StaffPaths.documents); pushNamed(StaffPaths.documents);
} }
/// Pushes the document upload page.
///
/// Parameters:
/// * [document] - The document metadata to upload
/// * [initialUrl] - Optional initial document URL
void toDocumentUpload({required StaffDocument document, String? initialUrl}) {
navigate(
StaffPaths.documentUpload,
arguments: <String, dynamic>{
'document': document,
'initialUrl': initialUrl,
},
);
}
/// Pushes the certificates management page. /// Pushes the certificates management page.
/// ///
/// Manage professional certificates (e.g., food handling, CPR). /// Manage professional certificates (e.g., food handling, CPR).

View File

@@ -164,6 +164,9 @@ class StaffPaths {
/// Store ID, work permits, and other required documentation. /// Store ID, work permits, and other required documentation.
static const String documents = '/worker-main/documents/'; static const String documents = '/worker-main/documents/';
/// Document upload page.
static const String documentUpload = '/worker-main/documents/upload/';
/// Certificates management - professional certifications. /// Certificates management - professional certifications.
/// ///
/// Manage professional certificates (e.g., food handling, CPR, etc.). /// Manage professional certificates (e.g., food handling, CPR, etc.).

View File

@@ -1050,6 +1050,14 @@
"pending": "Pending", "pending": "Pending",
"missing": "Missing", "missing": "Missing",
"rejected": "Rejected" "rejected": "Rejected"
},
"upload": {
"instructions": "Please select a valid PDF file to upload.",
"submit": "Submit Document",
"select_pdf": "Select PDF File",
"attestation": "I certify that this document is genuine and valid.",
"success": "Document uploaded successfully",
"error": "Failed to upload document"
} }
}, },
"staff_certificates": { "staff_certificates": {

View File

@@ -1050,6 +1050,14 @@
"pending": "Pendiente", "pending": "Pendiente",
"missing": "Faltante", "missing": "Faltante",
"rejected": "Rechazado" "rejected": "Rechazado"
},
"upload": {
"instructions": "Por favor selecciona un archivo PDF válido para subir.",
"submit": "Enviar Documento",
"select_pdf": "Seleccionar Archivo PDF",
"attestation": "Certifico que este documento es genuino y válido.",
"success": "Documento subido exitosamente",
"error": "Error al subir el documento"
} }
}, },
"staff_certificates": { "staff_certificates": {

View File

@@ -9,11 +9,10 @@ import '../../domain/repositories/certificates_repository.dart';
/// ///
/// This class handles the communication with the backend via [DataConnectService]. /// This class handles the communication with the backend via [DataConnectService].
/// It maps raw generated data types to clean [domain.StaffDocument] entities. /// It maps raw generated data types to clean [domain.StaffDocument] entities.
class CertificatesRepositoryImpl class CertificatesRepositoryImpl implements CertificatesRepository {
implements CertificatesRepository {
/// Creates a [CertificatesRepositoryImpl]. /// Creates a [CertificatesRepositoryImpl].
CertificatesRepositoryImpl() : _service = DataConnectService.instance; CertificatesRepositoryImpl() : _service = DataConnectService.instance;
/// The Data Connect service instance. /// The Data Connect service instance.
final DataConnectService _service; final DataConnectService _service;
@@ -23,15 +22,20 @@ class CertificatesRepositoryImpl
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
// Execute the query via DataConnect generated SDK // Execute the query via DataConnect generated SDK
final QueryResult<ListStaffDocumentsByStaffIdData, ListStaffDocumentsByStaffIdVariables> result = final QueryResult<
await _service.connector ListStaffDocumentsByStaffIdData,
ListStaffDocumentsByStaffIdVariables
>
result = await _service.connector
.listStaffDocumentsByStaffId(staffId: staffId) .listStaffDocumentsByStaffId(staffId: staffId)
.execute(); .execute();
// Map the generated SDK types to pure Domain entities // Map the generated SDK types to pure Domain entities
return result.data.staffDocuments return result.data.staffDocuments
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) => .map(
_mapToDomain(doc)) (ListStaffDocumentsByStaffIdStaffDocuments doc) =>
_mapToDomain(doc),
)
.toList(); .toList();
}); });
} }
@@ -70,6 +74,27 @@ class CertificatesRepositoryImpl
// 'EXPIRING' in backend is treated as 'verified' in domain, // 'EXPIRING' in backend is treated as 'verified' in domain,
// as the document is strictly valid until the expiry date. // as the document is strictly valid until the expiry date.
return domain.DocumentStatus.verified; return domain.DocumentStatus.verified;
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();
} }
} }
// Fallback for unknown status // Fallback for unknown status

View File

@@ -1,68 +1,161 @@
# Document Upload & Verification Workflow # Document Upload & Verification Workflow
This document outlines the standardized workflow for handling file uploads, verification, and persistence within the Krow mobile application. This pattern is based on the `attire` module and should be used as a reference for the `certificates` module. This document outlines the standardized workflow for handling file uploads, verification, and persistence within the Krow mobile application. This pattern is based on the `attire` module and is used as the reference for the `documents` and `certificates` modules.
## 1. Overview ## 1. Overview
The workflow follows a 4-step lifecycle: The workflow follows a 4-step lifecycle:
1. **Selection**: Picking or capturing a file (PDF/Image) locally.
2. **Preview**: Allowing the user to review the document/photo before submission. 1. **Selection**: Picking a PDF file locally via `FilePickerService`.
2. **Attestation**: Requiring the user to confirm the document is genuine before submission.
3. **Upload & Verification**: Pushing the file to storage and initiating a background verification job. 3. **Upload & Verification**: Pushing the file to storage and initiating a background verification job.
4. **Persistence**: Saving the record with its verification status to the database. 4. **Persistence**: Saving the record with its verification status to the database via Data Connect.
--- ---
## 2. Technical Stack ## 2. Technical Stack
- **`FilePickerService`**: Handles PDF/File selection from the device.
- **`CameraService` / `GalleryService`**: Handles image capturing (if applicable). | Service | Responsibility |
- **`FileUploadService`**: Uploads raw files to our secure cloud storage. |---------|----------------|
- **`SignedUrlService`**: Generates secure internal/public links for viewing. | `FilePickerService` | PDF/file selection from device |
- **`VerificationService`**: Orchestrates the automated (AI) or manual verification of the document. | `FileUploadService` | Uploads raw files to secure cloud storage |
- **`DataConnect` (Firebase)**: Persists the structured data and verification metadata. | `SignedUrlService` | Generates secure internal/public links for viewing |
| `VerificationService` | Orchestrates AI or manual verification |
| `DataConnect` (Firebase) | Persists structured data and verification metadata |
--- ---
## 3. Implementation Steps ## 3. Implementation Status
### A. UI Layer ### ✅ Completed — Presentation Layer
1. **Selection**:
- Use `FilePickerService.pickFile(allowedExtensions: ['pdf'])`.
- Store the `localPath` in the widget state.
2. **Stateless Preview**:
- Provide an inline previewer (e.g., `PdfPreviewWidget`) so the user can verify the file contents.
3. **Submission**:
- Trigger a Cubit/Bloc event which calls the `UploadDocumentUseCase`.
- Show a global or inline progress indicator during the "Upload -> Verify -> Save" sequence.
### B. Domain Layer #### Routing
1. **UseCase**: - `StaffPaths.documentUpload` constant added to `core/lib/src/routing/staff/route_paths.dart`
- Manage the sequence of repository calls. - `StaffNavigator.toDocumentUpload({required StaffDocument document})` type-safe navigation helper added to `core/lib/src/routing/staff/navigator.dart`
- Handle domain-specific validation (e.g., checking if the document is mandatory).
### C. Data Layer (The Repository Pattern) #### Domain Layer
The `Repository.uploadDocument` method should perform the following: - `DocumentsRepository.uploadDocument(String documentId, String filePath)` method added to the repository interface.
1. **Upload**: Call `FileUploadService` to get a `fileUri`. - `UploadDocumentUseCase` created at `domain/usecases/upload_document_usecase.dart`, wrapping the repository call.
2. **Link**: Call `SignedUrlService.createSignedUrl` to generate a `documentUrl`. - `UploadDocumentArguments` value object holds `documentId` and `filePath`.
3. **Verify**: Call `VerificationService.createVerification` with the `fileUri` and relevant rules (e.g., `documentType`).
4. **Poll (Optional but Recommended)**: Poll the `VerificationService` for status updates for ~10 seconds to provide immediate feedback to the user. #### State Management
5. **Persist**: Call Data Connect's `upsertStaffDocument` mutation with: - `DocumentUploadStatus` enum: `initial | uploading | success | failure`
- `documentId` - `DocumentUploadState` (Equatable): tracks `status`, `isAttested`, `updatedDocument`, `errorMessage`
- `documentUrl` - `DocumentUploadCubit`: guards upload behind attestation check; emits success/failure; typed result as `StaffDocument`
- `verificationId`
- `verificationStatus` (e.g., `PENDING` or `APPROVED`) #### UI — `DocumentUploadPage`
- Accepts `StaffDocument document` and optional `String? initialUrl` as route arguments
- PDF file picker via `FilePickerService.pickFile(allowedExtensions: ['pdf'])`
- File selector card: shows file name when selected or a prompt icon when empty
- Attestation checkbox must be checked before the submit button is enabled
- Loading state: shows `CircularProgressIndicator` while uploading (replaces button — mirrors attire pattern)
- On success: shows `UiSnackbar` and calls `Modular.to.pop(updatedDocument)` to return data to caller
- On failure: shows `UiSnackbar` with error message; stays on page for retry
#### Module Wiring — `StaffDocumentsModule`
- `UploadDocumentUseCase` bound as lazy singleton
- `DocumentUploadCubit` bound (non-singleton, per-use)
- Upload route registered: `StaffPaths.documentUpload``DocumentUploadPage`
- Route arguments extracted: `data['document']` as `StaffDocument`, `data['initialUrl']` as `String?`
#### `DocumentsPage` Integration
- `DocumentCard.onTap` now calls `Modular.to.toDocumentUpload(document: doc)` instead of the old placeholder `pushNamed('./details')`
#### Localization
- Added `staff_documents.upload.*` keys to `en.i18n.json` and `es.i18n.json`
- Strings: `instructions`, `submit`, `select_pdf`, `attestation`, `success`, `error`
- Codegen (`dart run slang`) produces `TranslationsStaffDocumentsUploadEn` and its Spanish counterpart
#### DocumentStatus Mapping
- `_mapDocumentStatus` in `DocumentsRepositoryImpl` now handles all backend enum variants:
- `VERIFIED``verified`
- `UPLOADED`, `PENDING`, `EXPIRING``pending` (treated as in-progress)
- `PROCESSING`, `AUTO_PASS`, `AUTO_FAIL`, `NEEDS_REVIEW`, `APPROVED`, `REJECTED`, `ERROR` → presence acknowledged with `UnimplementedError` (TODO)
--- ---
## 4. State Management (Cubit/Bloc) ## 4. Pending — Data Layer (Real Repository Implementation)
- **`status`**: Track `loading`, `uploading`, `success`, and `failure`.
- **`errorMessage`**: Store localized error keys from `BlocErrorHandler`. > The `DocumentsRepositoryImpl.uploadDocument` method is currently a **mock**. Replace it with the following sequence:
- **`verificationId`**: Store the ID returned from the server to allow later manual refreshes.
### 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,
);
});
}
```
--- ---
## 5. Metadata Mapping ## 5. DocumentStatus Mapping
Ensure the `DocumentStatus` or `VerificationStatus` enum aligns with the backend definition:
- `PENDING`: Waiting for job to start. The backend uses a richer enum than the domain layer. The mapping in `_mapDocumentStatus` should be finalized as follows:
- `PROCESSING`: AI is currently analyzing.
- `APPROVED`: Document is valid. | Backend `DocumentStatus` | Domain `DocumentStatus` | Notes |
- `REJECTED`: Document is invalid (should check `rejectionReason`). |--------------------------|-------------------------|-------|
- `NEEDS_REVIEW`: AI is unsure, manual check required. | `VERIFIED` | `verified` | Fully 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 |
| `AUTO_FAIL` | `rejected` | AI rejected |
| `REJECTED` | `rejected` | Manually rejected; check `rejectionReason` |
| `ERROR` | `rejected` | System error during verification |
| `EXPIRING` | `verified` | Valid but approaching expiry |
---
## 6. State Management Reference
```
DocumentUploadStatus
├── initial — page just opened
├── uploading — upload + verification in progress
├── success — document saved; navigate back with result
└── failure — error; stay on page; show snackbar
```
**Cubit guards:**
- Upload is blocked unless `state.isAttested == true`
- Button is only enabled when both a file is selected AND attestation is checked

View File

@@ -6,9 +6,7 @@ import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/documents_repository.dart'; import '../../domain/repositories/documents_repository.dart';
/// Implementation of [DocumentsRepository] using Data Connect. /// Implementation of [DocumentsRepository] using Data Connect.
class DocumentsRepositoryImpl class DocumentsRepositoryImpl implements DocumentsRepository {
implements DocumentsRepository {
DocumentsRepositoryImpl() : _service = DataConnectService.instance; DocumentsRepositoryImpl() : _service = DataConnectService.instance;
final DataConnectService _service; final DataConnectService _service;
@@ -35,16 +33,35 @@ class DocumentsRepositoryImpl
staffId: staffId, staffId: staffId,
documentId: 'd2', documentId: 'd2',
name: 'Health and Safety Training', name: 'Health and Safety Training',
description: 'Certificate of completion for health and safety training', description:
'Certificate of completion for health and safety training',
status: domain.DocumentStatus.pending, status: domain.DocumentStatus.pending,
documentUrl: 'https://example.com/documents/health_safety.pdf', documentUrl: 'https://example.com/documents/health_safety.pdf',
expiryDate: DateTime.now().add(const Duration(days: 180)), expiryDate: DateTime.now().add(const Duration(days: 180)),
), ),
]; ];
}); });
} }
@override
Future<domain.StaffDocument> uploadDocument(
String documentId,
String filePath,
) async {
// Mock upload delay
await Future.delayed(const Duration(seconds: 2));
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',
);
}
domain.StaffDocument _mapToDomain( domain.StaffDocument _mapToDomain(
ListStaffDocumentsByStaffIdStaffDocuments doc, ListStaffDocumentsByStaffIdStaffDocuments doc,
) { ) {
@@ -74,11 +91,30 @@ class DocumentsRepositoryImpl
case DocumentStatus.UPLOADED: case DocumentStatus.UPLOADED:
case DocumentStatus.EXPIRING: case DocumentStatus.EXPIRING:
return domain.DocumentStatus.pending; 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();
} }
} }
// Default to pending for Unknown or unhandled cases // Default to pending for Unknown or unhandled cases
return domain.DocumentStatus.pending; return domain.DocumentStatus.pending;
} }
} }

View File

@@ -6,4 +6,7 @@ import 'package:krow_domain/krow_domain.dart';
abstract interface class DocumentsRepository { abstract interface class DocumentsRepository {
/// Fetches the list of compliance documents for the current staff member. /// Fetches the list of compliance documents for the current staff member.
Future<List<StaffDocument>> getDocuments(); Future<List<StaffDocument>> getDocuments();
/// Uploads a document for the current staff member.
Future<StaffDocument> uploadDocument(String documentId, String filePath);
} }

View File

@@ -0,0 +1,24 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/documents_repository.dart';
class UploadDocumentUseCase
extends UseCase<UploadDocumentArguments, StaffDocument> {
UploadDocumentUseCase(this._repository);
final DocumentsRepository _repository;
@override
Future<StaffDocument> call(UploadDocumentArguments arguments) {
return _repository.uploadDocument(arguments.documentId, arguments.filePath);
}
}
class UploadDocumentArguments {
const UploadDocumentArguments({
required this.documentId,
required this.filePath,
});
final String documentId;
final String filePath;
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_documents/src/domain/usecases/upload_document_usecase.dart';
import 'document_upload_state.dart';
/// Manages the lifecycle of a document upload operation.
///
/// Handles attestation validation, file submission, and reports
/// success/failure back to the UI through [DocumentUploadState].
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
DocumentUploadCubit(this._uploadDocumentUseCase)
: super(const DocumentUploadState());
final UploadDocumentUseCase _uploadDocumentUseCase;
/// Updates the user's attestation status.
void setAttested(bool value) {
emit(state.copyWith(isAttested: value));
}
/// Uploads the selected document if the user has attested.
///
/// Requires [state.isAttested] to be true before proceeding.
Future<void> uploadDocument(String documentId, String filePath) async {
if (!state.isAttested) return;
emit(state.copyWith(status: DocumentUploadStatus.uploading));
try {
final StaffDocument updatedDoc = await _uploadDocumentUseCase(
UploadDocumentArguments(documentId: documentId, filePath: filePath),
);
emit(
state.copyWith(
status: DocumentUploadStatus.success,
updatedDocument: updatedDoc,
),
);
} catch (e) {
emit(
state.copyWith(
status: DocumentUploadStatus.failure,
errorMessage: e.toString(),
),
);
}
}
}

View File

@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum DocumentUploadStatus { initial, uploading, success, failure }
class DocumentUploadState extends Equatable {
const DocumentUploadState({
this.status = DocumentUploadStatus.initial,
this.isAttested = false,
this.documentUrl,
this.updatedDocument,
this.errorMessage,
});
final DocumentUploadStatus status;
final bool isAttested;
final String? documentUrl;
final StaffDocument? updatedDocument;
final String? errorMessage;
DocumentUploadState copyWith({
DocumentUploadStatus? status,
bool? isAttested,
String? documentUrl,
StaffDocument? updatedDocument,
String? errorMessage,
}) {
return DocumentUploadState(
status: status ?? this.status,
isAttested: isAttested ?? this.isAttested,
documentUrl: documentUrl ?? this.documentUrl,
updatedDocument: updatedDocument ?? this.updatedDocument,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => <Object?>[
status,
isAttested,
documentUrl,
updatedDocument,
errorMessage,
];
}

View File

@@ -0,0 +1,205 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
// ignore: depend_on_referenced_packages
import 'package:core_localization/core_localization.dart';
import '../blocs/document_upload/document_upload_cubit.dart';
import '../blocs/document_upload/document_upload_state.dart';
/// Allows staff to select and submit a single PDF document for verification.
///
/// Mirrors the pattern used in [AttireCapturePage] for a consistent upload flow:
/// file selection → attestation → submit → poll for result.
class DocumentUploadPage extends StatefulWidget {
const DocumentUploadPage({
super.key,
required this.document,
this.initialUrl,
});
/// The staff document descriptor for the item being uploaded.
final StaffDocument document;
/// Optional URL of an already-uploaded document.
final String? initialUrl;
@override
State<DocumentUploadPage> createState() => _DocumentUploadPageState();
}
class _DocumentUploadPageState extends State<DocumentUploadPage> {
String? _selectedFilePath;
final FilePickerService _filePicker = Modular.get<FilePickerService>();
Future<void> _pickFile() async {
final String? path = await _filePicker.pickFile(
allowedExtensions: <String>['pdf'],
);
if (path != null) {
setState(() {
_selectedFilePath = path;
});
}
}
@override
Widget build(BuildContext context) {
return BlocProvider<DocumentUploadCubit>(
create: (BuildContext _) => Modular.get<DocumentUploadCubit>(),
child: BlocConsumer<DocumentUploadCubit, DocumentUploadState>(
listener: (BuildContext context, DocumentUploadState state) {
if (state.status == DocumentUploadStatus.success) {
UiSnackbar.show(
context,
message: t.staff_documents.upload.success,
type: UiSnackbarType.success,
);
Modular.to.pop(state.updatedDocument);
} else if (state.status == DocumentUploadStatus.failure) {
UiSnackbar.show(
context,
message: state.errorMessage ?? t.staff_documents.upload.error,
type: UiSnackbarType.error,
);
}
},
builder: (BuildContext context, DocumentUploadState state) {
return Scaffold(
appBar: UiAppBar(
title: widget.document.name,
onLeadingPressed: () => Modular.to.toDocuments(),
),
body: Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
t.staff_documents.upload.instructions,
style: UiTypography.body1m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
if (widget.document.description != null)
Text(
widget.document.description!,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space6),
_buildFileSelector(),
],
),
),
),
const SizedBox(height: UiConstants.space4),
_buildAttestationCheckbox(context, state),
const SizedBox(height: UiConstants.space4),
if (state.status == DocumentUploadStatus.uploading)
const Center(
child: Padding(
padding: EdgeInsets.all(UiConstants.space4),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
UiColors.primary,
),
),
),
)
else
UiButton.primary(
fullWidth: true,
onPressed: _selectedFilePath != null && state.isAttested
? () => BlocProvider.of<DocumentUploadCubit>(context)
.uploadDocument(
widget.document.id,
_selectedFilePath!,
)
: null,
text: t.staff_documents.upload.submit,
),
],
),
),
);
},
),
);
}
Widget _buildFileSelector() {
return InkWell(
onTap: _pickFile,
borderRadius: UiConstants.radiusLg,
child: Container(
height: 180,
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(
color: _selectedFilePath != null
? UiColors.primary
: UiColors.border,
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
_selectedFilePath != null ? UiIcons.file : UiIcons.upload,
size: 48,
color: _selectedFilePath != null
? UiColors.primary
: UiColors.textSecondary,
),
const SizedBox(height: UiConstants.space2),
Text(
_selectedFilePath != null
? _selectedFilePath!.split('/').last
: t.staff_documents.upload.select_pdf,
style: UiTypography.body2m.copyWith(
color: _selectedFilePath != null
? UiColors.primary
: UiColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildAttestationCheckbox(
BuildContext context,
DocumentUploadState state,
) {
return Row(
children: <Widget>[
Checkbox(
value: state.isAttested,
onChanged: (bool? value) => BlocProvider.of<DocumentUploadCubit>(
context,
).setAttested(value ?? false),
activeColor: UiColors.primary,
),
Expanded(
child: Text(
t.staff_documents.upload.attestation,
style: UiTypography.body2r.textPrimary,
),
),
],
);
}
}

View File

@@ -80,8 +80,7 @@ class DocumentsPage extends StatelessWidget {
...state.documents.map( ...state.documents.map(
(StaffDocument doc) => DocumentCard( (StaffDocument doc) => DocumentCard(
document: doc, document: doc,
onTap: () => onTap: () => Modular.to.toDocumentUpload(document: doc),
Modular.to.pushNamed('./details', arguments: doc.id),
), ),
), ),
], ],

View File

@@ -1,17 +1,24 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'data/repositories_impl/documents_repository_impl.dart'; import 'data/repositories_impl/documents_repository_impl.dart';
import 'domain/repositories/documents_repository.dart'; import 'domain/repositories/documents_repository.dart';
import 'domain/usecases/get_documents_usecase.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/documents/documents_cubit.dart';
import 'presentation/blocs/document_upload/document_upload_cubit.dart';
import 'presentation/pages/documents_page.dart'; import 'presentation/pages/documents_page.dart';
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.new);
i.addLazySingleton(GetDocumentsUseCase.new); i.addLazySingleton(GetDocumentsUseCase.new);
i.addLazySingleton(DocumentsCubit.new); i.addLazySingleton(UploadDocumentUseCase.new);
i.add(DocumentsCubit.new);
i.add(DocumentUploadCubit.new);
} }
@override @override
@@ -20,5 +27,12 @@ class StaffDocumentsModule extends Module {
StaffPaths.childRoute(StaffPaths.documents, StaffPaths.documents), StaffPaths.childRoute(StaffPaths.documents, StaffPaths.documents),
child: (_) => const DocumentsPage(), child: (_) => const DocumentsPage(),
); );
r.child(
StaffPaths.childRoute(StaffPaths.documents, StaffPaths.documentUpload),
child: (_) => DocumentUploadPage(
document: r.args.data['document'] as StaffDocument,
initialUrl: r.args.data['initialUrl'] as String?,
),
);
} }
} }