2026-09-12 07:35:35 +00:00
|
|
|
|
"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";
|
2026-09-12 07:35:35 +00:00
|
|
|
|
import { Wrench, LayoutGrid, Sparkles, ChevronRight } from "lucide-react";
|
|
|
|
|
|
import { Sparkline } from "@/components/Sparkline";
|
|
|
|
|
|
import { MONTH_NAMES } from "@/lib/data";
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
import { COST_KINDS, COST_KIND_LABELS } from "@/lib/running-costs";
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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])}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
/** The four cost kinds. These ARE a vehicle's running cost — there is no fifth source —
|
|
|
|
|
|
* so the parts always add up to the vehicle's spend and the vehicles add up to the
|
|
|
|
|
|
* Overview. Showing them is what makes that checkable rather than a claim. */
|
|
|
|
|
|
const KINDS = COST_KINDS;
|
|
|
|
|
|
|
2026-09-12 07:35:35 +00:00
|
|
|
|
export function RunningView({
|
|
|
|
|
|
rows,
|
|
|
|
|
|
palette,
|
|
|
|
|
|
monthTotals,
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
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,
|
2026-10-09 10:35:52 +00:00
|
|
|
|
visibleMonths,
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
monthIndex,
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}: {
|
|
|
|
|
|
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. */
|
2026-09-12 07:35:35 +00:00
|
|
|
|
monthTotals: number[];
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
/** 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[] };
|
2026-10-09 10:35:52 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* The LIVE month, 0-based, decided on the server (lib/income-month.ts). It is the
|
|
|
|
|
|
* month that is still filling up, so its figure grows as repairs and purchases are
|
|
|
|
|
|
* entered against a vehicle.
|
|
|
|
|
|
*/
|
|
|
|
|
|
monthIndex?: number;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}) {
|
|
|
|
|
|
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" ? (
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
<CardsView rows={sorted} expanded={expanded} setExpanded={setExpanded} palette={palette} visibleMonths={visibleMonths} monthIndex={monthIndex} />
|
2026-09-12 07:35:35 +00:00
|
|
|
|
) : (
|
2026-10-09 10:35:52 +00:00
|
|
|
|
<GridView rows={sorted} monthTotals={monthTotals} monthTotalsExcl={monthTotalsExcl} byKind={byKind} visibleMonths={visibleMonths} grandTotal={grandTotal} grandExcl={(monthTotalsExcl || []).reduce((s, v) => s + v, 0)} />
|
2026-09-12 07:35:35 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function CardsView({
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
rows, expanded, setExpanded, palette, visibleMonths, monthIndex,
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}: {
|
|
|
|
|
|
rows: RunningRow[];
|
2026-10-09 10:35:52 +00:00
|
|
|
|
visibleMonths?: number;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
expanded: string | null;
|
|
|
|
|
|
setExpanded: (id: string | null) => void;
|
|
|
|
|
|
palette: { income: string; profit: string; loss: string };
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
monthIndex?: number;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}) {
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
const monthLabel = monthIndex != null && monthIndex >= 0 ? MONTH_NAMES[monthIndex] : null;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{rows.map((r) => {
|
|
|
|
|
|
const isOpen = expanded === r.id;
|
|
|
|
|
|
const hasData = r.ytd > 0;
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
const monthValue = monthIndex != null ? r.months[monthIndex] ?? 0 : 0;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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" />
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
{/* On a phone the name wraps onto its own line rather than being
|
|
|
|
|
|
cut short — the same rule the income cards follow. */}
|
|
|
|
|
|
<span className="sm:truncate">{r.name}</span>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
{/* The sparkline yields its width to the month figure below xl: the
|
|
|
|
|
|
content column is only ~670px wide between md and xl (the sidebar eats
|
|
|
|
|
|
the rest), and the vehicle NAME must not be truncated to pay for a
|
|
|
|
|
|
decorative trend. The same 12 months are on the strip in the expanded
|
|
|
|
|
|
panel, one click away. */}
|
|
|
|
|
|
<div className="hidden xl:block shrink-0">
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<Sparkline data={r.months} color={palette.income} />
|
|
|
|
|
|
</div>
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
{/* The money: the year figure and the live month, side by side — the same
|
|
|
|
|
|
shape the income cards use. */}
|
|
|
|
|
|
<div className="hidden md:flex shrink-0 items-start gap-4">
|
|
|
|
|
|
<div className="text-right w-28">
|
|
|
|
|
|
<div className="text-xs 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>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</div>
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
{monthLabel && (
|
|
|
|
|
|
<div className="text-right w-28">
|
|
|
|
|
|
<div className="text-xs text-gray-400 uppercase tracking-wider whitespace-nowrap">{monthLabel} so far</div>
|
|
|
|
|
|
<div className="text-sm font-semibold tabular-nums text-[#00798C] dark:text-[#EDAE49]">
|
|
|
|
|
|
{monthValue > 0 ? ZAR2(monthValue) : "—"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
|
feat(running): the year to date AND the live month on the running-cost pages
The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
2026-11-04 23:42:23 +00:00
|
|
|
|
{/* Below md the money row above is hidden, so the year and the live month
|
|
|
|
|
|
get their own line — the same shape the income cards use on a phone. */}
|
|
|
|
|
|
<div className="flex md:hidden items-end justify-between gap-3 px-4 pb-2 pt-1 text-xs">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="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>
|
|
|
|
|
|
{monthLabel && (
|
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
|
<div className="text-gray-400 uppercase tracking-wider whitespace-nowrap">{monthLabel} so far</div>
|
|
|
|
|
|
<div className="text-sm font-semibold tabular-nums text-[#00798C] dark:text-[#EDAE49]">
|
|
|
|
|
|
{monthValue > 0 ? ZAR2(monthValue) : "—"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
{/* What the vehicle's spend is MADE of, on the card itself. These four parts
|
|
|
|
|
|
are the whole of it, so they add up to the YTD figure above — and the
|
|
|
|
|
|
vehicles add up to the Overview. A vehicle with no spend shows nothing
|
|
|
|
|
|
here: "nothing recorded" is already on the card. */}
|
|
|
|
|
|
{hasData && (
|
|
|
|
|
|
<div className="px-4 pb-3 -mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
|
|
|
|
|
{KINDS.map((k, index) => (
|
|
|
|
|
|
<span key={k} className="whitespace-nowrap">
|
|
|
|
|
|
<span className={r.byKindYtd[k] > 0 ? "text-gray-400 dark:text-text-soft" : "text-gray-300 dark:text-gray-600"}>{COST_KIND_LABELS[k]}</span>{" "}
|
|
|
|
|
|
<span className={r.byKindYtd[k] > 0 ? "font-medium tabular-nums text-gray-600 dark:text-gray-300" : "tabular-nums text-gray-300 dark:text-gray-600"}>{ZAR2(r.byKindYtd[k])}</span>
|
|
|
|
|
|
{index < KINDS.length - 1 && <span className="ml-3 text-gray-300 dark:text-gray-600">·</span>}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-09-12 07:35:35 +00:00
|
|
|
|
{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. */}
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-12 gap-2 text-xs">
|
2026-10-09 10:35:52 +00:00
|
|
|
|
{r.months.slice(0, visibleMonths ?? 12).map((v, i) => (
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<div key={i} className="text-center">
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
<div className="text-gray-400 dark:text-text-soft text-xs mb-1">{MONTH_NAMES[i]}</div>
|
2026-10-02 13:34:19 +00:00
|
|
|
|
<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>
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
{/* The four kinds, and the total they add up to. This is the check: the
|
|
|
|
|
|
parts ARE the spend, so the parts and the total can never disagree. */}
|
|
|
|
|
|
<div className="mt-4 grid grid-cols-2 gap-x-4 gap-y-3 border-t border-gray-100 pt-3 text-xs dark:border-border sm:grid-cols-5">
|
|
|
|
|
|
{KINDS.map((k) => (
|
|
|
|
|
|
<div key={k}>
|
|
|
|
|
|
<div className="text-gray-400 uppercase tracking-wider text-xs">{COST_KIND_LABELS[k]}</div>
|
|
|
|
|
|
<div className="font-semibold tabular-nums text-[#003D5B] dark:text-white">{ZAR2(r.byKindYtd[k])}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-gray-400 uppercase tracking-wider text-xs">YTD total</div>
|
|
|
|
|
|
<div className="font-semibold tabular-nums text-[#EDAE49]">{ZAR(r.ytd)}</div>
|
|
|
|
|
|
</div>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function GridView({
|
2026-10-09 10:35:52 +00:00
|
|
|
|
rows, monthTotals, monthTotalsExcl, byKind, visibleMonths, grandTotal, grandExcl,
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}: {
|
|
|
|
|
|
rows: RunningRow[];
|
|
|
|
|
|
monthTotals: number[];
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
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[] };
|
2026-10-09 10:35:52 +00:00
|
|
|
|
visibleMonths?: number;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
grandTotal: number;
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
grandExcl: number;
|
2026-09-12 07:35:35 +00:00
|
|
|
|
}) {
|
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]);
|
|
|
|
|
|
|
2026-09-12 07:35:35 +00:00
|
|
|
|
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">
|
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" }}>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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>
|
2026-10-09 10:35:52 +00:00
|
|
|
|
{MONTH_NAMES.slice(0, visibleMonths ?? 12).map((m) => (
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<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>
|
2026-10-09 10:35:52 +00:00
|
|
|
|
{r.months.slice(0, visibleMonths ?? 12).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>
|
|
|
|
|
|
))}
|
feat(running): show what each vehicle's running cost is made of
The four kinds under running cost - repairs & parts, purchases, insurance
and finance - ARE the whole of a vehicle's running cost. That was true of
the arithmetic but not of the screen: a card showed one number with no
visible parts, and the only breakdown was a hover tooltip on the grid's
total row.
- Each card now shows its four parts, so what the vehicle's spend is made
of is visible without expanding it. A vehicle with no spend shows
nothing: "nothing recorded" is already on the card.
- The expanded panel shows the four parts against the vehicle's total.
- The grid's footer shows the fleet's four parts above the fleet total, so
a month column adds up on the screen instead of on trust.
- The page header states the fleet's four parts and the total side by side.
The parts ARE the spend and there is no fifth source, so the parts and the
total can never disagree. Any change that touches a cost kind must keep
that additivity true.
Also: the labels on this page were text-[10px], under the 12px floor the
mobile checklist sets. They are now 12px.
Two rulings recorded in AGENTS.md:
- FUEL IS NOT A PLANT RUNNING COST. The sites are invoiced for the fuel
they use. The only fuel that is a plant cost is the fuel a plant vehicle
takes from the tank for plant work, so a fuel log is never a running-cost
source however many litres it records.
- Only what is under running cost as it stands. The workbook's per-vehicle
fixed cost also carries depreciation/ownership and an operator
allocation; that is capital recovery and overhead, not running cost, so
it stays out - and the vehicles it covers (the excavator, the site and
office containers, the generators, the trailers) correctly show no
running cost on the Overview.
2026-11-04 23:09:45 +00:00
|
|
|
|
{/* The four kinds, then the total they add up to. The fleet's parts are
|
|
|
|
|
|
visible above its total, so "do the costs add up?" is answered on the
|
|
|
|
|
|
screen instead of taken on trust. */}
|
|
|
|
|
|
{byKind && KINDS.map((k) => {
|
|
|
|
|
|
const cells = byKind[k].slice(0, visibleMonths ?? 12);
|
|
|
|
|
|
const kindTotal = cells.reduce((s, v) => s + v, 0);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<tr key={k} className="border-t border-gray-50 dark:border-border">
|
|
|
|
|
|
<td colSpan={2} className="py-2 px-6 text-right text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{COST_KIND_LABELS[k]}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
{cells.map((v, i) => (
|
|
|
|
|
|
<td key={i} className="py-2 px-2 text-right tabular-nums whitespace-nowrap text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{v > 0 ? ZAR(v) : "—"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className="py-2 px-6 text-right tabular-nums whitespace-nowrap text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{kindTotal > 0 ? ZAR(kindTotal) : "—"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
{/* Bottom row: the month's total on BOTH bases, incl ABOVE excl, so a
|
|
|
|
|
|
reader never has to back the VAT out by hand. */}
|
2026-09-12 07:35:35 +00:00
|
|
|
|
<tr className="bg-gradient-to-r from-[#EDAE49]/10 dark:from-[#EDAE49]/15 to-transparent border-t-2 border-[#EDAE49]/30">
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
<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>
|
2026-10-09 10:35:52 +00:00
|
|
|
|
{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"
|
|
|
|
|
|
>
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
Fold purchase orders into the Running Costs Overview, with incl/excl totals
A purchase order buys parts and repairs against a plant item, so its money is
running cost and belongs in the same per-machine, per-month series the Overview
already reads. Without it September showed nothing at all.
Merged at READ time, never copied into monthly_running_costs: that collection is
deleted and rebuilt by every seed, so a purchase written there would vanish on the
next one and have to be re-uploaded. purchase_lines is never deleted, so the money
lives in one place and the Overview adds it up.
- Month comes from the PO date (when it was ordered), not "Required By":
MEGPPO50532 is dated 07/10 but required 29/08 and those must not be confused.
- Only lines allocated to a machine are counted. An unallocated line is real money
not yet on a plant item; it stays on /purchases, reported, rather than being
hidden or added to the fleet total.
- A PO with no readable date is excluded and named, never guessed into January.
- Bottom total row shows incl ABOVE excl per month and for the YTD. Workbook
figures carry no VAT column, so RUNNING_VAT_PCT (15, confirmed by every PO line)
is the single constant to revisit if they are ever restated ex-VAT.
September now reads R 5 842 incl / R 5 080 excl from the two September POs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 10:10:13 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</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>
|
2026-09-12 07:35:35 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|