feat(reports): prior-period comparison toggle on the Sales report

Adds a "Compare to prior period" toggle to the Sales report header.
When on, the API recomputes the KPI window for the equal-length window
immediately preceding the selected range (previousPeriodBounds) behind
`?compare=1`, and the five window-derived KPI tiles (Won, Lost, Win
rate, Avg time-to-close, New leads) render colour-correct "vs prior"
deltas. Point-in-time tiles (Active interests, Pipeline value) have no
prior-window analogue and intentionally show no delta. The prior-window
query runs in parallel with the main batch and resolves to null when the
toggle is off (zero cost). Toggle state persists in the saved-template
config.

Closes the spec's "period comparison on every report" gap for Sales;
Operational already rendered period-start deltas.

Pure helpers TDD'd: previousPeriodBounds (range.ts) +
computeSalesKpiComparison (sales-comparison.ts), 7 unit tests. tsc +
lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 18:49:35 +02:00
parent 172af02f81
commit 681b94a8ef
7 changed files with 333 additions and 9 deletions

View File

@@ -100,3 +100,23 @@ export function rangeSpanDays(range: DateRange): number {
}
return rangeToDays(range);
}
/**
* Given a concrete {from, to} window, return the equal-length window
* immediately preceding it — used for "this period vs prior period"
* comparison on report KPIs. The prior window is contiguous (its `to`
* equals the current window's `from`) and exactly the same duration, so
* a 30-day current window compares against the prior 30 days.
*
* Pure: takes Dates in, returns fresh Dates, never mutates the input.
*/
export function previousPeriodBounds(bounds: { from: Date; to: Date }): {
from: Date;
to: Date;
} {
const lengthMs = bounds.to.getTime() - bounds.from.getTime();
return {
from: new Date(bounds.from.getTime() - lengthMs),
to: new Date(bounds.from.getTime()),
};
}