feat: Implement certificate upload form with metadata fields, expiry date selection, and file upload functionality
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
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';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
|
import '../../domain/usecases/upload_certificate_usecase.dart';
|
||||||
import '../blocs/certificate_upload/certificate_upload_cubit.dart';
|
import '../blocs/certificate_upload/certificate_upload_cubit.dart';
|
||||||
import '../blocs/certificate_upload/certificate_upload_state.dart';
|
import '../blocs/certificate_upload/certificate_upload_state.dart';
|
||||||
import '../../domain/usecases/upload_certificate_usecase.dart';
|
import '../widgets/certificate_upload_page/index.dart';
|
||||||
|
|
||||||
/// Page for uploading a certificate with metadata (expiry, issuer, etc).
|
/// Page for uploading a certificate with metadata (expiry, issuer, etc).
|
||||||
class CertificateUploadPage extends StatefulWidget {
|
class CertificateUploadPage extends StatefulWidget {
|
||||||
@@ -179,62 +179,16 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
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),
|
||||||
|
|
||||||
// Name Field
|
CertificateMetadataFields(
|
||||||
Text(
|
nameController: _nameController,
|
||||||
t.staff_certificates.upload_modal.name_label,
|
issuerController: _issuerController,
|
||||||
style: UiTypography.body2m.textPrimary,
|
numberController: _numberController,
|
||||||
),
|
isNewCertificate: _isNewCertificate,
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
TextField(
|
|
||||||
controller: _nameController,
|
|
||||||
enabled: _isNewCertificate,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: t.staff_certificates.upload_modal.name_hint,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
|
|
||||||
// Issuer Field
|
|
||||||
Text(
|
|
||||||
t.staff_certificates.upload_modal.issuer_label,
|
|
||||||
style: UiTypography.body2m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
TextField(
|
|
||||||
controller: _issuerController,
|
|
||||||
enabled: _isNewCertificate,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: t.staff_certificates.upload_modal.issuer_hint,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
|
|
||||||
// Certificate Number Field
|
|
||||||
Text(
|
|
||||||
'Certificate Number',
|
|
||||||
style: UiTypography.body2m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
TextField(
|
|
||||||
controller: _numberController,
|
|
||||||
enabled: _isNewCertificate,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Enter number if applicable',
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
@@ -242,44 +196,9 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
// Expiry Date Field
|
ExpiryDateField(
|
||||||
Text(
|
selectedDate: _selectedExpiryDate,
|
||||||
t.staff_certificates.upload_modal.expiry_label,
|
|
||||||
style: UiTypography.body2m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
InkWell(
|
|
||||||
onTap: _selectDate,
|
onTap: _selectDate,
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space4,
|
|
||||||
vertical: UiConstants.space3,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(
|
|
||||||
UiIcons.calendar,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Text(
|
|
||||||
_selectedExpiryDate != null
|
|
||||||
? DateFormat(
|
|
||||||
'MMM dd, yyyy',
|
|
||||||
).format(_selectedExpiryDate!)
|
|
||||||
: t.staff_certificates.upload_modal.select_date,
|
|
||||||
style: _selectedExpiryDate != null
|
|
||||||
? UiTypography.body1m.textPrimary
|
|
||||||
: UiTypography.body1m.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
@@ -289,7 +208,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
style: UiTypography.body2m.textPrimary,
|
style: UiTypography.body2m.textPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
_FileSelector(
|
FileSelector(
|
||||||
selectedFilePath: _selectedFilePath,
|
selectedFilePath: _selectedFilePath,
|
||||||
onTap: _pickFile,
|
onTap: _pickFile,
|
||||||
),
|
),
|
||||||
@@ -299,110 +218,40 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Column(
|
child: CertificateUploadActions(
|
||||||
mainAxisSize: MainAxisSize.min,
|
isAttested: state.isAttested,
|
||||||
spacing: UiConstants.space4,
|
isFormValid: _selectedFilePath != null &&
|
||||||
children: <Widget>[
|
state.isAttested &&
|
||||||
// Attestation
|
_nameController.text.isNotEmpty,
|
||||||
Row(
|
isUploading: state.status == CertificateUploadStatus.uploading,
|
||||||
children: <Widget>[
|
hasExistingCertificate: widget.certificate != null,
|
||||||
Checkbox(
|
onUploadPressed: () {
|
||||||
value: state.isAttested,
|
final String? err = _validatePdfFile(
|
||||||
onChanged: (bool? val) =>
|
context,
|
||||||
BlocProvider.of<CertificateUploadCubit>(
|
_selectedFilePath!,
|
||||||
context,
|
);
|
||||||
).setAttested(val ?? false),
|
if (err != null) {
|
||||||
activeColor: UiColors.primary,
|
UiSnackbar.show(
|
||||||
),
|
context,
|
||||||
Expanded(
|
message: err,
|
||||||
child: Text(
|
type: UiSnackbarType.error,
|
||||||
t.staff_documents.upload.attestation,
|
margin: const EdgeInsets.all(UiConstants.space4),
|
||||||
style: UiTypography.body3r.textSecondary,
|
);
|
||||||
),
|
return;
|
||||||
),
|
}
|
||||||
],
|
BlocProvider.of<CertificateUploadCubit>(context)
|
||||||
),
|
.uploadCertificate(
|
||||||
SizedBox(
|
UploadCertificateParams(
|
||||||
width: double.infinity,
|
certificationType: _selectedType!,
|
||||||
child: ElevatedButton(
|
name: _nameController.text,
|
||||||
onPressed:
|
filePath: _selectedFilePath!,
|
||||||
(_selectedFilePath != null &&
|
expiryDate: _selectedExpiryDate,
|
||||||
state.isAttested &&
|
issuer: _issuerController.text,
|
||||||
_nameController.text.isNotEmpty)
|
certificateNumber: _numberController.text,
|
||||||
? () {
|
|
||||||
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<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,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: UiConstants.space4,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: state.status == CertificateUploadStatus.uploading
|
|
||||||
? const CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
t.staff_certificates.upload_modal.save,
|
|
||||||
style: UiTypography.body1m.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
// Remove Button (only if existing)
|
onRemovePressed: () => _showRemoveConfirmation(context),
|
||||||
if (widget.certificate != null) ...<Widget>[
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: () => _showRemoveConfirmation(context),
|
|
||||||
icon: const Icon(UiIcons.delete, size: 20),
|
|
||||||
label: Text(t.staff_certificates.card.remove),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.destructive,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: UiConstants.space4,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.destructive,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -412,104 +261,3 @@ 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.primaryForeground,
|
|
||||||
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});
|
|
||||||
|
|
||||||
final String? selectedFilePath;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (selectedFilePath != null) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: UiColors.primary),
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(UiIcons.certificate, color: UiColors.primary),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
selectedFilePath!.split('/').last,
|
|
||||||
style: UiTypography.body1m.primary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.staff_documents.upload.replace,
|
|
||||||
style: UiTypography.body3m.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
height: 120,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: UiColors.border, style: BorderStyle.solid),
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
color: UiColors.background,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(UiIcons.uploadCloud, size: 32, color: UiColors.primary),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
t.staff_certificates.upload_modal.drag_drop,
|
|
||||||
style: UiTypography.body2m,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.staff_certificates.upload_modal.supported_formats,
|
|
||||||
style: UiTypography.body3r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
/// Widget for certificate metadata input fields (name, issuer, number).
|
||||||
|
class CertificateMetadataFields extends StatelessWidget {
|
||||||
|
const CertificateMetadataFields({
|
||||||
|
required this.nameController,
|
||||||
|
required this.issuerController,
|
||||||
|
required this.numberController,
|
||||||
|
required this.isNewCertificate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController nameController;
|
||||||
|
final TextEditingController issuerController;
|
||||||
|
final TextEditingController numberController;
|
||||||
|
final bool isNewCertificate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Name Field
|
||||||
|
Text(
|
||||||
|
t.staff_certificates.upload_modal.name_label,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
enabled: isNewCertificate,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: t.staff_certificates.upload_modal.name_hint,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Issuer Field
|
||||||
|
Text(
|
||||||
|
t.staff_certificates.upload_modal.issuer_label,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
TextField(
|
||||||
|
controller: issuerController,
|
||||||
|
enabled: isNewCertificate,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: t.staff_certificates.upload_modal.issuer_hint,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Certificate Number Field
|
||||||
|
Text(
|
||||||
|
'Certificate Number',
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
TextField(
|
||||||
|
controller: numberController,
|
||||||
|
enabled: isNewCertificate,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Enter number if applicable',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
import '../../blocs/certificate_upload/certificate_upload_cubit.dart';
|
||||||
|
|
||||||
|
/// Widget for attestation checkbox and action buttons in certificate upload form.
|
||||||
|
class CertificateUploadActions extends StatelessWidget {
|
||||||
|
const CertificateUploadActions({
|
||||||
|
required this.isAttested,
|
||||||
|
required this.isFormValid,
|
||||||
|
required this.isUploading,
|
||||||
|
required this.hasExistingCertificate,
|
||||||
|
required this.onUploadPressed,
|
||||||
|
required this.onRemovePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isAttested;
|
||||||
|
final bool isFormValid;
|
||||||
|
final bool isUploading;
|
||||||
|
final bool hasExistingCertificate;
|
||||||
|
final VoidCallback onUploadPressed;
|
||||||
|
final VoidCallback onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: <Widget>[
|
||||||
|
// Attestation
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Checkbox(
|
||||||
|
value: isAttested,
|
||||||
|
onChanged: (bool? val) =>
|
||||||
|
BlocProvider.of<CertificateUploadCubit>(context).setAttested(
|
||||||
|
val ?? false,
|
||||||
|
),
|
||||||
|
activeColor: UiColors.primary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
t.staff_documents.upload.attestation,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: isFormValid ? onUploadPressed : null,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isUploading
|
||||||
|
? const CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
t.staff_certificates.upload_modal.save,
|
||||||
|
style: UiTypography.body1m.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Remove Button (only if existing)
|
||||||
|
if (hasExistingCertificate) ...<Widget>[
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: onRemovePressed,
|
||||||
|
icon: const Icon(UiIcons.delete, size: 20),
|
||||||
|
label: Text(t.staff_certificates.card.remove),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.destructive,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
side: const BorderSide(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
/// Widget for selecting certificate expiry date.
|
||||||
|
class ExpiryDateField extends StatelessWidget {
|
||||||
|
const ExpiryDateField({
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime? selectedDate;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.staff_certificates.upload_modal.expiry_label,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space4,
|
||||||
|
vertical: UiConstants.space3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Text(
|
||||||
|
selectedDate != null
|
||||||
|
? DateFormat('MMM dd, yyyy').format(selectedDate!)
|
||||||
|
: t.staff_certificates.upload_modal.select_date,
|
||||||
|
style: selectedDate != null
|
||||||
|
? UiTypography.body1m.textPrimary
|
||||||
|
: UiTypography.body1m.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
/// Widget for selecting certificate file.
|
||||||
|
class FileSelector extends StatelessWidget {
|
||||||
|
const FileSelector({
|
||||||
|
required this.selectedFilePath,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? selectedFilePath;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (selectedFilePath != null) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: UiColors.primary),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.certificate, color: UiColors.primary),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
selectedFilePath!.split('/').last,
|
||||||
|
style: UiTypography.body1m.primary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.staff_documents.upload.replace,
|
||||||
|
style: UiTypography.body3m.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 120,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: UiColors.border, style: BorderStyle.solid),
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
color: UiColors.background,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.uploadCloud, size: 32, color: UiColors.primary),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
t.staff_certificates.upload_modal.drag_drop,
|
||||||
|
style: UiTypography.body2m,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.staff_certificates.upload_modal.supported_formats,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export 'certificate_metadata_fields.dart';
|
||||||
|
export 'certificate_upload_actions.dart';
|
||||||
|
export 'expiry_date_field.dart';
|
||||||
|
export 'file_selector.dart';
|
||||||
|
export 'pdf_file_types_banner.dart';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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});
|
||||||
|
|
||||||
|
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.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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user