feat: add certificate regeneration functionality to LicenseReviewPage

This commit is contained in:
nati
2026-09-07 14:25:35 +00:00
parent 4c86b65e81
commit e807839d74
2 changed files with 70 additions and 0 deletions

View File

@@ -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")}
</Button>
{/* 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"]) && (
<Tooltip
label={t(
"review.regenerateCertificateHint",
"Re-render from the currently published design",
)}
>
<Button
size="xs"
variant="subtle"
color="gray"
leftSection={<IconRefresh size={14} />}
loading={regeneratingCertificate}
disabled={Boolean(certificateBusy) || regeneratingCertificate}
onClick={handleRegenerateCertificate}
>
{t("review.regenerateCertificate", "Regenerate")}
</Button>
</Tooltip>
)}
</Group>
</Paper>
)}

View File

@@ -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<Paginated<LicenseApplication>, QueueFilter | void>({
query: (params) => ({
@@ -1479,6 +1490,7 @@ export const {
useGetLicensesQuery,
useGetCertificateUrlMutation,
useGetCertificateUrlForOfficerMutation,
useRegenerateCertificateMutation,
useGetApplicationPaymentQuery,
usePatchSectionMutation,
useAddStaffMutation,