// The reporting period every report destination shares. It lives in the URL so a report can be // reloaded, bookmarked or printed with the same filters. export type ReportPreset = 'this-month' | 'last-month' | 'last-30' | 'this-quarter' | 'this-year' | 'custom'; export type ReportComparison = 'none' | 'previous' | 'year'; export interface ReportFilters { preset: ReportPreset; start: string; end: string; location: string; compare: ReportComparison; } export const PRESET_LABELS: Array<[ReportPreset, string]> = [ ['this-month', 'This month'], ['last-month', 'Last month'], ['last-30', 'Last 30 days'], ['this-quarter', 'This quarter'], ['this-year', 'This year'], ['custom', 'Custom'], ]; export const COMPARISON_LABELS: Array<[ReportComparison, string]> = [ ['none', 'No comparison'], ['previous', 'Previous period'], ['year', 'Same period last year'], ]; export const ALL_LOCATIONS = 'all'; const EN_DASH = String.fromCharCode(0x2013); const pad = (value: number) => String(value).padStart(2, '0'); const isoDate = (date: Date) => date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; export function presetRange(preset: ReportPreset, today: Date = new Date()): { start: string; end: string } | null { const year = today.getFullYear(); const month = today.getMonth(); switch (preset) { case 'this-month': return { start: isoDate(new Date(year, month, 1)), end: isoDate(new Date(year, month + 1, 0)) }; case 'last-month': return { start: isoDate(new Date(year, month - 1, 1)), end: isoDate(new Date(year, month, 0)) }; case 'last-30': return { start: isoDate(new Date(year, month, today.getDate() - 29)), end: isoDate(today) }; case 'this-quarter': { const first = month - (month % 3); return { start: isoDate(new Date(year, first, 1)), end: isoDate(new Date(year, first + 3, 0)) }; } case 'this-year': return { start: isoDate(new Date(year, 0, 1)), end: isoDate(new Date(year, 11, 31)) }; default: return null; } } const PRESETS = new Set(PRESET_LABELS.map(([value]) => value)); const COMPARISONS = new Set(COMPARISON_LABELS.map(([value]) => value)); export function filtersFromSearch(params: URLSearchParams, today: Date = new Date()): ReportFilters { const start = params.get('start') ?? ''; const end = params.get('end') ?? ''; const compareParam = params.get('compare') as ReportComparison | null; const compare = compareParam && COMPARISONS.has(compareParam) ? compareParam : 'previous'; const location = params.get('location') || ALL_LOCATIONS; if (DATE_PATTERN.test(start) && DATE_PATTERN.test(end)) { const presetParam = params.get('period') as ReportPreset | null; const preset = presetParam && PRESETS.has(presetParam) ? presetParam : 'custom'; return { preset, start, end, location, compare }; } const range = presetRange('this-month', today)!; return { preset: 'this-month', ...range, location, compare }; } export function searchFromFilters(filters: ReportFilters): string { const params = new URLSearchParams({ period: filters.preset, start: filters.start, end: filters.end }); if (filters.location !== ALL_LOCATIONS) params.set('location', filters.location); params.set('compare', filters.compare); return params.toString(); } /** What the server is asked for; the server validates the same fields again. */ export function reportQuery(filters: ReportFilters): string { const params = new URLSearchParams({ start: filters.start, end: filters.end, compare: filters.compare }); if (filters.location !== ALL_LOCATIONS) params.set('location', filters.location); return params.toString(); } const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const parts = (value: string) => value.split('-').map(Number) as [number, number, number]; /** "Sep 1-30, 2026", "Aug 30-Sep 5, 2026" or "Dec 28, 2025-Jan 3, 2026", with an en dash. */ // A loaded report is current only for the exact request it answered: kind, dates, comparison and location. export function reportRequestKey(report: string, filters: ReportFilters): string { return report + '?' + reportQuery(filters); } export function formatRange(start: string, end: string, withYear = true): string { const [startYear, startMonth, startDay] = parts(start); const [endYear, endMonth, endDay] = parts(end); const year = withYear ? ', ' + endYear : ''; if (startYear !== endYear) { return MONTHS[startMonth - 1] + ' ' + startDay + ', ' + startYear + EN_DASH + MONTHS[endMonth - 1] + ' ' + endDay + ', ' + endYear; } if (startMonth === endMonth) { const days = startDay === endDay ? String(startDay) : startDay + EN_DASH + endDay; return MONTHS[startMonth - 1] + ' ' + days + year; } return MONTHS[startMonth - 1] + ' ' + startDay + EN_DASH + MONTHS[endMonth - 1] + ' ' + endDay + year; }