Commit graph

7 commits

Author SHA1 Message Date
David
4a76b08617 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 12:28:29 +02:00
David
aac440cfef 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 12:10:13 +02:00
David
f85b9378a2 Add Running Costs sub-pages: insurance, financing, purchases, oil, operators, mechanics
Splits the Running Costs group into six purpose-built pages, each with its own
collection and its own source data, plus a coordinate parser for purchase orders.

Insurance (R503 330/yr, 20 vehicles)
  A premium can move every month or three, so it is stored as a RATE PERIOD in
  insurance_rates and each month's cost is resolved against the rate in force
  that month. cost_lines insurance rows are derived from those periods. An absent
  rate is null, never 0 — nobody has entered the date, which is not the same as
  being insured for free.

Financing (20 agreements, R428 585/mo)
  Instalment from the sheet; start date, paid-off date and the settled toggle are
  human-entered and preserved across re-seeds. Months-left and outstanding are
  null until a date is entered, never R0 on a live debt. The two paid-off
  agreements (WB02, DT01) show as settled and are excluded from the run-rate.

Purchases + Oil Stock
  Uploads PO PDFs the way /income/invoices does. The parser reads columns by
  x-position, because plain text extraction reorders them, and refuses any PO
  whose lines do not sum to the sub-total. A cost code either names a machine
  ("SS-02" -> SS02) or a stock store ("MPLANT, ENGOIL") — never both, never
  guessed. Oil POs move litres into oil_stock_movements instead of onto a machine;
  stock is the sum of movements so the first usage is a subtraction from history.

Operators (19) and Mechanics (2)
  Both are people with an employee number, separately reconcilable by the seed so
  someone added in the app survives. Mechanics get a badge QR; a mechanic with no
  employee number cannot be printed, because the code is what the QR encodes.

Cross-cutting
  - The seed no longer deletes operators, mechanics or oil items: people are added
    in the app, and those rows exist in no file.
  - Re-pointing allocations that PocketBase clears because `assets` is rebuilt
    with new ids each seed (operators, purchase lines).
  - One QR encoder for screen and PDF, so a printed code cannot drift from the one
    on screen. pdf-lib writes real vector PDFs; pdfjs-dist only reads them.
  - Every figure is null-safe: unknown is never rendered as zero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 01:06:27 +02:00
David
def147bc73 Income: hide stood-down machines, with an active/inactive filter
The field is `assets.is_active`. All 7 inactive machines have no income at all
— the three generators, two porta camps, AEB Plant Versekering and the Diesel
Truck — so on the income list they were a flat row of zeros and nothing else,
reading as "idle" when they are actually out of the fleet.

They are now hidden by default. The rows are not dropped from the data: the
filter lives in the view, so Active / All / Inactive is a client-side toggle
with no refetch, and each shows its count so a filter that silently hides rows
is obvious. A machine hidden by the filter also collapses if it was expanded.

The YTD income and profit tiles moved into the view so they sum only what is
visible — otherwise the headline would report the whole fleet while the list
below it showed a subset.

/charts is unchanged: it passes no filter and still sees everything.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-03 18:40:32 +02:00
David
baf0bddde6 Invoices: line-item extraction, booking to plant items, VAT-inclusive income
Adds an Invoices sub-menu under /income with a summary table (one row per
invoice, click to expand the full detail) and the data model behind it.

Data:
- invoices + invoice_lines PocketBase collections, created idempotently by
  scripts/setup-invoice-collections.mjs
- line items transcribed from PHI 166/167/171 with per-invoice arithmetic
  validation in the seed; a bad invoice is refused without blocking the rest
- invoice-*.json in data/source are auto-imported, so adding an invoice is
  one file

Booking:
- manual book-in per line (Booked to), grouped by category
- POST /api/invoice-lines/:id/book moves the income row rather than adding a
  second one, so a booking can be changed freely
- scripts/book-invoice.mjs is idempotent: rows are stamped with
  monthly_income.invoice_line and replaced on re-run
- booked amount follows lib/invoice-booking-policy.json (bookInclusiveVat),
  read by both the app and the CLI so they cannot disagree

Money:
- the R24,205.13 column on these invoices is PLUS VAT, not escalation. The
  seed now rejects VAT that disagrees with the stated rate.
- booked amounts are VAT-inclusive per the client ruling of 2026-10-01; the
  VAT stays visible rather than hidden.

Uploads:
- drag-and-drop PDF attached to invoices.source_pdf
- GET /api/invoices/:id/document proxies it — the collection is rule-locked,
  so a direct PocketBase /api/files link 404s in a browser

Fixes a month-aggregation bug this introduced: buildIncomeRows assigned into
months[i], so a machine with two income rows in one month (one per invoice,
which is normal multi-site use) showed only the last. SS01 September read
R20,700 instead of R35,305 while its own YTD included both. Months now
accumulate, per-month profit is computed once after accumulation, and
lib/rows.test.ts pins it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 15:59:30 +02:00
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
Mega Plant
acf9d52e5f Mega Plant dashboard — fix & harden (Parts 0-5) 2026-09-12 09:35:48 +02:00