mega-plant/app/overheads/page.tsx
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

79 lines
4.7 KiB
TypeScript

import { getOverheads } from "@/lib/data";
import { ZAR } from "@/lib/utils";
import { Receipt } from "lucide-react";
export default async function OverheadsPage() {
const overheads = await getOverheads();
const sections = Array.from(new Set(overheads.map((o) => o.section)));
const grandTotal = overheads.reduce((s, o) => s + (o.amount_incl || 0), 0);
return (
<div className="p-8 max-w-[1400px]">
<div className="flex items-end justify-between mb-6">
<div>
<div className="flex items-center gap-3 mb-1">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#30638E]/15 to-[#003D5B]/15 grid place-items-center text-[#30638E]">
<Receipt className="w-5 h-5" />
</div>
<h1 className="text-2xl font-semibold text-[#003D5B] dark:text-white">Overheads Roll-up</h1>
</div>
<p className="text-gray-500 dark:text-gray-400 text-sm">Full monthly overhead summary by section · 2026</p>
</div>
<div className="bg-gradient-to-br from-[#30638E] to-[#003D5B] rounded-2xl px-5 py-3 text-white shadow-lg shadow-blue-900/20">
<div className="text-xs text-white/80">Total monthly (incl VAT)</div>
<div className="text-2xl font-semibold tabular-nums">{ZAR(grandTotal)}</div>
</div>
</div>
{/* Part B.2: note that plant instalments are excluded (they live on the asset). */}
<div className="mb-6 px-4 py-3 rounded-xl bg-[#EDAE49]/10 border border-[#EDAE49]/30 text-sm text-[#003D5B] dark:text-white">
<strong className="font-semibold">Note:</strong> Machine instalments are excluded — they live on each asset, not in this roll-up.
Workbook grand total (Cost sheet row 98): R 1,110,486.87. Excluding plant (R 415,523.94) and Santam (R 5,322.44, double-counted in workbook subtotal per Part B.4) = expected R 689,640.49.
See <code className="text-xs bg-white/60 dark:bg-black/30 px-1.5 py-0.5 rounded">scripts/verify-cost-reconciliation.mjs</code> for the live diff.
</div>
{sections.map((sec) => {
const items = overheads.filter((o) => o.section === sec);
const secTotal = items.reduce((s, o) => s + (o.amount_incl || 0), 0);
return (
<section key={sec} className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 mb-5 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">{sec}</h2>
<div className="text-right">
<div className="text-xs text-gray-500 dark:text-gray-400">Monthly</div>
<div className="text-lg font-semibold text-[#003D5B] dark:text-white">{ZAR(secTotal)}</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-white/5">
<th className="text-left py-2.5 px-3 font-medium">Description</th>
<th className="text-left py-2.5 px-3 font-medium">GL Account</th>
<th className="text-left py-2.5 px-3 font-medium">Supplier</th>
<th className="text-right py-2.5 px-3 font-medium">Excl VAT</th>
<th className="text-right py-2.5 px-3 font-medium">VAT</th>
<th className="text-right py-2.5 px-3 font-medium">Incl VAT</th>
</tr>
</thead>
<tbody>
{items.map((o) => (
<tr key={o.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-3 font-medium text-[#003D5B] dark:text-white">{o.description}</td>
<td className="py-2.5 px-3 text-gray-500 dark:text-gray-400">{o.gl_account || "—"}</td>
<td className="py-2.5 px-3 text-gray-500 dark:text-gray-400">{o.supplier || "—"}</td>
<td className="py-2.5 px-3 text-right tabular-nums text-gray-700 dark:text-gray-300">{o.amount_excl ? ZAR(o.amount_excl) : "—"}</td>
<td className="py-2.5 px-3 text-right tabular-nums text-gray-500 dark:text-gray-400">{o.vat ? ZAR(o.vat) : "—"}</td>
<td className="py-2.5 px-3 text-right tabular-nums font-medium text-[#003D5B] dark:text-white">{o.amount_incl ? ZAR(o.amount_incl) : "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
})}
</div>
);
}