feat: add staff certificates feature with pubspec configuration
- Created pubspec.yaml for the staff_certificates feature with dependencies including flutter_bloc, equatable, get_it, and flutter_modular. - Established paths for KROW dependencies: design_system, core_localization, krow_domain, krow_core, and krow_data_connect. - Added firebase_auth and firebase_data_connect as dependencies. - Generated pubspec.lock file to lock dependency versions.
This commit is contained in:
@@ -600,5 +600,45 @@
|
||||
"missing": "Missing",
|
||||
"rejected": "Rejected"
|
||||
}
|
||||
},
|
||||
"staff_certificates": {
|
||||
"title": "Certificates",
|
||||
"progress": {
|
||||
"title": "Your Progress",
|
||||
"verified_count": "$completed of $total verified",
|
||||
"active": "Compliance Active"
|
||||
},
|
||||
"card": {
|
||||
"expires_in_days": "Expires in $days days - Renew now",
|
||||
"expired": "Expired - Renew now",
|
||||
"verified": "Verified",
|
||||
"expiring_soon": "Expiring Soon",
|
||||
"exp": "Exp: $date",
|
||||
"upload_button": "Upload Certificate",
|
||||
"edit_expiry": "Edit Expiration Date",
|
||||
"remove": "Remove Certificate",
|
||||
"renew": "Renew",
|
||||
"opened_snackbar": "Certificate opened in new tab"
|
||||
},
|
||||
"add_more": {
|
||||
"title": "Add Another Certificate",
|
||||
"subtitle": "Upload additional certifications"
|
||||
},
|
||||
"upload_modal": {
|
||||
"title": "Upload Certificate",
|
||||
"expiry_label": "Expiration Date (Optional)",
|
||||
"select_date": "Select date",
|
||||
"upload_file": "Upload File",
|
||||
"drag_drop": "Drag and drop or click to upload",
|
||||
"supported_formats": "PDF, JPG, PNG up to 10MB",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Certificate"
|
||||
},
|
||||
"delete_modal": {
|
||||
"title": "Remove Certificate?",
|
||||
"message": "This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Remove"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,5 +599,45 @@
|
||||
"missing": "Missing",
|
||||
"rejected": "Rejected"
|
||||
}
|
||||
},
|
||||
"staff_certificates": {
|
||||
"title": "Certificates",
|
||||
"progress": {
|
||||
"title": "Your Progress",
|
||||
"verified_count": "$completed of $total verified",
|
||||
"active": "Compliance Active"
|
||||
},
|
||||
"card": {
|
||||
"expires_in_days": "Expires in $days days - Renew now",
|
||||
"expired": "Expired - Renew now",
|
||||
"verified": "Verified",
|
||||
"expiring_soon": "Expiring Soon",
|
||||
"exp": "Exp: $date",
|
||||
"upload_button": "Upload Certificate",
|
||||
"edit_expiry": "Edit Expiration Date",
|
||||
"remove": "Remove Certificate",
|
||||
"renew": "Renew",
|
||||
"opened_snackbar": "Certificate opened in new tab"
|
||||
},
|
||||
"add_more": {
|
||||
"title": "Add Another Certificate",
|
||||
"subtitle": "Upload additional certifications"
|
||||
},
|
||||
"upload_modal": {
|
||||
"title": "Upload Certificate",
|
||||
"expiry_label": "Expiration Date (Optional)",
|
||||
"select_date": "Select date",
|
||||
"upload_file": "Upload File",
|
||||
"drag_drop": "Drag and drop or click to upload",
|
||||
"supported_formats": "PDF, JPG, PNG up to 10MB",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Certificate"
|
||||
},
|
||||
"delete_modal": {
|
||||
"title": "Remove Certificate?",
|
||||
"message": "This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Remove"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,4 +201,22 @@ class UiIcons {
|
||||
|
||||
/// Download icon
|
||||
static const IconData download = _IconLib.download;
|
||||
|
||||
/// Upload icon
|
||||
static const IconData upload = _IconLib.upload;
|
||||
|
||||
/// Upload Cloud icon
|
||||
static const IconData uploadCloud = _IconLib.uploadCloud;
|
||||
|
||||
/// File Check icon
|
||||
static const IconData fileCheck = _IconLib.fileCheck;
|
||||
|
||||
/// Utensils icon
|
||||
static const IconData utensils = _IconLib.utensils;
|
||||
|
||||
/// Wine icon
|
||||
static const IconData wine = _IconLib.wine;
|
||||
|
||||
/// Award icon
|
||||
static const IconData award = _IconLib.award;
|
||||
}
|
||||
|
||||
@@ -38,17 +38,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
||||
final Completer<String?> completer = Completer<String?>();
|
||||
|
||||
print('Starting phone number verification for $phoneNumber');
|
||||
|
||||
await firebaseAuth.verifyPhoneNumber(
|
||||
phoneNumber: phoneNumber,
|
||||
verificationCompleted: (_) {
|
||||
print('Phone verification completed automatically.');
|
||||
print(phoneNumber);
|
||||
},
|
||||
verificationFailed: (FirebaseAuthException e) {
|
||||
if (!completer.isCompleted) {
|
||||
print('Phone verification failed: ${e.message}');
|
||||
completer.completeError(
|
||||
Exception(e.message ?? 'Phone verification failed.'),
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include: ../../../../../../analysis_options.yaml
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/certificates_repository.dart';
|
||||
|
||||
class CertificatesRepositoryMock implements CertificatesRepository {
|
||||
@override
|
||||
Future<List<StaffDocument>> getCertificates() async {
|
||||
final DateTime now = DateTime.now();
|
||||
|
||||
// Create copies with dynamic dates
|
||||
final List<StaffDocument> dynamicDocuments = <StaffDocument>[
|
||||
StaffDocument(
|
||||
id: '1',
|
||||
documentId: 'background',
|
||||
staffId: 'current_user',
|
||||
name: 'Background Check',
|
||||
description: 'Required for all shifts',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 365)),
|
||||
),
|
||||
StaffDocument(
|
||||
id: '2',
|
||||
documentId: 'food_handler',
|
||||
staffId: 'current_user',
|
||||
name: 'Food Handler',
|
||||
description: 'Required for food service',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 15)),
|
||||
),
|
||||
const StaffDocument(
|
||||
id: '3',
|
||||
documentId: 'rbs',
|
||||
staffId: 'current_user',
|
||||
name: 'RBS Alcohol',
|
||||
description: 'Required for bar shifts',
|
||||
status: DocumentStatus.missing,
|
||||
expiryDate: null,
|
||||
),
|
||||
];
|
||||
|
||||
await Future<void>.delayed(const Duration(seconds: 1)); // Simulate network delay
|
||||
return dynamicDocuments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import '../../domain/repositories/certificates_repository.dart';
|
||||
|
||||
/// Implementation of [CertificatesRepository] using Data Connect.
|
||||
///
|
||||
/// This class handles the communication with the backend via [ExampleConnector].
|
||||
/// It maps raw generated data types to clean [domain.StaffDocument] entities.
|
||||
class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
/// The generated Data Connect SDK client.
|
||||
final ExampleConnector _dataConnect;
|
||||
|
||||
/// The Firebase Authentication instance.
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
/// Creates a [CertificatesRepositoryImpl].
|
||||
///
|
||||
/// Requires [ExampleConnector] for data access and [FirebaseAuth] for user context.
|
||||
CertificatesRepositoryImpl({
|
||||
required ExampleConnector dataConnect,
|
||||
required FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffDocument>> getCertificates() async {
|
||||
final User? currentUser = _firebaseAuth.currentUser;
|
||||
if (currentUser == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the query via DataConnect generated SDK
|
||||
final QueryResult<ListStaffDocumentsByStaffIdData,
|
||||
ListStaffDocumentsByStaffIdVariables> result =
|
||||
await _dataConnect
|
||||
.listStaffDocumentsByStaffId(staffId: currentUser.uid)
|
||||
.execute();
|
||||
|
||||
// Map the generated SDK types to pure Domain entities
|
||||
return result.data.staffDocuments
|
||||
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) =>
|
||||
_mapToDomain(doc))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// In a real app, we would map specific exceptions to domain Failures here.
|
||||
throw Exception('Failed to fetch certificates: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the Data Connect [ListStaffDocumentsByStaffIdStaffDocuments] to a domain [domain.StaffDocument].
|
||||
domain.StaffDocument _mapToDomain(
|
||||
ListStaffDocumentsByStaffIdStaffDocuments doc,
|
||||
) {
|
||||
return domain.StaffDocument(
|
||||
id: doc.id,
|
||||
staffId: doc.staffId,
|
||||
documentId: doc.documentId,
|
||||
name: doc.document.name,
|
||||
description: null, // Description not available in this query response
|
||||
status: _mapStatus(doc.status),
|
||||
documentUrl: doc.documentUrl,
|
||||
expiryDate: doc.expiryDate?.toDateTime(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Maps the Data Connect [DocumentStatus] enum to the domain [domain.DocumentStatus].
|
||||
domain.DocumentStatus _mapStatus(EnumValue<DocumentStatus> status) {
|
||||
if (status is Known<DocumentStatus>) {
|
||||
switch (status.value) {
|
||||
case DocumentStatus.VERIFIED:
|
||||
return domain.DocumentStatus.verified;
|
||||
case DocumentStatus.PENDING:
|
||||
return domain.DocumentStatus.pending;
|
||||
case DocumentStatus.MISSING:
|
||||
return domain.DocumentStatus.missing;
|
||||
case DocumentStatus.UPLOADED:
|
||||
return domain.DocumentStatus.pending;
|
||||
case DocumentStatus.EXPIRING:
|
||||
// 'EXPIRING' in backend is treated as 'verified' in domain,
|
||||
// as the document is strictly valid until the expiry date.
|
||||
return domain.DocumentStatus.verified;
|
||||
}
|
||||
}
|
||||
// Fallback for unknown status
|
||||
return domain.DocumentStatus.pending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the certificates repository.
|
||||
///
|
||||
/// Responsible for fetching staff compliance certificates.
|
||||
/// Implementations must reside in the data layer.
|
||||
abstract interface class CertificatesRepository {
|
||||
/// Fetches the list of compliance certificates for the current staff member.
|
||||
///
|
||||
/// Returns a list of [StaffDocument] entities.
|
||||
Future<List<StaffDocument>> getCertificates();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/certificates_repository.dart';
|
||||
|
||||
/// Use case for fetching staff compliance certificates.
|
||||
///
|
||||
/// Delegates the data retrieval to the [CertificatesRepository].
|
||||
/// Follows the strict one-to-one mapping between action and use case.
|
||||
class GetCertificatesUseCase extends NoInputUseCase<List<StaffDocument>> {
|
||||
final CertificatesRepository _repository;
|
||||
|
||||
/// Creates a [GetCertificatesUseCase].
|
||||
///
|
||||
/// Requires a [CertificatesRepository] to access the certificates data source.
|
||||
GetCertificatesUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<List<StaffDocument>> call() {
|
||||
return _repository.getCertificates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../../domain/usecases/get_certificates_usecase.dart';
|
||||
import 'certificates_state.dart';
|
||||
|
||||
class CertificatesCubit extends Cubit<CertificatesState> {
|
||||
final GetCertificatesUseCase _getCertificatesUseCase;
|
||||
|
||||
CertificatesCubit(this._getCertificatesUseCase) : super(const CertificatesState()) {
|
||||
loadCertificates();
|
||||
}
|
||||
|
||||
Future<void> loadCertificates() async {
|
||||
emit(state.copyWith(status: CertificatesStatus.loading));
|
||||
try {
|
||||
final List<StaffDocument> certificates = await _getCertificatesUseCase();
|
||||
emit(state.copyWith(
|
||||
status: CertificatesStatus.success,
|
||||
certificates: certificates,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: CertificatesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum CertificatesStatus { initial, loading, success, failure }
|
||||
|
||||
class CertificatesState extends Equatable {
|
||||
final CertificatesStatus status;
|
||||
final List<StaffDocument> certificates;
|
||||
final String? errorMessage;
|
||||
|
||||
const CertificatesState({
|
||||
this.status = CertificatesStatus.initial,
|
||||
List<StaffDocument>? certificates,
|
||||
this.errorMessage,
|
||||
}) : certificates = certificates ?? const <StaffDocument>[];
|
||||
|
||||
CertificatesState copyWith({
|
||||
CertificatesStatus? status,
|
||||
List<StaffDocument>? certificates,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return CertificatesState(
|
||||
status: status ?? this.status,
|
||||
certificates: certificates ?? this.certificates,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, certificates, errorMessage];
|
||||
|
||||
/// The number of verified certificates.
|
||||
int get completedCount => certificates
|
||||
.where((doc) => doc.status == DocumentStatus.verified)
|
||||
.length;
|
||||
|
||||
/// The total number of certificates.
|
||||
int get totalCount => certificates.length;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
/// Extension on [IModularNavigator] to provide strongly-typed navigation
|
||||
/// for the staff certificates feature.
|
||||
extension CertificatesNavigator on IModularNavigator {
|
||||
/// Navigates back.
|
||||
void popCertificates() {
|
||||
pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
import '../blocs/certificates/certificates_cubit.dart';
|
||||
import '../blocs/certificates/certificates_state.dart';
|
||||
import '../widgets/add_certificate_card.dart';
|
||||
import '../widgets/certificate_card.dart';
|
||||
import '../widgets/certificate_upload_modal.dart';
|
||||
import '../widgets/certificates_header.dart';
|
||||
|
||||
/// Page for viewing and managing staff certificates.
|
||||
///
|
||||
/// Refactored to be stateless and follow clean architecture.
|
||||
class CertificatesPage extends StatelessWidget {
|
||||
const CertificatesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Dependency Injection: Retrieve the Cubit
|
||||
final CertificatesCubit cubit = Modular.get<CertificatesCubit>();
|
||||
|
||||
return BlocBuilder<CertificatesCubit, CertificatesState>(
|
||||
bloc: cubit,
|
||||
builder: (BuildContext context, CertificatesState state) {
|
||||
if (state.status == CertificatesStatus.loading ||
|
||||
state.status == CertificatesStatus.initial) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == CertificatesStatus.failure) {
|
||||
return Scaffold(
|
||||
body: Center(child: Text('Error: ${state.errorMessage}')),
|
||||
);
|
||||
}
|
||||
|
||||
final List<StaffDocument> documents = state.certificates;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background, // Matches 0xFFF8FAFC
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CertificatesHeader(
|
||||
completedCount: state.completedCount,
|
||||
totalCount: state.totalCount,
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -48),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
...documents.map((StaffDocument doc) => CertificateCard(
|
||||
document: doc,
|
||||
onUpload: () => _showUploadModal(context, doc),
|
||||
onEditExpiry: () => _showEditExpiryDialog(context, doc),
|
||||
onRemove: () => _showRemoveConfirmation(context, doc),
|
||||
onView: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.staff_certificates.card.opened_snackbar),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
AddCertificateCard(
|
||||
onTap: () => _showUploadModal(context, null),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showUploadModal(BuildContext context, StaffDocument? document) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) => CertificateUploadModal(
|
||||
document: document,
|
||||
onSave: () {
|
||||
// TODO: Implement upload via Cubit
|
||||
// Modular.get<CertificatesCubit>().uploadCertificate(...);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onCancel: () => Navigator.pop(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditExpiryDialog(BuildContext context, StaffDocument document) {
|
||||
_showUploadModal(context, document);
|
||||
}
|
||||
|
||||
void _showRemoveConfirmation(BuildContext context, StaffDocument document) {
|
||||
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: () {
|
||||
// TODO: Implement delete via Cubit
|
||||
// Modular.get<CertificatesCubit>().deleteCertificate(document.id);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||
child: Text(t.staff_certificates.delete_modal.confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class AddCertificateCard extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
|
||||
const AddCertificateCard({super.key, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[Colors.grey[50]!, Colors.grey[100]!], // Keep prototype style
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.add_more.title,
|
||||
style: UiTypography.body1b.copyWith( // 16px Bold
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.staff_certificates.add_more.subtitle,
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class CertificateCard extends StatelessWidget {
|
||||
final StaffDocument document;
|
||||
final VoidCallback? onUpload;
|
||||
final VoidCallback? onEditExpiry;
|
||||
final VoidCallback? onRemove;
|
||||
final VoidCallback? onView;
|
||||
|
||||
const CertificateCard({
|
||||
super.key,
|
||||
required this.document,
|
||||
this.onUpload,
|
||||
this.onEditExpiry,
|
||||
this.onRemove,
|
||||
this.onView,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Determine UI state from document
|
||||
final bool isComplete = document.status == DocumentStatus.verified;
|
||||
// Todo: Better logic for expring. Assuming if expiryDate is close.
|
||||
// Prototype used 'EXPIRING' status. We map this logic:
|
||||
final bool isExpiring = _isExpiring(document.expiryDate);
|
||||
final bool isExpired = _isExpired(document.expiryDate);
|
||||
|
||||
// Override isComplete if expiring or expired
|
||||
final bool showComplete = isComplete && !isExpired && !isExpiring;
|
||||
|
||||
final bool isPending = document.status == DocumentStatus.pending;
|
||||
final bool isNotStarted = document.status == DocumentStatus.missing || document.status == DocumentStatus.rejected;
|
||||
|
||||
// UI Properties helper
|
||||
final _CertificateUiProps uiProps = _getUiProps(document.documentId);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.space4),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(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: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF9E547).withOpacity(0.2), // Yellow tint
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Color(0x66F9E547)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
size: 16,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expires_in_days(days: _daysUntilExpiry(document.expiryDate)),
|
||||
style: UiTypography.body3m.copyWith( // 12px Medium
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: uiProps.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
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: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
document.name,
|
||||
style: UiTypography.body1m.copyWith( // 16px Medium
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
document.description ?? '', // Optional description
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (showComplete) _buildCompleteStatus(document.expiryDate),
|
||||
|
||||
if (isExpiring || isExpired) _buildExpiringStatus(context, document.expiryDate),
|
||||
|
||||
if (isNotStarted)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onUpload,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.upload,
|
||||
size: 16,
|
||||
color: UiColors.white,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.card.upload_button,
|
||||
style: UiTypography.body2m.copyWith( // 14px Medium
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (showComplete || isExpiring || isExpired) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
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: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
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: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompleteStatus(DateTime? expiryDate) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.card.verified,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (expiryDate != null)
|
||||
Text(
|
||||
t.staff_certificates.card.exp(date: DateFormat('MMM d, yyyy').format(expiryDate)),
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.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: 8),
|
||||
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: 8),
|
||||
_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: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: Colors.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: const BorderSide(color: Color(0x660A39DF)), // Primary with opacity
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body3m.copyWith(color: UiColors.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isExpiring(DateTime? expiry) {
|
||||
if (expiry == null) return false;
|
||||
final int days = expiry.difference(DateTime.now()).inDays;
|
||||
return days >= 0 && days <= 30; // Close to expiry but not expired
|
||||
}
|
||||
|
||||
bool _isExpired(DateTime? expiry) {
|
||||
if (expiry == null) return false;
|
||||
return expiry.difference(DateTime.now()).inDays < 0;
|
||||
}
|
||||
|
||||
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(String id) {
|
||||
switch (id) {
|
||||
case 'background':
|
||||
return _CertificateUiProps(UiIcons.fileCheck, const Color(0xFF0A39DF));
|
||||
case 'food_handler':
|
||||
return _CertificateUiProps(UiIcons.utensils, const Color(0xFF0A39DF));
|
||||
case 'rbs':
|
||||
return _CertificateUiProps(UiIcons.wine, const Color(0xFF121826));
|
||||
default:
|
||||
// Default generic icon
|
||||
return _CertificateUiProps(UiIcons.award, UiColors.primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CertificateUiProps {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
_CertificateUiProps(this.icon, this.color);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Modal for uploading or editing a certificate expiry.
|
||||
class CertificateUploadModal extends StatelessWidget {
|
||||
/// The document being edited, or null for a new upload.
|
||||
// ignore: unused_field
|
||||
final dynamic document; // Using dynamic for now as we don't import domain here to avoid direct coupling if possible, but actually we should import domain.
|
||||
// Ideally, widgets should be dumb. Let's import domain.
|
||||
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const CertificateUploadModal({
|
||||
super.key,
|
||||
this.document,
|
||||
required this.onSave,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.title,
|
||||
style: UiTypography.headline3m.copyWith(color: UiColors.textPrimary),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onCancel,
|
||||
icon: const Icon(UiIcons.close, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.expiry_label,
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.calendar, size: 20, color: UiColors.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.select_date,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.upload_file,
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: UiColors.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.background,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEFF6FF), // Light blue
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.uploadCloud,
|
||||
size: 32,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.drag_drop,
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.supported_formats,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: onCancel,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
child: Text(t.staff_certificates.upload_modal.cancel,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textPrimary)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: onSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(t.staff_certificates.upload_modal.save,
|
||||
style: UiTypography.body1m.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class CertificatesHeader extends StatelessWidget {
|
||||
final int completedCount;
|
||||
final int totalCount;
|
||||
|
||||
const CertificatesHeader({
|
||||
super.key,
|
||||
required this.completedCount,
|
||||
required this.totalCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Prevent division by zero
|
||||
final double progressValue = totalCount == 0 ? 0 : completedCount / totalCount;
|
||||
final int progressPercent = totalCount == 0 ? 0 : (progressValue * 100).round();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 80),
|
||||
// Keeping gradient as per prototype layout requirement
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[UiColors.primary, Color(0xFF1E40AF)], // Using Primary and a darker shade
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () => Modular.to.pop(),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.chevronLeft, // Swapped LucideIcons
|
||||
color: UiColors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
t.staff_certificates.title,
|
||||
style: UiTypography.headline3m.copyWith( // 18px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 96,
|
||||
height: 96,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(
|
||||
value: progressValue,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: UiColors.white.withOpacity(0.2),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Color(0xFFF9E547), // Yellow from prototype
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'$progressPercent%',
|
||||
style: UiTypography.display1b.copyWith( // 26px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.progress.title,
|
||||
style: UiTypography.body1b.copyWith( // 16px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.staff_certificates.progress.verified_count(completed: completedCount, total: totalCount),
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.shield,
|
||||
color: Color(0xFFF9E547),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.progress.active,
|
||||
style: UiTypography.body3m.copyWith( // 12px Medium
|
||||
color: const Color(0xFFF9E547),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
import 'data/repositories_impl/certificates_repository_impl.dart';
|
||||
import 'domain/repositories/certificates_repository.dart';
|
||||
import 'domain/usecases/get_certificates_usecase.dart';
|
||||
import 'presentation/blocs/certificates/certificates_cubit.dart';
|
||||
import 'presentation/pages/certificates_page.dart';
|
||||
|
||||
class StaffCertificatesModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<CertificatesRepository>(
|
||||
() => CertificatesRepositoryImpl(
|
||||
dataConnect: i.get<ExampleConnector>(), // Assuming ExampleConnector is provided by parent module
|
||||
firebaseAuth: FirebaseAuth.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton(GetCertificatesUseCase.new);
|
||||
i.addLazySingleton(CertificatesCubit.new);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child('/', child: (_) => const CertificatesPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
library staff_certificates;
|
||||
|
||||
export 'src/staff_certificates_module.dart';
|
||||
export 'src/presentation/navigation/certificates_navigator.dart'; // Export navigator extension
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
name: staff_certificates
|
||||
description: Staff certificates feature
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.0
|
||||
equatable: ^2.0.5
|
||||
get_it: ^7.6.0
|
||||
flutter_modular: ^6.3.0
|
||||
|
||||
# KROW Dependencies
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../../../core_localization
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_core:
|
||||
path: ../../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
firebase_auth: ^5.1.0
|
||||
firebase_data_connect: ^0.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
bloc_test: ^9.1.0
|
||||
mocktail: ^1.0.0
|
||||
@@ -20,6 +20,70 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
final auth.User? user = firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
|
||||
// return some mock data for now
|
||||
return [
|
||||
BankAccount(
|
||||
id: '1',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****1234',
|
||||
accountName: 'My Checking Account',
|
||||
type: BankAccountType.checking,
|
||||
last4: '1234',
|
||||
isPrimary: true,
|
||||
),
|
||||
BankAccount(
|
||||
id: '2',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****5678',
|
||||
accountName: 'My Savings Account',
|
||||
type: BankAccountType.savings,
|
||||
last4: '5678',
|
||||
isPrimary: false,
|
||||
),
|
||||
BankAccount(
|
||||
id: '3',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****1234',
|
||||
accountName: 'My Checking Account',
|
||||
type: BankAccountType.checking,
|
||||
last4: '1234',
|
||||
isPrimary: true,
|
||||
),
|
||||
BankAccount(
|
||||
id: '4',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****5678',
|
||||
accountName: 'My Savings Account',
|
||||
type: BankAccountType.savings,
|
||||
last4: '5678',
|
||||
isPrimary: false,
|
||||
),
|
||||
BankAccount(
|
||||
id: '5',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****1234',
|
||||
accountName: 'My Checking Account',
|
||||
type: BankAccountType.checking,
|
||||
last4: '1234',
|
||||
isPrimary: true,
|
||||
),
|
||||
BankAccount(
|
||||
id: '6',
|
||||
userId: user.uid,
|
||||
bankName: 'Mock Bank',
|
||||
accountNumber: '****5678',
|
||||
accountName: 'My Savings Account',
|
||||
type: BankAccountType.savings,
|
||||
last4: '5678',
|
||||
isPrimary: false,
|
||||
),
|
||||
];
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables> result = await dataConnect.getAccountsByOwnerId(ownerId: user.uid).execute();
|
||||
|
||||
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
|
||||
|
||||
@@ -82,6 +82,9 @@ class BankAccountPage extends StatelessWidget {
|
||||
accountNumber: account,
|
||||
type: type);
|
||||
},
|
||||
onCancel: () {
|
||||
cubit.toggleForm(false);
|
||||
}
|
||||
),
|
||||
],
|
||||
// Add extra padding at bottom
|
||||
|
||||
@@ -6,8 +6,9 @@ import '../blocs/bank_account_cubit.dart';
|
||||
class AddAccountForm extends StatefulWidget {
|
||||
final dynamic strings;
|
||||
final Function(String routing, String account, String type) onSubmit;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const AddAccountForm({super.key, required this.strings, required this.onSubmit});
|
||||
const AddAccountForm({super.key, required this.strings, required this.onSubmit, required this.onCancel});
|
||||
|
||||
@override
|
||||
State<AddAccountForm> createState() => _AddAccountFormState();
|
||||
@@ -79,9 +80,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
Expanded(
|
||||
child: UiButton.text(
|
||||
text: widget.strings.cancel,
|
||||
onPressed: () {
|
||||
Modular.get<BankAccountCubit>().toggleForm(false);
|
||||
},
|
||||
onPressed: () => widget.onCancel(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
|
||||
Reference in New Issue
Block a user