Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1

This commit is contained in:
Estifo77
2026-08-20 15:30:37 +03:00
41 changed files with 1325 additions and 1807 deletions

View File

@@ -1,11 +1,12 @@
import { Badge, Text } from '@mantine/core';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import type {
MedicalCertificate,
SeaServiceRecord,
SeafarerProfileSummary,
SeafarerRecordStatus,
import {
seaServiceDays,
type MedicalCertificate,
type SeaServiceRecord,
type SeafarerProfileSummary,
type SeafarerRecordStatus,
} from '@ema-platform/api';
export function ownerName(profile?: SeafarerProfileSummary): string {
@@ -133,6 +134,17 @@ export function seaServiceColumns(
IMO {row.original.imoNumber}
</Text>
)}
{(row.original.vesselType || row.original.flagState || row.original.grossTonnage) && (
<Text size="xs" c="dimmed">
{[
row.original.vesselType,
row.original.flagState,
row.original.grossTonnage ? `${row.original.grossTonnage} GT` : null,
]
.filter(Boolean)
.join(' · ')}
</Text>
)}
</div>
),
},
@@ -151,6 +163,16 @@ export function seaServiceColumns(
</Text>
),
},
{
header: t('recordVerification.columns.days', 'Days'),
label: t('recordVerification.columns.days', 'Days'),
align: 'right',
cell: ({ row }) => (
<Text size="sm" fw={600}>
{seaServiceDays(row.original.engagementDate, row.original.dischargeDate) ?? '—'}
</Text>
),
},
statusColumn<SeaServiceRecord>(t),
];
}

View File

