mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 15:25:47 +00:00
ui componenet based on the requirements
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Paper,
|
||||
Progress,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBook2,
|
||||
IconCertificate,
|
||||
IconChartBar,
|
||||
IconDownload,
|
||||
IconHeart,
|
||||
IconId,
|
||||
IconShieldCheck,
|
||||
IconTrendingUp,
|
||||
IconUsers,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock statistics
|
||||
// ---------------------------------------------------------------------------
|
||||
const KPI = [
|
||||
{ label: 'Total Seafarers', value: 1284, delta: '+12 this month', color: 'blue', icon: IconUsers },
|
||||
{ label: 'Seaman Books Issued', value: 1102, delta: '85.8% of total', color: 'teal', icon: IconBook2 },
|
||||
{ label: 'BTC Issued', value: 1098, delta: '85.5% of total', color: 'teal', icon: IconCertificate },
|
||||
{ label: 'BSID Issued', value: 874, delta: '68.1% of total', color: 'violet', icon: IconId },
|
||||
{ label: 'CoC / CoP Issued', value: 342, delta: '26.6% of total', color: 'orange', icon: IconShieldCheck },
|
||||
{ label: 'Medical Expiring (90d)',value: 41, delta: 'Action required', color: 'red', icon: IconHeart },
|
||||
];
|
||||
|
||||
const MONTHLY_APPS = [
|
||||
{ month: 'Jan', seamanBook: 18, coc: 4 },
|
||||
{ month: 'Feb', seamanBook: 22, coc: 6 },
|
||||
{ month: 'Mar', seamanBook: 28, coc: 9 },
|
||||
{ month: 'Apr', seamanBook: 31, coc: 12 },
|
||||
{ month: 'May', seamanBook: 26, coc: 8 },
|
||||
{ month: 'Jun', seamanBook: 35, coc: 14 },
|
||||
];
|
||||
|
||||
const CERT_DISTRIBUTION = [
|
||||
{ label: 'Seaman Book', count: 1102, pct: 86, color: 'blue' },
|
||||
{ label: 'Basic Training Cert', count: 1098, pct: 85, color: 'teal' },
|
||||
{ label: 'BSID', count: 874, pct: 68, color: 'violet' },
|
||||
{ label: 'CoC / CoP', count: 342, pct: 27, color: 'orange' },
|
||||
];
|
||||
|
||||
const REVENUE = [
|
||||
{ cert: 'Seaman Book', apps: 1102, feePerApp: 700, revenue: 771400 },
|
||||
{ cert: 'BTC', apps: 1098, feePerApp: 400, revenue: 439200 },
|
||||
{ cert: 'BSID', apps: 874, feePerApp: 250, revenue: 218500 },
|
||||
{ cert: 'CoC / CoP', apps: 342, feePerApp: 1400, revenue: 478800 },
|
||||
];
|
||||
const TOTAL_REVENUE = REVENUE.reduce((s, r) => s + r.revenue, 0);
|
||||
|
||||
const EXPIRY_WATCH = [
|
||||
{ cert: 'Seaman Book', expiring30: 8, expiring90: 23, total: 1102 },
|
||||
{ cert: 'BTC', expiring30: 6, expiring90: 19, total: 1098 },
|
||||
{ cert: 'Medical Cert', expiring30: 12, expiring90: 41, total: 1284 },
|
||||
{ cert: 'CoC / CoP', expiring30: 4, expiring90: 17, total: 342 },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Components
|
||||
// ---------------------------------------------------------------------------
|
||||
function KpiCard({ label, value, delta, color, icon: Icon }: typeof KPI[0]) {
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Group justify="space-between" wrap="nowrap" mb="xs">
|
||||
<ThemeIcon variant="light" color={color} size={44} radius="md"><Icon size={22} stroke={1.6} /></ThemeIcon>
|
||||
<Text fz="xl" fw={800}>{value.toLocaleString()}</Text>
|
||||
</Group>
|
||||
<Text fz="sm" fw={600} lh={1.3}>{label}</Text>
|
||||
<Text fz="xs" c="dimmed" mt={2}>{delta}</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple bar chart using divs
|
||||
function BarChart() {
|
||||
const max = Math.max(...MONTHLY_APPS.map((m) => m.seamanBook + m.coc));
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{MONTHLY_APPS.map((m) => (
|
||||
<Group key={m.month} gap="sm" align="center" wrap="nowrap">
|
||||
<Text fz="xs" c="dimmed" w={28} style={{ flexShrink: 0 }}>{m.month}</Text>
|
||||
<div style={{ flex: 1, position: 'relative', height: 22 }}>
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, height: '100%', width: `${(m.seamanBook / max) * 100}%`, background: 'var(--mantine-color-blue-4)', borderRadius: 4 }} />
|
||||
<div style={{ position: 'absolute', left: `${(m.seamanBook / max) * 100}%`, top: 0, height: '100%', width: `${(m.coc / max) * 100}%`, background: 'var(--mantine-color-orange-4)', borderRadius: '0 4px 4px 0' }} />
|
||||
</div>
|
||||
<Text fz="xs" fw={600} w={40} style={{ flexShrink: 0 }}>{m.seamanBook + m.coc}</Text>
|
||||
</Group>
|
||||
))}
|
||||
<Group gap="md" mt="xs">
|
||||
<Group gap={6}><div style={{ width: 12, height: 12, borderRadius: 2, background: 'var(--mantine-color-blue-4)' }} /><Text fz="xs" c="dimmed">Seaman Book</Text></Group>
|
||||
<Group gap={6}><div style={{ width: 12, height: 12, borderRadius: 2, background: 'var(--mantine-color-orange-4)' }} /><Text fz="xs" c="dimmed">CoC / CoP</Text></Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main page
|
||||
// ---------------------------------------------------------------------------
|
||||
export function AnalyticsPage() {
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Title order={3}>Analytics & Reports</Title>
|
||||
<Text fz="sm" c="dimmed">Overview of seafarer registrations, certificate issuance, and revenue — mock data for demonstration</Text>
|
||||
</div>
|
||||
<Button variant="default" size="sm" leftSection={<IconDownload size={14} />}>
|
||||
Export PDF
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* KPIs */}
|
||||
<SimpleGrid cols={{ base: 2, sm: 3, lg: 6 }} spacing="sm">
|
||||
{KPI.map((k) => <KpiCard key={k.label} {...k} />)}
|
||||
</SimpleGrid>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
|
||||
{/* Monthly applications bar chart */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size={28} radius="md" color="blue" variant="light"><IconChartBar size={15} /></ThemeIcon>
|
||||
<Text fw={700} fz="sm">Monthly Applications (2025)</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<BarChart />
|
||||
</Paper>
|
||||
|
||||
{/* Certificate distribution */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="xs" mb="lg">
|
||||
<ThemeIcon size={28} radius="md" color="teal" variant="light"><IconTrendingUp size={15} /></ThemeIcon>
|
||||
<Text fw={700} fz="sm">Certificate Distribution</Text>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{CERT_DISTRIBUTION.map((c) => (
|
||||
<div key={c.label}>
|
||||
<Group justify="space-between" mb={4}>
|
||||
<Group gap="xs">
|
||||
<Badge color={c.color} variant="light" size="sm">{c.label}</Badge>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Text fz="xs" fw={600}>{c.count.toLocaleString()}</Text>
|
||||
<Text fz="xs" c="dimmed">{c.pct}%</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Progress value={c.pct} color={c.color} radius="xl" size="sm" />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Revenue breakdown */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Text fw={700} fz="sm">Revenue Breakdown by Certificate Type</Text>
|
||||
<Badge size="lg" variant="light" color="blue">Total: ETB {TOTAL_REVENUE.toLocaleString()}</Badge>
|
||||
</Group>
|
||||
<Table highlightOnHover fz="sm" verticalSpacing="sm">
|
||||
<Table.Thead bg="var(--mantine-color-default-hover)">
|
||||
<Table.Tr>
|
||||
{['Certificate Type', 'Applications', 'Fee per App', 'Total Revenue', '% of Revenue'].map((h) => (
|
||||
<Table.Th key={h} style={{ fontSize: rem(11), textTransform: 'uppercase', color: 'var(--mantine-color-dimmed)' }}>{h}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{REVENUE.map((r) => (
|
||||
<Table.Tr key={r.cert}>
|
||||
<Table.Td><Text fz="sm" fw={600}>{r.cert}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">{r.apps.toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">ETB {r.feePerApp.toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm" fw={700} c="blue">ETB {r.revenue.toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Progress value={(r.revenue / TOTAL_REVENUE) * 100} size="sm" radius="xl" style={{ flex: 1, minWidth: 60 }} />
|
||||
<Text fz="xs" c="dimmed">{((r.revenue / TOTAL_REVENUE) * 100).toFixed(1)}%</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
<Table.Tfoot>
|
||||
<Table.Tr>
|
||||
<Table.Td><Text fz="sm" fw={700}>Total</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm" fw={700}>{REVENUE.reduce((s, r) => s + r.apps, 0).toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td />
|
||||
<Table.Td><Text fz="sm" fw={800} c="blue">ETB {TOTAL_REVENUE.toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td />
|
||||
</Table.Tr>
|
||||
</Table.Tfoot>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
{/* Expiry watchlist */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" color="red" variant="light"><IconHeart size={15} /></ThemeIcon>
|
||||
<Text fw={700} fz="sm">Expiry Watchlist</Text>
|
||||
</Group>
|
||||
<Table highlightOnHover fz="sm" verticalSpacing="sm">
|
||||
<Table.Thead bg="var(--mantine-color-default-hover)">
|
||||
<Table.Tr>
|
||||
{['Certificate', 'Total Issued', 'Expiring in 30 days', 'Expiring in 90 days', '% at Risk'].map((h) => (
|
||||
<Table.Th key={h} style={{ fontSize: rem(11), textTransform: 'uppercase', color: 'var(--mantine-color-dimmed)' }}>{h}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{EXPIRY_WATCH.map((e) => (
|
||||
<Table.Tr key={e.cert}>
|
||||
<Table.Td><Text fz="sm" fw={600}>{e.cert}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">{e.total.toLocaleString()}</Text></Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={e.expiring30 > 5 ? 'red' : 'orange'} variant="light" size="sm">{e.expiring30}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={e.expiring90 > 20 ? 'orange' : 'yellow'} variant="light" size="sm">{e.expiring90}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Progress
|
||||
value={(e.expiring90 / e.total) * 100}
|
||||
color={(e.expiring90 / e.total) > 0.1 ? 'red' : 'orange'}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
style={{ flex: 1, minWidth: 60 }}
|
||||
/>
|
||||
<Text fz="xs" c="dimmed">{((e.expiring90 / e.total) * 100).toFixed(1)}%</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user