mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 17:38:14 +00:00
Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
} from '@mantine/core';
|
||||
import { IconAlertTriangle, IconFingerprint, IconPrinter, IconScan, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import {
|
||||
extractErrorMessage,
|
||||
openAuthedDocument,
|
||||
useEnrollBiometricMutation,
|
||||
useGenerateBsidMutation,
|
||||
useGetBiometricEnrollmentsQuery,
|
||||
useGetBiometricSimulateCapabilitiesQuery,
|
||||
useListSeafarerRegistrationsQuery,
|
||||
useRevokeBiometricEnrollmentMutation,
|
||||
type BiometricModality,
|
||||
type SeafarerRegistration,
|
||||
} from '@ema-platform/api';
|
||||
import { notify, PageHeader, StatusBadge } from '@ema-platform/ui';
|
||||
import { useDateDisplayer } from '@ema-platform/shared';
|
||||
|
||||
const MODALITIES: { value: BiometricModality; label: string }[] = [
|
||||
{ value: 'FINGERPRINT', label: 'Fingerprint' },
|
||||
{ value: 'FACE', label: 'Face' },
|
||||
];
|
||||
|
||||
function applicantName(r: Pick<SeafarerRegistration, 'firstName' | 'middleName' | 'lastName'>): string {
|
||||
return [r.firstName, r.middleName, r.lastName].filter(Boolean).join(' ') || '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* No scanner is wired yet (US-BIO placeholder): "Simulate Scan" stands in for
|
||||
* the real vendor SDK capture, producing a random template so the rest of the
|
||||
* pipeline — encrypt, store, print — is exercisable end to end. Swap the
|
||||
* simulated bytes for the SDK's real template once a vendor is chosen; the
|
||||
* API call shape (base64 template + format tag) does not change.
|
||||
*/
|
||||
function fakeTemplate(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(64));
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
/** Pick a registered seafarer to enroll — approved registrations carry a profileId. */
|
||||
function ProfilePicker({ onPick }: { onPick: (r: SeafarerRegistration) => void }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [debounced] = useDebouncedValue(search, 300);
|
||||
const { data, isFetching } = useListSeafarerRegistrationsQuery({
|
||||
status: 'APPROVED',
|
||||
search: debounced || undefined,
|
||||
take: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
<TextInput
|
||||
placeholder="Search seafarer by name, ID or registration number…"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
mb="sm"
|
||||
/>
|
||||
{isFetching && <Loader size="sm" />}
|
||||
<Table highlightOnHover fz="sm">
|
||||
<Table.Tbody>
|
||||
{(data?.items ?? []).map((r) => (
|
||||
<Table.Tr key={r.id} onClick={() => onPick(r)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td>
|
||||
<Text fz="sm" fw={600}>{applicantName(r)}</Text>
|
||||
<Text fz="xs" c="dimmed" ff="monospace">{r.seafarerNumber}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
{!isFetching && (data?.items ?? []).length === 0 && (
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Text fz="sm" c="dimmed">No registered seafarer matches.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/** Backoffice counter screen: enroll a scanner capture against a profile, view what's on file, print the slip. */
|
||||
export function BiometricEnrollmentPage() {
|
||||
const showDate = useDateDisplayer();
|
||||
const [selected, setSelected] = useState<SeafarerRegistration | null>(null);
|
||||
const [modality, setModality] = useState<BiometricModality>('FINGERPRINT');
|
||||
const [deviceId, setDeviceId] = useState('');
|
||||
|
||||
const profileId = selected?.profileId ?? '';
|
||||
const { data: enrollments, isLoading } = useGetBiometricEnrollmentsQuery(profileId, { skip: !profileId });
|
||||
// No vendor SDK integrated yet — "Simulate Scan" fakes a capture so the
|
||||
// rest of the flow is exercisable. Reports false in production unless
|
||||
// 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);
|
||||
|
||||
const hasActive = useMemo(
|
||||
() => (m: BiometricModality) => (enrollments ?? []).some((e) => e.modality === m),
|
||||
[enrollments],
|
||||
);
|
||||
|
||||
async function handleEnroll() {
|
||||
if (!profileId) return;
|
||||
try {
|
||||
await enroll({
|
||||
profileId,
|
||||
modality,
|
||||
template: fakeTemplate(),
|
||||
templateFormat: 'SIMULATED',
|
||||
deviceId: deviceId || undefined,
|
||||
consentAt: new Date().toISOString(),
|
||||
}).unwrap();
|
||||
notify.success(`${modality === 'FINGERPRINT' ? 'Fingerprint' : 'Face'} enrolled.`);
|
||||
} catch (err) {
|
||||
notify.error(extractErrorMessage(err, 'Enrollment failed.'));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
await revoke({ id, profileId, reason: 'Withdrawn at counter' }).unwrap();
|
||||
notify.success('Enrollment revoked.');
|
||||
} catch (err) {
|
||||
notify.error(extractErrorMessage(err, 'Could not revoke.'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePrint() {
|
||||
if (!profileId) return;
|
||||
setPrinting(true);
|
||||
try {
|
||||
await openAuthedDocument(
|
||||
`/biometric-enrollments/profile/${profileId}/certificate`,
|
||||
`biometric-enrollment-${profileId}.pdf`,
|
||||
);
|
||||
} catch (err) {
|
||||
notify.error(extractErrorMessage(err, 'Could not open the certificate.'));
|
||||
} finally {
|
||||
setPrinting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="md" py="md">
|
||||
<PageHeader
|
||||
title="Biometric Enrollment"
|
||||
subtitle="Capture a fingerprint or face template for a registered seafarer, and print the enrollment slip."
|
||||
/>
|
||||
|
||||
{!selected ? (
|
||||
<ProfilePicker
|
||||
onPick={(r) => {
|
||||
setSelected(r);
|
||||
setBsid(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<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);
|
||||
setBsid(null);
|
||||
}}
|
||||
>
|
||||
Change seafarer
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Text fz="sm" fw={600} mb="sm">Capture</Text>
|
||||
{simulateEnabled ? (
|
||||
<>
|
||||
<Alert color="yellow" icon={<IconAlertTriangle size={16} />} mb="sm" variant="light">
|
||||
No scanner is wired yet — this simulates a capture so the rest of the flow can be tested.
|
||||
</Alert>
|
||||
<Group align="flex-end">
|
||||
<Select label="Modality" data={MODALITIES} value={modality} onChange={(v) => setModality((v as BiometricModality) ?? 'FINGERPRINT')} w={160} />
|
||||
<TextInput label="Device (optional)" placeholder="scanner-01" value={deviceId} onChange={(e) => setDeviceId(e.currentTarget.value)} w={180} />
|
||||
<Button leftSection={<IconScan size={16} />} onClick={handleEnroll} loading={enrolling}>
|
||||
Simulate Scan & Enroll
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
) : (
|
||||
<Alert color="gray" icon={<IconAlertTriangle size={16} />} variant="light">
|
||||
No scanner is wired yet, and capture simulation is off in this environment.
|
||||
</Alert>
|
||||
)}
|
||||
</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>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
leftSection={<IconPrinter size={14} />}
|
||||
onClick={handlePrint}
|
||||
loading={printing}
|
||||
>
|
||||
Print certificate
|
||||
</Button>
|
||||
</Group>
|
||||
{isLoading ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{MODALITIES.map((m) => (
|
||||
<Group key={m.value} justify="space-between" p="xs" style={{ borderRadius: 8, background: 'var(--mantine-color-default-hover)' }}>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" color={hasActive(m.value) ? 'teal' : 'gray'} size={30} radius="md">
|
||||
<IconFingerprint size={15} />
|
||||
</ThemeIcon>
|
||||
<Text fz="sm">{m.label}</Text>
|
||||
</Group>
|
||||
{hasActive(m.value) ? (
|
||||
<Group gap="xs">
|
||||
<StatusBadge tone="success" label="Enrolled" />
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="subtle"
|
||||
loading={revoking}
|
||||
onClick={() => {
|
||||
const row = (enrollments ?? []).find((e) => e.modality === m.value);
|
||||
if (row) handleRevoke(row.id);
|
||||
}}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Badge color="gray" variant="light">Not enrolled</Badge>
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
{(enrollments ?? []).map((e) => (
|
||||
<Text key={e.id} fz="xs" c="dimmed">
|
||||
{e.modality} captured {showDate(e.enrolledAt)}{e.deviceId ? ` · ${e.deviceId}` : ''}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default BiometricEnrollmentPage;
|
||||
@@ -93,6 +93,7 @@ export const am: Translations = {
|
||||
// vesselRegistrationReport: 'የምዝገባ ሪፖርት',
|
||||
vesselTransfers: "የመርከብ ባለቤትነት ዝውውር",
|
||||
seafarerRegistry: "የመርከበኞች መዝገብ",
|
||||
biometricEnrollment: "ባዮሜትሪክ ምዝገባ",
|
||||
seafarerRegistrationQueue: "የመርከበኞች ምዝገባ ወረፋ",
|
||||
applications: "ማመልከቻዎች",
|
||||
paymentConfig: "የክፍያ ውቅረት",
|
||||
@@ -867,6 +868,7 @@ export const am: Translations = {
|
||||
DRAFT: "ረቂቅ",
|
||||
SUBMITTED: "ቀርቧል",
|
||||
UNDER_REVIEW: "በግምገማ ላይ",
|
||||
AWAITING_BIOMETRICS: "ባዮሜትሪክ በመጠባበቅ ላይ",
|
||||
UNDER_EVALUATION: "በምዘና ላይ",
|
||||
RESUBMIT_REQUIRED: "እንደገና ማቅረብ ያስፈልጋል",
|
||||
INSPECTION_PENDING: "ምርመራ በመጠባበቅ ላይ",
|
||||
|
||||
@@ -92,6 +92,7 @@ export const en = {
|
||||
vesselRegistrations: 'Vessel Registration',
|
||||
vesselTransfers: 'Vessel Ownership Transfer',
|
||||
seafarerRegistry: 'Seafarer Registry',
|
||||
biometricEnrollment: 'Biometric Enrollment',
|
||||
seafarerRegistrationQueue: 'Seafarer Registration Queue',
|
||||
applications: 'Applications',
|
||||
paymentConfig: 'Payment Config',
|
||||
@@ -874,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',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconCreditCard,
|
||||
IconFileDescription,
|
||||
IconFilePlus,
|
||||
IconFingerprint,
|
||||
IconGauge,
|
||||
IconGavel,
|
||||
IconHeart,
|
||||
@@ -25,9 +26,9 @@ import {
|
||||
IconTruck,
|
||||
IconUsers,
|
||||
IconUserShield,
|
||||
} from '@tabler/icons-react';
|
||||
import type { NavSection } from '@ema-platform/ui';
|
||||
import { LICENSE_PERMISSIONS as P } from '@ema-platform/auth';
|
||||
} from "@tabler/icons-react";
|
||||
import type { NavSection } from "@ema-platform/ui";
|
||||
import { LICENSE_PERMISSIONS as P } from "@ema-platform/auth";
|
||||
|
||||
/**
|
||||
* Every licence-type queue and its review workspace share one gate: the
|
||||
@@ -35,6 +36,12 @@ import { LICENSE_PERMISSIONS as P } from '@ema-platform/auth';
|
||||
*/
|
||||
const APPLICATION_QUEUE = [P.VIEW_APPLICATION_QUEUE, P.VIEW_APPLICATIONS];
|
||||
|
||||
/**
|
||||
* Seafarer queues are reachable by registry staff and application reviewers
|
||||
* alike, so both permission families gate them as any-of.
|
||||
*/
|
||||
const SEAFARER_QUEUE = [P.VIEW_SEAFARER_REGISTRY, ...APPLICATION_QUEUE];
|
||||
|
||||
/**
|
||||
* The backoffice information architecture.
|
||||
*
|
||||
@@ -50,127 +57,282 @@ const APPLICATION_QUEUE = [P.VIEW_APPLICATION_QUEUE, P.VIEW_APPLICATIONS];
|
||||
*/
|
||||
export const NAV_SECTIONS: NavSection[] = [
|
||||
{
|
||||
items: [{ to: '/dashboard', label: 'nav.dashboard', icon: IconLayoutDashboard }],
|
||||
items: [
|
||||
{ to: "/dashboard", label: "nav.dashboard", icon: IconLayoutDashboard },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupLicensing',
|
||||
label: "nav.groupLicensing",
|
||||
items: [
|
||||
{
|
||||
to: '/licence-review',
|
||||
label: 'nav.allApplications',
|
||||
to: "/licence-review",
|
||||
label: "nav.allApplications",
|
||||
icon: IconListCheck,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
// A disclosure, not a destination — each child deep-links the grid to
|
||||
// one type, which is a facet of the same workspace.
|
||||
label: 'nav.byType',
|
||||
label: "nav.byType",
|
||||
icon: IconTruck,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
children: [
|
||||
{ to: '/licence-review/type/FREIGHT_FORWARDER', label: 'nav.typeFreightForwarder', icon: IconTruck, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/SHIPPING_AGENT', label: 'nav.typeShippingAgent', icon: IconShip, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/COMBINED_SA_FF', label: 'nav.typeCombined', icon: IconFileDescription, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/JOINT_INVESTOR', label: 'nav.typeJointInvestment', icon: IconUsers, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/MULTIMODAL_TRANSPORT_OPERATOR', label: 'nav.typeMto', icon: IconAnchor, permissions: APPLICATION_QUEUE },
|
||||
{
|
||||
to: "/licence-review/type/FREIGHT_FORWARDER",
|
||||
label: "nav.typeFreightForwarder",
|
||||
icon: IconTruck,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/SHIPPING_AGENT",
|
||||
label: "nav.typeShippingAgent",
|
||||
icon: IconShip,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/COMBINED_SA_FF",
|
||||
label: "nav.typeCombined",
|
||||
icon: IconFileDescription,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/JOINT_INVESTOR",
|
||||
label: "nav.typeJointInvestment",
|
||||
icon: IconUsers,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/MULTIMODAL_TRANSPORT_OPERATOR",
|
||||
label: "nav.typeMto",
|
||||
icon: IconAnchor,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
to: '/licence-register',
|
||||
label: 'nav.licenceRegister',
|
||||
to: "/licence-register",
|
||||
label: "nav.licenceRegister",
|
||||
icon: IconListCheck,
|
||||
permissions: [P.VIEW_LICENSES],
|
||||
},
|
||||
{ to: '/licence-review/type/PRE_WAIVER', label: 'nav.preWaiverQueue', icon: IconShieldOff, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/POST_WAIVER', label: 'nav.postWaiverQueue', icon: IconShieldOff, permissions: APPLICATION_QUEUE },
|
||||
{
|
||||
to: "/licence-review/type/PRE_WAIVER",
|
||||
label: "nav.preWaiverQueue",
|
||||
icon: IconShieldOff,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/POST_WAIVER",
|
||||
label: "nav.postWaiverQueue",
|
||||
icon: IconShieldOff,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
// Every figure on it is derived from the licence application queue.
|
||||
to: '/logistics-head-dashboard',
|
||||
label: 'nav.logisticsHeadDashboard',
|
||||
to: "/logistics-head-dashboard",
|
||||
label: "nav.logisticsHeadDashboard",
|
||||
icon: IconGauge,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupSeafarer',
|
||||
label: "nav.groupSeafarer",
|
||||
items: [
|
||||
{ to: '/seafarer-registry', label: 'nav.seafarerRegistry', icon: IconUsers, permissions: [P.VIEW_SEAFARER_REGISTRY] },
|
||||
{ to: '/seafarer-registrations', label: 'nav.seafarerRegistrationQueue', icon: IconId, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/CERTIFICATE_OF_COMPETENCY', label: 'nav.cocQueue', icon: IconShieldCheck, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/CERTIFICATE_OF_PROFICIENCY', label: 'nav.copQueue', icon: IconShieldCheck, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/seaman-book-queue', label: 'nav.seamanBookQueue', icon: IconBook2, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/btc-queue', label: 'nav.btcQueue', icon: IconShieldCheck, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/licence-review/type/ENDORSEMENT_SEAFARER', label: 'nav.endorsementQueue', icon: IconRubberStamp, permissions: APPLICATION_QUEUE },
|
||||
{ to: '/sea-service-verification', label: 'nav.seaServiceVerification', icon: IconAnchor, permissions: [P.VERIFY_SEAFARER_RECORDS] },
|
||||
{ to: '/medical-verification', label: 'nav.medicalVerification', icon: IconHeart, permissions: [P.VERIFY_SEAFARER_RECORDS] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupVessels',
|
||||
items: [
|
||||
{ to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconChartBar, permissions: [P.VIEW_VESSEL_REGISTRY] },
|
||||
{ to: '/vessel-registration-queue', label: 'nav.vesselRegistrationQueue', icon: IconAnchor, permissions: [P.VIEW_VESSEL_REGISTRY] },
|
||||
{ to: '/licence-review/type/VESSEL_REGISTRATION', label: 'nav.vesselRegistrationApplicationQueue', icon: IconAnchor, permissions: [P.VIEW_VESSEL_REGISTRY] },
|
||||
{ to: '/licence-review/type/VESSEL_OWNERSHIP_TRANSFER', label: 'nav.ownershipTransferQueue', icon: IconArrowsExchange, permissions: [P.VIEW_VESSEL_REGISTRY] },
|
||||
{ to: '/vessel-registration-queue/new', label: 'nav.vesselFormBuilder', icon: IconFilePlus, soon: true, permissions: [P.VIEW_VESSEL_REGISTRY] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupExaminations',
|
||||
items: [
|
||||
{ to: '/questions', label: 'nav.questions', icon: IconQuestionMark, permissions: [P.APPROVE_QUESTION, P.AUTHOR_QUESTION] },
|
||||
{
|
||||
to: '/exams',
|
||||
label: 'nav.exams',
|
||||
to: "/seafarer-registry",
|
||||
label: "nav.seafarerRegistry",
|
||||
icon: IconUsers,
|
||||
permissions: [P.VIEW_SEAFARER_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/biometric-enrollment",
|
||||
label: "nav.biometricEnrollment",
|
||||
icon: IconFingerprint,
|
||||
permissions: [P.ENROLL_BIOMETRICS, P.VIEW_BIOMETRICS],
|
||||
},
|
||||
{
|
||||
to: "/seafarer-registrations",
|
||||
label: "nav.seafarerRegistrationQueue",
|
||||
icon: IconId,
|
||||
permissions: SEAFARER_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/CERTIFICATE_OF_COMPETENCY",
|
||||
label: "nav.cocQueue",
|
||||
icon: IconShieldCheck,
|
||||
permissions: SEAFARER_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/CERTIFICATE_OF_PROFICIENCY",
|
||||
label: "nav.copQueue",
|
||||
icon: IconShieldCheck,
|
||||
permissions: SEAFARER_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/seaman-book-queue",
|
||||
label: "nav.seamanBookQueue",
|
||||
icon: IconBook2,
|
||||
permissions: SEAFARER_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/btc-queue",
|
||||
label: "nav.btcQueue",
|
||||
icon: IconShieldCheck,
|
||||
permissions: SEAFARER_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/ENDORSEMENT_COC",
|
||||
label: "nav.endorsementCocQueue",
|
||||
icon: IconRubberStamp,
|
||||
permissions: [P.VIEW_SEAFARER_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/ENDORSEMENT_GOC",
|
||||
label: "nav.endorsementGocQueue",
|
||||
icon: IconRubberStamp,
|
||||
permissions: [P.VIEW_SEAFARER_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/ENDORSEMENT_SEAFARER",
|
||||
label: "nav.endorsementQueue",
|
||||
icon: IconRubberStamp,
|
||||
permissions: APPLICATION_QUEUE,
|
||||
},
|
||||
{
|
||||
to: "/sea-service-verification",
|
||||
label: "nav.seaServiceVerification",
|
||||
icon: IconAnchor,
|
||||
permissions: [P.VERIFY_SEAFARER_RECORDS],
|
||||
},
|
||||
{
|
||||
to: "/medical-verification",
|
||||
label: "nav.medicalVerification",
|
||||
icon: IconHeart,
|
||||
permissions: [P.VERIFY_SEAFARER_RECORDS],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "nav.groupVessels",
|
||||
items: [
|
||||
{
|
||||
to: "/vessel-registration-report",
|
||||
label: "nav.vesselRegistrationReport",
|
||||
icon: IconChartBar,
|
||||
permissions: [P.VIEW_VESSEL_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/vessel-registration-queue",
|
||||
label: "nav.vesselRegistrationQueue",
|
||||
icon: IconAnchor,
|
||||
permissions: [P.VIEW_VESSEL_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/VESSEL_REGISTRATION",
|
||||
label: "nav.vesselRegistrationApplicationQueue",
|
||||
icon: IconAnchor,
|
||||
permissions: [P.VIEW_VESSEL_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/licence-review/type/VESSEL_OWNERSHIP_TRANSFER",
|
||||
label: "nav.ownershipTransferQueue",
|
||||
icon: IconArrowsExchange,
|
||||
permissions: [P.VIEW_VESSEL_REGISTRY],
|
||||
},
|
||||
{
|
||||
to: "/vessel-registration-queue/new",
|
||||
label: "nav.vesselFormBuilder",
|
||||
icon: IconFilePlus,
|
||||
soon: true,
|
||||
permissions: [P.VIEW_VESSEL_REGISTRY],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "nav.groupExaminations",
|
||||
items: [
|
||||
{
|
||||
to: "/questions",
|
||||
label: "nav.questions",
|
||||
icon: IconQuestionMark,
|
||||
permissions: [P.APPROVE_QUESTION, P.AUTHOR_QUESTION],
|
||||
},
|
||||
{
|
||||
to: "/exams",
|
||||
label: "nav.exams",
|
||||
icon: IconClipboardList,
|
||||
permissions: [P.MANAGE_EXAMS, P.RECORD_EXAM_ATTENDANCE, P.MANAGE_EXAM_INCIDENTS, P.PUBLISH_EXAM_RESULT],
|
||||
permissions: [
|
||||
P.MANAGE_EXAMS,
|
||||
P.RECORD_EXAM_ATTENDANCE,
|
||||
P.MANAGE_EXAM_INCIDENTS,
|
||||
P.PUBLISH_EXAM_RESULT,
|
||||
],
|
||||
},
|
||||
{
|
||||
to: '/exam-results',
|
||||
label: 'nav.examResults',
|
||||
to: "/exam-results",
|
||||
label: "nav.examResults",
|
||||
icon: IconReport,
|
||||
permissions: [P.RECORD_EXAM_RESULT, P.MODERATE_EXAM_RESULT, P.APPROVE_EXAM_RESULT, P.PUBLISH_EXAM_RESULT],
|
||||
permissions: [
|
||||
P.RECORD_EXAM_RESULT,
|
||||
P.MODERATE_EXAM_RESULT,
|
||||
P.APPROVE_EXAM_RESULT,
|
||||
P.PUBLISH_EXAM_RESULT,
|
||||
],
|
||||
},
|
||||
{
|
||||
to: "/exam-appeals",
|
||||
label: "nav.examAppeals",
|
||||
icon: IconGavel,
|
||||
permissions: [P.DECIDE_EXAM_APPEAL],
|
||||
},
|
||||
{ to: '/exam-appeals', label: 'nav.examAppeals', icon: IconGavel, permissions: [P.DECIDE_EXAM_APPEAL] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupShared',
|
||||
label: "nav.groupShared",
|
||||
items: [
|
||||
{
|
||||
to: '/certificate-designer',
|
||||
label: 'nav.certificateDesigner',
|
||||
to: "/certificate-designer",
|
||||
label: "nav.certificateDesigner",
|
||||
icon: IconRosetteDiscountCheck,
|
||||
permissions: [P.VIEW_TEMPLATES],
|
||||
},
|
||||
{
|
||||
to: '/certificate-requirements',
|
||||
label: 'nav.certificateRequirements',
|
||||
to: "/certificate-requirements",
|
||||
label: "nav.certificateRequirements",
|
||||
icon: IconClipboardText,
|
||||
permissions: [P.VIEW_LICENSE_TYPES],
|
||||
},
|
||||
{
|
||||
to: '/payment-config',
|
||||
label: 'nav.paymentConfig',
|
||||
to: "/payment-config",
|
||||
label: "nav.paymentConfig",
|
||||
icon: IconCreditCard,
|
||||
permissions: [P.VIEW_PAYMENTS],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'nav.groupAdministration',
|
||||
label: "nav.groupAdministration",
|
||||
items: [
|
||||
{ to: '/um/user-management/dashboard', label: 'nav.userManagement', icon: IconUserShield },
|
||||
{
|
||||
to: "/um/user-management/dashboard",
|
||||
label: "nav.userManagement",
|
||||
icon: IconUserShield,
|
||||
},
|
||||
{
|
||||
// Professions, locations and certifications have no dedicated keys;
|
||||
// the config-view keys are the closest published contract.
|
||||
to: '/configuration',
|
||||
label: 'nav.configuration',
|
||||
to: "/configuration",
|
||||
label: "nav.configuration",
|
||||
icon: IconSettings,
|
||||
permissions: [P.VIEW_LICENSE_TYPES, P.VIEW_TEMPLATES],
|
||||
},
|
||||
{ to: '/analytics', label: 'nav.analytics', icon: IconChartBar, soon: true },
|
||||
{
|
||||
to: "/analytics",
|
||||
label: "nav.analytics",
|
||||
icon: IconChartBar,
|
||||
soon: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
// `/profile` deliberately absent: it is a property of the signed-in user,
|
||||
|
||||
@@ -48,6 +48,7 @@ import { LicenseReviewPage } from '../features/license-review/pages/LicenseRevie
|
||||
import { LogisticsHeadDashboardPage } from '../features/logistics-head/pages/LogisticsHeadDashboardPage';
|
||||
import { CertificateDesignerPage } from '../features/certificate-designer/pages/CertificateDesignerPage';
|
||||
import { CertificateRequirementsPage } from '../features/certificate-requirements/pages/CertificateRequirementsPage';
|
||||
import { BiometricEnrollmentPage } from '../features/biometric-enrollment/pages/BiometricEnrollmentPage';
|
||||
|
||||
/** Any-of gate shared by every licence-type queue and its review workspace. */
|
||||
const APPLICATION_QUEUE = [P.VIEW_APPLICATION_QUEUE, P.VIEW_APPLICATIONS];
|
||||
@@ -100,6 +101,7 @@ const router = createBrowserRouter([
|
||||
{ path: 'sea-service-verification', element: guard([P.VERIFY_SEAFARER_RECORDS], <SeaServiceVerificationPage />) },
|
||||
{ path: 'payment-config', element: guard([P.VIEW_PAYMENTS], <PaymentConfigPage />) },
|
||||
{ path: 'seafarer-registry', element: guard([P.VIEW_SEAFARER_REGISTRY], <SeafarerRegistryPage />) },
|
||||
{ path: 'biometric-enrollment', element: guard([P.ENROLL_BIOMETRICS, P.VIEW_BIOMETRICS], <BiometricEnrollmentPage />) },
|
||||
// Seafarer registration is not a licence: own queue, own review.
|
||||
{ path: 'seafarer-registrations', element: guard(APPLICATION_QUEUE, <SeafarerRegistrationQueuePage />) },
|
||||
{ path: 'seafarer-registrations/:id', element: guard(APPLICATION_QUEUE, <SeafarerRegistrationReviewPage />) },
|
||||
|
||||
Reference in New Issue
Block a user