mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-02 06:53:40 +00:00
Merge branch 'feature/pencil-design' of github.com:Tria-plc/emaui into feature/pencil-design
This commit is contained in:
@@ -1,43 +1,437 @@
|
||||
import type { ElementType } from 'react';
|
||||
import { Grid, Paper, Text, Title, Stack, Group } from '@mantine/core';
|
||||
import { IconBox, IconUsers, IconActivity } from '@tabler/icons-react';
|
||||
import { useState, type ElementType, type ReactNode } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Text,
|
||||
Title,
|
||||
Stack,
|
||||
Group,
|
||||
SimpleGrid,
|
||||
Grid,
|
||||
ThemeIcon,
|
||||
Badge,
|
||||
Avatar,
|
||||
SegmentedControl,
|
||||
Button,
|
||||
Box,
|
||||
UnstyledButton,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconUsers,
|
||||
IconUserCheck,
|
||||
IconUserPlus,
|
||||
IconClockHour4,
|
||||
IconShieldLock,
|
||||
IconMail,
|
||||
IconUpload,
|
||||
IconDownload,
|
||||
IconClipboardList,
|
||||
IconArrowUpRight,
|
||||
IconArrowDownRight,
|
||||
IconArrowRight,
|
||||
} from '@tabler/icons-react';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Cell,
|
||||
PieChart,
|
||||
Pie,
|
||||
} from 'recharts';
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ElementType;
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* sample data — swap for live API results */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const REGISTRATIONS = [
|
||||
{ month: 'Nov', value: 320 },
|
||||
{ month: 'Dec', value: 410 },
|
||||
{ month: 'Jan', value: 380 },
|
||||
{ month: 'Feb', value: 520 },
|
||||
{ month: 'Mar', value: 470 },
|
||||
{ month: 'Apr', value: 610 },
|
||||
{ month: 'May', value: 560 },
|
||||
{ month: 'Jun', value: 720 },
|
||||
];
|
||||
|
||||
const ROLES = [
|
||||
{ name: 'Admin', value: 18, color: '#1d4ed8' },
|
||||
{ name: 'Staff', value: 42, color: '#3b82f6' },
|
||||
{ name: 'Viewer', value: 30, color: '#60a5fa' },
|
||||
{ name: 'Guest', value: 10, color: '#93c5fd' },
|
||||
];
|
||||
|
||||
const KPIS = [
|
||||
{ label: 'Total Users', value: '9,431', icon: IconUsers, color: 'blue', trend: '+12.5%', up: true },
|
||||
{ label: 'Active Users', value: '7,218', icon: IconUserCheck, color: 'indigo', trend: '+8.2%', up: true },
|
||||
{ label: 'New This Month', value: '642', icon: IconUserPlus, color: 'cyan', trend: '+23.1%', up: true },
|
||||
{ label: 'Pending Approvals', value: '37', icon: IconClockHour4, color: 'orange', trend: '-4.0%', up: false },
|
||||
];
|
||||
|
||||
const QUICK_LINKS = [
|
||||
{ label: 'Add User', desc: 'Create a new account', icon: IconUserPlus, color: 'blue' },
|
||||
{ label: 'Roles & Permissions', desc: 'Manage access levels', icon: IconShieldLock, color: 'indigo' },
|
||||
{ label: 'Invite Members', desc: 'Send email invites', icon: IconMail, color: 'cyan' },
|
||||
{ label: 'Import Users', desc: 'Bulk CSV import', icon: IconUpload, color: 'violet' },
|
||||
{ label: 'Export Data', desc: 'Download reports', icon: IconDownload, color: 'blue' },
|
||||
{ label: 'Audit Log', desc: 'Review activity', icon: IconClipboardList, color: 'grape' },
|
||||
];
|
||||
|
||||
type UserStatus = 'Active' | 'Pending' | 'Invited';
|
||||
const STATUS_COLOR: Record<UserStatus, string> = {
|
||||
Active: 'teal',
|
||||
Pending: 'orange',
|
||||
Invited: 'blue',
|
||||
};
|
||||
|
||||
const RECENT_USERS: {
|
||||
name: string;
|
||||
email: string;
|
||||
initials: string;
|
||||
color: string;
|
||||
}
|
||||
status: UserStatus;
|
||||
}[] = [
|
||||
{ name: 'Sara Tesfaye', email: 'sara.t@ema.gov.et', initials: 'ST', color: 'blue', status: 'Active' },
|
||||
{ name: 'Daniel Bekele', email: 'daniel.b@ema.gov.et', initials: 'DB', color: 'indigo', status: 'Active' },
|
||||
{ name: 'Hanna Girma', email: 'hanna.g@ema.gov.et', initials: 'HG', color: 'violet', status: 'Pending' },
|
||||
{ name: 'Yonas Alemu', email: 'yonas.a@ema.gov.et', initials: 'YA', color: 'cyan', status: 'Active' },
|
||||
{ name: 'Meron Tadesse', email: 'meron.t@ema.gov.et', initials: 'MT', color: 'grape', status: 'Invited' },
|
||||
];
|
||||
|
||||
function StatCard({ label, value, icon: Icon, color }: StatCardProps) {
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* small building blocks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function SectionCard({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Paper p="md" shadow="sm" radius="md" withBorder>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
<Title order={3}>{value}</Title>
|
||||
</Stack>
|
||||
<Icon size={32} color={color} />
|
||||
</Group>
|
||||
<Paper p="lg" radius="lg" withBorder h="100%">
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
function CardHeading({ title, subtitle }: { title: string; subtitle?: string }) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2}>Dashboard</Title>
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Total Items" value="—" icon={IconBox} color="#2563eb" />
|
||||
<Stack gap={2}>
|
||||
<Text fw={700} fz="lg">
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
color,
|
||||
trend,
|
||||
up,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ElementType;
|
||||
color: string;
|
||||
trend: string;
|
||||
up: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Paper p="lg" radius="lg" withBorder>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<ThemeIcon size={46} radius="md" variant="light" color={color}>
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={up ? 'teal' : 'red'}
|
||||
radius="sm"
|
||||
leftSection={up ? <IconArrowUpRight size={12} /> : <IconArrowDownRight size={12} />}
|
||||
>
|
||||
{trend}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz={30} fw={800} mt="md" lh={1.1}>
|
||||
{value}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{label}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickLinkTile({
|
||||
label,
|
||||
desc,
|
||||
icon: Icon,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
desc: string;
|
||||
icon: ElementType;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
style={{
|
||||
border: '1px solid var(--mantine-color-gray-2)',
|
||||
borderRadius: 'var(--mantine-radius-md)',
|
||||
padding: 'var(--mantine-spacing-md)',
|
||||
transition: 'border-color 120ms ease, box-shadow 120ms ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--mantine-color-blue-3)';
|
||||
e.currentTarget.style.boxShadow = 'var(--mantine-shadow-sm)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--mantine-color-gray-2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<ThemeIcon size={42} radius="md" variant="light" color={color}>
|
||||
<Icon size={20} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600} size="sm" mt="sm">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={2}>
|
||||
{desc}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function DashboardPage() {
|
||||
const [range, setRange] = useState('week');
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
{/* header */}
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap">
|
||||
<Stack gap={4}>
|
||||
<Title order={2}>Overview</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Welcome back — here's what's happening across your platform.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Group gap="sm">
|
||||
<SegmentedControl
|
||||
value={range}
|
||||
onChange={setRange}
|
||||
radius="md"
|
||||
data={[
|
||||
{ label: 'Today', value: 'today' },
|
||||
{ label: 'This Week', value: 'week' },
|
||||
{ label: 'This Month', value: 'month' },
|
||||
]}
|
||||
/>
|
||||
<Button leftSection={<IconDownload size={16} />} radius="md">
|
||||
Export
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* KPIs */}
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
||||
{KPIS.map((k) => (
|
||||
<StatCard key={k.label} {...k} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* charts */}
|
||||
<Grid gutter="lg" align="stretch">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<SectionCard>
|
||||
<Group justify="space-between" align="flex-start" mb="lg">
|
||||
<CardHeading
|
||||
title="User Registrations"
|
||||
subtitle="New sign-ups over the last 8 months"
|
||||
/>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Text fw={700} c="teal">
|
||||
+18.2%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
vs previous period
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Box h={260}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={REGISTRATIONS} margin={{ top: 8, right: 4, left: -22, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="barBlue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#60a5fa" />
|
||||
<stop offset="100%" stopColor="#2563eb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" stroke="#eef2f7" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#8d9bb3' }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: '#f1f5fb' }}
|
||||
contentStyle={{
|
||||
borderRadius: 12,
|
||||
border: '1px solid #e4e9f2',
|
||||
fontSize: 12,
|
||||
boxShadow: '0 4px 20px rgba(15,23,42,0.08)',
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[6, 6, 0, 0]} maxBarSize={34}>
|
||||
{REGISTRATIONS.map((entry, i) => (
|
||||
<Cell
|
||||
key={entry.month}
|
||||
fill={i === REGISTRATIONS.length - 1 ? '#1d4ed8' : 'url(#barBlue)'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Total Users" value="—" icon={IconUsers} color="#16a34a" />
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<SectionCard>
|
||||
<CardHeading title="Users by Role" subtitle="Distribution across access levels" />
|
||||
<Group mt="lg" justify="center" wrap="nowrap" gap="lg">
|
||||
<Box pos="relative" w={168} h={168} style={{ flexShrink: 0 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={ROLES}
|
||||
dataKey="value"
|
||||
innerRadius={56}
|
||||
outerRadius={82}
|
||||
paddingAngle={2}
|
||||
stroke="none"
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<Cell key={r.name} fill={r.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: 12,
|
||||
border: '1px solid #e4e9f2',
|
||||
fontSize: 12,
|
||||
}}
|
||||
formatter={(value, name) => [`${value}%`, name]}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Stack
|
||||
gap={0}
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
||||
>
|
||||
<Text fw={800} fz={22}>
|
||||
9,431
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Total users
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack gap="md" style={{ flex: 1 }}>
|
||||
{ROLES.map((r) => (
|
||||
<Group key={r.name} justify="space-between" wrap="nowrap">
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<Box w={10} h={10} style={{ borderRadius: 999, background: r.color }} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{r.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" fw={700}>
|
||||
{r.value}%
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Group>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Activity" value="—" icon={IconActivity} color="#d97706" />
|
||||
</Grid>
|
||||
|
||||
{/* quick links + recent users */}
|
||||
<Grid gutter="lg" align="stretch">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<SectionCard>
|
||||
<CardHeading title="Quick Links" subtitle="Common administrative shortcuts" />
|
||||
<SimpleGrid cols={{ base: 2, sm: 3 }} spacing="md" mt="lg">
|
||||
{QUICK_LINKS.map((q) => (
|
||||
<QuickLinkTile key={q.label} {...q} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<SectionCard>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Text fw={700} fz="lg">
|
||||
Recent Users
|
||||
</Text>
|
||||
<Anchor size="sm" fw={600}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
View all
|
||||
<IconArrowRight size={14} />
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Group>
|
||||
<Stack gap={0}>
|
||||
{RECENT_USERS.map((u, i) => (
|
||||
<Group
|
||||
key={u.email}
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
py="sm"
|
||||
style={{
|
||||
borderBottom:
|
||||
i < RECENT_USERS.length - 1
|
||||
? '1px solid var(--mantine-color-gray-2)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar color={u.color} radius="xl" size={38}>
|
||||
{u.initials}
|
||||
</Avatar>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={600}>
|
||||
{u.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{u.email}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge variant="light" color={STATUS_COLOR[u.status]} radius="sm">
|
||||
{u.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user