mega-plant/components/Sparkline.tsx
David 876619c3ac Invoices: keep the PDF on the invoice, and search the invoice's own words
Two things this question turned up.

The upload DID store the PDF, but on the intake record, never on the invoice
— so "Open invoice PDF" 404'd for every invoice imported through the app
while working for the older ones. The file now lives on invoices.source_pdf
and the intake record keeps only the provenance (who sent it, what was
parsed), rather than a second copy of the file. The document endpoint also
falls back to the intake record, which makes the seven already-stored
documents reachable again without a migration. All 10 now open.

The machine combobox searched only the register's own words, so an invoice
line reading "CB22 2.2 TON Roller Vibration" could never be matched to the
asset filed as "CAT 2,4TON RV" — the words simply were not in the list. The
line's own invoice code and description are now searchable too, so the two
sides of the same machine can actually be found from either direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 19:09:16 +02:00

49 lines
1.9 KiB
TypeScript

"use client";
import { ResponsiveContainer, AreaChart, Area } from "recharts";
type Props = {
data: (number | null)[];
color: string;
height?: number;
// Sites renders a narrower sparkline on mobile so the 375px table row keeps its
// budget. Defaults to the width /income has always used.
width?: number;
};
export function Sparkline({ data, color, height = 36, width = 120 }: Props) {
const points = data.map((v, i) => ({ i, v: v ?? 0 }));
const max = Math.max(0.001, ...data.filter((v): v is number => v != null));
// Pad values: if all 0, draw a flat line at 0 for visual presence
const flat = data.every((v) => !v);
const displayData = flat ? points.map((p) => ({ ...p, v: 1 })) : points;
const displayMax = flat ? 1 : max;
return (
<div style={{ width, height }} className="shrink-0">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={displayData} margin={{ top: 2, right: 2, left: 2, bottom: 2 }}>
<defs>
{/* Keyed by colour AND width: /sites and /income can both mount a sparkline
with the same colour in one document, and identical ids would make the
second one reuse the first one's gradient. */}
<linearGradient id={`spark-${color.replace("#", "")}-${width}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.4} />
<stop offset="100%" stopColor={color} stopOpacity={0.02} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="v"
stroke={color}
strokeWidth={1.5}
fill={`url(#spark-${color.replace("#", "")}-${width})`}
isAnimationActive={false}
baseValue={0}
/>
</AreaChart>
</ResponsiveContainer>
<div className="hidden">{displayMax}</div>
</div>
);
}