mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 04:33:11 +00:00
222 lines
7.1 KiB
TypeScript
222 lines
7.1 KiB
TypeScript
|
|
import type { ReactNode } from "react";
|
||
|
|
import { useNavigate } from "react-router-dom";
|
||
|
|
import AddIcon from "@mui/icons-material/Add";
|
||
|
|
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
||
|
|
import FolderOpenOutlinedIcon from "@mui/icons-material/FolderOpenOutlined";
|
||
|
|
import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined";
|
||
|
|
import {
|
||
|
|
Alert,
|
||
|
|
Box,
|
||
|
|
CircularProgress,
|
||
|
|
Paper,
|
||
|
|
Stack,
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableContainer,
|
||
|
|
TableHead,
|
||
|
|
TableRow,
|
||
|
|
Typography,
|
||
|
|
} from "@mui/material";
|
||
|
|
import { useDashboardStats } from "@/domain/dashboard/use-cases/use-dashboard-stats";
|
||
|
|
|
||
|
|
const INVOICE_STATUSES = ["Draft", "Challenged", "Pending Approval", "Approved", "Paid"] as const;
|
||
|
|
|
||
|
|
const PRIORITY_LEVELS = [5, 4, 3, 2, 1] as const;
|
||
|
|
|
||
|
|
interface KpiCardProps {
|
||
|
|
title: string;
|
||
|
|
description?: string;
|
||
|
|
value?: string | number;
|
||
|
|
icon: ReactNode;
|
||
|
|
color: "success" | "primary" | "info" | "warning";
|
||
|
|
onClick: () => void;
|
||
|
|
loading?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
function KpiCard({ title, description, value, icon, color, onClick, loading }: KpiCardProps) {
|
||
|
|
const colorMap = {
|
||
|
|
success: "success.main",
|
||
|
|
primary: "primary.main",
|
||
|
|
info: "info.main",
|
||
|
|
warning: "warning.main",
|
||
|
|
} as const;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Paper
|
||
|
|
variant="outlined"
|
||
|
|
onClick={onClick}
|
||
|
|
className="flex min-h-[140px] flex-1 cursor-pointer transition-shadow hover:shadow-md"
|
||
|
|
sx={{
|
||
|
|
borderLeftWidth: 4,
|
||
|
|
borderLeftStyle: "solid",
|
||
|
|
borderLeftColor: colorMap[color],
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Box className="flex w-full gap-4 p-4">
|
||
|
|
<Box
|
||
|
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full"
|
||
|
|
sx={{ bgcolor: `${colorMap[color]}22`, color: colorMap[color] }}
|
||
|
|
>
|
||
|
|
{icon}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Box className="flex min-w-0 flex-col justify-center gap-1">
|
||
|
|
<Typography variant="subtitle1" className="font-semibold">
|
||
|
|
{title}
|
||
|
|
</Typography>
|
||
|
|
{value !== undefined && (
|
||
|
|
<Typography variant="h4" component="p" className="font-bold leading-tight">
|
||
|
|
{loading ? "..." : typeof value === "number" ? value.toLocaleString() : value}
|
||
|
|
</Typography>
|
||
|
|
)}
|
||
|
|
{description && (
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
{description}
|
||
|
|
</Typography>
|
||
|
|
)}
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</Paper>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function DashboardPage() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const { data: stats, isLoading, isFetching, error } = useDashboardStats();
|
||
|
|
|
||
|
|
const formatStat = (value: number | undefined) => (isLoading ? undefined : (value ?? 0));
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Box className="flex flex-col gap-4 p-4">
|
||
|
|
<Stack direction="row" spacing={2} alignItems="baseline">
|
||
|
|
<Typography variant="h5" component="h1">
|
||
|
|
Dashboard
|
||
|
|
</Typography>
|
||
|
|
{isFetching && !isLoading && <CircularProgress size={16} />}
|
||
|
|
</Stack>
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<Alert severity="error">
|
||
|
|
{error instanceof Error ? error.message : "Failed to load dashboard stats"}
|
||
|
|
</Alert>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Stack direction={{ xs: "column", lg: "row" }} spacing={2} useFlexGap flexWrap="wrap">
|
||
|
|
<KpiCard
|
||
|
|
title="Create a Work Order"
|
||
|
|
description="Report a problem you need fixed"
|
||
|
|
icon={<AddIcon />}
|
||
|
|
color="success"
|
||
|
|
onClick={() => navigate("/workorders/new")}
|
||
|
|
/>
|
||
|
|
<KpiCard
|
||
|
|
title="Open Work Orders"
|
||
|
|
value={formatStat(stats?.open)}
|
||
|
|
icon={<FolderOpenOutlinedIcon />}
|
||
|
|
color="primary"
|
||
|
|
loading={isLoading}
|
||
|
|
onClick={() => navigate("/workorders")}
|
||
|
|
/>
|
||
|
|
<KpiCard
|
||
|
|
title="Not Dispatched"
|
||
|
|
value={formatStat(stats?.notDispatched)}
|
||
|
|
icon={<ScheduleOutlinedIcon />}
|
||
|
|
color="info"
|
||
|
|
loading={isLoading}
|
||
|
|
onClick={() => navigate("/workorders")}
|
||
|
|
/>
|
||
|
|
<KpiCard
|
||
|
|
title="Completed Orders"
|
||
|
|
value={formatStat(stats?.completed)}
|
||
|
|
icon={<CheckCircleOutlineIcon />}
|
||
|
|
color="warning"
|
||
|
|
loading={isLoading}
|
||
|
|
onClick={() => navigate("/workorders")}
|
||
|
|
/>
|
||
|
|
</Stack>
|
||
|
|
|
||
|
|
<Paper variant="outlined" className="overflow-hidden">
|
||
|
|
<Box className="border-b border-border px-4 py-3">
|
||
|
|
<Typography variant="h6" component="h2">
|
||
|
|
Priority Analysis
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
Age of open Work Orders by Priority
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<TableContainer>
|
||
|
|
<Table size="small">
|
||
|
|
<TableHead>
|
||
|
|
<TableRow>
|
||
|
|
<TableCell className="font-semibold">Priority</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
Total
|
||
|
|
</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
0-14 Days
|
||
|
|
</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
15-29 Days
|
||
|
|
</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
30-59 Days
|
||
|
|
</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
60+ Days
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
</TableHead>
|
||
|
|
<TableBody>
|
||
|
|
{PRIORITY_LEVELS.map((priority) => (
|
||
|
|
<TableRow key={priority}>
|
||
|
|
<TableCell>{priority}</TableCell>
|
||
|
|
<TableCell align="right">—</TableCell>
|
||
|
|
<TableCell align="right">—</TableCell>
|
||
|
|
<TableCell align="right">—</TableCell>
|
||
|
|
<TableCell align="right">—</TableCell>
|
||
|
|
<TableCell align="right">—</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</TableContainer>
|
||
|
|
</Paper>
|
||
|
|
|
||
|
|
<Paper variant="outlined" className="overflow-hidden">
|
||
|
|
<Box className="border-b border-border px-4 py-3">
|
||
|
|
<Typography variant="h6" component="h2">
|
||
|
|
Invoice Analysis
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<TableContainer>
|
||
|
|
<Table size="small">
|
||
|
|
<TableHead>
|
||
|
|
<TableRow>
|
||
|
|
<TableCell className="font-semibold">Status</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
Count
|
||
|
|
</TableCell>
|
||
|
|
<TableCell align="right" className="font-semibold">
|
||
|
|
Amount
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
</TableHead>
|
||
|
|
<TableBody>
|
||
|
|
{INVOICE_STATUSES.map((status) => (
|
||
|
|
<TableRow key={status}>
|
||
|
|
<TableCell>{status}</TableCell>
|
||
|
|
<TableCell align="right">0</TableCell>
|
||
|
|
<TableCell align="right">$0.00</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</TableContainer>
|
||
|
|
</Paper>
|
||
|
|
</Box>
|
||
|
|
);
|
||
|
|
}
|