import Link from "next/link"; import { getAssets } from "@/lib/server-data"; import { serverAuth } from "@/lib/pb"; import { ZAR } from "@/lib/utils"; import { Wrench, TriangleAlert } from "lucide-react"; import { RunningView } from "@/components/RunningView"; import { buildRunningRows } from "@/lib/rows"; import { MONTH_NAMES } from "@/lib/data"; import { machineRunningCosts, RUNNING_VAT_PCT } from "@/lib/running-costs"; import type { FinancingTerm, InsuranceRate, MonthlyRunningCost, Purchase, PurchaseLine, } from "@/lib/data"; // Reads PocketBase on every request. Without this Next tries to prerender the // page at build time, which makes `docker build` depend on the database being // reachable and then serves a frozen build-time snapshot to users. export const dynamic = "force-dynamic"; /** * Running Costs Overview — everything a vehicle costs to run, per machine per month. * * Client ruling, 2026-10-09: insurance, finance, purchases and repairs ALL belong here. * A machine with no repairs and no purchase order still carries an insurance premium * and a monthly instalment while it is financed, so it still has a figure. * * Everything is read from POCKETBASE — the registers the client maintains in the app. * The original spreadsheet only bootstrapped the page; it is not a source here. * * ⚠️ This is a RE-CUT, not new spend: insurance and instalments are already inside * `monthly_income.fixed_expenses`. Adding this total to fixed expenses would count the * same premium twice. */ export default async function RunningPage() { const pb = await serverAuth(); const [assets, repairs, purchases, lines, insurance, finance] = await Promise.all([ getAssets(), pb.collection("monthly_running_costs").getFullList(), pb.collection("purchases").getFullList(), pb.collection("purchase_lines").getFullList(), pb.collection("insurance_rates").getFullList(), pb.collection("financing_terms").getFullList(), ]); // Never project past the month we are IN — see lib/running-costs.ts. const now = new Date(); const lastMonth = now.getFullYear() === 2026 ? now.getMonth() + 1 : 12; const { byAsset, undatedPurchases } = machineRunningCosts({ assets, repairs, purchases, purchaseLines: lines, insurance, finance, }, lastMonth); const rows = buildRunningRows(assets, byAsset); const monthTotals = Array(12).fill(0); const monthTotalsExcl = Array(12).fill(0); const monthByKind = { repairs: Array(12).fill(0), purchases: Array(12).fill(0), insurance: Array(12).fill(0), finance: Array(12).fill(0) }; for (const r of rows) { r.months.forEach((v, i) => (monthTotals[i] += v)); r.monthsExcl.forEach((v, i) => (monthTotalsExcl[i] += v)); for (const k of ["repairs", "purchases", "insurance", "finance"] as const) r.byKind[k].forEach((v, i) => (monthByKind[k][i] += v)); } const grand = monthTotals.reduce((s, v) => s + v, 0); const grandExcl = monthTotalsExcl.reduce((s, v) => s + v, 0); const unallocated = lines.filter((l) => !l.asset).length; return (

Running Costs

Everything a vehicle costs to run · repairs, purchases, insurance, finance · 2026

Shown to {MONTH_NAMES.slice(0, lastMonth).join(", ")}. A month ahead is not shown — it cannot be calculated, only guessed at, and the last month keeps growing as things are entered.

A re-cut of costs already counted in fixed expenses — do not add this to them. Figures are incl VAT, with the excl figure derived at {RUNNING_VAT_PCT}%.

YTD running cost · incl VAT
{ZAR(grand)}
excl VAT {ZAR(grandExcl)}
{undatedPurchases.length > 0 && (
{undatedPurchases.length} purchase line{undatedPurchases.length !== 1 && "s"} could not be filed {" "} — no readable PO date ({undatedPurchases.slice(0, 3).join(", ")}). Excluded rather than guessed into January.
)} {unallocated > 0 && (
{unallocated} purchase line{unallocated !== 1 && "s"} not yet allocated {" "} to a machine, so they are{" "} not in these totals — see{" "} Purchases .
)}
); }