feat: refactor certificate generation and download logic to use licenseId and integrate new API mutation

This commit is contained in:
nati
2026-09-05 07:46:10 +00:00
parent 1f397d3571
commit 29990d3142

View File

@@ -30,12 +30,13 @@ import {
IconShieldCheck,
IconTrash,
} from '@tabler/icons-react';
import { authStorage, useCurrentProfile } from '@ema-platform/auth';
import { useCurrentProfile } from '@ema-platform/auth';
import {
extractErrorMessage,
useApiQuery,
useBypassPaymentMutation,
useDiscardApplicationMutation,
useGetCertificateUrlMutation,
useGetPaymentCapabilitiesQuery,
} from '@ema-platform/api';
import {
@@ -54,6 +55,9 @@ import type { MyRegistration } from '../../exams/pages/ExamsPage';
interface CertificatesOverview {
certificates: {
id: string;
// The licence's own id — what `/licenses/:id/certificate` takes. Distinct
// from `id` above, which is the human-readable certificate number.
licenseId: string;
type: string;
issued: string;
expiry: string;
@@ -141,31 +145,8 @@ function formatDate(value: string | null | undefined): string {
});
}
import { BASE_API_URL as API_BASE } from '@ema-platform/api';
async function generateCertificate(profileId: string): Promise<Blob> {
const token = authStorage.getToken();
if (!token) throw new Error('No auth token found');
const res = await fetch(
`${API_BASE}/profiles/generate-seafarer-certificate/${profileId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!res.ok) throw new Error(`Failed to generate certificate (${res.status})`);
return res.blob();
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
export function CertificatesPage() {
const navigate = useNavigate();
const profileId = authStorage.getProfileId() ?? '';
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [previewTitle, setPreviewTitle] = useState('');
const [loading, setLoading] = useState(false);
@@ -175,6 +156,7 @@ export function CertificatesPage() {
const { data: capabilities } = useGetPaymentCapabilitiesQuery();
const [bypassPayment, { isLoading: bypassing }] = useBypassPaymentMutation();
const [discardApplication, { isLoading: discarding }] = useDiscardApplicationMutation();
const [getCertificateUrl] = useGetCertificateUrlMutation();
const [discardTarget, setDiscardTarget] = useState<{ id: string; applicationId: string } | null>(null);
const { data, refetch } = useApiQuery<CertificatesOverview>({
@@ -261,38 +243,36 @@ export function CertificatesPage() {
!hasVerifiedMedical && 'No verified medical certificate on file.',
].filter((r): r is string => Boolean(r));
const openPreview = async (profileId: string, title: string) => {
// Same endpoint the main dashboard downloads from
// (DashboardPage's `downloadCertificate`): the PDF this returns is
// rendered from whatever design is published for the licence's type/rank
// in the Certificate Designer, not a generic profile-only certificate.
const openPreview = async (licenseId: string, title: string) => {
setLoading(true);
try {
const blob = await generateCertificate(profileId);
const url = URL.createObjectURL(blob);
const result = await getCertificateUrl(licenseId).unwrap();
setPreviewTitle(title);
setPreviewUrl(url);
setPreviewUrl(result.url);
} catch (err) {
notifications.show({
color: 'red',
title: 'Error',
message: err instanceof Error ? err.message : 'Could not generate certificate',
message: extractErrorMessage(err) || 'Could not generate certificate',
});
} finally {
setLoading(false);
}
};
const handleDownload = async (profileId: string, title: string) => {
const handleDownload = async (licenseId: string) => {
try {
const blob = await generateCertificate(profileId);
downloadBlob(blob, `certificate-${Date.now()}.pdf`);
notifications.show({
color: 'teal',
title: 'Downloaded',
message: 'Certificate PDF downloaded successfully',
});
const result = await getCertificateUrl(licenseId).unwrap();
window.open(result.url, '_blank', 'noopener');
} catch (err) {
notifications.show({
color: 'red',
title: 'Error',
message: err instanceof Error ? err.message : 'Could not download certificate',
message: extractErrorMessage(err) || 'Could not download certificate',
});
}
};
@@ -489,8 +469,8 @@ export function CertificatesPage() {
<div><Text fz="xs" c="dimmed">Expires</Text><Text fz="sm" fw={500}>{cert.expiry}</Text></div>
</SimpleGrid>
<Group mt="sm" gap="xs">
<Button size="xs" variant="light" leftSection={loading ? <Loader size={12} /> : <IconEye size={12} />} onClick={() => openPreview(profileId, cert.type)}>View</Button>
<Button size="xs" variant="default" leftSection={<IconDownload size={12} />} onClick={() => handleDownload(profileId, cert.type)}>Download</Button>
<Button size="xs" variant="light" leftSection={loading ? <Loader size={12} /> : <IconEye size={12} />} onClick={() => openPreview(cert.licenseId, cert.type)}>View</Button>
<Button size="xs" variant="default" leftSection={<IconDownload size={12} />} onClick={() => handleDownload(cert.licenseId)}>Download</Button>
</Group>
</Card>
))}