- 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
79 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|