From 0e9d06fe71fb84adeeb92111c4e3c16f2e666458 Mon Sep 17 00:00:00 2001 From: Hardik Date: Wed, 24 Jun 2026 11:44:32 +0530 Subject: [PATCH] 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) --- App/CLAUDE.md | 2 +- .../reports/accounting-codes/page.tsx | 29 ++++++----------- .../(portal)/reports/cost-centres/page.tsx | 31 +++++++------------ 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/App/CLAUDE.md b/App/CLAUDE.md index 38e9495..7a40344 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -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`. -**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 `` links, then `cmp=1` compares just the selected set). Weekly focuses one FY month and buckets by week-of-month (W1–W5). The shared `` (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 `` links, then `cmp=1` compares just the selected set). Weekly focuses one FY month and buckets by week-of-month (W1–W5). The shared `` (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). diff --git a/App/app/(portal)/reports/accounting-codes/page.tsx b/App/app/(portal)/reports/accounting-codes/page.tsx index cf5a9c8..c48f9ec 100644 --- a/App/app/(portal)/reports/accounting-codes/page.tsx +++ b/App/app/(portal)/reports/accounting-codes/page.tsx @@ -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 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]; - let chartData: Record[]; - let series: Series[]; - if (yearly) { - chartData = shown.map((r) => { - const row: Record = { name: r.node.code }; - ds.fys.forEach((y, i) => (row[fyLabel(y)] = r.fyTotals[i])); - return row; - }); - 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 = { 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 chartLabels = yearly ? ds.fys.map(fyLabel) : weekly ? [...WEEK_LABELS] : [...FY_MONTHS]; + const chartData: Record[] = chartLabels.map((lab, i) => { + const row: Record = { x: lab }; + shown.forEach((r) => (row[r.node.code] = sparkOf(r)[i])); + return row; + }); + const series: 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 periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy); @@ -190,7 +181,7 @@ export default async function AccountingCodesReport({

{periodLabel} - +
diff --git a/App/app/(portal)/reports/cost-centres/page.tsx b/App/app/(portal)/reports/cost-centres/page.tsx index acccab0..98ea42f 100644 --- a/App/app/(portal)/reports/cost-centres/page.tsx +++ b/App/app/(portal)/reports/cost-centres/page.tsx @@ -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 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]; - let chartData: Record[]; - let series: Series[]; - if (yearly) { - chartData = shown.map((r) => { - const row: Record = { name: r.name }; - ds.fys.forEach((y, i) => (row[fyLabel(y)] = r.fyTotals[i])); - return row; - }); - 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 = { 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 chartLabels = yearly ? ds.fys.map(fyLabel) : weekly ? [...WEEK_LABELS] : [...FY_MONTHS]; + const chartData: Record[] = chartLabels.map((lab, i) => { + const row: Record = { x: lab }; + shown.forEach((r) => (row[r.name] = sparkOf(r)[i])); + return row; + }); + const series: 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 periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy); @@ -158,7 +149,7 @@ export default async function CostCentresReport({

{periodLabel}
- +
-- 2.45.3