231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useMemo } from "react";
|
||
import { Wrench, LayoutGrid, Sparkles, ChevronRight } from "lucide-react";
|
||
import { Sparkline } from "@/components/Sparkline";
|
||
import { MONTH_NAMES } from "@/lib/data";
|
||
import type { RunningRow } from "@/lib/rows";
|
||
import { ZAR, ZAR2 } from "@/lib/utils";
|
||
|
||
export function RunningView({
|
||
rows,
|
||
palette,
|
||
monthTotals,
|
||
}: {
|
||
rows: RunningRow[];
|
||
palette: { income: string; profit: string; loss: string };
|
||
monthTotals: number[];
|
||
}) {
|
||
const [view, setView] = useState<"cards" | "grid">("cards");
|
||
const [sort, setSort] = useState<"ytd" | "code">("ytd");
|
||
const [expanded, setExpanded] = useState<string | null>(null);
|
||
|
||
const sorted = useMemo(() => {
|
||
return [...rows].sort((a, b) =>
|
||
sort === "ytd" ? b.ytd - a.ytd : a.code.localeCompare(b.code)
|
||
);
|
||
}, [rows, sort]);
|
||
|
||
const grandTotal = monthTotals.reduce((s, v) => s + v, 0);
|
||
|
||
return (
|
||
<>
|
||
<div className="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||
<span className="px-2.5 py-1 rounded-md bg-[#EDAE49]/10 text-[#EDAE49] font-medium">
|
||
{rows.filter((r) => r.ytd > 0).length} machines with costs
|
||
</span>
|
||
<span className="hidden md:inline">· sorted by {sort === "ytd" ? "total spend" : "code"}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-white/5">
|
||
<button
|
||
onClick={() => setView("cards")}
|
||
className={`px-3 h-8 text-xs font-medium rounded-md transition flex items-center gap-1.5 ${
|
||
view === "cards"
|
||
? "bg-[#003D5B] text-white dark:bg-[#00798C]"
|
||
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||
}`}
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" /> Cards
|
||
</button>
|
||
<button
|
||
onClick={() => setView("grid")}
|
||
className={`px-3 h-8 text-xs font-medium rounded-md transition flex items-center gap-1.5 ${
|
||
view === "grid"
|
||
? "bg-[#003D5B] text-white dark:bg-[#00798C]"
|
||
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||
}`}
|
||
>
|
||
<LayoutGrid className="w-3.5 h-3.5" /> Grid
|
||
</button>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-xs text-gray-400 dark:text-gray-500 mr-1">Sort:</span>
|
||
{[
|
||
{ id: "ytd", label: "Total" },
|
||
{ id: "code", label: "Code" },
|
||
].map((s) => (
|
||
<button
|
||
key={s.id}
|
||
onClick={() => setSort(s.id as typeof sort)}
|
||
className={`px-2 h-7 text-xs font-medium rounded transition ${
|
||
sort === s.id ? "bg-[#EDAE49] text-white" : "text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/5"
|
||
}`}
|
||
>
|
||
{s.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{view === "cards" ? (
|
||
<CardsView rows={sorted} expanded={expanded} setExpanded={setExpanded} palette={palette} />
|
||
) : (
|
||
<GridView rows={sorted} monthTotals={monthTotals} grandTotal={grandTotal} />
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function CardsView({
|
||
rows, expanded, setExpanded, palette,
|
||
}: {
|
||
rows: RunningRow[];
|
||
expanded: string | null;
|
||
setExpanded: (id: string | null) => void;
|
||
palette: { income: string; profit: string; loss: string };
|
||
}) {
|
||
return (
|
||
<div className="space-y-2">
|
||
{rows.map((r) => {
|
||
const isOpen = expanded === r.id;
|
||
const hasData = r.ytd > 0;
|
||
return (
|
||
<div
|
||
key={r.id}
|
||
className="bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
|
||
>
|
||
<button
|
||
onClick={() => setExpanded(isOpen ? null : r.id)}
|
||
className="w-full flex items-center gap-4 p-4 text-left"
|
||
>
|
||
<span className="font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] font-semibold shrink-0">
|
||
{r.code}
|
||
</span>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 text-sm font-medium text-[#003D5B] dark:text-white">
|
||
<Wrench className="w-3.5 h-3.5 text-[#EDAE49] shrink-0" />
|
||
<span className="truncate">{r.name}</span>
|
||
</div>
|
||
</div>
|
||
<div className="hidden sm:block shrink-0">
|
||
<Sparkline data={r.months} color={palette.income} />
|
||
</div>
|
||
<div className="text-right shrink-0 w-32 hidden md:block">
|
||
<div className="text-[10px] text-gray-400 uppercase tracking-wider">YTD spend</div>
|
||
<div className="text-sm font-semibold tabular-nums text-[#003D5B] dark:text-white">
|
||
{hasData ? ZAR2(r.ytd) : "—"}
|
||
</div>
|
||
</div>
|
||
<div
|
||
className={`shrink-0 px-2.5 py-1.5 rounded-lg text-xs font-semibold ${
|
||
hasData ? "bg-[#EDAE49]/10 text-[#EDAE49] dark:bg-[#EDAE49]/20" : "bg-gray-100 text-gray-400 dark:bg-white/5"
|
||
}`}
|
||
>
|
||
{hasData ? `${r.months.filter((v) => v > 0).length} months` : "—"}
|
||
</div>
|
||
<ChevronRight
|
||
className={`w-4 h-4 text-gray-400 shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
|
||
/>
|
||
</button>
|
||
|
||
{isOpen && (
|
||
<div className="border-t border-gray-100 dark:border-white/5 px-4 py-4 bg-gray-50/50 dark:bg-white/[0.02]">
|
||
{/* Mobile: 3 cols (4 rows × 3 cols = 12 months). md+: 6 cols. lg+: 12 cols. */}
|
||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-12 gap-2 text-[11px]">
|
||
{r.months.map((v, i) => (
|
||
<div key={i} className="text-center">
|
||
<div className="text-gray-400 dark:text-gray-500 text-[10px] mb-1">{MONTH_NAMES[i]}</div>
|
||
<div className={`py-1.5 rounded tabular-nums ${v > 0 ? "bg-[#EDAE49]/5 text-[#EDAE49] dark:bg-[#EDAE49]/15" : "text-gray-300 dark:text-gray-600"}`}>
|
||
{v > 0 ? ZAR2(v) : "—"}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-white/5 text-xs">
|
||
<span className="text-gray-400 uppercase tracking-wider text-[10px]">YTD total</span>
|
||
<span className="ml-2 font-semibold tabular-nums text-[#EDAE49]">{ZAR(r.ytd)}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function GridView({
|
||
rows, monthTotals, grandTotal,
|
||
}: {
|
||
rows: RunningRow[];
|
||
monthTotals: number[];
|
||
grandTotal: number;
|
||
}) {
|
||
return (
|
||
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
|
||
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur z-10">Code</th>
|
||
<th className="text-left py-3 px-3 font-medium">Machine</th>
|
||
{MONTH_NAMES.map((m) => (
|
||
<th key={m} className="text-right py-3 px-2 font-medium min-w-[78px]">{m}</th>
|
||
))}
|
||
<th className="text-right py-3 px-6 font-medium">YTD</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((r) => (
|
||
<tr key={r.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
|
||
<td className="py-2.5 px-6 sticky left-0 bg-white dark:bg-[#131a21]">
|
||
<span className="font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] font-semibold">
|
||
{r.code}
|
||
</span>
|
||
</td>
|
||
<td className="py-2.5 px-3 font-medium text-[#003D5B] dark:text-white whitespace-nowrap">
|
||
<div className="flex items-center gap-2">
|
||
<Wrench className="w-3 h-3 text-[#EDAE49]" />
|
||
{r.name}
|
||
</div>
|
||
</td>
|
||
{r.months.map((v, i) => (
|
||
<td key={i} className={`py-2.5 px-2 text-right tabular-nums ${v > 0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-gray-600"}`}>
|
||
{v > 0 ? ZAR2(v) : "—"}
|
||
</td>
|
||
))}
|
||
<td className={`py-2.5 px-6 text-right tabular-nums font-semibold whitespace-nowrap ${r.ytd > 0 ? "text-[#EDAE49]" : "text-gray-300 dark:text-gray-600"}`}>
|
||
{r.ytd > 0 ? ZAR(r.ytd) : "—"}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
<tr className="bg-gradient-to-r from-[#EDAE49]/10 dark:from-[#EDAE49]/15 to-transparent border-t-2 border-[#EDAE49]/30">
|
||
<td colSpan={2} className="py-3 px-6 font-semibold text-right text-[#003D5B] dark:text-white">Total</td>
|
||
{monthTotals.map((v, i) => (
|
||
<td key={i} className="py-3 px-2 text-right tabular-nums font-semibold text-[#003D5B] dark:text-white">
|
||
{v > 0 ? ZAR(v) : "—"}
|
||
</td>
|
||
))}
|
||
<td className="py-3 px-6 text-right tabular-nums font-bold text-base text-[#EDAE49]">
|
||
{ZAR(grandTotal)}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|