2026-09-12 07:35:35 +00:00
|
|
|
|
"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 (
|
|
|
|
|
|
<>
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
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">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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>
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
2026-09-12 10:05:43 +00:00
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-border">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setView("cards")}
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
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 ${
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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")}
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
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 ${
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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>
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
2026-09-12 10:05:43 +00:00
|
|
|
|
<div className="hidden sm:flex items-center gap-1">
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<span className="text-xs text-gray-400 dark:text-text-soft mr-1">Sort:</span>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{[
|
|
|
|
|
|
{ id: "ytd", label: "Total" },
|
|
|
|
|
|
{ id: "code", label: "Code" },
|
|
|
|
|
|
].map((s) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={s.id}
|
|
|
|
|
|
onClick={() => setSort(s.id as typeof sort)}
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
2026-09-12 10:05:43 +00:00
|
|
|
|
className={`min-h-[44px] px-2 text-xs font-medium rounded transition ${
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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>
|
Round 4: regressions fixed + mobile standard
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
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>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</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}
|
2026-10-02 13:34:19 +00:00
|
|
|
|
className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
|
2026-09-12 07:35:35 +00:00
|
|
|
|
>
|
|
|
|
|
|
<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 && (
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<div className="border-t border-gray-100 dark:border-border px-4 py-4 bg-gray-50/50 dark:bg-white/[0.04]">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{/* 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">
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<div className="text-gray-400 dark:text-text-soft 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-text-soft"}`}>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{v > 0 ? ZAR2(v) : "—"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-border text-xs">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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 (
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
|
<table className="w-full text-sm">
|
|
|
|
|
|
<thead>
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
|
|
|
|
|
|
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur z-10">Code</th>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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) => (
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<tr key={r.id} className="border-b border-gray-50 dark:border-border 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-surface">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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) => (
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<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-text-soft"}`}>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{v > 0 ? ZAR2(v) : "—"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<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-text-soft"}`}>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|