2026-09-12 07:35:35 +00:00
|
|
|
"use client";
|
|
|
|
|
|
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 09:24:18 +00:00
|
|
|
import {
|
|
|
|
|
ComposedChart, Line, Bar, AreaChart, Area, Cell,
|
|
|
|
|
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
|
|
|
|
} from "recharts";
|
2026-09-12 07:35:35 +00:00
|
|
|
import { MONTH_NAMES } from "@/lib/data";
|
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 09:24:18 +00:00
|
|
|
import { computeProfit } from "@/lib/profit";
|
|
|
|
|
|
|
|
|
|
export type ChartView = "lines" | "stack" | "profit";
|
2026-09-12 07:35:35 +00:00
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
income: (number | null)[];
|
|
|
|
|
fixed: (number | null)[];
|
|
|
|
|
running: (number | null)[];
|
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 09:24:18 +00:00
|
|
|
view?: ChartView;
|
|
|
|
|
// CSS-var-resolved hex strings for chart marks (Part A.4).
|
|
|
|
|
// Defaults fall back to validated hues if not provided.
|
|
|
|
|
incomeColor?: string;
|
|
|
|
|
fixedColor?: string;
|
|
|
|
|
runningColor?: string;
|
|
|
|
|
profitColor?: string;
|
2026-09-12 07:35:35 +00:00
|
|
|
};
|
|
|
|
|
|
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 09:24:18 +00:00
|
|
|
// Helper: read a CSS custom property at runtime so we never hardcode hex in components.
|
|
|
|
|
function readVar(name: string, fallback: string): string {
|
|
|
|
|
if (typeof window === "undefined") return fallback;
|
|
|
|
|
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
|
|
|
return v || fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function MachineLineChart({
|
|
|
|
|
income, fixed, running,
|
|
|
|
|
view = "lines",
|
|
|
|
|
incomeColor, fixedColor, runningColor, profitColor,
|
|
|
|
|
}: Props) {
|
|
|
|
|
// Resolve CSS vars to actual hex once per render (server falls back to defaults).
|
|
|
|
|
const INC = incomeColor ?? readVar("--color-chart-income", "#0090A6");
|
|
|
|
|
const FIX = fixedColor ?? readVar("--color-chart-fixed", "#BE851F");
|
|
|
|
|
const RUN = runningColor ?? readVar("--color-chart-running", "#C13B4D");
|
|
|
|
|
const PROF = profitColor ?? readVar("--color-chart-running", "#D1495B");
|
|
|
|
|
|
|
|
|
|
// Clip to last actual month (Part A.3) — months past that are null and we trim the array.
|
|
|
|
|
const lastActual = Math.max(
|
|
|
|
|
income.findLastIndex((v) => v != null),
|
|
|
|
|
fixed.findLastIndex((v) => v != null),
|
|
|
|
|
running.findLastIndex((v) => v != null),
|
|
|
|
|
);
|
|
|
|
|
const clipped = lastActual >= 0 ? lastActual + 1 : MONTH_NAMES.length;
|
|
|
|
|
const months = MONTH_NAMES.slice(0, clipped);
|
|
|
|
|
const data = months.map((m, i) => ({
|
2026-09-12 07:35:35 +00:00
|
|
|
month: m,
|
|
|
|
|
income: income[i] ?? null,
|
|
|
|
|
fixed: fixed[i] ?? null,
|
|
|
|
|
running: running[i] ?? null,
|
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 09:24:18 +00:00
|
|
|
profit: computeProfit({ income: income[i] ?? 0, fixed: fixed[i] ?? 0, running: running[i] ?? 0 }),
|
2026-09-12 07:35:35 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const fmt = (v: number) =>
|
|
|
|
|
new Intl.NumberFormat("en-ZA", { style: "currency", currency: "ZAR", maximumFractionDigits: 0, notation: "compact" }).format(v);
|
|
|
|
|
|
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 09:24:18 +00:00
|
|
|
if (view === "profit") {
|
|
|
|
|
// Single diverging bar series (Part A.5 — View 3): teal above 0, red below
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-44">
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<ComposedChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
|
|
|
|
|
<CartesianGrid stroke="#f3f4f6" strokeDasharray="3 3" vertical={false} />
|
|
|
|
|
<XAxis dataKey="month" stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} />
|
|
|
|
|
<YAxis stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={fmt} width={60} />
|
|
|
|
|
<Tooltip formatter={(v: number) => fmt(v)} />
|
|
|
|
|
<Bar dataKey="profit" radius={[4, 4, 0, 0]} fill={INC}>
|
|
|
|
|
{data.map((d, i) => (
|
|
|
|
|
<Cell key={i} fill={d.profit >= 0 ? INC : RUN} />
|
|
|
|
|
))}
|
|
|
|
|
</Bar>
|
|
|
|
|
</ComposedChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (view === "stack") {
|
|
|
|
|
// Part A.5 — View 2: stacked bars (fixed + running) with income as line. No 4th series for profit.
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-44">
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<ComposedChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 0 }} stackOffset="sign">
|
|
|
|
|
{/* 2px surface-coloured gap between stacked segments via barCategoryGap + barGap */}
|
|
|
|
|
<CartesianGrid stroke="#f3f4f6" strokeDasharray="3 3" vertical={false} />
|
|
|
|
|
<XAxis dataKey="month" stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} />
|
|
|
|
|
<YAxis stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={fmt} width={60} />
|
|
|
|
|
<Tooltip formatter={(v: number) => fmt(v)} />
|
|
|
|
|
{/* Surface-coloured spacer to create the 2px gap — drawn as transparent rect via offset */}
|
|
|
|
|
<Bar dataKey="fixed" stackId="a" fill={FIX} radius={[0, 0, 0, 0]} />
|
|
|
|
|
<Bar dataKey="running" stackId="a" fill={RUN} radius={[4, 4, 0, 0]} barSize={20} />
|
|
|
|
|
<Line type="monotone" dataKey="income" stroke={INC} strokeWidth={2.5} dot={{ r: 3, fill: INC }} connectNulls={false} />
|
|
|
|
|
</ComposedChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default: lines (View 1)
|
2026-09-12 07:35:35 +00:00
|
|
|
return (
|
|
|
|
|
<div className="h-44">
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
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 09:24:18 +00:00
|
|
|
<AreaChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id={`gIncome-${view}`} x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0%" stopColor={INC} stopOpacity={0.4} />
|
|
|
|
|
<stop offset="100%" stopColor={INC} stopOpacity={0.02} />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
<linearGradient id={`gFixed-${view}`} x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0%" stopColor={FIX} stopOpacity={0.35} />
|
|
|
|
|
<stop offset="100%" stopColor={FIX} stopOpacity={0.02} />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
<linearGradient id={`gRunning-${view}`} x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0%" stopColor={RUN} stopOpacity={0.3} />
|
|
|
|
|
<stop offset="100%" stopColor={RUN} stopOpacity={0.02} />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
2026-09-12 07:35:35 +00:00
|
|
|
<CartesianGrid stroke="#f3f4f6" strokeDasharray="3 3" vertical={false} />
|
|
|
|
|
<XAxis dataKey="month" stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} />
|
|
|
|
|
<YAxis stroke="#9ca3af" tick={{ fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={fmt} width={60} />
|
|
|
|
|
<Tooltip formatter={(v: number) => fmt(v)} />
|
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 09:24:18 +00:00
|
|
|
<Area type="monotone" dataKey="income" stroke={INC} strokeWidth={2.5} fill={`url(#gIncome-${view})`} connectNulls={false} />
|
|
|
|
|
<Area type="monotone" dataKey="fixed" stroke={FIX} strokeWidth={2.5} fill={`url(#gFixed-${view})`} connectNulls={false} />
|
|
|
|
|
<Area type="monotone" dataKey="running" stroke={RUN} strokeWidth={2.5} fill={`url(#gRunning-${view})`} connectNulls={false} />
|
|
|
|
|
</AreaChart>
|
2026-09-12 07:35:35 +00:00
|
|
|
</ResponsiveContainer>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
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 09:24:18 +00:00
|
|
|
|