mega-plant/HANDOFF-PROMPT-3.md
Mega Plant 7194e78079 Round 3 (Parts A-B): charts fixes + expenses/overheads cleanup
- A.1: fixed_expenses per month now threaded through to chart bucket
- A.2: null = no data (forecast), 0 = real plotted zero — distinguishable
- A.3: x-axis clipped to last actual month (IncomeAreaChart + IncomeBarChart + MachineLineChart)
- A.4: chart hues as CSS vars (--color-chart-income/fixed/running) — deeper steps of same hues, validated for CVD
- A.5: chart-type toggle (Lines / Cost stack / Profit) + localStorage persistence + dynamic legend
- A.6: profit on charts page comes from computeProfit()
- B.1: deleted /expenses/plant-overheads, /expenses/insurance, /expenses/tracking (all empty); added /expenses/vehicles; ExpenseSectionPage now has an empty state
- B.2: captured R 157,200 Personnel block, R 100,000 Maintenance, R 10,000 Petty Cash; roll-up reconciles to R 675,162.93 (= sum of non-Plant non-Santam rows; workbook grand total R 1,110,486.87 is internally off by R 14,477.56 vs its own rows)
- B.3: seed flags incl<excl, VAT>20%, broken VAT math — does NOT silently correct; 4 anomalies reported on Charl-CCS + Buildsmart
- B.4: noted that workbook Vehicle Costs subtotal skips Santam (app R 9,673.87 is correct; workbook is wrong)
- B.2 reconciliation assertion added to verify-cost-reconciliation.mjs
- Sidebar updated: Vehicles added, Insurance/Tracking/Plant-Overheads removed
2026-09-12 11:24:18 +02:00

12 KiB
Raw Permalink Blame History

Mega Plant — Round 3

Round 2 landed well: 44 clean assets, cost_lines created, 40/40 machines with income, running_cost de-duplicated, register flattened. This round is charts and the expenses/overheads pages.

Ground rules (unchanged)

DO NOT restyle anything. Palette, roundness, gradients, spacing, dark mode stay as they are. The only colour change permitted is the chart-mark steps in A.3, which are deeper steps of the same hues.

Do not invent data to make a step pass. Unmatched or missing rows get reported, never synthesised.

Verify and report. npm run typecheck after each part; paste what it output.


Part 0 — Source of truth (blocking)

