From c534584836f31c0f5b9bb9bf93b7b249e6ce65fb Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 27 Feb 2026 14:36:34 -0500 Subject: [PATCH] feat: Add certificate number field, update "Add Certificate" card UI with blur effect, and consolidate certificate view/upload actions. --- .../ios/Runner/GeneratedPluginRegistrant.m | 8 +- .../Flutter/GeneratedPluginRegistrant.swift | 4 +- apps/mobile/packages/core/pubspec.yaml | 2 +- .../lib/src/l10n/en.i18n.json | 2 + .../lib/src/l10n/es.i18n.json | 4 +- .../certificate_upload_cubit.dart | 23 +- .../pages/certificate_upload_page.dart | 86 +++- .../presentation/pages/certificates_page.dart | 49 +- .../widgets/add_certificate_card.dart | 73 +-- .../widgets/certificate_card.dart | 477 ++++++------------ apps/mobile/pubspec.lock | 24 +- 11 files changed, 324 insertions(+), 428 deletions(-) diff --git a/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m index 7458edf0..72d03754 100644 --- a/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m +++ b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m @@ -54,10 +54,10 @@ @import permission_handler_apple; #endif -#if __has_include() -#import +#if __has_include() +#import #else -@import record_darwin; +@import record_ios; #endif #if __has_include() @@ -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"]]; } diff --git a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift index 8c95507f..e919f640 100644 --- a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/apps/mobile/packages/core/pubspec.yaml b/apps/mobile/packages/core/pubspec.yaml index f7ea1031..83f084cd 100644 --- a/apps/mobile/packages/core/pubspec.yaml +++ b/apps/mobile/packages/core/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 3d281bed..82b9cebe 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -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", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 8b8fef08..dd5a0c73 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -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", diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/blocs/certificate_upload/certificate_upload_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/blocs/certificate_upload/certificate_upload_cubit.dart index 4fd0266f..4cb2b2e4 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/blocs/certificate_upload/certificate_upload_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/blocs/certificate_upload/certificate_upload_cubit.dart @@ -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 with BlocErrorHandler { - 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 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 uploadCertificate(UploadCertificateParams params) async { if (!state.isAttested) return; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart index 8a896e9e..0af4d6fa 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart @@ -64,9 +64,13 @@ class _CertificateUploadPageState extends State { allowedExtensions: ['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 { } } + Future _showRemoveConfirmation(BuildContext context) async { + final bool? confirmed = await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(t.staff_certificates.delete_modal.title), + content: Text(t.staff_certificates.delete_modal.message), + actions: [ + 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( + context, + ).deleteCertificate(widget.certificate!.certificationType); + } + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -225,6 +256,23 @@ class _CertificateUploadPageState extends State { ), 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 { selectedFilePath: _selectedFilePath, onTap: _pickFile, ), + + // Remove Button (only if existing) + if (widget.certificate != null) ...[ + 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 { 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), ), ], ), diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart index c5946362..2b0d95ee 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart @@ -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().loadCertificates(); } - - void _showEditExpiryDialog( - BuildContext context, - StaffCertificate certificate, - ) { - _navigateToUpload(context, certificate); - } - - void _showRemoveConfirmation( - BuildContext context, - StaffCertificate certificate, - ) { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.staff_certificates.delete_modal.title), - content: Text(t.staff_certificates.delete_modal.message), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(t.staff_certificates.delete_modal.cancel), - ), - TextButton( - onPressed: () { - Modular.get().deleteCertificate( - certificate.certificationType, - ); - Navigator.pop(context); - }, - style: TextButton.styleFrom(foregroundColor: UiColors.destructive), - child: Text(t.staff_certificates.delete_modal.confirm), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/add_certificate_card.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/add_certificate_card.dart index bf8f26f7..0b18ec77 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/add_certificate_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/add_certificate_card.dart @@ -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: [ - UiColors.bgSecondary.withValues(alpha: 0.5), - UiColors.bgSecondary, - ], - ), - borderRadius: UiConstants.radiusLg, - border: Border.all( - color: UiColors.border, - style: BorderStyle.solid, - ), - ), - child: Row( - children: [ - 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: [ + 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: [ - 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: [ + Text( + t.staff_certificates.add_more.title, + style: UiTypography.body1b.textPrimary, + ), + Text( + t.staff_certificates.add_more.subtitle, + style: UiTypography.body3r.textSecondary, + ), + ], + ), ), ], ), - ], + ), ), ), ); diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_card.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_card.dart index 403a3165..313a848e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_card.dart @@ -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( - 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: [ - 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: [ + 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: [ + 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: [ - 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: [ + 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: [ + Text( + certificate.name, + style: UiTypography.body1m.textPrimary, + ), + const SizedBox(height: 2), + Text( + certificate.description ?? '', + style: UiTypography.body3r.textSecondary, + ), + if (showComplete) ...[ + const SizedBox(height: UiConstants.space2), + _buildMiniStatus( + t.staff_certificates.card.verified, + UiColors.primary, + certificate.expiryDate, + ), + ], + if (isExpiring || isExpired) ...[ + 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) ...[ + 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: [ - Stack( - clipBehavior: Clip.none, - children: [ - 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: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - 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) ...[ - 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: [ - Row( - children: [ - 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) ...[ + 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: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - 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: [ - _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); } } diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index f7a81d21..7fd533da 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -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: