mega-plant/components/RunningView.tsx

246 lines
11 KiB
TypeScript
Raw Normal View History

"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 (
<>
2026-09-12 10:05:43 +00:00
<div className="flex flex-col sm:flex-row sm:flex-wrap sm:items-center sm:justify-between gap-3 mb-5">
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 flex-wrap">
<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>
2026-09-12 10:05:43 +00:00
<div className="flex items-center gap-2 flex-wrap">
<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")}
2026-09-12 10:05:43 +00:00
aria-label="Cards view"
className={`min-h-[44px] px-3 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")}
2026-09-12 10:05:43 +00:00
aria-label="Grid view"
className={`min-h-[44px] px-3 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>
2026-09-12 10:05:43 +00:00
<div className="hidden sm: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)}
2026-09-12 10:05:43 +00:00
className={`min-h-[44px] px-2 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>
2026-09-12 10:05:43 +00:00
<div className="sm:hidden">
<label className="sr-only" htmlFor="running-sort">Sort by</label>
<select
id="running-sort"
value={sort}
onChange={(e) => setSort(e.target.value as typeof sort)}
className="min-h-[44px] px-3 text-xs rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-white/5"
>
<option value="ytd">Sort by Total</option>
<option value="code">Sort by Code</option>
</select>
</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>
);
}