mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 17:45:43 +00:00
236 lines
8.8 KiB
TypeScript
236 lines
8.8 KiB
TypeScript
import { useMemo } from 'react';
|
|
import {
|
|
Badge,
|
|
Card,
|
|
Group,
|
|
Paper,
|
|
Progress,
|
|
SimpleGrid,
|
|
Stack,
|
|
Table,
|
|
Text,
|
|
ThemeIcon,
|
|
Title,
|
|
} from '@mantine/core';
|
|
import {
|
|
IconAnchor,
|
|
IconAlertCircle,
|
|
IconCircleCheck,
|
|
IconShip,
|
|
IconWaveSine,
|
|
} from '@tabler/icons-react';
|
|
import { MOCK_VESSEL_REGISTRATIONS } from './VesselRegistrationQueuePage';
|
|
import type { VesselRegistration } from './VesselRegistrationQueuePage';
|
|
|
|
const STATUS_COLOR: Record<string, string> = {
|
|
Pending: 'gray',
|
|
'Under Review': 'yellow',
|
|
Approved: 'teal',
|
|
Rejected: 'red',
|
|
'Correction Required': 'orange',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Component
|
|
// ---------------------------------------------------------------------------
|
|
export function VesselRegistrationReportPage() {
|
|
const data = MOCK_VESSEL_REGISTRATIONS;
|
|
|
|
const stats = useMemo(() => {
|
|
const total = data.length;
|
|
const inland = data.filter((r) => r.category === 'Inland Waterway Vessel').length;
|
|
const seagoing = data.filter((r) => r.category === 'Sea-going Vessel (International)').length;
|
|
const currentYear = new Date().getFullYear();
|
|
const approvedThisYear = data.filter(
|
|
(r) => r.status === 'Approved' && r.approvalDate?.startsWith(String(currentYear))
|
|
).length;
|
|
|
|
const statusCounts: Record<string, number> = {};
|
|
data.forEach((r) => { statusCounts[r.status] = (statusCounts[r.status] ?? 0) + 1; });
|
|
const statusDist = Object.entries(statusCounts).map(([label, count]) => ({
|
|
label,
|
|
count,
|
|
pct: total > 0 ? Math.round((count / total) * 100) : 0,
|
|
color: STATUS_COLOR[label] ?? 'gray',
|
|
}));
|
|
|
|
const typeCounts: Record<string, number> = {};
|
|
data.forEach((r) => { typeCounts[r.vesselType] = (typeCounts[r.vesselType] ?? 0) + 1; });
|
|
const typeDist = Object.entries(typeCounts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([label, count]) => ({
|
|
label,
|
|
count,
|
|
pct: total > 0 ? Math.round((count / total) * 100) : 0,
|
|
}));
|
|
|
|
const renewalDue = data.filter(
|
|
(r) => r.renewalStatus === 'Due Soon' || r.renewalStatus === 'Overdue'
|
|
);
|
|
|
|
const recent = [...data]
|
|
.sort((a, b) => new Date(b.submittedDate).getTime() - new Date(a.submittedDate).getTime())
|
|
.slice(0, 10);
|
|
|
|
return { total, inland, seagoing, approvedThisYear, statusDist, typeDist, renewalDue, recent };
|
|
}, [data]);
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
<div>
|
|
<Title order={3}>Vessel Registration Report</Title>
|
|
<Text fz="sm" c="dimmed">Summary of all vessel registrations and renewal status</Text>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
|
{[
|
|
{ label: 'Total Registered', value: stats.total, color: 'blue', icon: IconAnchor },
|
|
{ label: 'Inland Vessels', value: stats.inland, color: 'cyan', icon: IconWaveSine },
|
|
{ label: 'Sea-going Vessels', value: stats.seagoing, color: 'indigo', icon: IconShip },
|
|
{ label: 'Approved This Year', value: stats.approvedThisYear, color: 'teal', icon: IconCircleCheck },
|
|
].map((s) => {
|
|
const Icon = s.icon;
|
|
return (
|
|
<Card key={s.label} withBorder radius="md" p="md">
|
|
<Group gap="sm" mb={4}>
|
|
<ThemeIcon size={32} radius="md" color={s.color} variant="light">
|
|
<Icon size={18} />
|
|
</ThemeIcon>
|
|
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>{s.label}</Text>
|
|
</Group>
|
|
<Text fz="2xl" fw={700} c={`${s.color}.6`}>{s.value}</Text>
|
|
</Card>
|
|
);
|
|
})}
|
|
</SimpleGrid>
|
|
|
|
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
|
|
{/* Status Distribution */}
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="md">Status Distribution</Text>
|
|
<Stack gap="sm">
|
|
{stats.statusDist.map((s) => (
|
|
<div key={s.label}>
|
|
<Group justify="space-between" mb={4}>
|
|
<Group gap="xs">
|
|
<Badge color={s.color} size="xs" variant="light">{s.label}</Badge>
|
|
</Group>
|
|
<Text fz="xs" c="dimmed">{s.count} ({s.pct}%)</Text>
|
|
</Group>
|
|
<Progress value={s.pct} color={s.color} size="sm" radius="xl" />
|
|
</div>
|
|
))}
|
|
</Stack>
|
|
</Paper>
|
|
|
|
{/* Vessel Type Breakdown */}
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="md">Vessel Type Breakdown</Text>
|
|
<Stack gap="sm">
|
|
{stats.typeDist.map((t) => (
|
|
<div key={t.label}>
|
|
<Group justify="space-between" mb={4}>
|
|
<Text fz="sm">{t.label}</Text>
|
|
<Text fz="xs" c="dimmed">{t.count} ({t.pct}%)</Text>
|
|
</Group>
|
|
<Progress value={t.pct} color="blue" size="sm" radius="xl" />
|
|
</div>
|
|
))}
|
|
</Stack>
|
|
</Paper>
|
|
</SimpleGrid>
|
|
|
|
{/* Renewal Tracking */}
|
|
<Paper withBorder radius="md" p="md">
|
|
<Group gap="xs" mb="sm">
|
|
<IconAlertCircle size={17} color="var(--mantine-color-orange-6)" />
|
|
<Text fw={700} fz="sm">Renewal Tracking</Text>
|
|
{stats.renewalDue.length > 0 && (
|
|
<Badge color="orange" size="sm">{stats.renewalDue.length} due</Badge>
|
|
)}
|
|
</Group>
|
|
|
|
{stats.renewalDue.length === 0 ? (
|
|
<Group gap="xs">
|
|
<IconCircleCheck size={16} color="var(--mantine-color-teal-6)" />
|
|
<Text fz="sm" c="dimmed">No vessel registrations due for renewal in the next 180 days.</Text>
|
|
</Group>
|
|
) : (
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Reg ID</Table.Th>
|
|
<Table.Th>Vessel Name</Table.Th>
|
|
<Table.Th>Category</Table.Th>
|
|
<Table.Th>Owner</Table.Th>
|
|
<Table.Th>Expiry Date</Table.Th>
|
|
<Table.Th>Renewal Status</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{stats.renewalDue.map((reg) => (
|
|
<Table.Tr key={reg.id}>
|
|
<Table.Td><Text fz="sm" fw={500}>{reg.id}</Text></Table.Td>
|
|
<Table.Td><Text fz="sm">{reg.vesselName}</Text></Table.Td>
|
|
<Table.Td>
|
|
<Badge size="xs" color={reg.category === 'Inland Waterway Vessel' ? 'blue' : 'indigo'} variant="light">
|
|
{reg.category === 'Inland Waterway Vessel' ? 'Inland' : 'Sea-going'}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td><Text fz="sm">{reg.ownerName}</Text></Table.Td>
|
|
<Table.Td><Text fz="sm" c="dimmed">{reg.expiryDate ?? '—'}</Text></Table.Td>
|
|
<Table.Td>
|
|
<Badge color={reg.renewalStatus === 'Overdue' ? 'red' : 'orange'} size="sm">
|
|
{reg.renewalStatus}
|
|
</Badge>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
</Paper>
|
|
|
|
{/* Recent Registrations */}
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="sm">Recent Registrations (last {stats.recent.length})</Text>
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Reg ID</Table.Th>
|
|
<Table.Th>Vessel Name</Table.Th>
|
|
<Table.Th>Category</Table.Th>
|
|
<Table.Th>Type</Table.Th>
|
|
<Table.Th>Owner</Table.Th>
|
|
<Table.Th>Submitted</Table.Th>
|
|
<Table.Th>Status</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{stats.recent.map((reg) => (
|
|
<Table.Tr key={reg.id}>
|
|
<Table.Td><Text fz="sm" fw={500}>{reg.id}</Text></Table.Td>
|
|
<Table.Td><Text fz="sm">{reg.vesselName}</Text></Table.Td>
|
|
<Table.Td>
|
|
<Badge size="xs" color={reg.category === 'Inland Waterway Vessel' ? 'blue' : 'indigo'} variant="light">
|
|
{reg.category === 'Inland Waterway Vessel' ? 'Inland' : 'Sea-going'}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td><Text fz="sm">{reg.vesselType}</Text></Table.Td>
|
|
<Table.Td><Text fz="sm">{reg.ownerName}</Text></Table.Td>
|
|
<Table.Td><Text fz="sm" c="dimmed">{reg.submittedDate}</Text></Table.Td>
|
|
<Table.Td>
|
|
<Badge color={STATUS_COLOR[reg.status] ?? 'gray'} variant="light" size="sm">
|
|
{reg.status}
|
|
</Badge>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Paper>
|
|
</Stack>
|
|
);
|
|
}
|