Files
emaui/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationReportPage.tsx
2026-07-03 16:47:19 +03:00

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>
);
}