feat: Refactor medical and sea service verification pages

- Split MedicalVerificationPage into MedicalVerificationPage and SeaServiceVerificationPage for better separation of concerns.
- Update navigation to include separate entries for Sea Service Verification and Medical Verification.
- Enhance sea service columns to display additional vessel information and days served.
- Add translations for new and updated labels in both English and Amharic.
- Introduce seaServiceDays helper function to calculate days served based on engagement and discharge dates.
- Remove deprecated MedicalCertificatePage from the portal.
- Update routing to direct to the new sea service and medical pages.
This commit is contained in:
Nati
2026-08-20 11:01:04 +00:00
parent db5b7bdf70
commit 558cfe20a1
16 changed files with 239 additions and 471 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

@@ -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

@@ -102,6 +102,7 @@ export const NAV_SECTIONS: NavSection[] = [
{ 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,7 +22,10 @@ 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';
@@ -85,6 +88,7 @@ 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.