feat: add status filtering and display to medical and sea service verification queues

This commit is contained in:
estifanos
2026-08-18 10:37:57 +00:00
parent de0dcbfbe8
commit 5897e73442
5 changed files with 132 additions and 45 deletions

View File

@@ -33,24 +33,28 @@ export function medicalActionsColumn(
anyOf={[LICENSE_PERMISSIONS.VERIFY_SEAFARER_RECORDS]}
hideOnly
>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
{row.original.status === 'SUBMITTED' && (
<>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
</>
)}
</RequirePermission>
</Group>
),
@@ -85,24 +89,28 @@ export function seaServiceActionsColumn(
anyOf={[LICENSE_PERMISSIONS.VERIFY_SEAFARER_RECORDS]}
hideOnly
>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
{row.original.status === 'SUBMITTED' && (
<>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
</>
)}
</RequirePermission>
</Group>
),

View File

@@ -5,6 +5,7 @@ import type {
MedicalCertificate,
SeaServiceRecord,
SeafarerProfileSummary,
SeafarerRecordStatus,
} from '@ema-platform/api';
export function ownerName(profile?: SeafarerProfileSummary): string {
@@ -16,6 +17,28 @@ export function ownerName(profile?: SeafarerProfileSummary): string {
);
}
const STATUS_COLOR: Record<SeafarerRecordStatus, string> = {
SUBMITTED: 'yellow',
VERIFIED: 'teal',
REJECTED: 'red',
};
/** Only meaningful now the queue can show ruled records too. */
function statusColumn<T extends { status: SeafarerRecordStatus }>(
t: TFunction,
): AdvancedColumn<T> {
return {
header: t('recordVerification.columns.status', 'Status'),
label: t('recordVerification.columns.status', 'Status'),
accessorKey: 'status',
cell: ({ row }) => (
<Badge size="sm" variant="light" color={STATUS_COLOR[row.original.status]}>
{t(`recordVerification.status.${row.original.status}`, row.original.status)}
</Badge>
),
};
}
export function medicalColumns(
t: TFunction,
showDate: (date: string) => string,
@@ -72,6 +95,7 @@ export function medicalColumns(
</Badge>
),
},
statusColumn<MedicalCertificate>(t),
];
}
@@ -127,5 +151,6 @@ export function seaServiceColumns(
</Text>
),
},
statusColumn<SeaServiceRecord>(t),
];
}

View File

