feat: Refactor document upload components to improve file selection and validation
This commit is contained in:
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user