mega-plant/components/RunningView.tsx

231 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}