@@ -9,6 +9,7 @@ import {
Loader,
Modal,
Paper,
SegmentedControl,
Stack,
Tabs,
Text,
@@ -32,7 +33,11 @@ import {
useVerifyMedicalCertificateMutation,
useVerifySeaServiceRecordMutation,
} from '@ema-platform/api';
import type { MedicalCertificate, SeaServiceRecord } from '@ema-platform/api';
import type {
MedicalCertificate,
RecordQueueFilter,
SeaServiceRecord,
} from '@ema-platform/api';
import { medicalColumns, seaServiceColumns, ownerName } from './columns';
import { medicalActionsColumn, seaServiceActionsColumn } from './actions';
@@ -180,19 +185,20 @@ function AttachmentsModal({
*/
export function MedicalVerificationPage() {
const { t } = useTranslation();
const [filter, setFilter] = useState<RecordQueueFilter>('SUBMITTED');
const {
data: pendingMedical,
isLoading: loadingMedical,
isFetching: fetchingMedical,
refetch: refetchMedical,
} = useGetPendingMedicalQuery();
} = useGetPendingMedicalQuery(filter);
const {
data: pendingSeaService,
isLoading: loadingSeaService,
isFetching: fetchingSeaService,
refetch: refetchSeaService,
} = useGetPendingSeaServiceQuery();
} = useGetPendingSeaServiceQuery(filter);
const [verifyMedical, { isLoading: rulingMedical }] =
useVerifyMedicalCertificateMutation();
@@ -251,6 +257,12 @@ export function MedicalVerificationPage() {
return pendingSeaServiceList.slice(start, start + seaServicePageSize);
}, [pendingSeaServiceList, seaServicePage, seaServicePageSize]);
const changeFilter = useCallback((value: string) => {
setFilter(value as RecordQueueFilter);
setMedicalPage(0);
setSeaServicePage(0);
}, []);
const handleMedicalPageSizeChange = useCallback((size: number) => {
setMedicalPageSize(size);
setMedicalPage(0);
@@ -261,6 +273,11 @@ export function MedicalVerificationPage() {
setSeaServicePage(0);
}, []);
const emptyText =
filter === 'SUBMITTED'
? t('recordVerification.emptyText', 'Nothing awaiting verification.')
: t('recordVerification.emptyTextFiltered', 'No records match this filter.');
const medicalTableColumns: AdvancedColumn<MedicalCertificate>[] = useMemo(
() => [
...medicalColumns(t, showDate),
@@ -331,6 +348,27 @@ export function MedicalVerificationPage() {
)}
</Text>
<SegmentedControl
mb="md"
value={filter}
onChange={changeFilter}
data={[
{
value: 'SUBMITTED',
label: t('recordVerification.filter.pending', 'Pending'),
},
{
value: 'VERIFIED',
label: t('recordVerification.filter.verified', 'Accepted'),
},
{
value: 'REJECTED',
label: t('recordVerification.filter.rejected', 'Rejected'),
},
{ value: 'ALL', label: t('recordVerification.filter.all', 'All') },
]}
/>
<Tabs defaultValue="medical" keepMounted={false}>
<Tabs.List>
<Tabs.Tab value="medical" leftSection={<IconStethoscope size={16} />}>
@@ -362,7 +400,7 @@ export function MedicalVerificationPage() {
onPageSizeChange={handleMedicalPageSizeChange}
refresh={refetchMedical}
isLoading={loadingMedical || fetchingMedical}
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
emptyText={emptyText}
/>
</Tabs.Panel>
@@ -381,7 +419,7 @@ export function MedicalVerificationPage() {
onPageSizeChange={handleSeaServicePageSizeChange}
refresh={refetchSeaService}
isLoading={loadingSeaService || fetchingSeaService}
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
emptyText={emptyText}
/>
</Tabs.Panel>
</Tabs>

View File

@@ -3,6 +3,7 @@ import type {
CreateMedicalCertificate,
CreateSeaServiceRecord,
MedicalCertificate,
RecordQueueFilter,
SeaServiceRecord,
SeaTimeSummary,
SeafarerStatus,
@@ -104,13 +105,25 @@ export const seafarerApi = baseApi
}),
// -------------------------------------------------------- verification
getPendingSeaService: builder.query<SeaServiceRecord[], void>({
query: () => ({ url: '/sea-service-records/pending' }),
getPendingSeaService: builder.query<
SeaServiceRecord[],
RecordQueueFilter | void
>({
query: (status) => ({
url: '/sea-service-records/pending',
params: { status: status || 'SUBMITTED' },
}),
providesTags: () => [listTag('SeaServiceRecord')],
}),
getPendingMedical: builder.query<MedicalCertificate[], void>({
query: () => ({ url: '/medical-certificates/pending' }),
getPendingMedical: builder.query<
MedicalCertificate[],
RecordQueueFilter | void
>({
query: (status) => ({
url: '/medical-certificates/pending',
params: { status: status || 'SUBMITTED' },
}),
providesTags: () => [listTag('MedicalCertificate')],
}),

View File

@@ -1,4 +1,7 @@
export type SeafarerRecordStatus = 'SUBMITTED' | 'VERIFIED' | 'REJECTED';
/** Verification-queue filter: a record status, or ALL for no filter. */
export type RecordQueueFilter = SeafarerRecordStatus | 'ALL';
export type MedicalFitness = 'FIT' | 'FIT_WITH_RESTRICTIONS' | 'UNFIT';
export type SeafarerDepartment = 'DECK' | 'ENGINE' | 'CATERING';
export type SeafarerStatus = 'ACTIVE' | 'INACTIVE' | 'PENDING' | 'SUSPENDED';