feat: Refactor document upload components to improve file selection and validation

This commit is contained in:
Achintha Isuru
2026-03-01 19:44:43 -05:00
parent 2596249cd2
commit 632e0cca3d
3 changed files with 90 additions and 102 deletions

View File

@@ -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<DocumentUploadPage> {
String? _selectedFilePath;
final FilePickerService _filePicker = Modular.get<FilePickerService>();
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
Future<void> _pickFile() async {
final String? path = await _filePicker.pickFile(
allowedExtensions: <String>['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<DocumentUploadPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_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<DocumentUploadPage> {
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<DocumentUploadCubit>(
context,
).uploadDocument(
@@ -183,36 +130,3 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
);
}
}
/// 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: <Widget>[
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(message, style: UiTypography.body2r.textSecondary),
),
],
),
);
}
}

View File

@@ -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<DocumentFileSelector> createState() => _DocumentFileSelectorState();
}
class _DocumentFileSelectorState extends State<DocumentFileSelector> {
late String? _selectedFilePath;
final FilePickerService _filePicker = Modular.get<FilePickerService>();
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
@override
void initState() {
super.initState();
_selectedFilePath = widget.selectedFilePath;
}
bool get _hasFile => _selectedFilePath != null;
Future<void> _pickFile() async {
final String? path = await _filePicker.pickFile(
allowedExtensions: <String>['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,

View File

@@ -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);
}
}