feat: Add certificate number field, update "Add Certificate" card UI with blur effect, and consolidate certificate view/upload actions.

This commit is contained in:
Achintha Isuru
2026-02-27 14:36:34 -05:00
parent 7875506e86
commit c534584836
11 changed files with 324 additions and 428 deletions

View File

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

View File

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

View File

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

View File

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

View 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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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