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
|
// Phase 3 — Endorsement
|
||||||
import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
|
import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
|
||||||
import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
|
import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
|
||||||
|
import { VesselTransferPage } from "./features/vessel-registration/pages/VesselTransferPage";
|
||||||
import { MyApplicationsPage } from "./features/licensing/pages/MyApplicationsPage";
|
import { MyApplicationsPage } from "./features/licensing/pages/MyApplicationsPage";
|
||||||
import { PaymentCheckPage } from "./features/payments/pages/PaymentCheckPage";
|
import { PaymentCheckPage } from "./features/payments/pages/PaymentCheckPage";
|
||||||
import { PaymentSuccessPage } from "./features/payments/pages/PaymentSuccessPage";
|
import { PaymentSuccessPage } from "./features/payments/pages/PaymentSuccessPage";
|
||||||
@@ -176,10 +177,7 @@ export const router = createBrowserRouter([
|
|||||||
path: "/vessel-registration-dashboard",
|
path: "/vessel-registration-dashboard",
|
||||||
element: <Navigate to="/vessel-registration" replace />,
|
element: <Navigate to="/vessel-registration" replace />,
|
||||||
},
|
},
|
||||||
{
|
{ path: "/vessel-registration/transfer", element: <VesselTransferPage /> },
|
||||||
path: "/vessel-registration/transfer",
|
|
||||||
element: <Navigate to="/licensing/VESSEL_OWNERSHIP_TRANSFER/apply" replace />,
|
|
||||||
},
|
|
||||||
// Legacy per-licence-type URLs. Each once had its own hand-written page
|
// Legacy per-licence-type URLs. Each once had its own hand-written page
|
||||||
// that posted to a `/logistics-licenses/*` endpoint the API never had,
|
// that posted to a `/logistics-licenses/*` endpoint the API never had,
|
||||||
// and dropped every uploaded document on the floor. They are kept as
|
// and dropped every uploaded document on the floor. They are kept as
|
||||||
|
|||||||
Reference in New Issue
Block a user