Two changes to the income overview, sharing one rule for what "the month"
means (lib/income-month.ts).
POSTED INCOME PER SITE
/income gains Vehicles / Sites views, and /sites defaults to the SAME
posted-income view with a separate Internal billed invoices view.
buildSiteIncomeRows (lib/site-income.ts) is the shared calculation: actual
monthly_income supplies the amount, year and month, and the linked invoice
supplies its historical site. It never uses assets.site, fuel-log
locations or today's map assignment.
- Unposted invoice lines are billing, not posted income.
- Historical workbook income with no site evidence stays in Unallocated
historical income; external income, whose site field names the SUPPLIER,
stays in Unallocated external income.
- Missing source references stay visible as unallocated invoice income.
No group is dropped to make a site list look cleaner and no site is
guessed to force a total.
- No site profit is calculated without an explicit cost-allocation rule.
- Invoice writes use revalidateInvoicePages, which invalidates both
registers, Income, Sites/details, Charts and Dashboard.
THE LIVE MONTH
The tiles show the year-to-date income and profit AND the current month,
and every vehicle card shows the current month income beside its year
figure.
- The month is the live CALENDAR month and it is shown even at R0. Income
for a month is usually entered near the end of it, so the current month
is a figure that starts at R0 and grows.
- The month index is computed on the SERVER and passed down as a number.
The browser never works it out for itself: two clocks disagreeing would
show two different months on the same page.
- A month profit is a dash until that month costs are posted. September
and October carry invoice income with no posted fixed costs (their costs
exist only as forecast rows), so a profit there would be income minus
zero - a 100% margin that does not tie back to the year-to-date profit.
It becomes a number by itself the moment the workbook posts that month.
- The 12-month strip shows the live month as R0 rather than a dash, so a
card cannot contradict itself. Months after it stay a dash.
- The sparkline on a collapsed card is hidden below xl: the content column
is only ~700px wide between md and xl, and the vehicle name must not be
truncated to pay for a decorative trend.
Renames the existing /income/invoices page to "Internal Invoices" and adds a
sibling /income/external-invoices page, splitting ONE `invoices` collection by
the debtor printed on the invoice: a Mega group company is internal, anything
else is external.
Never two collections. An invoice must not be able to exist on neither page, and
the halves must stay reconcilable against /income. Both routes render through the
single components/InvoiceRegisterPage.tsx so they cannot drift apart.
lib/invoice-party-policy.json holds the group entity list and lib/invoice-party.ts
does the matching. Matching is on debtor name OR debtor_no, never name alone:
all three external invoices (PHI 178/186/192) carry an empty debtor name and only
a third-party number, so a name-only match would misfile every one of them. An
invoice naming neither is never internal - it lands on External and is reported
as unclassified rather than silently filed.
The split is 16 internal / 3 external across the 19 invoices in the register.
Also fixes three things the split would otherwise break:
- Booking a line, importing a PDF and retrying an intake can all land on either
side depending on the debtor, so the five write paths only revalidated
/income/invoices and would have left the other page stale. They now call
revalidateInvoicePages() from lib/invoice-revalidate.ts. Route paths live in
lib/invoice-routes.ts, which imports no next/* so a client component may import
it; the revalidate helper imports next/cache and is server-only.
- A site's "Go and assign them" link hard-coded the internal path, so a site whose
outstanding lines sit on external invoices would dead-end. SiteSummary now
carries the outstanding lines split by party.
- verify-auth-routes.mjs now covers the new route.
The two routes are siblings by design: isGroupActive matches a child by path
prefix, so a nested /income/invoices/external would highlight both leaves at once.
Verified: typecheck clean, 41 tests pass (7 new), production build green,
anonymous access to both routes 307s to /login, and the mobile checklist passes at
375x812 and 768x1024 - no horizontal page scroll, no tap target under 44px, no
text under 12px. The nested line-item table still scrolls inside its own container
without stretching the outer table.
Note: lib/rows.ts, lib/server-data.ts and the running/insurance feature are
someone else's in-flight work and were deliberately left uncommitted here.