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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_attestation_checkbox.dart';
|
||||||
import '../widgets/document_upload/document_file_selector.dart';
|
import '../widgets/document_upload/document_file_selector.dart';
|
||||||
import '../widgets/document_upload/document_upload_footer.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.
|
/// 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> {
|
class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||||
String? _selectedFilePath;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.initialUrl != null) {
|
if (widget.initialUrl != null) {
|
||||||
@@ -118,13 +73,17 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_PdfFileTypesBanner(
|
PdfFileTypesBanner(
|
||||||
message: t.staff_documents.upload.pdf_banner,
|
message: t.staff_documents.upload.pdf_banner,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
DocumentFileSelector(
|
DocumentFileSelector(
|
||||||
selectedFilePath: _selectedFilePath,
|
selectedFilePath: _selectedFilePath,
|
||||||
onTap: _pickFile,
|
onFileSelected: (String path) {
|
||||||
|
setState(() {
|
||||||
|
_selectedFilePath = path;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -152,19 +111,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
state.status == DocumentUploadStatus.uploading,
|
state.status == DocumentUploadStatus.uploading,
|
||||||
canSubmit: _selectedFilePath != null && state.isAttested,
|
canSubmit: _selectedFilePath != null && state.isAttested,
|
||||||
onSubmit: () {
|
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>(
|
BlocProvider.of<DocumentUploadCubit>(
|
||||||
context,
|
context,
|
||||||
).uploadDocument(
|
).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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
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 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import 'document_selected_card.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
|
/// Shows the selected file name when a file has been chosen, or an
|
||||||
/// upload icon with a prompt when no file is selected yet.
|
/// upload icon with a prompt when no file is selected yet.
|
||||||
class DocumentFileSelector extends StatelessWidget {
|
class DocumentFileSelector extends StatefulWidget {
|
||||||
const DocumentFileSelector({
|
const DocumentFileSelector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onTap,
|
this.onFileSelected,
|
||||||
this.selectedFilePath,
|
this.selectedFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Called when the user taps the selector to pick a file.
|
/// Called when a file is successfully selected and validated.
|
||||||
final VoidCallback onTap;
|
final Function(String)? onFileSelected;
|
||||||
|
|
||||||
/// The local path of the currently selected file, or null if none chosen.
|
/// The local path of the currently selected file, or null if none chosen.
|
||||||
final String? selectedFilePath;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_hasFile) {
|
if (_hasFile) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: _pickFile,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
child: DocumentSelectedCard(selectedFilePath: selectedFilePath!),
|
child: DocumentSelectedCard(selectedFilePath: _selectedFilePath!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: _pickFile,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 180,
|
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