feat: localization, file restriction banners, test credentials, edit icon fix
- #553: Audit and verify localizations (en/es), replace hardcoded strings - #549: Incomplete profile banner in Find Shifts (staff app) - #550: File restriction banner on document upload page - #551: File restriction banner on certificate upload page - #552: File restriction banner on attire upload page - #492: Hide edit icon for past/completed orders (client app) - #524: Display worker benefits in staff app - Add test credentials to seed: testclient@gmail.com, staff +1-555-555-1234 - Fix document upload validation (context arg in _validatePdfFile on submit) - Add PR_LOCALIZATION.md Made-with: Cursor
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -55,18 +57,44 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
|
||||
Future<void> _pickFile() async {
|
||||
final String? path = await _filePicker.pickFile(
|
||||
allowedExtensions: <String>['pdf', 'jpg', 'png'],
|
||||
allowedExtensions: <String>['pdf'],
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
final String? error = _validatePdfFile(context, path);
|
||||
if (error != null && mounted) {
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -117,12 +145,8 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t
|
||||
.staff_documents
|
||||
.upload
|
||||
.instructions, // Reusing instructions logic
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
_PdfFileTypesBanner(
|
||||
message: t.staff_documents.upload.pdf_banner,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
@@ -135,7 +159,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "e.g. Food Handler Permit",
|
||||
hintText: t.staff_certificates.upload_modal.name_hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
@@ -193,7 +217,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
TextField(
|
||||
controller: _issuerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "e.g. Department of Health",
|
||||
hintText: t.staff_certificates.upload_modal.issuer_hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
@@ -247,19 +271,33 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
(_selectedFilePath != null &&
|
||||
state.isAttested &&
|
||||
_nameController.text.isNotEmpty)
|
||||
? () =>
|
||||
BlocProvider.of<CertificateUploadCubit>(
|
||||
? () {
|
||||
final String? err =
|
||||
_validatePdfFile(context, _selectedFilePath!);
|
||||
if (err != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).uploadCertificate(
|
||||
UploadCertificateParams(
|
||||
certificationType: _selectedType!,
|
||||
name: _nameController.text,
|
||||
filePath: _selectedFilePath!,
|
||||
expiryDate: _selectedExpiryDate,
|
||||
issuer: _issuerController.text,
|
||||
certificateNumber: _numberController.text,
|
||||
message: err,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(
|
||||
UiConstants.space4,
|
||||
),
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
BlocProvider.of<CertificateUploadCubit>(
|
||||
context,
|
||||
).uploadCertificate(
|
||||
UploadCertificateParams(
|
||||
certificationType: _selectedType!,
|
||||
name: _nameController.text,
|
||||
filePath: _selectedFilePath!,
|
||||
expiryDate: _selectedExpiryDate,
|
||||
issuer: _issuerController.text,
|
||||
certificateNumber: _numberController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
@@ -291,6 +329,42 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.tagActive,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FileSelector extends StatelessWidget {
|
||||
const _FileSelector({this.selectedFilePath, required this.onTap});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -37,18 +39,44 @@ 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 (path != null) {
|
||||
final String? error = _validatePdfFile(context, path);
|
||||
if (error != null && mounted) {
|
||||
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) {
|
||||
return BlocProvider<DocumentUploadCubit>(
|
||||
@@ -82,9 +110,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_documents.upload.instructions,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
_PdfFileTypesBanner(
|
||||
message: t.staff_documents.upload.pdf_banner,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
DocumentFileSelector(
|
||||
@@ -116,9 +143,22 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
isUploading:
|
||||
state.status == DocumentUploadStatus.uploading,
|
||||
canSubmit: _selectedFilePath != null && state.isAttested,
|
||||
onSubmit: () => BlocProvider.of<DocumentUploadCubit>(
|
||||
context,
|
||||
).uploadDocument(widget.document.id, _selectedFilePath!),
|
||||
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(widget.document.id, _selectedFilePath!);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -130,3 +170,39 @@ 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.tagActive,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DocumentsPage extends StatelessWidget {
|
||||
: t.staff_documents.list.error(
|
||||
message: state.errorMessage!,
|
||||
))
|
||||
: t.staff_documents.list.error(message: 'Unknown'),
|
||||
: t.staff_documents.list.error(message: t.staff_documents.list.unknown),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
|
||||
Reference in New Issue
Block a user