From 632e0cca3d591433e104acf784ce1639f93a5f10 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Mar 2026 19:44:43 -0500 Subject: [PATCH] feat: Refactor document upload components to improve file selection and validation --- .../pages/document_upload_page.dart | 102 ++---------------- .../document_file_selector.dart | 76 +++++++++++-- .../pdf_file_types_banner.dart | 14 +++ 3 files changed, 90 insertions(+), 102 deletions(-) create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/pdf_file_types_banner.dart diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart index 6d962b99..5894b363 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,6 +11,7 @@ import '../blocs/document_upload/document_upload_state.dart'; import '../widgets/document_upload/document_attestation_checkbox.dart'; import '../widgets/document_upload/document_file_selector.dart'; import '../widgets/document_upload/document_upload_footer.dart'; +import '../widgets/document_upload/pdf_file_types_banner.dart'; /// Allows staff to select and submit a single PDF document for verification. /// @@ -37,50 +36,6 @@ class DocumentUploadPage extends StatefulWidget { class _DocumentUploadPageState extends State { String? _selectedFilePath; - final FilePickerService _filePicker = Modular.get(); - - static const int _kMaxFileSizeBytes = 10 * 1024 * 1024; - - Future _pickFile() async { - final String? path = await _filePicker.pickFile( - allowedExtensions: ['pdf'], - ); - - if (!mounted) { - return; - } - - if (path != null) { - final String? error = _validatePdfFile(context, path); - if (error != null) { - UiSnackbar.show( - context, - message: error, - type: UiSnackbarType.error, - margin: const EdgeInsets.all(UiConstants.space4), - ); - return; - } - setState(() { - _selectedFilePath = path; - }); - } - } - - String? _validatePdfFile(BuildContext context, String path) { - final File file = File(path); - if (!file.existsSync()) return context.t.common.file_not_found; - final String ext = path.split('.').last.toLowerCase(); - if (ext != 'pdf') { - return context.t.staff_documents.upload.pdf_banner; - } - final int size = file.lengthSync(); - if (size > _kMaxFileSizeBytes) { - return context.t.staff_documents.upload.pdf_banner; - } - return null; - } - @override Widget build(BuildContext context) { if (widget.initialUrl != null) { @@ -118,13 +73,17 @@ class _DocumentUploadPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _PdfFileTypesBanner( + PdfFileTypesBanner( message: t.staff_documents.upload.pdf_banner, ), const SizedBox(height: UiConstants.space6), DocumentFileSelector( selectedFilePath: _selectedFilePath, - onTap: _pickFile, + onFileSelected: (String path) { + setState(() { + _selectedFilePath = path; + }); + }, ), ], ), @@ -152,19 +111,7 @@ class _DocumentUploadPageState extends State { state.status == DocumentUploadStatus.uploading, canSubmit: _selectedFilePath != null && state.isAttested, onSubmit: () { - final String? err = _validatePdfFile( - context, - _selectedFilePath!, - ); - if (err != null) { - UiSnackbar.show( - context, - message: err, - type: UiSnackbarType.error, - margin: const EdgeInsets.all(UiConstants.space4), - ); - return; - } + BlocProvider.of( context, ).uploadDocument( @@ -183,36 +130,3 @@ class _DocumentUploadPageState extends State { ); } } - -/// Banner displaying accepted file types and size limit for PDF upload. -class _PdfFileTypesBanner extends StatelessWidget { - const _PdfFileTypesBanner({required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - color: UiColors.primaryForeground, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(UiIcons.info, size: 20, color: UiColors.primary), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Text(message, style: UiTypography.body2r.textSecondary), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart index 4c112749..abaf5ec5 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; // ignore: depend_on_referenced_packages import 'package:core_localization/core_localization.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; import 'document_selected_card.dart'; @@ -9,33 +13,89 @@ import 'document_selected_card.dart'; /// /// Shows the selected file name when a file has been chosen, or an /// upload icon with a prompt when no file is selected yet. -class DocumentFileSelector extends StatelessWidget { +class DocumentFileSelector extends StatefulWidget { const DocumentFileSelector({ super.key, - required this.onTap, + this.onFileSelected, this.selectedFilePath, }); - /// Called when the user taps the selector to pick a file. - final VoidCallback onTap; + /// Called when a file is successfully selected and validated. + final Function(String)? onFileSelected; /// The local path of the currently selected file, or null if none chosen. final String? selectedFilePath; - bool get _hasFile => selectedFilePath != null; + @override + State createState() => _DocumentFileSelectorState(); +} + +class _DocumentFileSelectorState extends State { + late String? _selectedFilePath; + final FilePickerService _filePicker = Modular.get(); + static const int _kMaxFileSizeBytes = 10 * 1024 * 1024; + + @override + void initState() { + super.initState(); + _selectedFilePath = widget.selectedFilePath; + } + + bool get _hasFile => _selectedFilePath != null; + + Future _pickFile() async { + final String? path = await _filePicker.pickFile( + allowedExtensions: ['pdf'], + ); + + if (!mounted) { + return; + } + + if (path != null) { + final String? error = _validatePdfFile(context, path); + if (error != null) { + UiSnackbar.show( + context, + message: error, + type: UiSnackbarType.error, + margin: const EdgeInsets.all(UiConstants.space4), + ); + return; + } + setState(() { + _selectedFilePath = path; + }); + widget.onFileSelected?.call(path); + } + } + + String? _validatePdfFile(BuildContext context, String path) { + final File file = File(path); + if (!file.existsSync()) return context.t.common.file_not_found; + final String ext = path.split('.').last.toLowerCase(); + if (ext != 'pdf') { + return context.t.staff_documents.upload.pdf_banner; + } + final int size = file.lengthSync(); + if (size > _kMaxFileSizeBytes) { + return context.t.staff_documents.upload.pdf_banner; + } + return null; + } @override Widget build(BuildContext context) { if (_hasFile) { return InkWell( - onTap: onTap, + onTap: _pickFile, borderRadius: UiConstants.radiusLg, - child: DocumentSelectedCard(selectedFilePath: selectedFilePath!), + child: DocumentSelectedCard(selectedFilePath: _selectedFilePath!), ); } return InkWell( - onTap: onTap, + onTap: _pickFile, borderRadius: UiConstants.radiusLg, child: Container( height: 180, diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/pdf_file_types_banner.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/pdf_file_types_banner.dart new file mode 100644 index 00000000..6c6dabfe --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/pdf_file_types_banner.dart @@ -0,0 +1,14 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Banner displaying accepted file types and size limit for PDF upload. +class PdfFileTypesBanner extends StatelessWidget { + const PdfFileTypesBanner({required this.message, super.key}); + + final String message; + + @override + Widget build(BuildContext context) { + return UiNoticeBanner(title: message, icon: UiIcons.info); + } +}