Merge branch 'WorkflowChange' into estif-branch-1

This commit is contained in:
Nati Nigussie
2026-09-07 12:06:52 +03:00
committed by GitHub

View File

@@ -30,13 +30,14 @@ import {
IconShieldCheck, IconShieldCheck,
IconTrash, IconTrash,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { authStorage, useCurrentProfile } from '@ema-platform/auth'; import { useCurrentProfile } from '@ema-platform/auth';
import { import {
extractErrorMessage, extractErrorMessage,
useApiQuery, useApiQuery,
useBypassPaymentMutation, useBypassPaymentMutation,
useDiscardApplicationMutation, useDiscardApplicationMutation,
useGetLicenseTypesQuery, useGetLicenseTypesQuery,
useGetCertificateUrlMutation,
useGetPaymentCapabilitiesQuery, useGetPaymentCapabilitiesQuery,
type LicenseType, type LicenseType,
} from '@ema-platform/api'; } from '@ema-platform/api';
@@ -56,6 +57,9 @@ import type { MyRegistration } from '../../exams/pages/ExamsPage';
interface CertificatesOverview { interface CertificatesOverview {
certificates: { certificates: {
id: string; 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; type: string;
issued: string; issued: string;
expiry: string; expiry: string;
@@ -184,7 +188,6 @@ function validityLabel(type: LicenseType): string {
export function CertificatesPage() { export function CertificatesPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const profileId = authStorage.getProfileId() ?? '';
const [previewUrl, setPreviewUrl] = useState<string | null>(null); const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [previewTitle, setPreviewTitle] = useState(''); const [previewTitle, setPreviewTitle] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -194,6 +197,7 @@ export function CertificatesPage() {
const { data: capabilities } = useGetPaymentCapabilitiesQuery(); const { data: capabilities } = useGetPaymentCapabilitiesQuery();
const [bypassPayment, { isLoading: bypassing }] = useBypassPaymentMutation(); const [bypassPayment, { isLoading: bypassing }] = useBypassPaymentMutation();
const [discardApplication, { isLoading: discarding }] = useDiscardApplicationMutation(); const [discardApplication, { isLoading: discarding }] = useDiscardApplicationMutation();
const [getCertificateUrl] = useGetCertificateUrlMutation();
const [discardTarget, setDiscardTarget] = useState<{ id: string; applicationId: string } | null>(null); const [discardTarget, setDiscardTarget] = useState<{ id: string; applicationId: string } | null>(null);
const { data, refetch } = useApiQuery<CertificatesOverview>({ const { data, refetch } = useApiQuery<CertificatesOverview>({
@@ -307,38 +311,36 @@ export function CertificatesPage() {
!hasVerifiedMedical && 'No verified medical certificate on file.', !hasVerifiedMedical && 'No verified medical certificate on file.',
].filter((r): r is string => Boolean(r)); ].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); setLoading(true);
try { try {
const blob = await generateCertificate(profileId); const result = await getCertificateUrl(licenseId).unwrap();
const url = URL.createObjectURL(blob);
setPreviewTitle(title); setPreviewTitle(title);
setPreviewUrl(url); setPreviewUrl(result.url);
} catch (err) { } catch (err) {
notifications.show({ notifications.show({
color: 'red', color: 'red',
title: 'Error', title: 'Error',
message: err instanceof Error ? err.message : 'Could not generate certificate', message: extractErrorMessage(err) || 'Could not generate certificate',
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleDownload = async (profileId: string, title: string) => { const handleDownload = async (licenseId: string) => {
try { try {
const blob = await generateCertificate(profileId); const result = await getCertificateUrl(licenseId).unwrap();
downloadBlob(blob, `certificate-${Date.now()}.pdf`); window.open(result.url, '_blank', 'noopener');
notifications.show({
color: 'teal',
title: 'Downloaded',
message: 'Certificate PDF downloaded successfully',
});
} catch (err) { } catch (err) {
notifications.show({ notifications.show({
color: 'red', color: 'red',
title: 'Error', title: 'Error',
message: err instanceof Error ? err.message : 'Could not download certificate', message: extractErrorMessage(err) || 'Could not download certificate',
}); });
} }
}; };
@@ -532,8 +534,8 @@ export function CertificatesPage() {
<div><Text fz="xs" c="dimmed">Expires</Text><Text fz="sm" fw={500}>{cert.expiry}</Text></div> <div><Text fz="xs" c="dimmed">Expires</Text><Text fz="sm" fw={500}>{cert.expiry}</Text></div>
</SimpleGrid> </SimpleGrid>
<Group mt="sm" gap="xs"> <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="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(profileId, cert.type)}>Download</Button> <Button size="xs" variant="default" leftSection={<IconDownload size={12} />} onClick={() => handleDownload(cert.licenseId)}>Download</Button>
</Group> </Group>
</Card> </Card>
))} ))}