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;
|
@import permission_handler_apple;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<record_darwin/RecordPlugin.h>)
|
#if __has_include(<record_ios/RecordIosPlugin.h>)
|
||||||
#import <record_darwin/RecordPlugin.h>
|
#import <record_ios/RecordIosPlugin.h>
|
||||||
#else
|
#else
|
||||||
@import record_darwin;
|
@import record_ios;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
||||||
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||||
[RecordPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordPlugin"]];
|
[RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]];
|
||||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import firebase_app_check
|
|||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import record_darwin
|
import record_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
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"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
file_picker: ^8.1.7
|
file_picker: ^8.1.7
|
||||||
record: ^5.2.0
|
record: ^6.2.0
|
||||||
firebase_auth: ^6.1.4
|
firebase_auth: ^6.1.4
|
||||||
|
|
||||||
|
|||||||
@@ -1125,6 +1125,8 @@
|
|||||||
"name_hint": "e.g. Food Handler Permit",
|
"name_hint": "e.g. Food Handler Permit",
|
||||||
"issuer_label": "Certificate Issuer",
|
"issuer_label": "Certificate Issuer",
|
||||||
"issuer_hint": "e.g. Department of Health",
|
"issuer_hint": "e.g. Department of Health",
|
||||||
|
"certificate_number_label": "Certificate Number",
|
||||||
|
"certificate_number_hint": "Enter number if applicable",
|
||||||
"expiry_label": "Expiration Date (Optional)",
|
"expiry_label": "Expiration Date (Optional)",
|
||||||
"select_date": "Select date",
|
"select_date": "Select date",
|
||||||
"upload_file": "Upload File",
|
"upload_file": "Upload File",
|
||||||
|
|||||||
@@ -1118,12 +1118,14 @@
|
|||||||
"title": "Subir Certificado",
|
"title": "Subir Certificado",
|
||||||
"name_label": "Nombre del Certificado",
|
"name_label": "Nombre del Certificado",
|
||||||
"issuer_label": "Emisor 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)",
|
"expiry_label": "Fecha de Expiraci\u00f3n (Opcional)",
|
||||||
"select_date": "Seleccionar fecha",
|
"select_date": "Seleccionar fecha",
|
||||||
"upload_file": "Subir Archivo",
|
"upload_file": "Subir Archivo",
|
||||||
"drag_drop": "Arrastra y suelta o haz clic para subir",
|
"drag_drop": "Arrastra y suelta o haz clic para subir",
|
||||||
"supported_formats": "PDF hasta 10MB",
|
"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",
|
"issuer_hint": "ej. Departamento de Salud",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"save": "Guardar Certificado",
|
"save": "Guardar Certificado",
|
||||||
|
|||||||
@@ -2,19 +2,38 @@ import 'package:flutter_bloc/flutter_bloc.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 '../../../domain/usecases/upload_certificate_usecase.dart';
|
import '../../../domain/usecases/upload_certificate_usecase.dart';
|
||||||
|
import '../../../domain/usecases/delete_certificate_usecase.dart';
|
||||||
import 'certificate_upload_state.dart';
|
import 'certificate_upload_state.dart';
|
||||||
|
|
||||||
class CertificateUploadCubit extends Cubit<CertificateUploadState>
|
class CertificateUploadCubit extends Cubit<CertificateUploadState>
|
||||||
with BlocErrorHandler<CertificateUploadState> {
|
with BlocErrorHandler<CertificateUploadState> {
|
||||||
CertificateUploadCubit(this._uploadCertificateUseCase)
|
CertificateUploadCubit(
|
||||||
: super(const CertificateUploadState());
|
this._uploadCertificateUseCase,
|
||||||
|
this._deleteCertificateUseCase,
|
||||||
|
) : super(const CertificateUploadState());
|
||||||
|
|
||||||
final UploadCertificateUseCase _uploadCertificateUseCase;
|
final UploadCertificateUseCase _uploadCertificateUseCase;
|
||||||
|
final DeleteCertificateUseCase _deleteCertificateUseCase;
|
||||||
|
|
||||||
void setAttested(bool value) {
|
void setAttested(bool value) {
|
||||||
emit(state.copyWith(isAttested: 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 {
|
Future<void> uploadCertificate(UploadCertificateParams params) async {
|
||||||
if (!state.isAttested) return;
|
if (!state.isAttested) return;
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,13 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
allowedExtensions: <String>['pdf'],
|
allowedExtensions: <String>['pdf'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
final String? error = _validatePdfFile(context, path);
|
final String? error = _validatePdfFile(context, path);
|
||||||
if (error != null && mounted) {
|
if (error != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: error,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<CertificateUploadCubit>(
|
return BlocProvider<CertificateUploadCubit>(
|
||||||
@@ -225,6 +256,23 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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
|
// File Selector
|
||||||
Text(
|
Text(
|
||||||
t.staff_certificates.upload_modal.upload_file,
|
t.staff_certificates.upload_modal.upload_file,
|
||||||
@@ -235,6 +283,29 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
|||||||
selectedFilePath: _selectedFilePath,
|
selectedFilePath: _selectedFilePath,
|
||||||
onTap: _pickFile,
|
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 &&
|
state.isAttested &&
|
||||||
_nameController.text.isNotEmpty)
|
_nameController.text.isNotEmpty)
|
||||||
? () {
|
? () {
|
||||||
final String? err =
|
final String? err = _validatePdfFile(
|
||||||
_validatePdfFile(context, _selectedFilePath!);
|
context,
|
||||||
|
_selectedFilePath!,
|
||||||
|
);
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
@@ -344,7 +417,7 @@ class _PdfFileTypesBanner extends StatelessWidget {
|
|||||||
vertical: UiConstants.space3,
|
vertical: UiConstants.space3,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.tagActive,
|
color: UiColors.primaryForeground,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
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 Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||||
const SizedBox(width: UiConstants.space3),
|
const SizedBox(width: UiConstants.space3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(message, style: UiTypography.body2r.textSecondary),
|
||||||
message,
|
|
||||||
style: UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -73,19 +73,8 @@ class CertificatesPage extends StatelessWidget {
|
|||||||
...documents.map(
|
...documents.map(
|
||||||
(StaffCertificate doc) => CertificateCard(
|
(StaffCertificate doc) => CertificateCard(
|
||||||
certificate: doc,
|
certificate: doc,
|
||||||
|
onView: () => _navigateToUpload(context, doc),
|
||||||
onUpload: () => _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),
|
const SizedBox(height: UiConstants.space4),
|
||||||
@@ -116,40 +105,4 @@ class CertificatesPage extends StatelessWidget {
|
|||||||
// Reload certificates after returning from the upload page
|
// Reload certificates after returning from the upload page
|
||||||
await Modular.get<CertificatesCubit>().loadCertificates();
|
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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
class AddCertificateCard extends StatelessWidget {
|
class AddCertificateCard extends StatelessWidget {
|
||||||
|
|
||||||
const AddCertificateCard({super.key, required this.onTap});
|
const AddCertificateCard({super.key, required this.onTap});
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@@ -11,6 +12,10 @@ class AddCertificateCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -18,21 +23,22 @@ class AddCertificateCard extends StatelessWidget {
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
UiColors.bgSecondary.withValues(alpha: 0.5),
|
UiColors.bgSecondary.withValues(alpha: 0.8),
|
||||||
UiColors.bgSecondary,
|
UiColors.bgSecondary.withValues(alpha: 0.4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: UiColors.border,
|
color: UiColors.bgSecondary.withValues(alpha: 0.2),
|
||||||
style: BorderStyle.solid,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
||||||
const SizedBox(width: UiConstants.space4),
|
const SizedBox(width: UiConstants.space4),
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
@@ -45,9 +51,12 @@ class AddCertificateCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,13 @@ class CertificateCard extends StatelessWidget {
|
|||||||
const CertificateCard({
|
const CertificateCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.certificate,
|
required this.certificate,
|
||||||
this.onUpload,
|
|
||||||
this.onEditExpiry,
|
|
||||||
this.onRemove,
|
|
||||||
this.onView,
|
this.onView,
|
||||||
|
this.onUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
final StaffCertificate certificate;
|
final StaffCertificate certificate;
|
||||||
final VoidCallback? onUpload;
|
|
||||||
final VoidCallback? onEditExpiry;
|
|
||||||
final VoidCallback? onRemove;
|
|
||||||
final VoidCallback? onView;
|
final VoidCallback? onView;
|
||||||
|
final VoidCallback? onUpload;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -47,18 +43,13 @@ class CertificateCard extends StatelessWidget {
|
|||||||
certificate.certificationType,
|
certificate.certificationType,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
|
onTap: onView,
|
||||||
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: UiConstants.radiusLg,
|
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),
|
border: Border.all(color: UiColors.border),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
@@ -100,14 +91,13 @@ class CertificateCard extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Stack(
|
Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: 64,
|
width: 48,
|
||||||
height: 64,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: uiProps.color.withValues(alpha: 0.1),
|
color: uiProps.color.withValues(alpha: 0.1),
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
@@ -116,48 +106,41 @@ class CertificateCard extends StatelessWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
uiProps.icon,
|
uiProps.icon,
|
||||||
color: uiProps.color,
|
color: uiProps.color,
|
||||||
size: 28,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showComplete)
|
if (showComplete)
|
||||||
const Positioned(
|
const Positioned(
|
||||||
bottom: -4,
|
bottom: -2,
|
||||||
right: -4,
|
right: -2,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 12,
|
radius: 8,
|
||||||
backgroundColor: UiColors.primary,
|
backgroundColor: UiColors.primary,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
UiIcons.success,
|
UiIcons.success,
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
size: 16,
|
size: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isPending)
|
if (isPending)
|
||||||
const Positioned(
|
const Positioned(
|
||||||
bottom: -4,
|
bottom: -2,
|
||||||
right: -4,
|
right: -2,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 12,
|
radius: 8,
|
||||||
backgroundColor: UiColors.textPrimary,
|
backgroundColor: UiColors.textPrimary,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
UiIcons.clock,
|
UiIcons.clock,
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
size: 16,
|
size: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space4),
|
const SizedBox(width: UiConstants.space4),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -171,6 +154,34 @@ class CertificateCard extends StatelessWidget {
|
|||||||
certificate.description ?? '',
|
certificate.description ?? '',
|
||||||
style: UiTypography.body3r.textSecondary,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -181,207 +192,31 @@ class CertificateCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: UiConstants.space2,
|
width: 6,
|
||||||
height: UiConstants.space2,
|
height: 6,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||||
color: UiColors.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(label, style: UiTypography.body3m.copyWith(color: color)),
|
||||||
|
if (expiryDate != null) ...<Widget>[
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
t.staff_certificates.card.verified,
|
'• ${DateFormat('MMM d, yyyy').format(expiryDate)}',
|
||||||
style: UiTypography.body2m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (expiryDate != null)
|
|
||||||
Text(
|
|
||||||
t.staff_certificates.card.exp(
|
|
||||||
date: DateFormat('MMM d, yyyy').format(expiryDate),
|
|
||||||
),
|
|
||||||
style: UiTypography.body3r.textSecondary,
|
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),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +225,6 @@ class CertificateCard extends StatelessWidget {
|
|||||||
return expiry.difference(DateTime.now()).inDays;
|
return expiry.difference(DateTime.now()).inDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock mapping for UI props based on ID
|
|
||||||
_CertificateUiProps _getUiProps(ComplianceType type) {
|
_CertificateUiProps _getUiProps(ComplianceType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ComplianceType.backgroundCheck:
|
case ComplianceType.backgroundCheck:
|
||||||
@@ -400,7 +234,6 @@ class CertificateCard extends StatelessWidget {
|
|||||||
case ComplianceType.rbs:
|
case ComplianceType.rbs:
|
||||||
return _CertificateUiProps(UiIcons.wine, UiColors.foreground);
|
return _CertificateUiProps(UiIcons.wine, UiColors.foreground);
|
||||||
default:
|
default:
|
||||||
// Default generic icon
|
|
||||||
return _CertificateUiProps(UiIcons.award, UiColors.primary);
|
return _CertificateUiProps(UiIcons.award, UiColors.primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1225,10 +1225,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record
|
name: record
|
||||||
sha256: "2e3d56d196abcd69f1046339b75e5f3855b2406fc087e5991f6703f188aa03a6"
|
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.1"
|
version: "6.2.0"
|
||||||
record_android:
|
record_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1237,22 +1237,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.1"
|
||||||
record_darwin:
|
record_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_darwin
|
name: record_ios
|
||||||
sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c
|
sha256: "8df7c136131bd05efc19256af29b2ba6ccc000ccc2c80d4b6b6d7a8d21a3b5a9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.0"
|
||||||
record_linux:
|
record_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_linux
|
name: record_linux
|
||||||
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
|
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
record_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user