feat: Add certificate number field, update "Add Certificate" card UI with blur effect, and consolidate certificate view/upload actions.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user