diff --git a/apps/backoffice/src/app/features/configuration/components/NumberFormatTab.tsx b/apps/backoffice/src/app/features/configuration/components/NumberFormatTab.tsx index 62ee9460e..4d131def1 100644 --- a/apps/backoffice/src/app/features/configuration/components/NumberFormatTab.tsx +++ b/apps/backoffice/src/app/features/configuration/components/NumberFormatTab.tsx @@ -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) => diff --git a/apps/backoffice/src/app/features/configuration/types/configuration.ts b/apps/backoffice/src/app/features/configuration/types/configuration.ts index 7ee61fabe..92832ab80 100644 --- a/apps/backoffice/src/app/features/configuration/types/configuration.ts +++ b/apps/backoffice/src/app/features/configuration/types/configuration.ts @@ -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 diff --git a/apps/portal/src/app/features/seafarer/pages/Biometrics/index.tsx b/apps/portal/src/app/features/seafarer/pages/Biometrics/index.tsx index 522a07ba1..0f4bc40cb 100644 --- a/apps/portal/src/app/features/seafarer/pages/Biometrics/index.tsx +++ b/apps/portal/src/app/features/seafarer/pages/Biometrics/index.tsx @@ -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 }).env?.['VITE_BASE_API_URL'] ?? - 'http://localhost:3000/api'; - -async function fetchCertificate(): Promise { - 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 = { FINGERPRINT: 'Fingerprint', @@ -54,44 +32,17 @@ const MODALITY_LABEL: Record = { }; /** - * 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(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() { + + + + +
+ + {t('biometrics.bsidLabel', 'Biometric Subject ID')} + + {profileLoading ? ( + + ) : bsid ? ( + + {({ copied, copy }) => ( + + + + + {bsid} + + {copied ? : } + + + + )} + + ) : ( + + {t( + 'biometrics.bsidPending', + 'Not issued yet. Your BSID is generated once your enrolment is confirmed at the counter.', + )} + + )} +
+
+ {isLoading ? ( @@ -147,34 +137,9 @@ export function BiometricsPage() { ))} - - - - )}
- - setPreviewUrl(null)} - url={previewUrl ?? ''} - title={t('biometrics.title', 'Biometrics')} - /> ); }