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,
IconTrash,
} from '@tabler/icons-react';
import { authStorage, useCurrentProfile } from '@ema-platform/auth';
import { useCurrentProfile } from '@ema-platform/auth';
import {
extractErrorMessage,
useApiQuery,
useBypassPaymentMutation,
useDiscardApplicationMutation,
useGetLicenseTypesQuery,
useGetCertificateUrlMutation,
useGetPaymentCapabilitiesQuery,
type LicenseType,
} from '@ema-platform/api';
@@ -56,6 +57,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;
@@ -184,7 +188,6 @@ function validityLabel(type: LicenseType): string {
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);
@@ -194,6 +197,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>({
@@ -307,38 +311,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',
});
}
};
@@ -532,8 +534,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>
))}