feat: Add certificate number field, update "Add Certificate" card UI with blur effect, and consolidate certificate view/upload actions.
This commit is contained in:
@@ -54,10 +54,10 @@
|
||||
@import permission_handler_apple;
|
||||
#endif
|
||||
|
||||
#if __has_include(<record_darwin/RecordPlugin.h>)
|
||||
#import <record_darwin/RecordPlugin.h>
|
||||
#if __has_include(<record_ios/RecordIosPlugin.h>)
|
||||
#import <record_ios/RecordIosPlugin.h>
|
||||
#else
|
||||
@import record_darwin;
|
||||
@import record_ios;
|
||||
#endif
|
||||
|
||||
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
||||
@@ -83,7 +83,7 @@
|
||||
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
||||
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||
[RecordPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordPlugin"]];
|
||||
[RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]];
|
||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import firebase_app_check
|
||||
import firebase_auth
|
||||
import firebase_core
|
||||
import geolocator_apple
|
||||
import record_darwin
|
||||
import record_macos
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
@@ -22,7 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ dependencies:
|
||||
image_picker: ^1.1.2
|
||||
path_provider: ^2.1.3
|
||||
file_picker: ^8.1.7
|
||||
record: ^5.2.0
|
||||
record: ^6.2.0
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
|
||||
@@ -1125,6 +1125,8 @@
|
||||
"name_hint": "e.g. Food Handler Permit",
|
||||
"issuer_label": "Certificate Issuer",
|
||||
"issuer_hint": "e.g. Department of Health",
|
||||
"certificate_number_label": "Certificate Number",
|
||||
"certificate_number_hint": "Enter number if applicable",
|
||||
"expiry_label": "Expiration Date (Optional)",
|
||||
"select_date": "Select date",
|
||||
"upload_file": "Upload File",
|
||||
|
||||
@@ -1118,12 +1118,14 @@
|
||||
"title": "Subir Certificado",
|
||||
"name_label": "Nombre del Certificado",
|
||||
"issuer_label": "Emisor del Certificado",
|
||||
"certificate_number_label": "Número de Certificado",
|
||||
"certificate_number_hint": "Ingrese el número si corresponde",
|
||||
"expiry_label": "Fecha de Expiraci\u00f3n (Opcional)",
|
||||
"select_date": "Seleccionar fecha",
|
||||
"upload_file": "Subir Archivo",
|
||||
"drag_drop": "Arrastra y suelta o haz clic para subir",
|
||||
"supported_formats": "PDF hasta 10MB",
|
||||
"name_hint": "ej. Permiso de Manipulación de Alimentos",
|
||||
"name_hint": "ej. Permiso de Manipulaci\u00f3n de Alimentos",
|
||||
"issuer_hint": "ej. Departamento de Salud",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar Certificado",
|
||||
|
||||
@@ -2,19 +2,38 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../../domain/usecases/upload_certificate_usecase.dart';
|
||||
import '../../../domain/usecases/delete_certificate_usecase.dart';
|
||||
import 'certificate_upload_state.dart';
|
||||
|
||||
class CertificateUploadCubit extends Cubit<CertificateUploadState>
|
||||
with BlocErrorHandler<CertificateUploadState> {
|
||||
CertificateUploadCubit(this._uploadCertificateUseCase)
|
||||
: super(const CertificateUploadState());
|
||||
CertificateUploadCubit(
|
||||
this._uploadCertificateUseCase,
|
||||
this._deleteCertificateUseCase,
|
||||
) : super(const CertificateUploadState());
|
||||
|
||||
final UploadCertificateUseCase _uploadCertificateUseCase;
|
||||
final DeleteCertificateUseCase _deleteCertificateUseCase;
|
||||
|
||||
void setAttested(bool value) {
|
||||
emit(state.copyWith(isAttested: value));
|
||||
}
|
||||
|
||||
Future<void> deleteCertificate(ComplianceType type) async {
|
||||
emit(state.copyWith(status: CertificateUploadStatus.uploading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _deleteCertificateUseCase(type);
|
||||
emit(state.copyWith(status: CertificateUploadStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: CertificateUploadStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> uploadCertificate(UploadCertificateParams params) async {
|
||||
if (!state.isAttested) return;
|
||||
|
||||
|
||||
@@ -64,9 +64,13 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
allowedExtensions: <String>['pdf'],
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
final String? error = _validatePdfFile(context, path);
|
||||
if (error != null && mounted) {
|
||||
if (error != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
@@ -111,6 +115,33 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showRemoveConfirmation(BuildContext context) async {
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.staff_certificates.delete_modal.title),
|
||||
content: Text(t.staff_certificates.delete_modal.message),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(t.staff_certificates.delete_modal.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||
child: Text(t.staff_certificates.delete_modal.confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
BlocProvider.of<CertificateUploadCubit>(
|
||||
context,
|
||||
).deleteCertificate(widget.certificate!.certificationType);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CertificateUploadCubit>(
|
||||
@@ -225,6 +256,23 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Certificate Number Field
|
||||
Text(
|
||||
'Certificate Number',
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
TextField(
|
||||
controller: _numberController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter number if applicable',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// File Selector
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.upload_file,
|
||||
@@ -235,6 +283,29 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
selectedFilePath: _selectedFilePath,
|
||||
onTap: _pickFile,
|
||||
),
|
||||
|
||||
// Remove Button (only if existing)
|
||||
if (widget.certificate != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -272,8 +343,10 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
state.isAttested &&
|
||||
_nameController.text.isNotEmpty)
|
||||
? () {
|
||||
final String? err =
|
||||
_validatePdfFile(context, _selectedFilePath!);
|
||||
final String? err = _validatePdfFile(
|
||||
context,
|
||||
_selectedFilePath!,
|
||||
);
|
||||
if (err != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
@@ -344,7 +417,7 @@ class _PdfFileTypesBanner extends StatelessWidget {
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagActive,
|
||||
color: UiColors.primaryForeground,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
@@ -354,10 +427,7 @@ class _PdfFileTypesBanner extends StatelessWidget {
|
||||
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
child: Text(message, style: UiTypography.body2r.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -73,19 +73,8 @@ class CertificatesPage extends StatelessWidget {
|
||||
...documents.map(
|
||||
(StaffCertificate doc) => CertificateCard(
|
||||
certificate: doc,
|
||||
onView: () => _navigateToUpload(context, doc),
|
||||
onUpload: () => _navigateToUpload(context, doc),
|
||||
onEditExpiry: () =>
|
||||
_showEditExpiryDialog(context, doc),
|
||||
onRemove: () =>
|
||||
_showRemoveConfirmation(context, doc),
|
||||
onView: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message:
|
||||
t.staff_certificates.card.opened_snackbar,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
@@ -116,40 +105,4 @@ class CertificatesPage extends StatelessWidget {
|
||||
// Reload certificates after returning from the upload page
|
||||
await Modular.get<CertificatesCubit>().loadCertificates();
|
||||
}
|
||||
|
||||
void _showEditExpiryDialog(
|
||||
BuildContext context,
|
||||
StaffCertificate certificate,
|
||||
) {
|
||||
_navigateToUpload(context, certificate);
|
||||
}
|
||||
|
||||
void _showRemoveConfirmation(
|
||||
BuildContext context,
|
||||
StaffCertificate certificate,
|
||||
) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.staff_certificates.delete_modal.title),
|
||||
content: Text(t.staff_certificates.delete_modal.message),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(t.staff_certificates.delete_modal.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Modular.get<CertificatesCubit>().deleteCertificate(
|
||||
certificate.certificationType,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||
child: Text(t.staff_certificates.delete_modal.confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class AddCertificateCard extends StatelessWidget {
|
||||
|
||||
const AddCertificateCard({super.key, required this.onTap});
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -11,41 +12,49 @@ class AddCertificateCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
UiColors.bgSecondary.withValues(alpha: 0.5),
|
||||
UiColors.bgSecondary,
|
||||
],
|
||||
),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: UiColors.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: ClipRRect(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
UiColors.bgSecondary.withValues(alpha: 0.8),
|
||||
UiColors.bgSecondary.withValues(alpha: 0.4),
|
||||
],
|
||||
),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: UiColors.bgSecondary.withValues(alpha: 0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.add_more.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
t.staff_certificates.add_more.subtitle,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.add_more.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
t.staff_certificates.add_more.subtitle,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,17 +8,13 @@ class CertificateCard extends StatelessWidget {
|
||||
const CertificateCard({
|
||||
super.key,
|
||||
required this.certificate,
|
||||
this.onUpload,
|
||||
this.onEditExpiry,
|
||||
this.onRemove,
|
||||
this.onView,
|
||||
this.onUpload,
|
||||
});
|
||||
|
||||
final StaffCertificate certificate;
|
||||
final VoidCallback? onUpload;
|
||||
final VoidCallback? onEditExpiry;
|
||||
final VoidCallback? onRemove;
|
||||
final VoidCallback? onView;
|
||||
final VoidCallback? onUpload;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -47,350 +43,188 @@ class CertificateCard extends StatelessWidget {
|
||||
certificate.certificationType,
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (isExpiring || isExpired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent.withValues(alpha: 0.2), // Yellow tint
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: UiColors.accent.withValues(alpha: 0.4),
|
||||
return GestureDetector(
|
||||
onTap: onView,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (isExpiring || isExpired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent.withValues(alpha: 0.2), // Yellow tint
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: UiColors.accent.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
size: 16,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expires_in_days(
|
||||
days: _daysUntilExpiry(certificate.expiryDate),
|
||||
),
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
size: 16,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expires_in_days(
|
||||
days: _daysUntilExpiry(certificate.expiryDate),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: uiProps.color.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
uiProps.icon,
|
||||
color: uiProps.color,
|
||||
size: 24,
|
||||
),
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
),
|
||||
if (showComplete)
|
||||
const Positioned(
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
child: CircleAvatar(
|
||||
radius: 8,
|
||||
backgroundColor: UiColors.primary,
|
||||
child: Icon(
|
||||
UiIcons.success,
|
||||
color: UiColors.white,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isPending)
|
||||
const Positioned(
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
child: CircleAvatar(
|
||||
radius: 8,
|
||||
backgroundColor: UiColors.textPrimary,
|
||||
child: Icon(
|
||||
UiIcons.clock,
|
||||
color: UiColors.white,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
certificate.name,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
certificate.description ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
if (showComplete) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_buildMiniStatus(
|
||||
t.staff_certificates.card.verified,
|
||||
UiColors.primary,
|
||||
certificate.expiryDate,
|
||||
),
|
||||
],
|
||||
if (isExpiring || isExpired) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_buildMiniStatus(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expiring_soon,
|
||||
isExpired ? UiColors.destructive : UiColors.primary,
|
||||
certificate.expiryDate,
|
||||
),
|
||||
],
|
||||
if (isNotStarted) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
GestureDetector(
|
||||
onTap: onUpload,
|
||||
child: Text(
|
||||
t.staff_certificates.card.upload_button,
|
||||
style: UiTypography.body3m.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: uiProps.color.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
uiProps.icon,
|
||||
color: uiProps.color,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showComplete)
|
||||
const Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: UiColors.primary,
|
||||
child: Icon(
|
||||
UiIcons.success,
|
||||
color: UiColors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isPending)
|
||||
const Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: UiColors.textPrimary,
|
||||
child: Icon(
|
||||
UiIcons.clock,
|
||||
color: UiColors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
certificate.name,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
certificate.description ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
if (showComplete)
|
||||
_buildCompleteStatus(certificate.expiryDate),
|
||||
if (isExpiring || isExpired)
|
||||
_buildExpiringStatus(context, certificate.expiryDate),
|
||||
if (isNotStarted)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onUpload,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.upload,
|
||||
size: 16,
|
||||
color: UiColors.white,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
t.staff_certificates.card.upload_button,
|
||||
style: UiTypography.body2m.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showComplete || isExpiring || isExpired) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onEditExpiry,
|
||||
icon: const Icon(UiIcons.edit, size: 16),
|
||||
label: Text(t.staff_certificates.card.edit_expiry),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.textPrimary,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
onPressed: onRemove,
|
||||
icon: const Icon(UiIcons.delete, size: 16),
|
||||
label: Text(t.staff_certificates.card.remove),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompleteStatus(DateTime? expiryDate) {
|
||||
Widget _buildMiniStatus(String label, Color color, DateTime? expiryDate) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: UiConstants.space2,
|
||||
height: UiConstants.space2,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
t.staff_certificates.card.verified,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
],
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
if (expiryDate != null)
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(label, style: UiTypography.body3m.copyWith(color: color)),
|
||||
if (expiryDate != null) ...<Widget>[
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
t.staff_certificates.card.exp(
|
||||
date: DateFormat('MMM d, yyyy').format(expiryDate),
|
||||
),
|
||||
'• ${DateFormat('MMM d, yyyy').format(expiryDate)}',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpiringStatus(BuildContext context, DateTime? expiryDate) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
t.staff_certificates.card.expiring_soon,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (expiryDate != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
t.staff_certificates.card.exp(
|
||||
date: DateFormat('MMM d, yyyy').format(expiryDate),
|
||||
),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
_buildIconButton(UiIcons.eye, onView),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildSmallOutlineButton(t.staff_certificates.card.renew, onUpload),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconButton(IconData icon, VoidCallback? onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: UiColors.transparent,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(icon, size: 16, color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallOutlineButton(String label, VoidCallback? onTap) {
|
||||
return OutlinedButton(
|
||||
onPressed: onTap,
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: UiColors.primary.withValues(alpha: 0.4),
|
||||
), // Primary with opacity
|
||||
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusFull),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body3m.copyWith(color: UiColors.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _daysUntilExpiry(DateTime? expiry) {
|
||||
if (expiry == null) return 0;
|
||||
return expiry.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
// Mock mapping for UI props based on ID
|
||||
_CertificateUiProps _getUiProps(ComplianceType type) {
|
||||
switch (type) {
|
||||
case ComplianceType.backgroundCheck:
|
||||
@@ -400,7 +234,6 @@ class CertificateCard extends StatelessWidget {
|
||||
case ComplianceType.rbs:
|
||||
return _CertificateUiProps(UiIcons.wine, UiColors.foreground);
|
||||
default:
|
||||
// Default generic icon
|
||||
return _CertificateUiProps(UiIcons.award, UiColors.primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,10 +1225,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record
|
||||
sha256: "2e3d56d196abcd69f1046339b75e5f3855b2406fc087e5991f6703f188aa03a6"
|
||||
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
version: "6.2.0"
|
||||
record_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1237,22 +1237,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
record_darwin:
|
||||
record_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_darwin
|
||||
sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c
|
||||
name: record_ios
|
||||
sha256: "8df7c136131bd05efc19256af29b2ba6ccc000ccc2c80d4b6b6d7a8d21a3b5a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.0"
|
||||
record_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_linux
|
||||
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
|
||||
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "1.3.0"
|
||||
record_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_macos
|
||||
sha256: "084902e63fc9c0c224c29203d6c75f0bdf9b6a40536c9d916393c8f4c4256488"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
record_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user