mega-plant/components/RunningView.tsx

322 lines
15 KiB
TypeScript
Raw Permalink Normal View History

"use client";
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
import { useState, useMemo, useRef, useEffect } from "react";
import { ScrollFade } from "@/components/ScrollFade";
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";
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
/** Month total split, for the cell tooltip. Rounded — raw float sums read as
* "41944.159999999996", which looks like a bug even though it is arithmetic. */
function kindTitle(byKind: { repairs: number[]; purchases: number[]; insurance: number[]; finance: number[] }, i: number) {
const r2 = (n: number) => n.toFixed(2).replace(/\.00$/, "");
return (
`Repairs R${r2(byKind.repairs[i])} · Purchases R${r2(byKind.purchases[i])}` +
` · Insurance R${r2(byKind.insurance[i])} · Finance R${r2(byKind.finance[i])}`
);
}
export function RunningView({
rows,
palette,
monthTotals,
monthTotalsExcl,
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
byKind,
visibleMonths,
}: {
rows: RunningRow[];
palette: { income: string; profit: string; loss: string };
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
/** Per month, VAT-INCLUSIVE, all cost kinds combined. */
monthTotals: number[];
/** The same months net of VAT, shown UNDER each incl figure. */
monthTotalsExcl?: number[];
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
/** Per-month split behind the total, offered on hover rather than as columns. */
byKind?: { repairs: number[]; purchases: number[]; insurance: number[]; finance: number[] };
/**
* How many months of the year to RENDER. A month ahead cannot be calculated, only
* guessed at, so the columns for it are not shown at all rather than shown empty
* (client ruling, 2026-10-09).
*/
visibleMonths?: 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-border">
<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-text-soft 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} visibleMonths={visibleMonths} />
) : (
<GridView rows={sorted} monthTotals={monthTotals} monthTotalsExcl={monthTotalsExcl} byKind={byKind} visibleMonths={visibleMonths} grandTotal={grandTotal} grandExcl={(monthTotalsExcl || []).reduce((s, v) => s + v, 0)} />
)}
</>
);
}
function CardsView({
rows, expanded, setExpanded, palette, visibleMonths,
}: {
rows: RunningRow[];
visibleMonths?: number;
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-surface border border-gray-100 dark:border-border 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-border px-4 py-4 bg-gray-50/50 dark:bg-white/[0.04]">
{/* 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.slice(0, visibleMonths ?? 12).map((v, i) => (
<div key={i} className="text-center">
<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"}`}>
{v > 0 ? ZAR2(v) : "—"}
</div>
</div>
))}
</div>
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-border 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, monthTotalsExcl, byKind, visibleMonths, grandTotal, grandExcl,
}: {
rows: RunningRow[];
monthTotals: number[];
monthTotalsExcl?: number[];
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
byKind?: { repairs: number[]; purchases: number[]; insurance: number[]; finance: number[] };
visibleMonths?: number;
grandTotal: number;
grandExcl: number;
}) {
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
// Shown only when the table actually overflows — a permanent fade on a table that
// cannot scroll tells the reader to drag a page with nowhere to go.
const scroller = useRef<HTMLDivElement>(null);
const [overflows, setOverflows] = useState(false);
useEffect(() => {
const el = scroller.current;
if (!el) return;
const check = () => setOverflows(el.scrollWidth > el.clientWidth + 1);
check();
const ro = new ResizeObserver(check);
ro.observe(el);
return () => ro.disconnect();
}, [rows.length]);
return (
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
{/* `relative` + the conditional fade give the right-edge affordance a wide table
needs on a phone — but only when there is actually something to scroll to. */}
<div className="relative">
<div ref={scroller} className="overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<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-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>
<th className="text-left py-3 px-3 font-medium">Machine</th>
{MONTH_NAMES.slice(0, visibleMonths ?? 12).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-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">
<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.slice(0, visibleMonths ?? 12).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-text-soft"}`}>
{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-text-soft"}`}>
{r.ytd > 0 ? ZAR(r.ytd) : "—"}
</td>
</tr>
))}
{/* Bottom row: the month's total on BOTH bases, incl ABOVE excl, so a
reader never has to back the VAT out by hand. */}
<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 align-bottom text-[#003D5B] dark:text-white">
Total
<span className="block text-[12px] font-normal text-gray-400 dark:text-text-soft">
incl / excl
</span>
</td>
{monthTotals.slice(0, visibleMonths ?? 12).map((v, i) => (
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
<td
key={i}
title={byKind ? kindTitle(byKind, i) : undefined}
className="py-3 px-2 text-right tabular-nums align-bottom"
>
{v > 0 ? (
<>
<span className="block font-semibold text-[#003D5B] dark:text-white">{ZAR(v)}</span>
{monthTotalsExcl && (
<span className="block text-[12px] font-normal text-gray-400 dark:text-text-soft">
{ZAR(monthTotalsExcl[i] ?? 0)}
</span>
)}
</>
) : (
<span className="text-[#003D5B] dark:text-white">—</span>
)}
</td>
))}
<td className="py-3 px-6 text-right tabular-nums align-bottom">
<span className="block font-bold text-base text-[#EDAE49]">{ZAR(grandTotal)}</span>
{grandExcl > 0 && (
<span className="block text-[12px] font-normal text-gray-400 dark:text-text-soft">
{ZAR(grandExcl)}
</span>
)}
</td>
</tr>
</tbody>
</table>
</div>
Running cost = everything linked to a vehicle: repairs, purchases, insurance, finance A machine with no repairs and no purchase order still carries an insurance premium and, while it is financed, a monthly instalment. So a blank row was never "this vehicle costs nothing" — it was "nothing recorded", which is a gap rather than a figure. The Overview now totals all four kinds per machine per month. Everything is read from POCKETBASE, the registers the client maintains in the app. The original spreadsheet only bootstrapped the page (monthly_running_costs, Jan-Aug); it is not a source here. September having no workbook data is expected and correct — the figures there now come from the app. Two finance sources existed and OVERLAPPED. The cost_lines instalment rows are the old workbook's subset (5 machines, R115 576,14/mo); financing_terms is the client's current sheet (18 agreements, R428 585,04/mo). Using both double-counted MG02, RV04, TLB06 and TLB07, so financing_terms wins and the stale rows are ignored. TB01 has no financing_terms row, so its old instalment no longer counts — consistent with it being absent from the current sheet. Insurance has one source and no such problem. Insurance is resolved from the RATE PERIODS rather than the derived cost_lines, so a premium that moves mid-year is charged at the price that applied that month. Also: settled agreements contribute nothing (a paid-off machine is not a running cost), purchase lines are filed under the PO date, and the bottom row keeps showing incl above excl. The page carries one line saying this is a re-cut of costs already inside fixed expenses — adding it to them would count the same premium twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:28:29 +00:00
{overflows && <ScrollFade />}
</div>
</div>
);
}