feat(biometric-enrollment): add BSID generation functionality and update translations

This commit is contained in:
Nati
2026-08-28 09:40:13 +00:00
parent 5006e4685b
commit 4b733d4dfb
7 changed files with 77 additions and 2 deletions

View File

@@ -20,6 +20,7 @@ import {
extractErrorMessage,
openAuthedDocument,
useEnrollBiometricMutation,
useGenerateBsidMutation,
useGetBiometricEnrollmentsQuery,
useGetBiometricSimulateCapabilitiesQuery,
useListSeafarerRegistrationsQuery,
@@ -108,6 +109,11 @@ export function BiometricEnrollmentPage() {
// ALLOW_BIOMETRIC_SIMULATION=true, same shortcut the payment bypass uses.
const { data: capabilities } = useGetBiometricSimulateCapabilitiesQuery();
const simulateEnabled = capabilities?.simulateEnabled ?? false;
const [generateBsid, { isLoading: generatingBsid }] = useGenerateBsidMutation();
// Seeded from the seafarer registration list (which does not carry BSID
// yet) and updated locally once generated — this screen's only source of
// truth for it until the registry surfaces the profile's BSID directly.
const [bsid, setBsid] = useState<string | null>(null);
const [enroll, { isLoading: enrolling }] = useEnrollBiometricMutation();
const [revoke, { isLoading: revoking }] = useRevokeBiometricEnrollmentMutation();
const [printing, setPrinting] = useState(false);
@@ -134,6 +140,17 @@ export function BiometricEnrollmentPage() {
}
}
async function handleGenerateBsid() {
if (!profileId) return;
try {
const result = await generateBsid(profileId).unwrap();
setBsid(result.bsid);
notify.success(`BSID ${result.bsid} generated.`);
} catch (err) {
notify.error(extractErrorMessage(err, 'Could not generate BSID.'));
}
}
async function handleRevoke(id: string) {
if (!profileId) return;
try {
@@ -167,7 +184,12 @@ export function BiometricEnrollmentPage() {
/>
{!selected ? (
<ProfilePicker onPick={setSelected} />
<ProfilePicker
onPick={(r) => {
setSelected(r);
setBsid(null);
}}
/>
) : (
<Stack gap="md">
<Card withBorder radius="md" p="md">
@@ -176,7 +198,15 @@ export function BiometricEnrollmentPage() {
<Text fw={600}>{applicantName(selected)}</Text>
<Text fz="xs" c="dimmed" ff="monospace">{selected.seafarerNumber}</Text>
</div>
<Button variant="subtle" size="xs" leftSection={<IconX size={14} />} onClick={() => setSelected(null)}>
<Button
variant="subtle"
size="xs"
leftSection={<IconX size={14} />}
onClick={() => {
setSelected(null);
setBsid(null);
}}
>
Change seafarer
</Button>
</Group>
@@ -204,6 +234,31 @@ export function BiometricEnrollmentPage() {
)}
</Card>
<Card withBorder radius="md" p="md">
<Text fz="sm" fw={600} mb="sm">Biometric Subject ID (BSID)</Text>
<Text fz="xs" c="dimmed" mb="sm">
Required before this registration can be approved. Generating it is final
confirm the capture is good first.
</Text>
<Group justify="space-between">
{bsid ? (
<StatusBadge tone="success" label={`BSID ${bsid}`} />
) : (
<Badge color="gray" variant="light">Not generated</Badge>
)}
{!bsid && (
<Button
size="xs"
onClick={handleGenerateBsid}
loading={generatingBsid}
disabled={!hasActive('FINGERPRINT') && !hasActive('FACE')}
>
Generate BSID
</Button>
)}
</Group>
</Card>
<Card withBorder radius="md" p="md">
<Group justify="space-between" mb="sm">
<Text fz="sm" fw={600}>On file</Text>

View File

@@ -868,6 +868,7 @@ export const am: Translations = {
DRAFT: "ረቂቅ",
SUBMITTED: "ቀርቧል",
UNDER_REVIEW: "በግምገማ ላይ",
AWAITING_BIOMETRICS: "ባዮሜትሪክ በመጠባበቅ ላይ",
UNDER_EVALUATION: "በምዘና ላይ",
RESUBMIT_REQUIRED: "እንደገና ማቅረብ ያስፈልጋል",
INSPECTION_PENDING: "ምርመራ በመጠባበቅ ላይ",

View File

@@ -875,6 +875,7 @@ export const en = {
DRAFT: 'Draft',
SUBMITTED: 'Submitted',
UNDER_REVIEW: 'Under Review',
AWAITING_BIOMETRICS: 'Awaiting Biometrics',
UNDER_EVALUATION: 'Under Evaluation',
RESUBMIT_REQUIRED: 'Resubmit Required',
INSPECTION_PENDING: 'Inspection Pending',

View File

@@ -44,6 +44,15 @@ export const biometricEnrollmentApi = baseApi
}),
invalidatesTags: (_r, error, { profileId }) => (error ? [] : [forProfile(profileId)]),
}),
/** Stamps the profile's BSID once enrollment is confirmed. Requires an active enrollment. */
generateBsid: builder.mutation<{ id: string; bsid: string | null }, string>({
query: (profileId) => ({
url: `/biometric-enrollments/profile/${profileId}/generate-bsid`,
method: 'POST',
}),
invalidatesTags: (_r, error, profileId) => (error ? [] : [forProfile(profileId)]),
}),
}),
overrideExisting: false,
});
@@ -54,4 +63,5 @@ export const {
useGetMyBiometricEnrollmentsQuery,
useGetBiometricSimulateCapabilitiesQuery,
useRevokeBiometricEnrollmentMutation,
useGenerateBsidMutation,
} = biometricEnrollmentApi;

