import DownloadIcon from "@mui/icons-material/Download"; import TableChartIcon from "@mui/icons-material/TableChart"; import { Box, Button, Card, CardContent, CardHeader, CircularProgress, Divider, Grid, MenuItem, Stack, Table, TableBody, TableCell, TableHead, TableRow, TextField, ToggleButton, Typography, } from "@mui/material"; import { useState } from "react"; import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { useLookups, useReportsSummary } from "../api/hooks"; /* Validated categorical palette (dataviz reference instance, light mode). * Fixed slot order — color follows the entity: labor=1 material=2 service=3 * other=4. Aqua/yellow sit below 3:1 on white, so the cost chart ships a * table view (relief rule). */ const SERIES = { labor: "#2a78d6", material: "#1baf7a", service: "#eda100", other: "#008300", }; const SINGLE_HUE = "#2a78d6"; const GRID = "#eceff1"; const TICK = { fill: "#52514e", fontSize: 12 }; function money(n: number): string { return n.toLocaleString(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 0, }); } function csvDownload(filename: string, rows: Record[]): void { if (rows.length === 0) return; const headers = Object.keys(rows[0]); const esc = (v: unknown) => `"${String(v ?? "").replace(/"/g, '""')}"`; const csv = [ headers.join(","), ...rows.map((r) => headers.map((h) => esc(r[h])).join(",")), ].join("\n"); const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 30_000); } function StatTile({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function ChartCard({ title, subheader, action, children, }: { title: string; subheader?: string; action?: React.ReactNode; children: React.ReactNode; }) { return ( {children} ); } export function ReportsPage() { const lookups = useLookups(); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [departmentId, setDepartmentId] = useState(""); const [categoryId, setCategoryId] = useState(""); const [costAsTable, setCostAsTable] = useState(false); const summary = useReportsSummary({ date_from: dateFrom || undefined, date_to: dateTo || undefined, department_id: departmentId || undefined, category_id: categoryId || undefined, }); const data = summary.data; const costRows = (data?.cost_over_time ?? []).map((c) => ({ month: c.month, Labor: Number(c.labor), Material: Number(c.material), Service: Number(c.service), Other: Number(c.other), Total: Number(c.total), })); return ( Reports {/* Filters — one row above the charts */} setDateFrom(e.target.value)} /> setDateTo(e.target.value)} /> setDepartmentId(e.target.value === "" ? "" : Number(e.target.value)) } sx={{ minWidth: 180 }} > All departments {(lookups.data?.departments ?? []).map((d) => ( {d.name} ))} setCategoryId(e.target.value === "" ? "" : Number(e.target.value)) } sx={{ minWidth: 180 }} > All categories {(lookups.data?.deviation_categories ?? []).map((c) => ( {c.name} ))} {summary.isLoading || !data ? ( ) : ( <> setCostAsTable(!costAsTable)} title="Toggle table view" > } > {costAsTable ? ( Month Labor Material Service Other Total {costRows.map((r) => ( {r.month} {money(r.Labor)} {money(r.Material)} {money(r.Service)} {money(r.Other)} {money(r.Total)} ))}
) : ( money(v)} width={72} /> money(v)} /> {/* 2px surface gap between stacked segments via stroke */} )}
`${v} days`} /> } onClick={() => csvDownload("top-jobs.csv", data.top_jobs)} > CSV } > Job Number NCRs {data.top_jobs.map((j) => ( {j.job_number} {j.count} ))} {data.top_jobs.length === 0 && ( No data. )}
)}
); }