mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 15:25:47 +00:00
feat: refactor certificate generation and download logic to use licenseId and integrate new API mutation
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user