@@ -11,18 +11,11 @@ import {
Paper,
SegmentedControl,
Stack,
Tabs,
Text,
Textarea,
Title,
} from '@mantine/core';
import {
IconAnchor,
IconEye,
IconInbox,
IconPaperclip,
IconStethoscope,
} from '@tabler/icons-react';
import { IconEye, IconInbox, IconPaperclip } from '@tabler/icons-react';
import {
AdvancedTable,
notify,
@@ -196,7 +189,14 @@ function AttachmentsModal({
* freezes a record — sea service starts counting toward sea time, a medical
* certificate starts satisfying the submission gate.
*/
export function MedicalVerificationPage() {
export type VerificationKind = 'medical' | 'sea-service';
/**
* One kind per page — the sidebar lists "Sea Service Verification" and
* "Medical Verification" separately, so an officer lands on the queue they
* came for rather than on a tab.
*/
export function MedicalVerificationPage({ kind = 'medical' }: { kind?: VerificationKind }) {
const { t } = useTranslation();
const [filter, setFilter] = useState<RecordQueueFilter>('SUBMITTED');
const {
@@ -372,74 +372,62 @@ export function MedicalVerificationPage() {
[rulingSeaService, rule, verifySeaService, showDate, t],
);
const isMedical = kind === 'medical';
return (
<Container size="xl" py="md">
<Title order={3} mb={4}>
{t('recordVerification.title', 'Record verification')}
{isMedical
? t('recordVerification.medicalTitle', 'Medical Certificate Verification')
: t('recordVerification.seaServiceTitle', 'Sea Service Verification')}
</Title>
<Text size="sm" c="dimmed" mb="md">
{t(
'recordVerification.subtitle',
'Submitted sea-service records and medical certificates awaiting a ruling. Verified records are frozen; rejections return to the seafarer with your remark.',
)}
{isMedical
? t(
'recordVerification.medicalSubtitle',
'Submitted medical certificates awaiting a ruling. Verified certificates are frozen; rejections return to the seafarer with your remark.',
)
: t(
'recordVerification.seaServiceSubtitle',
'Submitted sea-service records awaiting a ruling. Verified records are frozen and count toward sea time; rejections return to the seafarer with your remark.',
)}
</Text>
<Tabs defaultValue="medical" keepMounted={false}>
<Tabs.List>
<Tabs.Tab value="medical" leftSection={<IconStethoscope size={16} />}>
{t('recordVerification.tabs.medical', {
count: pendingMedicalList.length,
defaultValue: 'Medical ({{count}})',
})}
</Tabs.Tab>
<Tabs.Tab value="sea-service" leftSection={<IconAnchor size={16} />}>
{t('recordVerification.tabs.seaService', {
count: pendingSeaServiceList.length,
defaultValue: 'Sea Service ({{count}})',
})}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="medical" pt="md">
{statusFilter}
<AdvancedTable
columns={medicalTableColumns}
data={pagedMedical}
tableName={t('recordVerification.tabs.medical', {
count: pendingMedicalList.length,
defaultValue: 'Medical ({{count}})',
})}
itemCount={pendingMedicalList.length}
pageIndex={medicalPage}
onPageChange={setMedicalPage}
pageSize={medicalPageSize}
onPageSizeChange={handleMedicalPageSizeChange}
refresh={refetchMedical}
isLoading={loadingMedical || fetchingMedical}
emptyText={emptyText}
/>
</Tabs.Panel>
<Tabs.Panel value="sea-service" pt="md">
{statusFilter}
<AdvancedTable
columns={seaServiceTableColumns}
data={pagedSeaService}
tableName={t('recordVerification.tabs.seaService', {
count: pendingSeaServiceList.length,
defaultValue: 'Sea Service ({{count}})',
})}
itemCount={pendingSeaServiceList.length}
pageIndex={seaServicePage}
onPageChange={setSeaServicePage}
pageSize={seaServicePageSize}
onPageSizeChange={handleSeaServicePageSizeChange}
refresh={refetchSeaService}
isLoading={loadingSeaService || fetchingSeaService}
emptyText={emptyText}
/>
</Tabs.Panel>
</Tabs>
{isMedical ? (
<AdvancedTable
columns={medicalTableColumns}
data={pagedMedical}
tableName={t('recordVerification.tabs.medical', {
count: pendingMedicalList.length,
defaultValue: 'Medical ({{count}})',
})}
itemCount={pendingMedicalList.length}
pageIndex={medicalPage}
onPageChange={setMedicalPage}
pageSize={medicalPageSize}
onPageSizeChange={handleMedicalPageSizeChange}
refresh={refetchMedical}
isLoading={loadingMedical || fetchingMedical}
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
/>
) : (
<AdvancedTable
columns={seaServiceTableColumns}
data={pagedSeaService}
tableName={t('recordVerification.tabs.seaService', {
count: pendingSeaServiceList.length,
defaultValue: 'Sea Service ({{count}})',
})}
itemCount={pendingSeaServiceList.length}
pageIndex={seaServicePage}
onPageChange={setSeaServicePage}
pageSize={seaServicePageSize}
onPageSizeChange={handleSeaServicePageSizeChange}
refresh={refetchSeaService}
isLoading={loadingSeaService || fetchingSeaService}
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
/>
)}
<AttachmentsModal
opened={Boolean(attachmentModal)}
@@ -491,4 +479,6 @@ export function MedicalVerificationPage() {
);
}
export const SeaServiceVerificationPage = () => <MedicalVerificationPage kind="sea-service" />;
export default MedicalVerificationPage;

View File

@@ -0,0 +1,161 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Badge, Container, Group, Select, Text, TextInput, Title } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { useDebouncedValue } from '@mantine/hooks';
import {
SEAFARER_DOCUMENT_KIND_LABELS,
SEAFARER_DOCUMENT_STATUS_COLORS,
SEAFARER_DOCUMENT_STATUS_LABELS,
useListSeafarerDocumentsQuery,
type SeafarerDocumentKind,
type SeafarerDocumentRow,
type SeafarerDocumentStatus,
} from '@ema-platform/api';
import { AdvancedTable, type AdvancedColumn } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
const PAGE_SIZE = 10;
/** Statuses an officer filters by — held and withdrawn requests are not work. */
const STATUS_FILTERS = (
['PAYMENT_PENDING', 'PAID', 'PAYMENT_CONFIRMED', 'SCHEDULED', 'ISSUED', 'REJECTED'] as SeafarerDocumentStatus[]
).map((value) => ({ value, label: SEAFARER_DOCUMENT_STATUS_LABELS[value] }));
/**
* The Seaman Book queue and the BTC queue — one page, keyed by kind. Requests
* appear here once the seafarer registration that opened them is approved.
*/
export function SeafarerDocumentQueuePage({ kind }: { kind: SeafarerDocumentKind }) {
const navigate = useNavigate();
const showDate = useDateDisplayer();
const [status, setStatus] = useState<SeafarerDocumentStatus | null>(null);
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 300);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const { data, isLoading, isFetching, refetch } = useListSeafarerDocumentsQuery({
kind,
status: status ?? undefined,
search: debouncedSearch || undefined,
take: pageSize,
skip: page * pageSize,
});
const columns: AdvancedColumn<SeafarerDocumentRow>[] = useMemo(
() => [
{
header: 'Request №',
accessorKey: 'requestNumber',
cell: ({ row }) => (
<div>
<Text size="sm" ff="monospace">
{row.original.requestNumber}
</Text>
{row.original.documentNumber && (
<Text size="xs" c="teal" ff="monospace">
{row.original.documentNumber}
</Text>
)}
</div>
),
},
{
header: 'Seafarer',
accessorKey: 'applicant.name',
cell: ({ row }) => (
<div>
<Text size="sm" fw={500}>
{row.original.applicant?.name ?? '—'}
</Text>
<Text size="xs" c="dimmed" ff="monospace">
{row.original.applicant?.seafarerNumber ?? '—'}
</Text>
</div>
),
},
{
header: 'Fee',
accessorKey: 'feeAmount',
cell: ({ row }) => (
<Text size="sm">
{row.original.feeAmount !== null ? `${row.original.feeAmount} ${row.original.feeCurrency}` : '—'}
</Text>
),
},
{
header: 'Released',
accessorKey: 'submittedAt',
cell: ({ row }) => (
<Text size="sm">{row.original.submittedAt ? showDate(row.original.submittedAt) : '—'}</Text>
),
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => (
<Badge size="sm" variant="light" color={SEAFARER_DOCUMENT_STATUS_COLORS[row.original.status]}>
{SEAFARER_DOCUMENT_STATUS_LABELS[row.original.status]}
</Badge>
),
},
],
[showDate],
);
return (
<Container size="xl" py="md">
<Title order={3} mb={4}>
{SEAFARER_DOCUMENT_KIND_LABELS[kind]} Queue
</Title>
<Text size="sm" c="dimmed" mb="md">
Requests released by an approved seafarer registration: confirm payment, schedule the
collection date, then issue.
</Text>
<Group mb="md" gap="sm">
<TextInput
placeholder="Search number, name or seafarer №…"
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => {
setSearch(e.currentTarget.value);
setPage(0);
}}
w={280}
/>
<Select
placeholder="All statuses"
data={STATUS_FILTERS}
value={status}
onChange={(v) => {
setStatus(v as SeafarerDocumentStatus | null);
setPage(0);
}}
clearable
w={220}
/>
</Group>
<AdvancedTable
columns={columns}
data={data?.items ?? []}
tableName={`${SEAFARER_DOCUMENT_KIND_LABELS[kind]} requests`}
itemCount={data?.total ?? 0}
pageIndex={page}
onPageChange={setPage}
pageSize={pageSize}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(0);
}}
refresh={refetch}
isLoading={isLoading || isFetching}
emptyText="No requests match."
onRowClick={(row) => navigate(`/seafarer-documents/${row.id}`)}
/>
</Container>
);
}
export const SeamanBookQueuePage = () => <SeafarerDocumentQueuePage kind="SEAMAN_BOOK" />;
export const BtcQueuePage = () => <SeafarerDocumentQueuePage kind="BTC_BASIC_TRAINING" />;

View File

@@ -0,0 +1,276 @@
import { useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import {
Alert,
Badge,
Button,
Center,
Container,
Group,
Loader,
Modal,
Paper,
Stack,
Table,
Text,
Textarea,
Title,
} from '@mantine/core';
import { IconAlertTriangle, IconArrowLeft, IconCheck, IconDownload } from '@tabler/icons-react';
import {
SEAFARER_DOCUMENT_KIND_LABELS,
SEAFARER_DOCUMENT_STATUS_COLORS,
SEAFARER_DOCUMENT_STATUS_LABELS,
extractErrorMessage,
useConfirmSeafarerDocumentPaymentMutation,
useGetSeafarerDocumentReviewQuery,
useIssueSeafarerDocumentMutation,
useLazyGetSeafarerDocumentReviewDownloadQuery,
useRejectSeafarerDocumentMutation,
useScheduleSeafarerDocumentMutation,
} from '@ema-platform/api';
import { AmharicDatePicker, notify } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
const QUEUE_PATH = { SEAMAN_BOOK: '/seaman-book-queue', BTC_BASIC_TRAINING: '/btc-queue' } as const;
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<Table.Tr>
<Table.Td w="40%">
<Text size="xs" c="dimmed">
{label}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm" component="div">
{value ?? '—'}
</Text>
</Table.Td>
</Table.Tr>
);
}
/** One Seaman Book / BTC request: payment → collection date → issue, or reject. */
export function SeafarerDocumentReviewPage() {
const { id = '' } = useParams();
const navigate = useNavigate();
const showDate = useDateDisplayer();
const { data, isLoading, error } = useGetSeafarerDocumentReviewQuery(id, { skip: !id });
const [confirmPayment, { isLoading: confirming }] = useConfirmSeafarerDocumentPaymentMutation();
const [schedule, { isLoading: scheduling }] = useScheduleSeafarerDocumentMutation();
const [issue, { isLoading: issuing }] = useIssueSeafarerDocumentMutation();
const [reject, { isLoading: rejecting }] = useRejectSeafarerDocumentMutation();
const [getDownload, { isFetching: downloading }] = useLazyGetSeafarerDocumentReviewDownloadQuery();
const [scheduleOpen, setScheduleOpen] = useState(false);
const [pickupDate, setPickupDate] = useState('');
const [rejectOpen, setRejectOpen] = useState(false);
const [reason, setReason] = useState('');
if (isLoading) {
return (
<Center h={300}>
<Loader />
</Center>
);
}
if (error || !data) {
return (
<Container size="md" py="xl">
<Alert color="red" icon={<IconAlertTriangle size={16} />}>
{extractErrorMessage(error, 'Could not load this request.')}
</Alert>
</Container>
);
}
const { document, applicant, payment } = data;
const kindLabel = SEAFARER_DOCUMENT_KIND_LABELS[document.kind];
const terminal = ['ISSUED', 'REJECTED', 'CANCELLED'].includes(document.status);
async function run(action: () => Promise<unknown>, done: string) {
try {
await action();
notify.success(done);
setScheduleOpen(false);
setRejectOpen(false);
setReason('');
} catch (err) {
notify.error(extractErrorMessage(err, 'Could not record the action'));
}
}
async function download() {
try {
const { url } = await getDownload(id).unwrap();
window.open(url, '_blank', 'noopener');
} catch (err) {
notify.error(extractErrorMessage(err, 'Could not fetch the PDF'));
}
}
return (
<Container size="lg" py="md">
<Button
variant="subtle"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate(QUEUE_PATH[document.kind])}
mb="xs"
>
Back to queue
</Button>
<Group justify="space-between" align="flex-start" mb="md">
<div>
<Title order={3}>
{kindLabel} {applicant?.name ?? '—'}
</Title>
<Group gap="xs" mt={4}>
<Text size="sm" c="dimmed" ff="monospace">
{document.requestNumber}
</Text>
<Badge size="sm" variant="light" color={SEAFARER_DOCUMENT_STATUS_COLORS[document.status]}>
{SEAFARER_DOCUMENT_STATUS_LABELS[document.status]}
</Badge>
{document.documentNumber && (
<Badge size="sm" color="teal" leftSection={<IconCheck size={10} />}>
{document.documentNumber}
</Badge>
)}
</Group>
</div>
<Group gap="xs">
{(document.status === 'PAYMENT_PENDING' || document.status === 'PAID') && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_APPLICATION]} hideOnly>
<Button loading={confirming} onClick={() => run(() => confirmPayment(id).unwrap(), 'Payment confirmed')}>
Confirm payment
</Button>
</RequirePermission>
)}
{document.status === 'PAYMENT_CONFIRMED' && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_APPLICATION]} hideOnly>
<Button onClick={() => setScheduleOpen(true)}>Schedule pickup</Button>
</RequirePermission>
)}
{(document.status === 'SCHEDULED' || document.status === 'PAYMENT_CONFIRMED') && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_APPLICATION]} hideOnly>
<Button color="teal" loading={issuing} onClick={() => run(() => issue(id).unwrap(), `${kindLabel} issued`)}>
Issue
</Button>
</RequirePermission>
)}
{document.status === 'ISSUED' && (
<Button variant="light" leftSection={<IconDownload size={14} />} loading={downloading} onClick={download}>
Download PDF
</Button>
)}
{!terminal && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.REJECT_APPLICATION]} hideOnly>
<Button color="red" variant="light" onClick={() => setRejectOpen(true)}>
Reject
</Button>
</RequirePermission>
)}
</Group>
</Group>
{document.status === 'REJECTED' && (
<Alert color="red" icon={<IconAlertTriangle size={16} />} title="Rejected" mb="md">
{document.rejectionReason}
</Alert>
)}
<Paper withBorder p="lg" radius="md">
<Stack gap="md">
<div>
<Text fw={600} size="sm" mb={4}>
Seafarer
</Text>
<Table withTableBorder withColumnBorders>
<Table.Tbody>
<Row label="Name" value={applicant?.name} />
<Row label="Seafarer №" value={applicant?.seafarerNumber} />
<Row
label="Registration"
value={
applicant?.registrationId ? (
<Link to={`/seafarer-registrations/${applicant.registrationId}`}>
{applicant.registrationNumber}
</Link>
) : (
applicant?.registrationNumber
)
}
/>
</Table.Tbody>
</Table>
</div>
<div>
<Text fw={600} size="sm" mb={4}>
Payment
</Text>
<Table withTableBorder withColumnBorders>
<Table.Tbody>
<Row label="Fee" value={document.feeAmount !== null ? `${document.feeAmount} ${document.feeCurrency}` : null} />
<Row label="Released to payment" value={document.submittedAt ? showDate(document.submittedAt) : null} />
<Row label="Paid" value={document.paidAt ? showDate(document.paidAt) : null} />
<Row label="Provider" value={payment?.provider} />
<Row label="Reference" value={document.paymentReference ?? payment?.providerRef} />
</Table.Tbody>
</Table>
</div>
<div>
<Text fw={600} size="sm" mb={4}>
Issuance
</Text>
<Table withTableBorder withColumnBorders>
<Table.Tbody>
<Row label="Pickup date" value={document.scheduledIssuanceDate ? showDate(document.scheduledIssuanceDate) : null} />
<Row label="Document №" value={document.documentNumber} />
<Row label="Issued" value={document.issueDate ? showDate(document.issueDate) : null} />
<Row label="Valid until" value={document.expiryDate ? showDate(document.expiryDate) : null} />
</Table.Tbody>
</Table>
</div>
</Stack>
</Paper>
<Modal opened={scheduleOpen} onClose={() => setScheduleOpen(false)} title="Schedule pickup" centered>
<Stack>
<AmharicDatePicker label="Pickup date" dateFormat="date" value={pickupDate} onChange={setPickupDate} required />
<Group justify="flex-end">
<Button variant="default" onClick={() => setScheduleOpen(false)}>
Cancel
</Button>
<Button
disabled={!pickupDate}
loading={scheduling}
onClick={() => run(() => schedule({ id, scheduledDate: pickupDate }).unwrap(), 'Pickup scheduled')}
>
Schedule
</Button>
</Group>
</Stack>
</Modal>
<Modal opened={rejectOpen} onClose={() => setRejectOpen(false)} title={`Reject ${kindLabel}`} centered>
<Stack>
<Textarea label="Reason (shown to the seafarer)" required minRows={3} value={reason} onChange={(e) => setReason(e.currentTarget.value)} data-autofocus />
<Group justify="flex-end">
<Button variant="default" onClick={() => setRejectOpen(false)}>
Cancel
</Button>
<Button color="red" disabled={reason.trim().length < 3} loading={rejecting} onClick={() => run(() => reject({ id, reason: reason.trim() }).unwrap(), 'Request rejected')}>
Confirm
</Button>
</Group>
</Stack>
</Modal>
</Container>
);
}
export default SeafarerDocumentReviewPage;

