diff --git a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx index d76514579..ce6450686 100644 --- a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx +++ b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx @@ -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} )} + {(row.original.vesselType || row.original.flagState || row.original.grossTonnage) && ( + + {[ + row.original.vesselType, + row.original.flagState, + row.original.grossTonnage ? `${row.original.grossTonnage} GT` : null, + ] + .filter(Boolean) + .join(' · ')} + + )} ), }, @@ -151,6 +163,16 @@ export function seaServiceColumns( ), }, + { + header: t('recordVerification.columns.days', 'Days'), + label: t('recordVerification.columns.days', 'Days'), + align: 'right', + cell: ({ row }) => ( + + {seaServiceDays(row.original.engagementDate, row.original.dischargeDate) ?? '—'} + + ), + }, statusColumn(t), ]; } diff --git a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx index d4bc9e434..9d79075a7 100644 --- a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx +++ b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx @@ -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('SUBMITTED'); const { @@ -372,74 +372,62 @@ export function MedicalVerificationPage() { [rulingSeaService, rule, verifySeaService, showDate, t], ); + const isMedical = kind === 'medical'; + return ( - {t('recordVerification.title', 'Record verification')} + {isMedical + ? t('recordVerification.medicalTitle', 'Medical Certificate Verification') + : t('recordVerification.seaServiceTitle', 'Sea Service Verification')} - {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.', + )} - - - }> - {t('recordVerification.tabs.medical', { - count: pendingMedicalList.length, - defaultValue: 'Medical ({{count}})', - })} - - }> - {t('recordVerification.tabs.seaService', { - count: pendingSeaServiceList.length, - defaultValue: 'Sea Service ({{count}})', - })} - - - - - {statusFilter} - - - - - {statusFilter} - - - + {isMedical ? ( + + ) : ( + + )} ; + export default MedicalVerificationPage; diff --git a/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx b/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx new file mode 100644 index 000000000..961545a97 --- /dev/null +++ b/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx @@ -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(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[] = useMemo( + () => [ + { + header: 'Request №', + accessorKey: 'requestNumber', + cell: ({ row }) => ( +
+ + {row.original.requestNumber} + + {row.original.documentNumber && ( + + {row.original.documentNumber} + + )} +
+ ), + }, + { + header: 'Seafarer', + accessorKey: 'applicant.name', + cell: ({ row }) => ( +
+ + {row.original.applicant?.name ?? '—'} + + + {row.original.applicant?.seafarerNumber ?? '—'} + +
+ ), + }, + { + header: 'Fee', + accessorKey: 'feeAmount', + cell: ({ row }) => ( + + {row.original.feeAmount !== null ? `${row.original.feeAmount} ${row.original.feeCurrency}` : '—'} + + ), + }, + { + header: 'Released', + accessorKey: 'submittedAt', + cell: ({ row }) => ( + {row.original.submittedAt ? showDate(row.original.submittedAt) : '—'} + ), + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => ( + + {SEAFARER_DOCUMENT_STATUS_LABELS[row.original.status]} + + ), + }, + ], + [showDate], + ); + + return ( + + + {SEAFARER_DOCUMENT_KIND_LABELS[kind]} Queue + + + Requests released by an approved seafarer registration: confirm payment, schedule the + collection date, then issue. + + + } + value={search} + onChange={(e) => { + setSearch(e.currentTarget.value); + setPage(0); + }} + w={280} + /> +