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>
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import { previousPeriodBounds } from '@/lib/analytics/range';
|
|
|
|
describe('previousPeriodBounds', () => {
|
|
it('returns the equal-length window immediately preceding the given bounds', () => {
|
|
const from = new Date('2026-05-01T00:00:00.000Z');
|
|
const to = new Date('2026-05-31T00:00:00.000Z'); // 30-day span
|
|
const prior = previousPeriodBounds({ from, to });
|
|
|
|
// Prior window is contiguous (prior.to === current.from) and equal length.
|
|
expect(prior.to.toISOString()).toBe('2026-05-01T00:00:00.000Z');
|
|
expect(prior.from.toISOString()).toBe('2026-04-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('preserves sub-day spans (e.g. a single-day window)', () => {
|
|
const from = new Date('2026-05-10T00:00:00.000Z');
|
|
const to = new Date('2026-05-10T23:59:59.999Z');
|
|
const prior = previousPeriodBounds({ from, to });
|
|
|
|
const lenMs = to.getTime() - from.getTime();
|
|
expect(prior.to.getTime()).toBe(from.getTime());
|
|
expect(prior.from.getTime()).toBe(from.getTime() - lenMs);
|
|
});
|
|
|
|
it('does not mutate the input bounds', () => {
|
|
const from = new Date('2026-05-01T00:00:00.000Z');
|
|
const to = new Date('2026-05-31T00:00:00.000Z');
|
|
const fromCopy = from.getTime();
|
|
const toCopy = to.getTime();
|
|
previousPeriodBounds({ from, to });
|
|
expect(from.getTime()).toBe(fromCopy);
|
|
expect(to.getTime()).toBe(toCopy);
|
|
});
|
|
});
|