View File

@@ -63,6 +63,7 @@ export const STATUS_LABELS: Record<LicenseStatus, string> = {
DRAFT: 'Draft',
SUBMITTED: 'Submitted',
UNDER_REVIEW: 'Under Review',
AWAITING_BIOMETRICS: 'Awaiting Biometrics',
UNDER_EVALUATION: 'Under Evaluation',
RESUBMIT_REQUIRED: 'Resubmit Required',
INSPECTION_PENDING: 'Inspection Pending',
@@ -93,6 +94,7 @@ export const STATUS_COLORS: Record<LicenseStatus, string> = {
DRAFT: 'gray',
SUBMITTED: 'blue',
UNDER_REVIEW: 'indigo',
AWAITING_BIOMETRICS: 'indigo',
UNDER_EVALUATION: 'indigo',
RESUBMIT_REQUIRED: 'orange',
INSPECTION_PENDING: 'cyan',
@@ -132,6 +134,7 @@ export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
DRAFT: 5,
SUBMITTED: 15,
UNDER_REVIEW: 30,
AWAITING_BIOMETRICS: 30,
UNDER_EVALUATION: 45,
RESUBMIT_REQUIRED: 30,
INSPECTION_PENDING: 55,

View File

@@ -25,6 +25,9 @@ export type LicenseStatus =
| "DRAFT"
| "SUBMITTED"
| "UNDER_REVIEW"
// Seafarer registration only: same slot UNDER_REVIEW occupies elsewhere,
// but approval is blocked until the applicant's profile has a BSID.
| "AWAITING_BIOMETRICS"
| "UNDER_EVALUATION"
// Employee filed their review; parked with the team leader for a decision.
| "REVIEW_REPORTED"

View File

@@ -84,6 +84,8 @@ export interface CurrentProfile {
* registration is approved, null before that.
*/
seafarerNumber?: string | null;
/** Biometric Subject ID — stamped by staff once biometric enrollment is confirmed. */
bsid?: string | null;
seafarerStatus?: 'ACTIVE' | 'INACTIVE' | 'PENDING' | 'SUSPENDED' | null;
seafarerDepartment?: 'DECK' | 'ENGINE' | 'CATERING' | null;
seafarerStatusReason?: string | null;