mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 21:15:42 +00:00
Merge branch 'WorkflowChange' into estif-branch-1
This commit is contained in:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user