diff --git a/src/components/admin/AnalyticsControls/index.scss b/src/components/admin/AnalyticsControls/index.scss
new file mode 100644
index 0000000..f1af536
--- /dev/null
+++ b/src/components/admin/AnalyticsControls/index.scss
@@ -0,0 +1,98 @@
+.analytics-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.85rem 1.25rem;
+
+ &__ranges,
+ &__comparison {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.2rem;
+ }
+
+ &__range,
+ &__comparison a {
+ padding: 0.48rem 0.68rem;
+ border-radius: var(--style-radius-s);
+ color: var(--theme-elevation-600);
+ text-decoration: none;
+ transition:
+ background-color 120ms ease,
+ color 120ms ease;
+
+ &:hover {
+ color: var(--theme-elevation-900);
+ background: var(--theme-elevation-100);
+ }
+
+ &[aria-current] {
+ color: var(--theme-elevation-950);
+ background: var(--theme-elevation-150);
+ font-weight: 600;
+ }
+ }
+
+ &__comparison {
+ margin-left: auto;
+
+ > span {
+ margin-right: 0.3rem;
+ color: var(--theme-elevation-450);
+ font-size: 0.82rem;
+ }
+ }
+
+ &__custom {
+ flex: 1 0 100%;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ gap: 0.75rem;
+ padding-top: 0.75rem;
+
+ label {
+ display: grid;
+ gap: 0.35rem;
+ color: var(--theme-elevation-550);
+ font-size: 0.78rem;
+ }
+
+ input {
+ min-height: 2.4rem;
+ padding: 0.45rem 0.65rem;
+ border: 1px solid var(--theme-elevation-200);
+ border-radius: var(--style-radius-s);
+ color: var(--theme-elevation-900);
+ background: var(--theme-input-bg);
+ font: inherit;
+ }
+
+ button {
+ min-height: 2.4rem;
+ padding: 0.45rem 0.9rem;
+ border: 0;
+ border-radius: var(--style-radius-s);
+ color: var(--theme-elevation-0);
+ background: var(--theme-elevation-900);
+ font: inherit;
+ font-weight: 600;
+ cursor: pointer;
+ }
+
+ p {
+ flex: 1 0 100%;
+ margin: 0;
+ color: var(--theme-error-500);
+ font-size: 0.8rem;
+ }
+ }
+
+ @media (max-width: 58rem) {
+ &__comparison {
+ flex-basis: 100%;
+ margin-left: 0;
+ }
+ }
+}
diff --git a/src/components/admin/AnalyticsControls/index.tsx b/src/components/admin/AnalyticsControls/index.tsx
new file mode 100644
index 0000000..cb9b26f
--- /dev/null
+++ b/src/components/admin/AnalyticsControls/index.tsx
@@ -0,0 +1,139 @@
+'use client'
+
+import Link from 'next/link'
+import { useRouter } from 'next/navigation'
+import React from 'react'
+import { useForm } from 'react-hook-form'
+
+import type { AnalyticsComparison, AnalyticsRange } from '../Dashboard/analyticsRange'
+
+import './index.scss'
+
+type Props = {
+ adminPath: string
+ comparison: AnalyticsComparison
+ endDate: string
+ maxDate: string
+ range: AnalyticsRange
+ startDate: string
+}
+
+type CustomRangeFields = { from: string; to: string }
+
+const calendarDays = (from: string, to: string) =>
+ Math.floor((Date.parse(`${to}T12:00:00Z`) - Date.parse(`${from}T12:00:00Z`)) / 86_400_000) + 1
+
+const options: Array<{ label: string; value: AnalyticsRange }> = [
+ { label: 'Today', value: 'today' },
+ { label: '7 days', value: '7d' },
+ { label: '30 days', value: '30d' },
+ { label: '90 days', value: '90d' },
+ { label: 'Year to date', value: 'ytd' },
+ { label: 'Custom', value: 'custom' },
+]
+
+const hrefFor = (
+ adminPath: string,
+ range: AnalyticsRange,
+ comparison: AnalyticsComparison,
+ from?: string,
+ to?: string,
+) => {
+ const query = new URLSearchParams({ range, compare: comparison })
+ if (range === 'custom' && from && to) {
+ query.set('from', from)
+ query.set('to', to)
+ }
+ return `${adminPath}?${query.toString()}`
+}
+
+export const AnalyticsControls: React.FC = ({
+ adminPath,
+ comparison,
+ endDate,
+ maxDate,
+ range,
+ startDate,
+}) => {
+ const router = useRouter()
+ const {
+ formState: { errors },
+ handleSubmit,
+ register,
+ } = useForm({ defaultValues: { from: startDate, to: endDate } })
+
+ const applyCustom = handleSubmit(({ from, to }) => {
+ router.push(hrefFor(adminPath, 'custom', comparison, from, to))
+ })
+
+ return (
+
+
+
+
+ Compare
+
+ Previous period
+
+
+ Previous year
+
+
+
+ {range === 'custom' && (
+
+ )}
+
+ )
+}
diff --git a/src/components/admin/Dashboard/analyticsRange.ts b/src/components/admin/Dashboard/analyticsRange.ts
new file mode 100644
index 0000000..7c3c395
--- /dev/null
+++ b/src/components/admin/Dashboard/analyticsRange.ts
@@ -0,0 +1,147 @@
+import {
+ addCalendarDays,
+ dateTimeInZone,
+ type CalendarDate,
+ zonedMidnight,
+} from '@/utilities/storeTime'
+
+export const ANALYTICS_RANGES = ['today', '7d', '30d', '90d', 'ytd', 'custom'] as const
+export type AnalyticsRange = (typeof ANALYTICS_RANGES)[number]
+
+export const ANALYTICS_COMPARISONS = ['previous', 'year'] as const
+export type AnalyticsComparison = (typeof ANALYTICS_COMPARISONS)[number]
+
+const MAX_CUSTOM_DAYS = 366
+
+const dateKey = (date: CalendarDate) =>
+ `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`
+
+const parseDateKey = (value: string | undefined): CalendarDate | undefined => {
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '')
+ if (!match) return undefined
+
+ const date = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) }
+ const rendered = new Date(Date.UTC(date.year, date.month - 1, date.day, 12))
+ if (
+ rendered.getUTCFullYear() !== date.year ||
+ rendered.getUTCMonth() + 1 !== date.month ||
+ rendered.getUTCDate() !== date.day
+ ) {
+ return undefined
+ }
+ return date
+}
+
+const dayNumber = (date: CalendarDate) => Date.UTC(date.year, date.month - 1, date.day) / 86_400_000
+const daysBetween = (start: CalendarDate, end: CalendarDate) => dayNumber(end) - dayNumber(start)
+
+const daysInMonth = (year: number, month: number) => new Date(Date.UTC(year, month, 0)).getUTCDate()
+
+const previousYear = (date: CalendarDate): CalendarDate => ({
+ year: date.year - 1,
+ month: date.month,
+ day: Math.min(date.day, daysInMonth(date.year - 1, date.month)),
+})
+
+const isRange = (value: string | undefined): value is AnalyticsRange =>
+ ANALYTICS_RANGES.includes(value as AnalyticsRange)
+
+const isComparison = (value: string | undefined): value is AnalyticsComparison =>
+ ANALYTICS_COMPARISONS.includes(value as AnalyticsComparison)
+
+export type AnalyticsWindow = {
+ comparison: AnalyticsComparison
+ comparisonLabel: string
+ days: number
+ end: Date
+ endDate: string
+ priorEnd: Date
+ priorStart: Date
+ range: AnalyticsRange
+ rangeLabel: string
+ start: Date
+ startDate: string
+ today: Date
+ todayDate: string
+}
+
+export const analyticsWindow = ({
+ comparison: comparisonInput,
+ from,
+ now,
+ range: rangeInput,
+ timeZone,
+ to,
+}: {
+ comparison?: string
+ from?: string
+ now: Date
+ range?: string
+ timeZone: string
+ to?: string
+}): AnalyticsWindow => {
+ const localNow = dateTimeInZone(now, timeZone)
+ const today = { year: localNow.year, month: localNow.month, day: localNow.day }
+ const comparison = isComparison(comparisonInput) ? comparisonInput : 'previous'
+ let range: AnalyticsRange = isRange(rangeInput) ? rangeInput : '30d'
+ let startDate: CalendarDate
+ let endDate = today
+
+ if (range === 'custom') {
+ const requestedStart = parseDateKey(from)
+ const requestedEnd = parseDateKey(to)
+ const requestedDays =
+ requestedStart && requestedEnd ? daysBetween(requestedStart, requestedEnd) + 1 : 0
+
+ if (
+ !requestedStart ||
+ !requestedEnd ||
+ requestedDays < 1 ||
+ requestedDays > MAX_CUSTOM_DAYS ||
+ daysBetween(today, requestedEnd) > 0
+ ) {
+ range = '30d'
+ startDate = addCalendarDays(today, -29)
+ } else {
+ startDate = requestedStart
+ endDate = requestedEnd
+ }
+ } else if (range === 'ytd') {
+ startDate = { year: today.year, month: 1, day: 1 }
+ } else {
+ const days = range === 'today' ? 1 : Number.parseInt(range, 10)
+ startDate = addCalendarDays(today, -(days - 1))
+ }
+
+ const days = daysBetween(startDate, endDate) + 1
+ const start = zonedMidnight(startDate, timeZone)
+ const end = zonedMidnight(addCalendarDays(endDate, 1), timeZone)
+
+ const priorStartDate =
+ comparison === 'year' ? previousYear(startDate) : addCalendarDays(startDate, -days)
+ const priorEndDate = comparison === 'year' ? addCalendarDays(previousYear(endDate), 1) : startDate
+
+ const labels: Record, string> = {
+ today: 'Today',
+ '7d': 'Last 7 days',
+ '30d': 'Last 30 days',
+ '90d': 'Last 90 days',
+ ytd: 'Year to date',
+ }
+
+ return {
+ comparison,
+ comparisonLabel: comparison === 'year' ? 'vs previous year' : 'vs previous period',
+ days,
+ end,
+ endDate: dateKey(endDate),
+ priorEnd: zonedMidnight(priorEndDate, timeZone),
+ priorStart: zonedMidnight(priorStartDate, timeZone),
+ range,
+ rangeLabel: range === 'custom' ? `${dateKey(startDate)} to ${dateKey(endDate)}` : labels[range],
+ start,
+ startDate: dateKey(startDate),
+ today: zonedMidnight(today, timeZone),
+ todayDate: dateKey(today),
+ }
+}
diff --git a/src/components/admin/Dashboard/index.tsx b/src/components/admin/Dashboard/index.tsx
index 71d5502..3987b43 100644
--- a/src/components/admin/Dashboard/index.tsx
+++ b/src/components/admin/Dashboard/index.tsx
@@ -2,6 +2,7 @@ import config from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
+import { AnalyticsControls } from '../AnalyticsControls'
import { IconAverage, IconOrders, IconProducts, IconRevenue, IconStock } from '../icons'
import { KpiStrip, type Kpi } from '../KpiStrip'
import { OrderHealth } from '../OrderHealth'
@@ -12,15 +13,14 @@ import {
addCalendarDays,
dateTimeInZone,
DEFAULT_STORE_TIME_ZONE,
- reportingWindow,
zonedMidnight,
} from '@/utilities/storeTime'
+import { analyticsWindow } from './analyticsRange'
import { getDashboardOrderData } from './queries'
import './index.scss'
const baseClass = 'store-overview'
-const WINDOW = 30
const LOW_STOCK_AT = 25
const money = (minor: number, currency: string) =>
@@ -32,7 +32,14 @@ const change = (now: number, before: number) =>
type StockRow = { title: string; qty: number }
-async function getOverview() {
+type DashboardFilters = {
+ comparison?: string
+ from?: string
+ range?: string
+ to?: string
+}
+
+async function getOverview(filters: DashboardFilters) {
const payload = await getPayload({ config })
const [users, productCount, settings] = await Promise.all([
@@ -44,18 +51,22 @@ async function getOverview() {
const currency = settings.currency || DEFAULT_CURRENCY_CODE
const timeZone = settings.timeZone || DEFAULT_STORE_TIME_ZONE
- const { end, priorStart, start: windowStart, today: startOfToday } = reportingWindow({
- days: WINDOW,
+ const window = analyticsWindow({
+ comparison: filters.comparison,
+ from: filters.from,
now: new Date(),
+ range: filters.range,
timeZone,
+ to: filters.to,
})
const orderData = await getDashboardOrderData({
currency,
- days: WINDOW,
- end,
+ days: window.days,
+ end: window.end,
payload,
- priorStart,
- start: windowStart,
+ priorEnd: window.priorEnd,
+ priorStart: window.priorStart,
+ start: window.start,
timeZone,
})
const { current, prior } = orderData
@@ -101,7 +112,10 @@ async function getOverview() {
}),
])
const lowStock: StockRow[] = [
- ...simple.docs.map((p) => ({ title: String(p.title ?? 'Untitled'), qty: Number(p.inventory ?? 0) })),
+ ...simple.docs.map((p) => ({
+ title: String(p.title ?? 'Untitled'),
+ qty: Number(p.inventory ?? 0),
+ })),
...variants.docs.map((v) => ({
title: String((typeof v.product === 'object' && v.product?.title) || v.title || 'Variant'),
qty: Number(v.inventory ?? 0),
@@ -133,8 +147,7 @@ async function getOverview() {
popular,
lowStock,
unfinishedPayments: orderData.unfinishedPayments,
- windowStart,
- endLabel: startOfToday,
+ window,
}
}
@@ -155,8 +168,12 @@ const dayLabelsFrom = (start: Date, count: number, timeZone: string) => {
)
}
-export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => {
- const s = await getOverview()
+export const Dashboard: React.FC<{
+ adminPath: string
+ filters?: DashboardFilters
+ name?: string
+}> = async ({ adminPath, filters = {}, name }) => {
+ const s = await getOverview(filters)
const kpis: Kpi[] = [
{
@@ -164,11 +181,28 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => {
label: 'Net sales',
value: money(s.currentRevenue, s.currency),
delta: s.revenueChange,
- compare: `${money(s.grossSales, s.currency)} gross ยท ${money(s.refunds, s.currency)} refunded`,
+ compare: s.window.comparisonLabel,
+ },
+ {
+ Icon: IconOrders,
+ label: 'Orders',
+ value: s.currentOrders.toLocaleString('en-US'),
+ delta: s.ordersChange,
+ compare: s.window.comparisonLabel,
+ },
+ {
+ Icon: IconAverage,
+ label: 'Average order',
+ value: money(s.currentAov, s.currency),
+ delta: s.aovChange,
+ compare: s.window.comparisonLabel,
+ },
+ {
+ Icon: IconProducts,
+ label: 'Products live',
+ value: String(s.products),
+ compare: `${s.customers} customers`,
},
- { Icon: IconOrders, label: 'Orders', value: s.currentOrders.toLocaleString('en-US'), delta: s.ordersChange, compare: 'vs previous 30 days' },
- { Icon: IconAverage, label: 'Average order', value: money(s.currentAov, s.currency), delta: s.aovChange, compare: 'vs previous 30 days' },
- { Icon: IconProducts, label: 'Products live', value: String(s.products), compare: `${s.customers} customers` },
]
return (
@@ -186,6 +220,15 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => {
+
+
@@ -201,12 +244,13 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => {
{ label: 'Net tax', value: money(s.taxes - s.taxRefunds, s.currency) },
{ label: 'Total sales', value: money(s.totalSales, s.currency) },
]}
- current={{ label: 'Last 30 days', points: s.currentDaily }}
+ comparisonLabel={s.window.comparisonLabel}
+ current={{ label: s.window.rangeLabel, points: s.currentDaily }}
currency={s.currency}
- dayLabels={dayLabelsFrom(s.windowStart, s.currentDaily.length, s.timeZone)}
+ dayLabels={dayLabelsFrom(s.window.start, s.currentDaily.length, s.timeZone)}
delta={s.revenueChange}
- endLabel={dayLabel(s.endLabel, s.timeZone)}
- startLabel={dayLabel(s.windowStart, s.timeZone)}
+ endLabel={dayLabel(new Date(s.window.end.getTime() - 1), s.timeZone)}
+ startLabel={dayLabel(s.window.start, s.timeZone)}
title="Net sales"
total={money(s.currentRevenue, s.currency)}
/>
diff --git a/src/components/admin/Dashboard/queries.ts b/src/components/admin/Dashboard/queries.ts
index 51b87f8..e2eee2d 100644
--- a/src/components/admin/Dashboard/queries.ts
+++ b/src/components/admin/Dashboard/queries.ts
@@ -39,7 +39,7 @@ type StatusRow = { count: string | number; status: keyof StatusCount }
type ProductRow = { product_id: number; units: string | number }
type TransactionRow = { count: string | number; status: string }
-const rowsFrom =
(result: unknown): T[] => {
+const rowsFrom = (result: unknown): T[] => {
if (typeof result !== 'object' || result === null || !('rows' in result)) return []
return Array.isArray(result.rows) ? (result.rows as T[]) : []
}
@@ -74,6 +74,7 @@ export async function getDashboardOrderData({
days,
end,
payload,
+ priorEnd,
priorStart,
start,
timeZone,
@@ -82,6 +83,7 @@ export async function getDashboardOrderData({
days: number
end: Date
payload: Payload
+ priorEnd: Date
priorStart: Date
start: Date
timeZone: string
@@ -99,13 +101,18 @@ export async function getDashboardOrderData({
COALESCE("shipping_refund_total", 0) AS "shipping_refund_total",
COALESCE("tax_refund_total", 0) AS "tax_refund_total"
FROM "orders"
- WHERE "created_at" >= ${priorStart}
- AND "created_at" < ${end}
+ WHERE (
+ ("created_at" >= ${start} AND "created_at" < ${end})
+ OR ("created_at" >= ${priorStart} AND "created_at" < ${priorEnd})
+ )
AND ("currency" = ${currency} OR "currency" IS NULL)
AND "status" IS DISTINCT FROM 'cancelled'
)
SELECT
- CASE WHEN "created_at" >= ${start} THEN 'current' ELSE 'prior' END AS "period",
+ CASE
+ WHEN "created_at" >= ${start} AND "created_at" < ${end} THEN 'current'
+ ELSE 'prior'
+ END AS "period",
TO_CHAR("created_at" AT TIME ZONE ${timeZone}, 'YYYY-MM-DD') AS "day",
SUM("subtotal") AS "gross_sales",
SUM("discount_total") AS "discounts",
@@ -135,6 +142,8 @@ export async function getDashboardOrderData({
payload.db.drizzle.execute(sql`
SELECT COALESCE("status"::text, 'processing') AS "status", COUNT(*) AS "count"
FROM "orders"
+ WHERE "created_at" >= ${start} AND "created_at" < ${end}
+ AND ("currency" = ${currency} OR "currency" IS NULL)
GROUP BY "status"
`),
payload.db.drizzle.execute(sql`
diff --git a/src/components/admin/DashboardView/index.tsx b/src/components/admin/DashboardView/index.tsx
index f9ac4ab..56354f5 100644
--- a/src/components/admin/DashboardView/index.tsx
+++ b/src/components/admin/DashboardView/index.tsx
@@ -14,7 +14,9 @@ import { Dashboard } from '../Dashboard'
* on its own. The surrounding chrome comes from the admin layout, so the nav and
* header are untouched.
*/
-export async function DashboardView({ initPageResult }: AdminViewServerProps) {
+const queryString = (value: unknown) => (typeof value === 'string' ? value : undefined)
+
+export async function DashboardView({ clientConfig, initPageResult }: AdminViewServerProps) {
const user = initPageResult.req.user
const fullName = typeof user?.name === 'string' ? user.name : undefined
const firstName = fullName ? fullName.split(' ')[0] : undefined
@@ -24,7 +26,16 @@ export async function DashboardView({ initPageResult }: AdminViewServerProps) {
-
+
)
diff --git a/src/components/admin/RevenueTrend/index.tsx b/src/components/admin/RevenueTrend/index.tsx
index 2915f86..fee1dc7 100644
--- a/src/components/admin/RevenueTrend/index.tsx
+++ b/src/components/admin/RevenueTrend/index.tsx
@@ -18,6 +18,7 @@ type Props = {
/** One label per point, so a hovered day can name itself. */
dayLabels?: string[]
currency?: string
+ comparisonLabel?: string
title?: string
}
@@ -100,6 +101,7 @@ export const RevenueTrend: React.FC = ({
endLabel,
dayLabels,
currency = 'USD',
+ comparisonLabel = 'vs previous period',
title = 'Total revenue',
}) => {
const [mode, setMode] = React.useState('bars')
@@ -133,14 +135,16 @@ export const RevenueTrend: React.FC = ({
const plotW = W - PAD.left - PAD.right
const plotH = H - PAD.top - PAD.bottom
- const n = Math.max(current.points.length, 2)
+ const pointCount = current.points.length
+ const n = Math.max(pointCount, 2)
const peak = Math.max(1, ...current.points)
const magnitude = Math.pow(10, Math.floor(Math.log10(peak)))
const top = Math.ceil(peak / (magnitude / 2)) * (magnitude / 2)
const ticks = [top, top / 2, 0]
- const x = (i: number) => PAD.left + (i / (n - 1)) * plotW
+ const x = (i: number) =>
+ pointCount === 1 ? PAD.left + plotW / 2 : PAD.left + (i / (n - 1)) * plotW
const y = (v: number) => PAD.top + plotH - (v / top) * plotH
const path = (pts: number[]) => smoothPath(pts.map((v, i) => ({ x: x(i), y: y(v) })))
@@ -200,7 +204,7 @@ export const RevenueTrend: React.FC = ({
{Math.abs(delta as number).toFixed(0)}%
)}
- vs previous 30 days
+ {comparisonLabel}
@@ -251,7 +255,7 @@ export const RevenueTrend: React.FC
= ({
tabIndex={0}
viewBox={`0 0 ${W} ${H}`}
>
- {`Revenue for each of the last ${n} days`}
+ {`${current.label} revenue by day`}
{ticks.map((t) => (
@@ -272,13 +276,13 @@ export const RevenueTrend: React.FC = ({
) : (
current.points.map((v, i) => {
// Keep each day visually distinct instead of forming a dense block.
- const w = Math.max(4, (plotW / n) * 0.66)
+ const w = Math.min(48, Math.max(0.8, (plotW / n) * 0.66))
return (
0 ? 1.5 : 0, PAD.top + plotH - y(v))}
key={i}
- rx={2}
+ rx={Math.min(2, w / 2)}
width={w}
x={x(i) - w / 2}
y={y(v)}
diff --git a/tests/unit/analytics-range.unit.spec.ts b/tests/unit/analytics-range.unit.spec.ts
new file mode 100644
index 0000000..e639a7a
--- /dev/null
+++ b/tests/unit/analytics-range.unit.spec.ts
@@ -0,0 +1,65 @@
+import { describe, expect, it } from 'vitest'
+
+import { analyticsWindow } from '@/components/admin/Dashboard/analyticsRange'
+
+describe('dashboard analytics ranges', () => {
+ const now = new Date('2026-09-27T12:00:00.000Z')
+
+ it('builds a store-local rolling range and previous-period comparison', () => {
+ const window = analyticsWindow({ now, range: '7d', timeZone: 'Europe/London' })
+
+ expect(window).toMatchObject({
+ comparison: 'previous',
+ days: 7,
+ endDate: '2026-09-27',
+ range: '7d',
+ startDate: '2026-09-21',
+ })
+ expect(window.start.toISOString()).toBe('2026-09-20T23:00:00.000Z')
+ expect(window.priorStart.toISOString()).toBe('2026-09-13T23:00:00.000Z')
+ expect(window.priorEnd.toISOString()).toBe(window.start.toISOString())
+ })
+
+ it('compares a custom range with the same dates in the previous year', () => {
+ const window = analyticsWindow({
+ comparison: 'year',
+ from: '2026-03-01',
+ now,
+ range: 'custom',
+ timeZone: 'Europe/London',
+ to: '2026-03-31',
+ })
+
+ expect(window.days).toBe(31)
+ expect(window.priorStart.toISOString()).toBe('2025-03-01T00:00:00.000Z')
+ expect(window.priorEnd.toISOString()).toBe('2025-03-31T23:00:00.000Z')
+ })
+
+ it('uses January 1 for year-to-date in the store timezone', () => {
+ const window = analyticsWindow({ now, range: 'ytd', timeZone: 'Europe/London' })
+
+ expect(window.startDate).toBe('2026-01-01')
+ expect(window.endDate).toBe('2026-09-27')
+ expect(window.days).toBe(270)
+ })
+
+ it('falls back safely when a custom range is invalid or too large', () => {
+ const reversed = analyticsWindow({
+ from: '2026-09-27',
+ now,
+ range: 'custom',
+ timeZone: 'Europe/London',
+ to: '2026-09-01',
+ })
+ const oversized = analyticsWindow({
+ from: '2025-01-01',
+ now,
+ range: 'custom',
+ timeZone: 'Europe/London',
+ to: '2026-09-01',
+ })
+
+ expect(reversed).toMatchObject({ days: 30, range: '30d', startDate: '2026-08-29' })
+ expect(oversized).toMatchObject({ days: 30, range: '30d', startDate: '2026-08-29' })
+ })
+})