diff --git a/app/income/page.tsx b/app/income/page.tsx index 6dbd564..453c7a9 100644 --- a/app/income/page.tsx +++ b/app/income/page.tsx @@ -25,8 +25,13 @@ export default async function IncomePage() { const totalProfit = rows.reduce((s, m) => s + m.ytdProfit, 0); return ( -
-
+
+ {/* Stacks until lg, not sm. As a side-by-side row the title block took 371px + of the 704px available at 768px, leaving the tile grid 317px — two 152px + columns, too narrow for text-2xl money, so the figures spilled over their + own tiles. At 375px the same row could not even fit, and the page scrolled + sideways by 158px. The tiles may only go 2-up once the title is above them. */} +
@@ -36,7 +41,11 @@ export default async function IncomePage() {

Project income per machine per month · 2026

-
+ {/* grid, not flex-wrap: a wrapping flex row sizes tiles by CONTENT, which + is the ragged-left-edge trap AGENTS.md records for /income/invoices. + Full-width stacked on mobile, two-up from sm — same shape the dashboard + cards already use there. */} +
YTD Income
{ZAR(totalIncome)}
diff --git a/app/sites/page.tsx b/app/sites/page.tsx index 129d6ff..e7a0d17 100644 --- a/app/sites/page.tsx +++ b/app/sites/page.tsx @@ -28,7 +28,10 @@ export default async function SitesPage() { return (
-
+ {/* Stacks until lg, not sm: at 768px a side-by-side row leaves the tile grid + only 317px (the title block takes 371px of 704px), so two columns of 152px + are too narrow for text-2xl money and the figures spill over their tiles. */} +
@@ -41,7 +44,11 @@ export default async function SitesPage() { {invoices.length === 1 ? "" : "s"} · {year}

-
+ {/* grid, not `flex flex-wrap items-stretch`: flex-wrap sizes tiles by + CONTENT, so these two hugged their text — 197px inside a 343px row — + leaving a ragged left-aligned pair with dead space beside each. Grid + items stretch by default, which is what items-stretch was doing here. */} +
Billed (incl VAT)
diff --git a/components/IncomeView.tsx b/components/IncomeView.tsx index 44e2aa8..9f9a9a7 100644 --- a/components/IncomeView.tsx +++ b/components/IncomeView.tsx @@ -183,8 +183,13 @@ function CardsView({
{/* Row 2 (mobile only): sparkline + YTD + P/L pill */}
- -
+ {/* 72/28, not the 120/36 default: Sparkline's root is a fixed-width + shrink-0 box, and the pill beside it is shrink-0 too, so with + nothing allowed to give the pill was pushed ~32px outside the + card and sat on the viewport edge. /sites already passes 72 here + for exactly this reason. */} + +
YTD income
{ZAR2(r.income)}
diff --git a/components/InvoiceList.tsx b/components/InvoiceList.tsx index 2779324..f4a209f 100644 --- a/components/InvoiceList.tsx +++ b/components/InvoiceList.tsx @@ -65,27 +65,38 @@ export function InvoiceList({ return ( <> - {/* A grid, not flex-wrap. The middle tile is far wider than the other two - because of its ex-VAT/VAT sub-line, so flex sized them by CONTENT and pushed - the third one onto a second row the moment the viewport dipped under ~644px — - which is squarely tablet territory. Equal columns keep all three on one row - from 640px up, and below that they stack as one clean column. */} -
+ {/* TWO tiles, not three, and a grid rather than flex-wrap. Both reasons are + the same lesson: "Booked" (summed over booked lines) and "Billed" (summed + over every invoice header) are the SAME figure whenever every line is + booked — which is the goal state — so two tiles showing one number read as + a duplicate rather than as a reconciliation. The reading that carries + information is "booked OF billed", so the single tile now says that, and + the two figures separate on their own the moment a line is left unbooked. + + Equal columns, not content sizing: this tile is far wider than the invoice + count beside it because of the "of … billed" line and the VAT breakdown, and + flex would size by content and shove the second tile onto its own row well + above the 640px where a tablet has plenty of room. */} +
- {/* What actually got booked against the machines, per the shared policy. */} -
-
{BOOKED_LONG_LABEL}
-
+ {/* What actually got booked against the machines, per the shared policy, + set against what the debtors have actually been invoiced. */} +
+
{BOOKED_LONG_LABEL}
+
{ZAR2(bookedAmount(bookedExVat, bookedVat))} + + {" "}of {ZAR2(totals.incl)} billed +
-
+ {/* VAT stays visible — the ex-VAT/incl-VAT split is the one figure a reader + cannot reconstruct from the headline above. */} +
{BOOK_INCLUSIVE_VAT ? `${ZAR2(bookedExVat)} ex VAT + ${ZAR2(bookedVat)} VAT` : `${ZAR2(bookedVat)} VAT not counted as income`}
- {/* What the debtors are actually invoiced. */} -