From e807839d74769dd0fbbb22ecc136dd9e76666195 Mon Sep 17 00:00:00 2001 From: nati Date: Mon, 7 Sep 2026 14:25:35 +0000 Subject: [PATCH] feat: add certificate regeneration functionality to LicenseReviewPage --- .../pages/LicenseReviewPage/index.tsx | 58 +++++++++++++++++++ .../lib/features/licensing/licensing-api.ts | 12 ++++ 2 files changed, 70 insertions(+) diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx index bcb410228..f253d8e16 100644 --- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx +++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx @@ -34,6 +34,7 @@ import { IconPaperclip, IconPencil, IconQuestionMark, + IconRefresh, IconX, } from "@tabler/icons-react"; import { notifications } from "@mantine/notifications"; @@ -71,6 +72,7 @@ import { useScheduleInspectionMutation, useRescheduleInspectionMutation, useGetCertificateUrlForOfficerMutation, + useRegenerateCertificateMutation, uploadDocument, type ApplicationRemark, type RemarkTargetType, @@ -236,6 +238,8 @@ export function LicenseReviewPage() { const [scheduleIssuance] = useScheduleIssuanceMutation(); const [issueCertificate] = useIssueCertificateMutation(); const [getCertificateUrlForOfficer] = useGetCertificateUrlForOfficerMutation(); + const [regenerateCertificate, { isLoading: regeneratingCertificate }] = + useRegenerateCertificateMutation(); const [certificateBusy, setCertificateBusy] = useState<"view" | "download" | null>( null, ); @@ -720,6 +724,37 @@ export function LicenseReviewPage() { } } + /** + * Re-renders the stored certificate from whatever design is published now. + * Unlike View/Download above, this overwrites the stored PDF even though + * one already exists — the fix for a certificate printed from a design + * that turned out to be wrong (e.g. a placeholder published for a rank), + * where View/Download would otherwise keep handing back the same bad file. + */ + async function handleRegenerateCertificate() { + if (!app.issuedLicenseId) return; + try { + await regenerateCertificate(app.issuedLicenseId).unwrap(); + notifications.show({ + color: "teal", + title: t("review.certificateRegenerated", "Certificate regenerated"), + message: t( + "review.certificateRegeneratedBody", + "Re-rendered from the currently published design.", + ), + }); + } catch (err) { + notifications.show({ + color: "red", + title: t( + "review.certificateRegenerateError", + "Could not regenerate the certificate", + ), + message: extractErrorMessage(err), + }); + } + } + /** Opens the booking modal seeded with the visit already on the books. */ function openReschedule() { if (!pendingInspection) return; @@ -1156,6 +1191,29 @@ export function LicenseReviewPage() { > {t("review.download", "Download")} + {/* Same authority as issuing in the first place — reprints + the stored PDF from whatever design is published now, + for when the one on file turns out to be wrong. */} + {can(["can:issue:license-certificate"]) && ( + + + + )} )} diff --git a/libs/api/src/lib/features/licensing/licensing-api.ts b/libs/api/src/lib/features/licensing/licensing-api.ts index 523f519cd..f82461c47 100644 --- a/libs/api/src/lib/features/licensing/licensing-api.ts +++ b/libs/api/src/lib/features/licensing/licensing-api.ts @@ -763,6 +763,17 @@ export const licensingApi = baseApi query: (id) => ({ url: `/licenses/${id}/certificate-backoffice` }), }), + /** + * Force re-renders a certificate that already has a stored PDF, from + * whatever design is published now — the two routes above only fill in + * a *missing* PDF, so a licence printed from a design that turns out to + * be wrong (a placeholder someone published, a broken background image) + * has no other way to pick up a fix once the design is corrected. + */ + regenerateCertificate: builder.mutation<{ url: string }, string>({ + query: (id) => ({ url: `/licenses/${id}/regenerate-certificate`, method: 'POST' }), + }), + // ------------------------------------------------------------- review getQueue: builder.query, QueueFilter | void>({ query: (params) => ({ @@ -1479,6 +1490,7 @@ export const { useGetLicensesQuery, useGetCertificateUrlMutation, useGetCertificateUrlForOfficerMutation, + useRegenerateCertificateMutation, useGetApplicationPaymentQuery, usePatchSectionMutation, useAddStaffMutation,