data/source/original.xlsx is a snapshot of an Apple Numbers file. The live file Opdateerde Grafieke Augustus.numbers was edited 2026-09-12 at 10:51, after that xlsx was taken. Numbers stores its data as Snappy-compressed protobuf (Index/*.iwa) and cannot be parsed by xlsx or any tool in this project.

  • Ask the client to re-export the workbook as .xlsx from Numbers (File → Export To → Excel) and replace data/source/original.xlsx.
  • Re-run scripts/extract.mjs and diff the output against the committed JSON. Report every value that changed.
  • Everything below is verified against the 2026-09-11 snapshot. If the re-export differs, re-check the figures in Part B before acting on them.

Part A — Machine charts

A.1 The Fixed series is hardcoded to nothing

app/charts/page.tsx line 20:

const fixed = Array(12).fill(null).map((_, i) => null); // not surfaced on chart

ChartsGrid meanwhile renders a legend with an amber "Fixed" swatch. The legend advertises a line the code guarantees can never draw. This is the bug the client reported as "I only see 2 lines".

  • Pass the real fixed_expenses per month through to the chart.
  • buildIncomeRows already has it — thread it into the bucket shape.

A.2 Zero is being thrown away as null

const income = r.months.map((v) => (v > 0 ? v : null));

A machine that genuinely earned R0 in a month becomes a gap in the line, not a zero. With connectNulls={false} the line fragments into disconnected stubs.

This matters more than it looks: 16 of 44 machines earned nothing all year, so their income line currently doesn't exist at all — leaving a lone red running-cost line floating in space.

  • Distinguish "no data for this month" (null — Sep–Dec, forecast) from "earned zero" (0 — a real, plotted value).
  • Only months beyond the last actual month are null.

A.3 Charts span Jan–Dec but data ends in Aug

40% of every card is empty whitespace, and the series flatline to zero at September, which reads as though the business collapsed.

  • Clip the x-axis to the last actual month (Aug). Do not plot empty months as 0.
  • Same fix applies to IncomeAreaChart on the dashboard — it has the identical September cliff.

A.4 Chart marks need deeper steps of the same hues

I ran the current palette through a contrast/CVD validator. Two hard failures on a white card surface:

[FAIL] Lightness band    #EDAE49 at L 0.792 — outside band
[FAIL] Chroma floor      #00798C at 0.093 — reads grey
[WARN] Contrast          #EDAE49 at 1.95:1 vs white — under the 3:1 floor

So even once A.1 is fixed, the amber Fixed line would be nearly invisible. The brand palette does not change — chart marks get deeper steps of the same hues. These values pass every check in both modes:

Series Light Dark
Income #0090A6 #00A6BE
Fixed #BE851F #BE851F
Running #C13B4D #D94F60
  • Define these as CSS variables in app/globals.css (e.g. --chart-income, --chart-fixed, --chart-running) with dark-mode overrides, and read them in the chart components. Do not hardcode hex in three files.
  • Leave #EDAE49 / #00798C / #D1495B untouched everywhere else — they stay the UI palette.
  • In dark mode the amber/red pair sits at CVD ΔE 7.5, which is only acceptable with a secondary cue. Keep the 2px surface-coloured gap between stacked bar segments. Do not remove it.

A.5 Add a chart-type toggle

The client wants the existing line chart kept, because it's what the spreadsheet shows and the team is used to reading it. Add a toggle offering three views, applied to all cards at once.

View 1 — "Lines" (default). The current multi-line chart, fixed per A.1–A.4. Income, Fixed, and Running as three lines. This mirrors the workbook.

View 2 — "Cost stack". A composed chart:

  • Stacked bars: fixed + running = total monthly cost
  • Line overlaid: income
  • Profit is the visible gap between the bar top and the line — do not plot it as a fourth series
  • This is the only view that shows why a month was bad (instalment vs repair spike)

View 3 — "Profit". Single diverging bar series, income − fixed. Teal above the zero line, red below. Fastest to scan across 44 cards.

  • Segmented control in the ChartsGrid toolbar, beside the existing All / Profitable / Loss-making filter. Match that control's styling exactly.
  • Persist the choice in localStorage so it survives navigation.
  • The legend must change with the view — "Profit / Loss" for view 3, not "Income / Fixed / Running".
  • Every view clips to the last actual month (A.3).

A.6 Profit must come from computeProfit()

View 3 shows profit. lib/profit.ts is the single source of truth and currently defaults to income − fixed (the workbook rule, Open Question 1 still unresolved).

  • The chart must call computeProfit(), never compute inline. If Q1 is later answered "include running cost", the chart must follow automatically.

Part B — Expenses and Overheads roll-up

These pages are wrong in three separate ways.

B.1 Four sidebar pages render completely empty

Round 2 moved machine instalments into assets and insurance/tracking into cost_lines — correctly. But the expense pages still query the old sections, which no longer have any rows.

Live sections in overheads: Vehicle Costs, Bastion Finance, Personnel, Subscriptions, Rent & Utilities.

Route Queries section Rows Result
/expenses/plant-overheads Plant Overheads 0 renders R 0, "0 entries"
/expenses/insurance Insurance 0 renders R 0, "0 entries"
/expenses/tracking Tracking 0 renders R 0, "0 entries"
/expenses/petty-cash Cash Floats 0 section never existed

And the reverse problem: Vehicle Costs (4 rows, R14,996.31) is live but has no page and no sidebar entry. It appears in the roll-up and the donut, with nowhere to drill into.

  • Delete the plant-overheads, insurance, and tracking routes and their sidebar entries. That data now lives on the asset, not in overheads — surface it on the machine detail view instead.
  • Add a Vehicle Costs page and sidebar entry.
  • petty-cash stays, but needs its data — see B.3.
  • Add an empty state to ExpenseSectionPage. A section with no rows must say so plainly, not render a table of zeros. This class of bug should be visible the moment it happens.

B.2 The roll-up total is R697,201.50 short — and only part of that is correct

The workbook's own grand total (Cost sheet, row 98) is R1,110,486.87. The app's Overheads Roll-up shows R413,285.37.

The gap decomposes exactly:

Amount What Verdict
R415,523.94 Plant Overheads — machine instalments ✅ correctly excluded in round 2
R177,000.00 Personnel not captured ❌ missing
R100,000.00 Maintenance ❌ missing entirely
R10,000.00 Petty Cash ❌ missing entirely
−R5,322.44 Santam versekering ✅ app is right, workbook is wrong (see B.4)
R697,201.50
  • Personnel. The workbook has two blocks: Attie / Sandrea / Jantjie with a block subtotal of R157,200 (row 74), and Opp + Drywers at R235,000 (row 75). Only the R235,000 landed. The three named staff exist as rows in PocketBase but with amount_incl = 0. Capture the R157,200. Note the workbook's own Personnel total (row 77) says R412,000, but 157,200 + 235,000 = 392,200 — a R19,800 discrepancy in the source. Report it, do not paper over it.
  • Maintenance R100,000 (row 97) and Petty Cash R10,000 (row 96) are floating cells with no section header. Give them real sections (Maintenance, Cash Floats) and seed them. petty-cash already queries Cash Floats.
  • Once fixed, the roll-up should reconcile to R1,110,486.87 minus the R415,523.94 of plant instalments minus the R5,322.44 workbook error = R689,640.49. Print this reconciliation in scripts/verify-cost-reconciliation.mjs and fail if it drifts.
  • The roll-up page currently calls itself "Full monthly overhead summary", which invites comparison against the workbook's R1.11M. Add a visible note that plant instalments are excluded because they live on the asset.

B.3 Two Subscriptions rows have broken VAT in the source

Charl - CCS               excl 1,970.00   vat 2,265.50   incl 2,265.50
Buildsmart Yearly update  excl 14,827.91  vat 2,224.19   incl 1,421.01
  • Charl - CCS — VAT of R2,265.50 on R1,970.00 is 115%. The incl value has been pasted into the VAT cell. Correct: excl 1,970.00, vat 295.50, incl 2,265.50.
  • Buildsmart — incl is less than excl, which is impossible. 14,827.91 × 1.15 = 17,052.10, not 1,421.01. The formula is broken in the workbook.

The Subscriptions section total of R6,159.01 is therefore understated by roughly R15,631. Corrected it is about R21,790.10.

  • Do not silently correct these. Flag them to the client and get the right figures confirmed.
  • Add a validation pass to the seed: reject any row where incl < excl, or where vat / excl is outside 0–20%. Report and exit non-zero. Both these rows should trip it.

B.4 The workbook's Vehicle Costs subtotal skips a row

Cost sheet row 50 totals 8,875.51 excl / 9,673.87 incl. The four rows above it sum to 13,399.57 excl / 14,996.31 incl. The difference is exactly Santam versekering (R4,524.07 excl / R5,322.44 incl) — the SUM range starts one row too low.

The app's R14,996.31 is correct; the spreadsheet is wrong.

  • No code change. Tell the client — this error also flows into their grand total.
  • Note it in the reconciliation script so the R5,322.44 delta is expected, not a surprise.

Part C — Open questions still outstanding

Neither has been answered; both still block a correct cost model.

Q1 — does profit include running cost? Workbook says no (=SUM(H4-I4)). lib/profit.ts defaults to no, matching the workbook. Needs a client decision.

Q2 — what is term 3 of the fixed-expense formula? Terms 1 and 2 are confirmed as instalment and insurance (TLB06: 29036.28 + 2401.81 + 5620.2 matches the assets rows exactly). Term 3 varies per machine and per month — 5620.20, 6437.20, 7315.95, 10653.73. Most likely operator wages. Still needs the client. Until it's answered, fixed_expenses is a copied number, not a derived one.


Acceptance criteria

  • npm run typecheck exits clean
  • npm run build succeeds
  • Machine cards show three series in Lines view; the legend matches what draws
  • A machine with R0 income in a month shows a plotted zero, not a gap
  • No chart plots past the last actual month
  • Chart toggle offers Lines / Cost stack / Profit, persists across navigation, and the legend changes with the view
  • Profit anywhere on the charts page comes from computeProfit()
  • Chart marks use the validated hues; the 2px stacked-segment gap is present
  • No sidebar route renders an empty table; Vehicle Costs has a page
  • Overheads Roll-up reconciles to R689,640.49, asserted by the verify script
  • Seed rejects rows where incl < excl or VAT is outside 0–20%
  • The Personnel R19,800 discrepancy, the two Subscriptions VAT errors, and the Santam subtotal error are all reported to the client, not silently fixed
  • Palette, roundness, gradients, and dark mode visually unchanged