The field is `assets.is_active`. All 7 inactive machines have no income at all — the three generators, two porta camps, AEB Plant Versekering and the Diesel Truck — so on the income list they were a flat row of zeros and nothing else, reading as "idle" when they are actually out of the fleet. They are now hidden by default. The rows are not dropped from the data: the filter lives in the view, so Active / All / Inactive is a client-side toggle with no refetch, and each shows its count so a filter that silently hides rows is obvious. A machine hidden by the filter also collapses if it was expanded. The YTD income and profit tiles moved into the view so they sum only what is visible — otherwise the headline would report the whole fleet while the list below it showed a subset. /charts is unchanged: it passes no filter and still sees everything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
// Per-segment colouring for a profit line.
|
|
//
|
|
// Recharts draws a <Line> as a single <Curve> path, so <Cell> cannot recolour one
|
|
// segment of it — Cells only work on <Bar>. Getting a line that is green where the
|
|
// month made money and red where it did not therefore means one <Line> per same-sign
|
|
// run, plus one extra <Line> per genuine crossing to carry the gradient between them.
|
|
//
|
|
// This is the shared implementation. `/charts` (MachineLineChart) and the dashboard
|
|
// (IncomeAreaChart) both draw the same shape of series, and two private copies of this
|
|
// logic would drift the moment one of them was fixed.
|
|
//
|
|
// The colours come from the tokens already ruled for this purpose — see AGENTS.md:
|
|
// `--color-chart-profit` is a deliberate exception to the "never change the palette"
|
|
// rule, and `--color-chart-loss` has its own token precisely so a loss does not follow
|
|
// the Running line's colour when that hue is recoloured.
|
|
|
|
export type ProfitRun = {
|
|
/** Index of the first month in the run. */
|
|
start: number;
|
|
/** Index of the last month in the run, inclusive. */
|
|
end: number;
|
|
/** true = the month made money (or broke even). */
|
|
up: boolean;
|
|
};
|
|
|
|
// Split a series into maximal contiguous runs of the same sign. A null month ends a
|
|
// run, so the line never bridges a month we have no figure for — treating a missing
|
|
// month as 0 would draw a confident line across a gap in the data.
|
|
export function splitProfitRuns(profit: readonly (number | null)[]): ProfitRun[] {
|
|
const runs: ProfitRun[] = [];
|
|
let i = 0;
|
|
while (i < profit.length) {
|
|
const first = profit[i];
|
|
if (first == null) {
|
|
i++;
|
|
continue;
|
|
}
|
|
const up = first >= 0;
|
|
let j = i;
|
|
while (
|
|
j + 1 < profit.length &&
|
|
profit[j + 1] != null &&
|
|
(profit[j + 1] as number) >= 0 === up
|
|
) {
|
|
j++;
|
|
}
|
|
runs.push({ start: i, end: j, up });
|
|
i = j + 1;
|
|
}
|
|
return runs;
|
|
}
|
|
|
|
export type ProfitSeriesDef = { key: string; stroke: string };
|
|
export type ProfitGradient = { id: string; from: string; to: string };
|
|
|
|
/**
|
|
* Write one key per profit variant onto the SHARED chart data, and describe the series
|
|
* and gradients to draw.
|
|
*
|
|
* Nothing is given a `data` prop of its own: Recharts builds a categorical axis by
|
|
* concatenating the data of every series, so a Line carrying a short `data` re-adds the
|
|
* month labels and the axis renders "Jan … Aug, Jan … Aug" while that series plots at
|
|
* the wrong months. Instead each variant is a key on the one dataset, nulled outside
|
|
* its own run, and `connectNulls={false}` then draws exactly that run in the right
|
|
* place.
|
|
*
|
|
* `mutate` is called for every variant with a `keep` index list and the key to write.
|
|
*/
|
|
export function buildProfitSeries({
|
|
profit,
|
|
upColor,
|
|
downColor,
|
|
idPrefix,
|
|
mutate,
|
|
}: {
|
|
profit: readonly (number | null)[];
|
|
upColor: string;
|
|
downColor: string;
|
|
/** Unique per chart instance, or two charts would share one gradient def. */
|
|
idPrefix: string;
|
|
mutate: (key: string, keep: number[]) => void;
|
|
}): { series: ProfitSeriesDef[]; gradients: ProfitGradient[] } {
|
|
const runs = splitProfitRuns(profit);
|
|
const series: ProfitSeriesDef[] = [];
|
|
const gradients: ProfitGradient[] = [];
|
|
|
|
const keepRange = (from: number, to: number) => {
|
|
const keep: number[] = [];
|
|
for (let i = from; i <= to; i++) keep.push(i);
|
|
return keep;
|
|
};
|
|
|
|
// A run of a single point has no segment to draw, so it is skipped. The point still
|
|
// shows as a dot on the axis below, which is where a lone month's profit belongs.
|
|
runs.forEach((r, idx) => {
|
|
if (r.end <= r.start) return;
|
|
const key = `profitRun${idx}`;
|
|
mutate(key, keepRange(r.start, r.end));
|
|
series.push({ key, stroke: r.up ? upColor : downColor });
|
|
});
|
|
|
|
// One gradient series per genuine crossing: two adjacent runs, same sign flip.
|
|
runs.forEach((a, idx) => {
|
|
const b = runs[idx + 1];
|
|
// Not adjacent means a null month sits between them — never bridge that gap.
|
|
if (!b || a.end + 1 !== b.start) return;
|
|
const key = `profitCross${idx}`;
|
|
const id = `${idPrefix}-g${idx}`;
|
|
mutate(key, [a.end, b.start]);
|
|
series.push({ key, stroke: `url(#${id})` });
|
|
gradients.push({ id, from: a.up ? upColor : downColor, to: b.up ? upColor : downColor });
|
|
});
|
|
|
|
return { series, gradients };
|
|
}
|