feat: Introduce DocumentSelectedCard and refactor DocumentFileSelector for improved display of selected documents, and update upload success navigation.
This commit is contained in:
@@ -46,12 +46,25 @@ The workflow follows a 4-step lifecycle:
|
|||||||
#### UI — `DocumentUploadPage`
|
#### UI — `DocumentUploadPage`
|
||||||
- Accepts `StaffDocument document` and optional `String? initialUrl` as route arguments
|
- Accepts `StaffDocument document` and optional `String? initialUrl` as route arguments
|
||||||
- PDF file picker via `FilePickerService.pickFile(allowedExtensions: ['pdf'])`
|
- PDF file picker via `FilePickerService.pickFile(allowedExtensions: ['pdf'])`
|
||||||
- File selector card: shows file name when selected or a prompt icon when empty
|
- **File selector card**: displays the file name with a document icon when selected, otherwise a prompt to upload.
|
||||||
|
- *Note:* PDF preview/opening functionality is explicitly omitted to maintain a clean, minimal upload flow without relying on external native viewers.
|
||||||
- Attestation checkbox must be checked before the submit button is enabled
|
- Attestation checkbox must be checked before the submit button is enabled
|
||||||
- Loading state: shows `CircularProgressIndicator` while uploading (replaces button — mirrors attire pattern)
|
- 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 success: shows `UiSnackbar` and calls `Modular.to.toDocuments()` to return to the list
|
||||||
- On failure: shows `UiSnackbar` with error message; stays on page for retry
|
- On failure: shows `UiSnackbar` with error message; stays on page for retry
|
||||||
|
|
||||||
|
#### UI Guidelines (For Documents & Certificates)
|
||||||
|
To ensure a consistent experience across all compliance uploads (documents, certificates), adhere to the following UI patterns:
|
||||||
|
1. **Header & Instructions:** Use `UiAppBar` with the item name as the title and description as the subtitle. Provide clear instructions at the top of the body (`UiTypography.body1m.textPrimary`).
|
||||||
|
2. **File Selection Card:**
|
||||||
|
- When empty: Show a neutral/primary bordered card inviting the user to pick a file.
|
||||||
|
- When selected: Show an elegant card with `UiColors.bgPopup`, rounded corners (`UiConstants.radiusLg`), bordered by `UiColors.primary`.
|
||||||
|
- The selected card must contain an identifying icon, the truncated file name, and an explicit inline action (e.g., "Replace" or "Upload") using `UiTypography.body3m.textSecondary`.
|
||||||
|
- **Do not** embed native PDF viewers or link out to external readers.
|
||||||
|
3. **Bottom Footer / Attestation:**
|
||||||
|
- Fix the attestation checkbox and the submit button to the bottom using `bottomNavigationBar` wrapped in a `SafeArea` and `Padding`.
|
||||||
|
- The submit button state must be tightly coupled to both the file presence and the attestation state.
|
||||||
|
|
||||||
#### Module Wiring — `StaffDocumentsModule`
|
#### Module Wiring — `StaffDocumentsModule`
|
||||||
- `UploadDocumentUseCase` bound as lazy singleton
|
- `UploadDocumentUseCase` bound as lazy singleton
|
||||||
- `DocumentUploadCubit` bound (non-singleton, per-use)
|
- `DocumentUploadCubit` bound (non-singleton, per-use)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
message: t.staff_documents.upload.success,
|
message: t.staff_documents.upload.success,
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
Modular.to.pop(state.updatedDocument);
|
Modular.to.toDocuments();
|
||||||
} else if (state.status == DocumentUploadStatus.failure) {
|
} else if (state.status == DocumentUploadStatus.failure) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
import 'document_selected_card.dart';
|
||||||
|
|
||||||
/// Displays a tappable card that prompts the user to pick a PDF file.
|
/// Displays a tappable card that prompts the user to pick a PDF file.
|
||||||
///
|
///
|
||||||
/// Shows the selected file name when a file has been chosen, or an
|
/// Shows the selected file name when a file has been chosen, or an
|
||||||
@@ -24,6 +26,14 @@ class DocumentFileSelector extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_hasFile) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
child: DocumentSelectedCard(selectedFilePath: selectedFilePath!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
@@ -32,27 +42,21 @@ class DocumentFileSelector extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(
|
border: Border.all(color: UiColors.border),
|
||||||
color: _hasFile ? UiColors.primary : UiColors.border,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
const Icon(
|
||||||
_hasFile ? UiIcons.file : UiIcons.upload,
|
UiIcons.upload,
|
||||||
size: 48,
|
size: 48,
|
||||||
color: _hasFile ? UiColors.primary : UiColors.textSecondary,
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
_hasFile
|
t.staff_documents.upload.select_pdf,
|
||||||
? selectedFilePath!.split('/').last
|
style: UiTypography.body2m.textError,
|
||||||
: t.staff_documents.upload.select_pdf,
|
|
||||||
style: UiTypography.body2m.copyWith(
|
|
||||||
color: _hasFile ? UiColors.primary : UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A card that displays the selected document file name and an icon.
|
||||||
|
class DocumentSelectedCard extends StatelessWidget {
|
||||||
|
const DocumentSelectedCard({super.key, required this.selectedFilePath});
|
||||||
|
|
||||||
|
/// The local path of the currently selected file.
|
||||||
|
final String selectedFilePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.primary),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.space2),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(UiIcons.file, color: UiColors.primary, size: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
selectedFilePath.split('/').last,
|
||||||
|
style: UiTypography.body1m.textPrimary,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ dependencies:
|
|||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
firebase_auth: ^6.1.4
|
firebase_auth: ^6.1.4
|
||||||
firebase_data_connect: ^0.2.2+2
|
firebase_data_connect: ^0.2.2+2
|
||||||
|
|
||||||
# Architecture Packages
|
# Architecture Packages
|
||||||
design_system:
|
design_system:
|
||||||
path: ../../../../../design_system
|
path: ../../../../../design_system
|
||||||
|
|||||||
Reference in New Issue
Block a user