feat: add in-flight registration application tracking and status dashboard to vessel registration page

This commit is contained in:
estifanos
2026-08-18 11:43:11 +00:00
parent 1509f8ca1f
commit de78052429

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { import {
Alert, Alert,
Badge, Badge,
@@ -8,6 +9,7 @@ import {
Divider, Divider,
Group, Group,
Paper, Paper,
Progress,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
@@ -25,13 +27,24 @@ import {
IconInfoCircle, IconInfoCircle,
IconClockHour4, IconClockHour4,
IconTransferIn, IconTransferIn,
IconArrowRight,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useApiMutation } from '@ema-platform/api'; import {
STATUS_COLORS,
STATUS_LABELS,
STATUS_PROGRESS,
TERMINAL_STATUSES,
useApiMutation,
useGetMyApplicationsQuery,
} from '@ema-platform/api';
import { authStorage } from '@ema-platform/auth'; import { authStorage } from '@ema-platform/auth';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** Registration applications run through the config-driven licensing wizard. */
const REGISTRATION_TYPE_KEY = 'VESSEL_REGISTRATION';
type VesselRegStatus = 'Pending' | 'Under Review' | 'Approved' | 'Rejected' | 'Correction Required'; type VesselRegStatus = 'Pending' | 'Under Review' | 'Approved' | 'Rejected' | 'Correction Required';
type VesselCategory = 'Inland Waterway Vessel' | 'Sea-going Vessel (International)'; type VesselCategory = 'Inland Waterway Vessel' | 'Sea-going Vessel (International)';
type RenewalStatus = 'Valid' | 'Due Soon' | 'Overdue' | 'Not Applicable'; type RenewalStatus = 'Valid' | 'Due Soon' | 'Overdue' | 'Not Applicable';
@@ -118,6 +131,8 @@ function CertificateCard({ label, description }: { label: string; description: s
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function VesselRegistrationPage() { export function VesselRegistrationPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
const { data: applications } = useGetMyApplicationsQuery();
const [registration, setRegistration] = useState<VesselRegistration | null>(null); const [registration, setRegistration] = useState<VesselRegistration | null>(null);
const [fetchTrigger] = useApiMutation<VesselRegistration>(); const [fetchTrigger] = useApiMutation<VesselRegistration>();
const fetched = useRef(false); const fetched = useRef(false);
@@ -132,6 +147,14 @@ export function VesselRegistrationPage() {
.catch(() => {/* no registration yet */}); .catch(() => {/* no registration yet */});
}, [fetchTrigger]); }, [fetchTrigger]);
// Drafts and anything still moving through review — the applicant's own
// registration applications, straight from the licensing queue.
const inFlight = (applications?.items ?? []).filter(
(app) =>
app.licenseType?.key === REGISTRATION_TYPE_KEY &&
!TERMINAL_STATUSES.includes(app.status),
);
const certs = registration?.category === 'Sea-going Vessel (International)' const certs = registration?.category === 'Sea-going Vessel (International)'
? SEAGOING_CERTIFICATES ? SEAGOING_CERTIFICATES
: INLAND_CERTIFICATES; : INLAND_CERTIFICATES;
@@ -148,6 +171,54 @@ export function VesselRegistrationPage() {
</div> </div>
</Group> </Group>
{/* ── Drafts / submitted applications ───────────────────────────── */}
{inFlight.length > 0 && (
<Stack gap="sm">
<Text fw={700} fz="md">{t('vesselRegistration.inFlight.title')}</Text>
{inFlight.map((app) => {
const isDraft = app.status === 'DRAFT';
const needsAction = app.status === 'RESUBMIT_REQUIRED';
return (
<Card key={app.id} withBorder radius="md" p="md">
<Group justify="space-between" wrap="nowrap">
<div>
<Group gap="xs">
<Text fw={600}>{app.applicationNumber}</Text>
{app.kind === 'RENEWAL' && (
<Badge size="sm" variant="light">
{t('vesselRegistration.inFlight.renewalBadge')}
</Badge>
)}
</Group>
<Progress value={STATUS_PROGRESS[app.status]} mt="xs" w={260} />
</div>
<Group wrap="nowrap">
<Badge color={STATUS_COLORS[app.status]}>
{STATUS_LABELS[app.status]}
</Badge>
<Button
size="compact-sm"
variant={needsAction ? 'filled' : 'light'}
color={needsAction ? 'orange' : undefined}
rightSection={<IconArrowRight size={14} />}
onClick={() =>
navigate(`/licensing/${REGISTRATION_TYPE_KEY}/applications/${app.id}`)
}
>
{isDraft
? t('common.continue')
: needsAction
? t('vesselRegistration.inFlight.fix')
: t('vesselRegistration.inFlight.view')}
</Button>
</Group>
</Group>
</Card>
);
})}
</Stack>
)}
{/* ── No registration yet ───────────────────────────────────────── */} {/* ── No registration yet ───────────────────────────────────────── */}
{!registration && ( {!registration && (
<> <>