mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: add VesselTransferPage for managing vessel ownership transfers
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Progress,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconArrowsExchange,
|
||||
IconShip,
|
||||
} from '@tabler/icons-react';
|
||||
import {
|
||||
STATUS_COLORS,
|
||||
STATUS_LABELS,
|
||||
STATUS_PROGRESS,
|
||||
TERMINAL_STATUSES,
|
||||
useGetMyApplicationsQuery,
|
||||
useGetMyVesselsQuery,
|
||||
} from '@ema-platform/api';
|
||||
|
||||
const TRANSFER_TYPE_KEY = 'VESSEL_OWNERSHIP_TRANSFER';
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
INLAND_WATERWAY: 'Inland Waterway',
|
||||
SEA_GOING: 'Sea-going',
|
||||
};
|
||||
|
||||
const VESSEL_STATUS_COLORS: Record<string, string> = {
|
||||
REGISTERED: 'green',
|
||||
SUSPENDED: 'orange',
|
||||
DEREGISTERED: 'gray',
|
||||
};
|
||||
|
||||
/**
|
||||
* The vessel owner's ownership-transfer home: transfers in flight, and the
|
||||
* registered vessels eligible to start one. Same config-driven wizard as
|
||||
* registration underneath (VESSEL_OWNERSHIP_TRANSFER) — this page mirrors
|
||||
* VesselRegistrationPage, just a different entry point and no
|
||||
* certificate/renewal/incident actions, which don't apply here.
|
||||
*/
|
||||
export function VesselTransferPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data: vessels, isLoading: loadingVessels } = useGetMyVesselsQuery();
|
||||
const { data: applications, isLoading: loadingApplications } =
|
||||
useGetMyApplicationsQuery();
|
||||
|
||||
const inFlight = (applications?.items ?? []).filter(
|
||||
(app) =>
|
||||
app.licenseType?.key === TRANSFER_TYPE_KEY &&
|
||||
!TERMINAL_STATUSES.includes(app.status),
|
||||
);
|
||||
|
||||
// A transfer moves ownership of a vessel already on the register — nothing
|
||||
// to transfer without at least one REGISTERED vessel.
|
||||
const hasTransferableVessel = (vessels ?? []).some(
|
||||
(v) => v.status === 'REGISTERED',
|
||||
);
|
||||
|
||||
if (loadingVessels || loadingApplications) {
|
||||
return (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Ownership Transfer</Title>
|
||||
<Tooltip
|
||||
label="Register a vessel first — there's nothing to transfer yet"
|
||||
disabled={hasTransferableVessel}
|
||||
>
|
||||
<Button
|
||||
leftSection={<IconArrowsExchange size={16} />}
|
||||
disabled={!hasTransferableVessel}
|
||||
onClick={() => navigate(`/licensing/${TRANSFER_TYPE_KEY}/apply`)}
|
||||
>
|
||||
Start transfer
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{/* ----------------------------------------------------- in-flight */}
|
||||
{inFlight.length > 0 && (
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Transfers in progress</Title>
|
||||
{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>
|
||||
<Text fw={600}>{app.applicationNumber}</Text>
|
||||
<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/${TRANSFER_TYPE_KEY}/applications/${app.id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isDraft ? 'Continue' : needsAction ? 'Fix' : 'View'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* ------------------------------------------------------- vessels */}
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>My vessels</Title>
|
||||
{(vessels ?? []).length === 0 ? (
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconShip size={40} color="var(--mantine-color-blue-5)" />
|
||||
<Text fw={600}>No registered vessels yet</Text>
|
||||
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
||||
Ownership can only be transferred for a vessel already on the
|
||||
register.
|
||||
</Text>
|
||||
<Button
|
||||
mt="xs"
|
||||
variant="light"
|
||||
onClick={() => navigate('/vessel-registration')}
|
||||
>
|
||||
Go to Vessel Registration
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={640}>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Registration №</Table.Th>
|
||||
<Table.Th>Vessel</Table.Th>
|
||||
<Table.Th>Category</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th />
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(vessels ?? []).map((vessel) => (
|
||||
<Table.Tr key={vessel.id}>
|
||||
<Table.Td>
|
||||
<Text ff="monospace" size="sm" fw={600}>
|
||||
{vessel.registrationNumber}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={500}>
|
||||
{vessel.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{vessel.vesselType ?? '—'}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{CATEGORY_LABELS[vessel.category] ?? vessel.category}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
size="sm"
|
||||
color={VESSEL_STATUS_COLORS[vessel.status]}
|
||||
>
|
||||
{vessel.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify="flex-end">
|
||||
{vessel.status === 'REGISTERED' ? (
|
||||
<Tooltip label="Start an ownership transfer for this vessel">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<IconArrowsExchange size={14} />}
|
||||
onClick={() =>
|
||||
navigate(`/licensing/${TRANSFER_TYPE_KEY}/apply`)
|
||||
}
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
Not transferable
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default VesselTransferPage;
|
||||
@@ -38,6 +38,7 @@ import { CoCApplicationPage } from "./features/certificates/pages/CoCApplication
|
||||
// Phase 3 — Endorsement
|
||||
import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
|
||||
import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
|
||||
import { VesselTransferPage } from "./features/vessel-registration/pages/VesselTransferPage";
|
||||
import { MyApplicationsPage } from "./features/licensing/pages/MyApplicationsPage";
|
||||
import { PaymentCheckPage } from "./features/payments/pages/PaymentCheckPage";
|
||||
import { PaymentSuccessPage } from "./features/payments/pages/PaymentSuccessPage";
|
||||
@@ -176,10 +177,7 @@ export const router = createBrowserRouter([
|
||||
path: "/vessel-registration-dashboard",
|
||||
element: <Navigate to="/vessel-registration" replace />,
|
||||
},
|
||||
{
|
||||
path: "/vessel-registration/transfer",
|
||||
element: <Navigate to="/licensing/VESSEL_OWNERSHIP_TRANSFER/apply" replace />,
|
||||
},
|
||||
{ path: "/vessel-registration/transfer", element: <VesselTransferPage /> },
|
||||
// Legacy per-licence-type URLs. Each once had its own hand-written page
|
||||
// that posted to a `/logistics-licenses/*` endpoint the API never had,
|
||||
// and dropped every uploaded document on the floor. They are kept as
|
||||
|
||||
Reference in New Issue
Block a user