View File

@@ -17,7 +17,7 @@ import {
Textarea,
Title,
} from '@mantine/core';
import { IconAlertTriangle, IconArrowLeft, IconCheck, IconInfoCircle } from '@tabler/icons-react';
import { IconAlertTriangle, IconArrowLeft, IconCheck } from '@tabler/icons-react';
import {
SEAFARER_REGISTRATION_DOCUMENTS,
SEAFARER_REGISTRATION_FIELD_LABELS,
@@ -27,14 +27,12 @@ import {
displaySeafarerAnswer,
extractErrorMessage,
useApproveSeafarerRegistrationMutation,
useClaimSeafarerRegistrationMutation,
useGetSeafarerRegistrationReviewQuery,
useRejectSeafarerRegistrationMutation,
useRequestSeafarerRegistrationChangesMutation,
} from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { useAppSelector } from '../../../store/hooks';
import { applicantName } from './SeafarerRegistrationQueuePage';
type Decision = 'approve' | 'reject' | 'changes';
@@ -49,10 +47,8 @@ const DECISION_COPY: Record<Decision, { title: string; label: string; color: str
export function SeafarerRegistrationReviewPage() {
const { id = '' } = useParams();
const navigate = useNavigate();
const me = useAppSelector((state) => state.auth.user);
const { data, isLoading, error } = useGetSeafarerRegistrationReviewQuery(id, { skip: !id });
const [claim, { isLoading: claiming }] = useClaimSeafarerRegistrationMutation();
const [approve, { isLoading: approving }] = useApproveSeafarerRegistrationMutation();
const [reject, { isLoading: rejecting }] = useRejectSeafarerRegistrationMutation();
const [requestChanges, { isLoading: requesting }] = useRequestSeafarerRegistrationChangesMutation();
@@ -78,8 +74,8 @@ export function SeafarerRegistrationReviewPage() {
}
const { registration, attachments } = data;
const mine = !registration.assignedOfficerId || registration.assignedOfficerId === me?.id;
const canDecide = registration.status === 'UNDER_REVIEW' && mine;
// Decided straight off the queue — no claim step.
const canDecide = registration.status === 'SUBMITTED';
const busy = approving || rejecting || requesting;
async function run(action: () => Promise<unknown>, done: string) {
@@ -131,13 +127,6 @@ export function SeafarerRegistrationReviewPage() {
</Group>
</div>
<Group gap="xs">
{registration.status === 'SUBMITTED' && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]} hideOnly>
<Button loading={claiming} onClick={() => run(() => claim(id).unwrap(), 'Claimed — it is yours to review.')}>
Claim
</Button>
</RequirePermission>
)}
{canDecide && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.REQUEST_ADJUSTMENT]} hideOnly>
@@ -160,11 +149,6 @@ export function SeafarerRegistrationReviewPage() {
</Group>
</Group>
{registration.status === 'UNDER_REVIEW' && !mine && (
<Alert color="gray" icon={<IconInfoCircle size={16} />} mb="md">
Assigned to another officer only they can decide it.
</Alert>
)}
{registration.status === 'RESUBMIT_REQUIRED' && (
<Alert color="orange" icon={<IconAlertTriangle size={16} />} title="Awaiting the applicant's corrections" mb="md">
{registration.reviewRemark}

View File

@@ -1,21 +0,0 @@
import { Container } from '@mantine/core';
import { FeatureUnavailable } from '@ema-platform/ui';
/**
* Placeholder until this feature has a backend.
*
* This page previously rendered hardcoded sample records, which were
* indistinguishable from real ones.
*/
export function SeamanBookQueuePage() {
return (
<Container size="lg" py="xl">
<FeatureUnavailable
title="Seaman Book queue"
description="Seaman Book applications are not connected to the backend yet."
/>
</Container>
);
}
export default SeamanBookQueuePage;

View File

@@ -92,7 +92,8 @@ export const am: Translations = {
applications: "ማመልከቻዎች",
paymentConfig: "የክፍያ ውቅረት",
analytics: "ትንታኔ",
medicalVerification: "የሕክምና እና የባህር አገልግሎት ማረጋገጫ",
seaServiceVerification: "የባህር አገልግሎት ማረጋገጫ",
medicalVerification: "የሕክምና ማረጋገጫ",
locations: "አካባቢዎች",
configuration: "ውቅረት",
profile: "መገለጫ",

View File

@@ -90,7 +90,8 @@ export const en = {
applications: 'Applications',
paymentConfig: 'Payment Config',
analytics: 'Analytics',
medicalVerification: 'Medical and Sea Service Verification',
seaServiceVerification: 'Sea Service Verification',
medicalVerification: 'Medical Verification',
locations: 'Locations',
configuration: 'Configuration',
profile: 'Profile',

View File

@@ -98,10 +98,11 @@ export const NAV_SECTIONS: NavSection[] = [
{ 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: '/licence-review/type/SEAMAN_BOOK', label: 'nav.seamanBookQueue', icon: IconBook2, permissions: APPLICATION_QUEUE },
{ to: '/licence-review/type/BTC_BASIC_TRAINING', label: 'nav.btcQueue', 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_COC', label: 'nav.endorsementCocQueue', icon: IconRubberStamp, permissions: APPLICATION_QUEUE },
{ to: '/licence-review/type/ENDORSEMENT_GOC', label: 'nav.endorsementGocQueue', 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] },
],
},

View File

@@ -22,12 +22,16 @@ import { ProfilePage } from '../features/profile/pages/ProfilePage';
import { ConfigurationPage } from '../features/configuration/pages/ConfigurationPage';
import { AnalyticsPage } from '../features/analytics/pages/AnalyticsPage';
import { ApplicationReviewPage } from '../features/applications/pages/ApplicationReviewPage';
import { MedicalVerificationPage } from '../features/medical-verification/pages/MedicalVerificationPage';
import {
MedicalVerificationPage,
SeaServiceVerificationPage,
} from '../features/medical-verification/pages/MedicalVerificationPage';
import { PaymentConfigPage } from '../features/payment-config/pages/PaymentConfigPage';
import { SeafarerRegistryPage } from '../features/seafarer-registry/pages/SeafarerRegistryPage';
import { SeafarerRegistrationQueuePage } from '../features/seafarer-registration-review/pages/SeafarerRegistrationQueuePage';
import { SeafarerRegistrationReviewPage } from '../features/seafarer-registration-review/pages/SeafarerRegistrationReviewPage';
import { SeamanBookQueuePage } from '../features/seaman-book-queue/pages/SeamanBookQueuePage';
import { BtcQueuePage, SeamanBookQueuePage } from '../features/seafarer-document-review/pages/SeafarerDocumentQueuePage';
import { SeafarerDocumentReviewPage } from '../features/seafarer-document-review/pages/SeafarerDocumentReviewPage';
import { QuestionPage } from '../features/question/pages/QuestionPage';
import { ExamPage } from '../features/exam/pages/ExamPage';
import { ExamDetailPage } from '../features/exam/pages/ExamDetailPage';
@@ -84,13 +88,19 @@ const router = createBrowserRouter([
{ path: 'endorsement-queue', element: <Navigate to="/licence-review/type/ENDORSEMENT_COC" replace /> },
{ path: 'endorsement-queue/:id', element: <Navigate to="/licence-review" replace /> },
{ path: 'medical-verification', element: guard([P.VERIFY_SEAFARER_RECORDS], <MedicalVerificationPage />) },
{ 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 />) },
// 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 />) },
{ path: 'licence-review/type/SEAFARER_REGISTRATION', element: <Navigate to="/seafarer-registrations" replace /> },
// Seaman Book and BTC are not licences: own queues, own review.
{ path: 'seaman-book-queue', element: guard(APPLICATION_QUEUE, <SeamanBookQueuePage />) },
{ path: 'btc-queue', element: guard(APPLICATION_QUEUE, <BtcQueuePage />) },
{ path: 'seafarer-documents/:id', element: guard(APPLICATION_QUEUE, <SeafarerDocumentReviewPage />) },
{ path: 'licence-review/type/SEAMAN_BOOK', element: <Navigate to="/seaman-book-queue" replace /> },
{ path: 'licence-review/type/BTC_BASIC_TRAINING', element: <Navigate to="/btc-queue" replace /> },
{ path: 'questions', element: guard([P.APPROVE_QUESTION, P.AUTHOR_QUESTION], <QuestionPage />) },
{ path: 'exams', element: guard([P.MANAGE_EXAMS, P.RECORD_EXAM_ATTENDANCE, P.MANAGE_EXAM_INCIDENTS, P.PUBLISH_EXAM_RESULT], <ExamPage />) },
{ path: 'exams/:id', element: guard([P.MANAGE_EXAMS, P.RECORD_EXAM_ATTENDANCE, P.MANAGE_EXAM_INCIDENTS, P.PUBLISH_EXAM_RESULT], <ExamDetailPage />) },