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:
2026-02-27 13:48:04 +05:30
parent 66ffce413a
commit 34afe09963
26 changed files with 865 additions and 132 deletions

View File

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

View File

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

View File

@@ -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,