fix(sites): split certificate chart into certified invoice and retention

Retention is about a tenth of the certified invoice, so on one shared
scale its bars were barely visible and paired bars sat off their month
labels. Each figure now gets its own chart and scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-10-27 10:55:50 +02:00
parent 05f6d551f2
commit bb94dccfce
2 changed files with 33 additions and 19 deletions

View file

@ -47,16 +47,19 @@ export function buildCertificateSeries(
const money = (value: number) => `R${formatNumber(value, { notation: "compact", maximumFractionDigits: 1 })}`;
export function CertificateChart({ points }: { points: CertificatePoint[] }) {
/** One figure per chart: monthly bars against the left axis, running total line against the right. */
export function CertificateChart({ points, metric }: { points: CertificatePoint[]; metric: "certified" | "retention" }) {
const color = metric === "certified" ? CERTIFIED_COLOR : RETENTION_COLOR;
const label = metric === "certified" ? "Certified invoice" : "Retention";
return (
<div className="h-72 w-full min-w-0">
<div className="h-64 w-full min-w-0">
{points.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
No certificates imported yet.
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={points} accessibilityLayer barGap={3} margin={{ top: 12, right: 8, bottom: 0, left: 0 }}>
<ComposedChart data={points} accessibilityLayer margin={{ top: 12, right: 8, bottom: 0, left: 0 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="month" tickFormatter={(value: string) => format(parseISO(`${value}-01`), "MMM yy")}
stroke="var(--muted-foreground)" tickLine={false} axisLine={false} fontSize={11} minTickGap={16} />
@ -71,10 +74,8 @@ export function CertificateChart({ points }: { points: CertificatePoint[] }) {
labelFormatter={(value) => format(parseISO(`${String(value)}-01`), "MMMM yyyy")}
formatter={(value: number) => formatCurrency(value)} />
<Legend iconType="circle" wrapperStyle={{ fontSize: 12 }} formatter={(value) => <span className="text-muted-foreground">{value}</span>} />
<Bar yAxisId="monthly" dataKey="certified" name="Certified invoice (month)" fill={CERTIFIED_COLOR} radius={[4, 4, 0, 0]} maxBarSize={32} isAnimationActive={false} />
<Bar yAxisId="monthly" dataKey="retention" name="Retention (month)" fill={RETENTION_COLOR} radius={[4, 4, 0, 0]} maxBarSize={32} isAnimationActive={false} />
<Line yAxisId="cumulative" type="monotone" dataKey="certifiedCumulative" name="Certified invoice (cumulative)" stroke={CERTIFIED_COLOR} strokeWidth={2} dot={false} isAnimationActive={false} />
<Line yAxisId="cumulative" type="monotone" dataKey="retentionCumulative" name="Retention (cumulative)" stroke={RETENTION_COLOR} strokeWidth={2} strokeDasharray="5 4" dot={false} isAnimationActive={false} />
<Bar yAxisId="monthly" dataKey={metric} name={`${label} (month)`} fill={color} fillOpacity={0.55} radius={[4, 4, 0, 0]} maxBarSize={40} isAnimationActive={false} />
<Line yAxisId="cumulative" type="monotone" dataKey={`${metric}Cumulative`} name={`${label} (cumulative)`} stroke={color} strokeWidth={2.5} dot={{ r: 3, fill: color }} isAnimationActive={false} />
</ComposedChart>
</ResponsiveContainer>
)}

View file

@ -627,18 +627,31 @@ export function SiteDetailView({ data }: { data: SiteDetailData }) {
<ImportCertificateDialog siteId={data.id} open={certOpen} onOpenChange={setCertOpen} />
{/* Certified invoice & retention, monthly and cumulative */}
<Card>
<CardHeader>
<CardTitle className="text-base">Certified invoice &amp; retention</CardTitle>
<p className="text-xs text-muted-foreground">
Bars show each month; lines show the running total (right axis). Certified invoice incl VAT, retention ex VAT.
</p>
</CardHeader>
<CardContent>
<CertificateChart points={certificatePoints} />
</CardContent>
</Card>
{/* Certified invoice and retention, each monthly and cumulative */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base">Certified invoice (VAT incl)</CardTitle>
<p className="text-xs text-muted-foreground">
Bars: each month (left axis) · line: running total (right axis).
</p>
</CardHeader>
<CardContent>
<CertificateChart points={certificatePoints} metric="certified" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Retention (ex VAT)</CardTitle>
<p className="text-xs text-muted-foreground">
Bars: each month (left axis) · line: running total (right axis).
</p>
</CardHeader>
<CardContent>
<CertificateChart points={certificatePoints} metric="retention" />
</CardContent>
</Card>
</div>
{/* PO commitments */}
{data.poList.length > 0 && (