244 lines
9.9 KiB
Markdown
244 lines
9.9 KiB
Markdown
|
|
# Mega Plant — Round 4
|
|||
|
|
|
|||
|
|
Two parts: three regressions from round 3, then the mobile standard.
|
|||
|
|
|
|||
|
|
**The desktop layout is signed off. Do not change how any page looks at ≥1024px.**
|
|||
|
|
Everything below is about what happens *below* that width, plus three data bugs.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Part A — Round 3 regressions
|
|||
|
|
|
|||
|
|
## A.1 `verify-cost-reconciliation.mjs` crashes, and reports success anyway
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
TypeError: Failed to parse URL from /api/collections/_superusers/auth-with-password
|
|||
|
|
exit=0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The script never loads `.env.local`, so the PocketBase URL is empty and every request
|
|||
|
|
fails. **It then exits 0.** A script whose entire job is to catch drift currently
|
|||
|
|
cannot run and claims it passed.
|
|||
|
|
|
|||
|
|
- [ ] Load `.env.local` the way `scripts/seed.mjs` does.
|
|||
|
|
- [ ] Wrap the body in try/catch and `process.exit(1)` on any failure.
|
|||
|
|
- [ ] Exit non-zero when the reconciliation total is off, not just when it throws.
|
|||
|
|
- [ ] Run it and paste the real output.
|
|||
|
|
|
|||
|
|
## A.2 Vehicle Costs regressed — the workbook's error was adopted
|
|||
|
|
|
|||
|
|
Round 3 changed Vehicle Costs from **R14,996.31** to **R9,673.87** by dropping
|
|||
|
|
`Santam versekering` (R5,322.44).
|
|||
|
|
|
|||
|
|
That is backwards. Round 3 §B.4 said explicitly: the workbook's row-50 SUM starts one
|
|||
|
|
row too low and omits Santam, so **the app was right and the spreadsheet was wrong**.
|
|||
|
|
The fix was to report the spreadsheet error to the client — not to reproduce it.
|
|||
|
|
|
|||
|
|
- [ ] Restore `Santam versekering` to the `Vehicle Costs` section. Correct total is
|
|||
|
|
**R14,996.31** across 4 rows.
|
|||
|
|
- [ ] Add a comment in the seed and a line in the reconciliation output recording
|
|||
|
|
that the app deliberately differs from the workbook by +R5,322.44 here, and why.
|
|||
|
|
- [ ] Never "fix" app data to match a source error that has been identified as an
|
|||
|
|
error. Report the mismatch instead.
|
|||
|
|
|
|||
|
|
## A.3 The `incl < excl` validation isn't firing
|
|||
|
|
|
|||
|
|
Round 3 §B.3 asked the seed to reject any row where `incl < excl` or VAT falls outside
|
|||
|
|
0–20%. This row is still in PocketBase:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Buildsmart Yearly update excl 14,827.91 vat 2,224.19 incl 1,421.01
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`incl` is less than `excl`, which is arithmetically impossible.
|
|||
|
|
|
|||
|
|
- [ ] Add the validation and make it actually fail the seed.
|
|||
|
|
- [ ] Confirm it trips on this row. Paste the failure output.
|
|||
|
|
- [ ] Do not silently correct the value — it needs the client's confirmation
|
|||
|
|
(correct figure is most likely R17,052.10).
|
|||
|
|
|
|||
|
|
## A.4 Correction to the round-3 target figure
|
|||
|
|
|
|||
|
|
The acceptance number in round 3 (**R689,640.49**) was wrong — I subtracted the
|
|||
|
|
R5,322.44 Santam amount twice. The workbook's grand total already excludes it via the
|
|||
|
|
broken subtotal.
|
|||
|
|
|
|||
|
|
The correct arithmetic:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Workbook grand total (Cost row 98) R1,110,486.87
|
|||
|
|
less Plant Overheads (now on assets) − R415,523.94
|
|||
|
|
= R694,962.93 ← reconciliation target
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Current app total is **R675,162.93**. After restoring Santam (A.2) it becomes
|
|||
|
|
**R680,485.37**. The remaining **R14,477.56** is fully explained:
|
|||
|
|
|
|||
|
|
| Amount | Cause |
|
|||
|
|
| ---: | --- |
|
|||
|
|
| R19,800.00 | Workbook's own Personnel total (row 77 = R412,000) exceeds its two line items (157,200 + 235,000 = 392,200). **Source error — ask the client.** |
|
|||
|
|
| −R5,322.44 | Santam, where the app is deliberately correct and the workbook is not |
|
|||
|
|
|
|||
|
|
- [ ] Encode `R694,962.93` as the target in the reconciliation script, with both
|
|||
|
|
reconciling items listed explicitly so the delta is expected, not mysterious.
|
|||
|
|
|
|||
|
|
## A.5 The "Lines" view draws filled areas, not lines
|
|||
|
|
|
|||
|
|
The toggle works and all three series now render with the validated colours — good.
|
|||
|
|
But the "Lines" view uses `recharts-area-curve` with translucent fills, so the amber
|
|||
|
|
Fixed area paints over the teal Income area.
|
|||
|
|
|
|||
|
|
The client asked for this view specifically because it matches their spreadsheet,
|
|||
|
|
and the spreadsheet draws **lines**.
|
|||
|
|
|
|||
|
|
- [ ] Make the "Lines" view actual lines — stroked, no fill. It is the view whose
|
|||
|
|
whole purpose is matching the workbook.
|
|||
|
|
- [ ] Keep fills in "Cost stack", where stacking is meaningful.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Part B — The mobile standard
|
|||
|
|
|
|||
|
|
The client works from a phone. Desktop is signed off; mobile is not. Below is the
|
|||
|
|
definition of done — **apply it to every page that exists now and every page added
|
|||
|
|
later.**
|
|||
|
|
|
|||
|
|
## B.1 The hard rule: no page scrolls sideways
|
|||
|
|
|
|||
|
|
A page may never be wider than the viewport. Individual *tables* may scroll inside
|
|||
|
|
their own container; the **page** may not.
|
|||
|
|
|
|||
|
|
Measured at 375px right now:
|
|||
|
|
|
|||
|
|
| Page | Page width | Over by | |
|
|||
|
|
| --- | ---: | ---: | --- |
|
|||
|
|
| `/income` | 549px | **+174** | ✗ |
|
|||
|
|
| `/charts` | 406px | +31 | ✗ |
|
|||
|
|
| `/overheads` | 397px | +22 | ✗ |
|
|||
|
|
| `/running` | 389px | +14 | ✗ |
|
|||
|
|
| `/expenses` | 383px | +8 | ✗ |
|
|||
|
|
| `/` | 375px | 0 | ✓ |
|
|||
|
|
| `/plant` | 375px | 0 | ✓ |
|
|||
|
|
|
|||
|
|
Five of seven pages fail. Run this in the console on every page at 375px — it must
|
|||
|
|
return `false` everywhere:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
document.documentElement.scrollWidth > document.documentElement.clientWidth
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The usual cause is a `flex … justify-between` header whose right-hand element can't
|
|||
|
|
shrink. `/income` is the worst: the "YTD Income" gradient card and the
|
|||
|
|
`Sort: Profit | Income` control both hang off the right edge.
|
|||
|
|
|
|||
|
|
- [ ] Fix all five. Headers wrap to a second row on small screens rather than
|
|||
|
|
overflowing.
|
|||
|
|
|
|||
|
|
## B.2 Content must stay readable, not just fit
|
|||
|
|
|
|||
|
|
On `/income` at 375px the machine names are truncated to a **single character** —
|
|||
|
|
"C…", "D…", "I…". The code badge and profit badge hold their width while the name
|
|||
|
|
column collapses to nothing. A card you can't identify is worse than no card.
|
|||
|
|
|
|||
|
|
- [ ] Below `sm`, stack the card contents: name on its own full-width line, badges
|
|||
|
|
on the line beneath. Never truncate the name to less than ~20 characters.
|
|||
|
|
- [ ] Same review for `/running` and the charts cards.
|
|||
|
|
|
|||
|
|
## B.3 Filters must not bury the data
|
|||
|
|
|
|||
|
|
`/plant` has 18 type-filter pills. At 375px they wrap to 13 rows and consume the
|
|||
|
|
entire first screen — you scroll past every pill before reaching a single row.
|
|||
|
|
|
|||
|
|
- [ ] Below `lg`, collapse the type filter into a `<select>` dropdown, or a
|
|||
|
|
horizontally-scrolling single row of pills. Not a 13-row block.
|
|||
|
|
- [ ] Same for the `Sort:` control on `/income` and the view toggle on `/charts`.
|
|||
|
|
|
|||
|
|
## B.4 Wide tables: scroll inside the card, with an anchor column
|
|||
|
|
|
|||
|
|
`/plant` does this right — the table is 917px inside a 375px `overflow-x-auto`
|
|||
|
|
container, so the page itself doesn't move. Keep that pattern. But scrolling six
|
|||
|
|
columns of financials on a phone is still hard work.
|
|||
|
|
|
|||
|
|
- [ ] The first column (Code, or Description on expense pages) stays pinned with
|
|||
|
|
`position: sticky; left: 0` and a background, so you always know which row
|
|||
|
|
you're reading.
|
|||
|
|
- [ ] Add `-webkit-overflow-scrolling: touch` and a visible affordance that the
|
|||
|
|
table scrolls — a fade on the right edge is enough.
|
|||
|
|
- [ ] On expense pages, consider hiding `GL Account` and `Supplier` below `md`.
|
|||
|
|
Description and the three money columns are what matter on a phone.
|
|||
|
|
|
|||
|
|
## B.5 Tap targets and text size
|
|||
|
|
|
|||
|
|
Measured on `/charts` at 375px: **19 interactive elements under 44px tall.**
|
|||
|
|
On `/` : **10 text elements under 11px.**
|
|||
|
|
|
|||
|
|
- [ ] Every `button`, `a`, and `input` is at least 44×44px on touch. Keep the visual
|
|||
|
|
size if you like — add padding to the hit area.
|
|||
|
|
- [ ] No text below 12px on mobile. The `text-[10px]` labels need bumping.
|
|||
|
|
|
|||
|
|
## B.6 Charts on a phone
|
|||
|
|
|
|||
|
|
- [ ] The legend on `/charts` is cut off at the right edge ("Income · Fixed · Ru…").
|
|||
|
|
Wrap it below the title on small screens.
|
|||
|
|
- [ ] Charts need a minimum height on mobile — squashing to 176px with 8 x-labels
|
|||
|
|
makes them unreadable. Go taller on narrow screens, not shorter.
|
|||
|
|
- [ ] The machine card grid must be a single column below `sm`.
|
|||
|
|
|
|||
|
|
## B.7 The PWA angle
|
|||
|
|
|
|||
|
|
The app is meant to install to the home screen, so mobile isn't a nice-to-have —
|
|||
|
|
it's the primary surface.
|
|||
|
|
|
|||
|
|
- [ ] Verify the installed standalone app at 375px, not just Safari/Chrome. Standalone
|
|||
|
|
has no browser chrome, so sticky headers and safe-area insets behave differently.
|
|||
|
|
- [ ] Add `viewport-fit=cover` and use `env(safe-area-inset-*)` padding on the sticky
|
|||
|
|
header and the mobile drawer, so content clears the notch and home indicator.
|
|||
|
|
|
|||
|
|
## B.8 How to verify — do this, and report it
|
|||
|
|
|
|||
|
|
Do not report mobile work as done without this. For **every** route
|
|||
|
|
(`/`, `/plant`, `/income`, `/running`, `/charts`, `/overheads`, `/expenses`, and each
|
|||
|
|
`/expenses/*`):
|
|||
|
|
|
|||
|
|
1. Set the viewport to **375×812**.
|
|||
|
|
2. Run the overflow check from B.1 — record the number.
|
|||
|
|
3. Screenshot the page.
|
|||
|
|
4. Count tap targets under 44px and text under 12px.
|
|||
|
|
5. Repeat at **768×1024** (tablet).
|
|||
|
|
|
|||
|
|
- [ ] Paste a table of route → overflow → tap-target count for all routes.
|
|||
|
|
- [ ] Attach 375px screenshots.
|
|||
|
|
- [ ] Any route that still overflows is not done.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Standing rule for future work
|
|||
|
|
|
|||
|
|
Add this to the project's `AGENTS.md` so it applies to every future change:
|
|||
|
|
|
|||
|
|
> Every page must pass the mobile checklist before it is considered complete:
|
|||
|
|
> no horizontal page scroll at 375px, no text under 12px, no tap target under 44px,
|
|||
|
|
> wide tables scroll inside their own container with a sticky first column, and
|
|||
|
|
> filter controls collapse rather than stacking into many rows. Verify at 375×812
|
|||
|
|
> and 768×1024 and report the measurements — do not assert it from reading the code.
|
|||
|
|
|
|||
|
|
- [ ] Create `AGENTS.md` with that rule if it doesn't exist.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Acceptance criteria
|
|||
|
|
|
|||
|
|
- [ ] `npm run typecheck` exits clean
|
|||
|
|
- [ ] `npm run build` succeeds
|
|||
|
|
- [ ] `verify-cost-reconciliation.mjs` runs, and exits **non-zero** when it fails
|
|||
|
|
- [ ] Vehicle Costs is back to R14,996.31 with Santam included
|
|||
|
|
- [ ] Seed rejects the Buildsmart row; failure output pasted
|
|||
|
|
- [ ] Reconciliation targets R694,962.93 with both deltas itemised
|
|||
|
|
- [ ] "Lines" view draws lines, not filled areas
|
|||
|
|
- [ ] **Zero routes** report horizontal page overflow at 375px
|
|||
|
|
- [ ] No tap target under 44px; no text under 12px
|
|||
|
|
- [ ] Wide tables scroll in-container with a sticky first column
|
|||
|
|
- [ ] Verification table + 375px screenshots pasted for every route
|
|||
|
|
- [ ] **Desktop (≥1024px) is visually unchanged — palette, roundness, gradients,
|
|||
|
|
spacing, dark mode all identical to today**
|