mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 01:48:13 +00:00
feat(configuration): add BSID to NumberFormatScope and update related UI
This commit is contained in:
@@ -38,6 +38,7 @@ const SCOPES: { value: NumberFormatScope; label: string }[] = [
|
||||
{ value: 'SEAFARER_NUMBER', label: 'Seafarer Number' },
|
||||
{ value: 'SEAMAN_BOOK_NUMBER', label: 'Seaman Book Number' },
|
||||
{ value: 'BTC_NUMBER', label: 'BTC Number' },
|
||||
{ value: 'BSID', label: 'Biometric Subject ID (BSID)' },
|
||||
];
|
||||
|
||||
const scopeLabel = (scope: NumberFormatScope) =>
|
||||
|
||||
@@ -51,7 +51,8 @@ export interface UpdateProfessionPayload {
|
||||
export type NumberFormatScope =
|
||||
| 'SEAFARER_NUMBER'
|
||||
| 'SEAMAN_BOOK_NUMBER'
|
||||
| 'BTC_NUMBER';
|
||||
| 'BTC_NUMBER'
|
||||
| 'BSID';
|
||||
|
||||
/**
|
||||
* The shape of a generated identifier — prefix, optional year, separator and
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CopyButton,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
@@ -12,41 +11,20 @@ import {
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconDownload,
|
||||
IconCheck,
|
||||
IconCopy,
|
||||
IconFingerprint,
|
||||
IconIdBadge2,
|
||||
IconInfoCircle,
|
||||
IconScan,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authStorage } from '@ema-platform/auth';
|
||||
import { useCurrentProfile } from '@ema-platform/auth';
|
||||
import { useGetMyBiometricEnrollmentsQuery } from '@ema-platform/api';
|
||||
import { PdfPreviewModal } from '@ema-platform/ui';
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3000/api';
|
||||
|
||||
async function fetchCertificate(): Promise<Blob> {
|
||||
const token = authStorage.getToken();
|
||||
if (!token) throw new Error('No auth token found');
|
||||
const res = await fetch(`${API_BASE}/biometric-enrollments/mine/certificate`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch 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);
|
||||
}
|
||||
|
||||
const MODALITY_LABEL: Record<string, string> = {
|
||||
FINGERPRINT: 'Fingerprint',
|
||||
@@ -54,44 +32,17 @@ const MODALITY_LABEL: Record<string, string> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* View-only: what's enrolled, plus a printable slip. Capture stays
|
||||
* counter-side with a scanner — there is no self-enrollment flow here.
|
||||
* View-only: the seafarer's BSID and what is enrolled against it. Capture
|
||||
* stays counter-side with a scanner — there is no self-enrollment flow here.
|
||||
*/
|
||||
export function BiometricsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: enrollments, isLoading } = useGetMyBiometricEnrollmentsQuery();
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const openPreview = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
setPreviewUrl(URL.createObjectURL(await fetchCertificate()));
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: 'Error',
|
||||
message: err instanceof Error ? err.message : 'Could not load certificate',
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
downloadBlob(await fetchCertificate(), 'biometric-enrollment-certificate.pdf');
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: 'Error',
|
||||
message: err instanceof Error ? err.message : 'Could not download certificate',
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
// The BSID lives on the profile, stamped by staff once a capture is
|
||||
// confirmed — it is not a property of any one enrollment, so it is read
|
||||
// from the profile rather than from the rows below.
|
||||
const { profile, isLoading: profileLoading } = useCurrentProfile();
|
||||
const bsid = profile?.bsid ?? null;
|
||||
|
||||
const rows = enrollments ?? [];
|
||||
|
||||
@@ -109,6 +60,45 @@ export function BiometricsPage() {
|
||||
</Alert>
|
||||
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="sm" mb={rows.length || isLoading ? 'lg' : 0} align="flex-start">
|
||||
<ThemeIcon size={40} radius="md" color="indigo" variant="light">
|
||||
<IconIdBadge2 size={20} />
|
||||
</ThemeIcon>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{t('biometrics.bsidLabel', 'Biometric Subject ID')}
|
||||
</Text>
|
||||
{profileLoading ? (
|
||||
<Loader size="xs" mt={6} />
|
||||
) : bsid ? (
|
||||
<CopyButton value={bsid} timeout={1500}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? t('biometrics.copied', 'Copied') : t('biometrics.copy', 'Copy')}
|
||||
withArrow
|
||||
>
|
||||
<UnstyledButton onClick={copy}>
|
||||
<Group gap={6} align="center">
|
||||
<Text ff="monospace" fw={700} fz="lg">
|
||||
{bsid}
|
||||
</Text>
|
||||
{copied ? <IconCheck size={15} /> : <IconCopy size={15} />}
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed" mt={2}>
|
||||
{t(
|
||||
'biometrics.bsidPending',
|
||||
'Not issued yet. Your BSID is generated once your enrolment is confirmed at the counter.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
@@ -147,34 +137,9 @@ export function BiometricsPage() {
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Group>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={busy ? <Loader size={12} /> : <IconInfoCircle size={12} />}
|
||||
onClick={openPreview}
|
||||
disabled={busy}
|
||||
>
|
||||
{t('biometrics.view', 'View certificate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<IconDownload size={12} />}
|
||||
onClick={handleDownload}
|
||||
disabled={busy}
|
||||
>
|
||||
{t('biometrics.download', 'Download')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<PdfPreviewModal
|
||||
opened={!!previewUrl}
|
||||
onClose={() => setPreviewUrl(null)}
|
||||
url={previewUrl ?? ''}
|
||||
title={t('biometrics.title', 'Biometrics')}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user