From 8e95589551e4293d7af666bb7405f525ca2bd830 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Mar 2026 21:17:45 -0500 Subject: [PATCH] feat: Enhance certificate upload process with file change verification and signed URL generation --- .../certificates_repository_impl.dart | 77 +++++++++++-------- .../presentation/pages/bank_account_page.dart | 14 +--- .../presentation/pages/time_card_page.dart | 25 ++---- .../lib/src/presentation/pages/faqs_page.dart | 4 - .../pages/legal/privacy_policy_page.dart | 4 - .../pages/legal/terms_of_service_page.dart | 4 - .../pages/privacy_security_page.dart | 4 - 7 files changed, 56 insertions(+), 76 deletions(-) diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart index bd4fbaf5..f816eff4 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart @@ -35,46 +35,63 @@ class CertificatesRepositoryImpl implements CertificatesRepository { String? certificateNumber, }) async { return _service.run(() async { - // 1. Upload the file to cloud storage - final FileUploadResponse uploadRes = await _uploadService.uploadFile( - filePath: filePath, - fileName: - 'staff_cert_${certificationType.name}_${DateTime.now().millisecondsSinceEpoch}.pdf', - visibility: domain.FileVisibility.private, - ); + // Get existing certificate to check if file has changed + final List existingCerts = await getCertificates(); + domain.StaffCertificate? existingCert; + try { + existingCert = existingCerts.firstWhere( + (domain.StaffCertificate c) => c.certificationType == certificationType, + ); + } catch (e) { + // Certificate doesn't exist yet + } - // 2. Generate a signed URL for verification service to access the file - // Wait, verification service might need this or just the URI. - // Following DocumentRepository behavior: - final SignedUrlResponse signedUrlRes = await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri); + String? signedUrl = existingCert?.certificateUrl; + String? verificationId = existingCert?.verificationId; + final bool fileChanged = existingCert == null || existingCert.certificateUrl != filePath; - // 3. Initiate verification - final String staffId = await _service.getStaffId(); - final VerificationResponse verificationRes = await _verificationService - .createVerification( - fileUri: uploadRes.fileUri, - type: 'certification', - subjectType: 'worker', - subjectId: staffId, - rules: { - 'certificateName': name, - 'certificateIssuer': issuer, - 'certificateNumber': certificateNumber, - }, - ); + // Only upload and verify if file path has changed + if (fileChanged) { + // 1. Upload the file to cloud storage + final FileUploadResponse uploadRes = await _uploadService.uploadFile( + filePath: filePath, + fileName: + 'staff_cert_${certificationType.name}_${DateTime.now().millisecondsSinceEpoch}.pdf', + visibility: domain.FileVisibility.private, + ); + + // 2. Generate a signed URL for verification service to access the file + final SignedUrlResponse signedUrlRes = await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri); + signedUrl = signedUrlRes.signedUrl; + + // 3. Initiate verification + final String staffId = await _service.getStaffId(); + final VerificationResponse verificationRes = await _verificationService + .createVerification( + fileUri: uploadRes.fileUri, + type: 'certification', + subjectType: 'worker', + subjectId: staffId, + rules: { + 'certificateName': name, + 'certificateIssuer': issuer, + 'certificateNumber': certificateNumber, + }, + ); + verificationId = verificationRes.verificationId; + } // 4. Update/Create Certificate in Data Connect await _service.getStaffRepository().upsertStaffCertificate( certificationType: certificationType, name: name, - status: domain.StaffCertificateStatus.pending, - fileUrl: signedUrlRes.signedUrl, + status: existingCert?.status ?? domain.StaffCertificateStatus.pending, + fileUrl: signedUrl, expiry: expiryDate, issuer: issuer, certificateNumber: certificateNumber, - validationStatus: - domain.StaffCertificateValidationStatus.pendingExpertReview, - verificationId: verificationRes.verificationId, + validationStatus: existingCert?.validationStatus ?? domain.StaffCertificateValidationStatus.pendingExpertReview, + verificationId: verificationId, ); // 5. Return updated list or the specific certificate diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index b81bae6f..1d9fd651 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -78,22 +78,16 @@ class BankAccountPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SecurityNotice(strings: strings), - const SizedBox(height: UiConstants.space32), - if (state.accounts.isEmpty) + if (state.accounts.isEmpty) ...[ + const SizedBox(height: UiConstants.space32), const UiEmptyState( icon: UiIcons.building, title: 'No accounts yet', description: 'Add your first bank account to get started', - ) - else ...[ - Text( - strings.linked_accounts, - style: UiTypography.headline4m.copyWith( - color: UiColors.textPrimary, - ), ), - const SizedBox(height: UiConstants.space3), + ] else ...[ + const SizedBox(height: UiConstants.space4), ...state.accounts.map( (StaffBankAccount account) => AccountCard(account: account, strings: strings), diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart index 5c66f590..80f5a327 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart @@ -19,11 +19,12 @@ class TimeCardPage extends StatefulWidget { } class _TimeCardPageState extends State { - final TimeCardBloc _bloc = Modular.get(); + late final TimeCardBloc _bloc; @override void initState() { super.initState(); + _bloc = Modular.get(); _bloc.add(LoadTimeCards(DateTime.now())); } @@ -33,25 +34,9 @@ class _TimeCardPageState extends State { return BlocProvider.value( value: _bloc, child: Scaffold( - backgroundColor: UiColors.bgPrimary, - appBar: AppBar( - backgroundColor: UiColors.bgPopup, - elevation: 0, - leading: IconButton( - icon: const Icon( - UiIcons.chevronLeft, - color: UiColors.iconSecondary, - ), - onPressed: () => Modular.to.popSafe(), - ), - title: Text( - t.staff_time_card.title, - style: UiTypography.headline4m.textPrimary, - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + appBar: UiAppBar( + title: t.staff_time_card.title, + showBackButton: true, ), body: BlocConsumer( listener: (BuildContext context, TimeCardState state) { diff --git a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/pages/faqs_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/pages/faqs_page.dart index 1c99a9ab..b1598d5b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/pages/faqs_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/pages/faqs_page.dart @@ -17,10 +17,6 @@ class FaqsPage extends StatelessWidget { appBar: UiAppBar( title: t.staff_faqs.title, showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container(color: UiColors.border, height: 1), - ), ), body: BlocProvider( create: (BuildContext context) => diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart index 510eca63..1f9c0379 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart @@ -18,10 +18,6 @@ class PrivacyPolicyPage extends StatelessWidget { appBar: UiAppBar( title: t.staff_privacy_security.privacy_policy.title, showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container(color: UiColors.border, height: 1), - ), ), body: BlocProvider( create: (BuildContext context) => Modular.get()..fetchPrivacyPolicy(), diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart index 8bd8daae..e5e30c13 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart @@ -18,10 +18,6 @@ class TermsOfServicePage extends StatelessWidget { appBar: UiAppBar( title: t.staff_privacy_security.terms_of_service.title, showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container(color: UiColors.border, height: 1), - ), ), body: BlocProvider( create: (BuildContext context) => Modular.get()..fetchTerms(), diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart index 28749dbe..df83b2cd 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart @@ -18,10 +18,6 @@ class PrivacySecurityPage extends StatelessWidget { appBar: UiAppBar( title: t.staff_privacy_security.title, showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container(color: UiColors.border, height: 1), - ), ), body: BlocProvider.value( value: Modular.get()