fix(reports): colour the comparison chart per item in yearly mode too
The monthly/weekly comparison already drew one colour per item (series = items). Yearly mode instead coloured by financial year (series = FYs, items on the x-axis), so multiple cost centres / accounting codes in the same yearly graph shared colours. Unify all three granularities to series = items: the x-axis is months / weeks / FYs and each item keeps its own distinct colour (yearly becomes grouped bars per item rather than per year). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91349f7564
commit
0e9d06fe71
3 changed files with 22 additions and 40 deletions
|
|
@ -162,7 +162,7 @@ Spend analytics under a **Reports** sidebar section (with a **"Purchasing"** sub
|
||||||
|
|
||||||
**Spend definition** (`lib/reports.ts`, the pure/unit-tested core): a PO counts once it reaches `POST_APPROVAL_STATUSES`, dated by `approvedAt`, valued at the full `totalAmount` — the same basis as the dashboard tiles. FY is the Indian **Apr–Mar** year. `getReportDataset()` does one query pass; everything else is pure functions over it. **`allocatePoSpend()`** splits each PO across the accounting codes its **line items** carry (line `accountId`, falling back to the PO-level account), **proportionally** so the per-PO rows always sum back to `totalAmount` — so multi-account POs are attributed correctly in the accounting-code report. `poCount` is **distinct POs** (a multi-account PO yields several rows). Account spend rolls leaf descendants up via `buildAccountIndex().leavesUnder`.
|
**Spend definition** (`lib/reports.ts`, the pure/unit-tested core): a PO counts once it reaches `POST_APPROVAL_STATUSES`, dated by `approvedAt`, valued at the full `totalAmount` — the same basis as the dashboard tiles. FY is the Indian **Apr–Mar** year. `getReportDataset()` does one query pass; everything else is pure functions over it. **`allocatePoSpend()`** splits each PO across the accounting codes its **line items** carry (line `accountId`, falling back to the PO-level account), **proportionally** so the per-PO rows always sum back to `totalAmount` — so multi-account POs are attributed correctly in the accounting-code report. `poCount` is **distinct POs** (a multi-account PO yields several rows). Account spend rolls leaf descendants up via `buildAccountIndex().leavesUnder`.
|
||||||
|
|
||||||
**Filters** live in the **URL query** so the server component re-renders — no client fetching: `gran` (**weekly** / monthly / yearly), `fy`, `month` (weekly), `scope` (Top/Bottom-N), `parent` (accounting drill), `tier` / `break` / `topn` (detail breakdowns), and `sel` + `cmp` (the **custom "Add to graph"** multi-select — tick rows via the `<SelectCheckbox>` links, then `cmp=1` compares just the selected set). Weekly focuses one FY month and buckets by week-of-month (W1–W5). The shared `<ReportsToolbar>` (client) writes the params; charts are **recharts** (`components/reports/charts.tsx`); KPIs/tables/breadcrumbs are server-rendered. Export → `/api/reports/spend?dim=…` (CSV mirroring the on-screen view, incl. the custom selection).
|
**Filters** live in the **URL query** so the server component re-renders — no client fetching: `gran` (**weekly** / monthly / yearly), `fy`, `month` (weekly), `scope` (Top/Bottom-N), `parent` (accounting drill), `tier` / `break` / `topn` (detail breakdowns), and `sel` + `cmp` (the **custom "Add to graph"** multi-select — tick rows via the `<SelectCheckbox>` links, then `cmp=1` compares just the selected set). Weekly focuses one FY month and buckets by week-of-month (W1–W5). The shared `<ReportsToolbar>` (client) writes the params; charts are **recharts** (`components/reports/charts.tsx`) — the comparison chart plots **one colour-coded series per item** (cost centre / accounting code) in every granularity, including the yearly grouped-bars view (x-axis = FYs, a coloured bar per item — not one colour per year); KPIs/tables/breadcrumbs are server-rendered. Export → `/api/reports/spend?dim=…` (CSV mirroring the on-screen view, incl. the custom selection).
|
||||||
|
|
||||||
Sites are **not** cost centres (only vessels are).
|
Sites are **not** cost centres (only vessels are).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,25 +79,16 @@ export default async function AccountingCodesReport({
|
||||||
const prevT = nf >= 2 ? shown.reduce((s, r) => s + r.fyTotals[nf - 2], 0) : 0;
|
const prevT = nf >= 2 ? shown.reduce((s, r) => s + r.fyTotals[nf - 2], 0) : 0;
|
||||||
const yoy = prevT ? ((curT - prevT) / prevT) * 100 : 0;
|
const yoy = prevT ? ((curT - prevT) / prevT) * 100 : 0;
|
||||||
|
|
||||||
|
// One distinct colour per accounting code (series) in every granularity; the
|
||||||
|
// x-axis is months / weeks / financial years.
|
||||||
const colored = (i: number) => SERIES_COLORS[i % SERIES_COLORS.length];
|
const colored = (i: number) => SERIES_COLORS[i % SERIES_COLORS.length];
|
||||||
let chartData: Record<string, string | number>[];
|
const chartLabels = yearly ? ds.fys.map(fyLabel) : weekly ? [...WEEK_LABELS] : [...FY_MONTHS];
|
||||||
let series: Series[];
|
const chartData: Record<string, string | number>[] = chartLabels.map((lab, i) => {
|
||||||
if (yearly) {
|
const row: Record<string, string | number> = { x: lab };
|
||||||
chartData = shown.map((r) => {
|
shown.forEach((r) => (row[r.node.code] = sparkOf(r)[i]));
|
||||||
const row: Record<string, string | number> = { name: r.node.code };
|
return row;
|
||||||
ds.fys.forEach((y, i) => (row[fyLabel(y)] = r.fyTotals[i]));
|
});
|
||||||
return row;
|
const series: Series[] = shown.map((r, i) => ({ key: r.node.code, color: colored(i) }));
|
||||||
});
|
|
||||||
series = ds.fys.map((y, i) => ({ key: fyLabel(y), color: colored(i) }));
|
|
||||||
} else {
|
|
||||||
const labels = weekly ? [...WEEK_LABELS] : [...FY_MONTHS];
|
|
||||||
chartData = labels.map((lab, i) => {
|
|
||||||
const row: Record<string, string | number> = { x: lab };
|
|
||||||
shown.forEach((r) => (row[r.node.code] = sparkOf(r)[i]));
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
series = shown.map((r, i) => ({ key: r.node.code, color: colored(i) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthLabel = (i: number) => `${FY_MONTHS[i]} '${String((fy + (i >= 9 ? 1 : 0)) % 100).padStart(2, "0")}`;
|
const monthLabel = (i: number) => `${FY_MONTHS[i]} '${String((fy + (i >= 9 ? 1 : 0)) % 100).padStart(2, "0")}`;
|
||||||
const periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy);
|
const periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy);
|
||||||
|
|
@ -190,7 +181,7 @@ export default async function AccountingCodesReport({
|
||||||
</p>
|
</p>
|
||||||
<span className="text-xs text-neutral-400">{periodLabel}</span>
|
<span className="text-xs text-neutral-400">{periodLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<ComparisonChart kind={yearly ? "bars" : "lines"} data={chartData} xKey={yearly ? "name" : "x"} series={series} />
|
<ComparisonChart kind={yearly ? "bars" : "lines"} data={chartData} xKey="x" series={series} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
<div className="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
||||||
|
|
|
||||||
|
|
@ -64,26 +64,17 @@ export default async function CostCentresReport({
|
||||||
const prevT = nf >= 2 ? shown.reduce((s, r) => s + r.fyTotals[nf - 2], 0) : 0;
|
const prevT = nf >= 2 ? shown.reduce((s, r) => s + r.fyTotals[nf - 2], 0) : 0;
|
||||||
const yoy = prevT ? ((curT - prevT) / prevT) * 100 : 0;
|
const yoy = prevT ? ((curT - prevT) / prevT) * 100 : 0;
|
||||||
|
|
||||||
// Chart data.
|
// Chart data — one distinct colour per item (series) in every granularity; the
|
||||||
|
// x-axis is months / weeks / financial years. (Yearly is grouped bars per item,
|
||||||
|
// not per FY, so each cost centre keeps its own colour.)
|
||||||
const colored = (i: number) => SERIES_COLORS[i % SERIES_COLORS.length];
|
const colored = (i: number) => SERIES_COLORS[i % SERIES_COLORS.length];
|
||||||
let chartData: Record<string, string | number>[];
|
const chartLabels = yearly ? ds.fys.map(fyLabel) : weekly ? [...WEEK_LABELS] : [...FY_MONTHS];
|
||||||
let series: Series[];
|
const chartData: Record<string, string | number>[] = chartLabels.map((lab, i) => {
|
||||||
if (yearly) {
|
const row: Record<string, string | number> = { x: lab };
|
||||||
chartData = shown.map((r) => {
|
shown.forEach((r) => (row[r.name] = sparkOf(r)[i]));
|
||||||
const row: Record<string, string | number> = { name: r.name };
|
return row;
|
||||||
ds.fys.forEach((y, i) => (row[fyLabel(y)] = r.fyTotals[i]));
|
});
|
||||||
return row;
|
const series: Series[] = shown.map((r, i) => ({ key: r.name, color: colored(i) }));
|
||||||
});
|
|
||||||
series = ds.fys.map((y, i) => ({ key: fyLabel(y), color: colored(i) }));
|
|
||||||
} else {
|
|
||||||
const labels = weekly ? [...WEEK_LABELS] : [...FY_MONTHS];
|
|
||||||
chartData = labels.map((lab, i) => {
|
|
||||||
const row: Record<string, string | number> = { x: lab };
|
|
||||||
shown.forEach((r) => (row[r.name] = sparkOf(r)[i]));
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
series = shown.map((r, i) => ({ key: r.name, color: colored(i) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthLabel = (i: number) => `${FY_MONTHS[i]} '${String((fy + (i >= 9 ? 1 : 0)) % 100).padStart(2, "0")}`;
|
const monthLabel = (i: number) => `${FY_MONTHS[i]} '${String((fy + (i >= 9 ? 1 : 0)) % 100).padStart(2, "0")}`;
|
||||||
const periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy);
|
const periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy);
|
||||||
|
|
@ -158,7 +149,7 @@ export default async function CostCentresReport({
|
||||||
</p>
|
</p>
|
||||||
<span className="text-xs text-neutral-400">{periodLabel}</span>
|
<span className="text-xs text-neutral-400">{periodLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<ComparisonChart kind={yearly ? "bars" : "lines"} data={chartData} xKey={yearly ? "name" : "x"} series={series} />
|
<ComparisonChart kind={yearly ? "bars" : "lines"} data={chartData} xKey="x" series={series} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
<div className="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue