/** * TEMPORARY sample data for the dashboard charts. * * ── Why this exists ────────────────────────────────────────────────────── * A new merchant's reports come back empty or near-empty, and an empty axis * teaches nothing about what the panel is for. Until the platform has history * for a shop, each chart falls back to this sample and wears a "Sample data" * token so it can never be mistaken for the merchant's own numbers. * * ── Rules ──────────────────────────────────────────────────────────────── * - Real data always wins — see `shared/mocks/withSample.ts`. * - The KPI row never uses this — a headline number must be real or absent. * - Values are deterministic (no Math.random) so SSR and hydration agree. * * ── Removing it ────────────────────────────────────────────────────────── * See `shared/mocks/withSample.ts` — one procedure for every mock folder. */ import type { ConversionReport, FootfallReport, ReportBucket, } from '@/features/dashboard/types/reports'; const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; /** A smooth, repeatable wave: weekly rhythm plus a gentle upward drift. */ function wave(i: number, base: number, amp: number): number { const weekly = Math.sin((i / 7) * Math.PI * 2 - 1.2); const drift = i * 0.012; const ripple = Math.sin(i * 1.7) * 0.18; return Math.max(0, Math.round(base * (1 + drift) + amp * (weekly + ripple))); } function dayBuckets(days: number): ReportBucket[] { // Fixed anchor so the labels are stable across renders and runtimes. const start = Date.UTC(2026, 7, 27); return Array.from({length: days}, (_, i) => { const d = new Date(start + i * 86_400_000); const visitors = wave(i, 140, 42); const newVisitors = Math.round(visitors * (0.38 + 0.06 * Math.sin(i / 3))); const purchases = Math.round(visitors * (0.22 + 0.04 * Math.sin(i / 4 + 1))); const revenue = purchases * (620 + Math.round(90 * Math.sin(i / 5))); return { label: `${MONTHS[d.getUTCMonth()]} ${String(d.getUTCDate()).padStart(2, '0')}`, visitors, newVisitors, returningVisitors: visitors - newVisitors, purchases, revenue, conversion: Number(((purchases / visitors) * 100).toFixed(1)), }; }); } function weekBuckets(days: ReportBucket[]): ReportBucket[] { const weeks: ReportBucket[] = []; for (let i = 0; i < days.length; i += 7) { const slice = days.slice(i, i + 7); const sum = (k: keyof ReportBucket) => slice.reduce((acc, b) => acc + (b[k] as number), 0); const visitors = sum('visitors'); const purchases = sum('purchases'); weeks.push({ label: `W${Math.floor(i / 7) + 1}`, visitors, newVisitors: sum('newVisitors'), returningVisitors: sum('returningVisitors'), purchases, revenue: sum('revenue'), conversion: Number(((purchases / visitors) * 100).toFixed(1)), }); } return weeks; } const DAYS = dayBuckets(30); const WEEKS = weekBuckets(DAYS); function footfallFrom(buckets: ReportBucket[]): FootfallReport { const visits = buckets.reduce((a, b) => a + b.visitors, 0); const newVisitors = buckets.reduce((a, b) => a + b.newVisitors, 0); // Unique people < visit events: regulars come more than once. const total = Math.round(visits * 0.46); return { total, newVisitors, returningVisitors: Math.max(0, Math.round(total * 0.9) - newVisitors), visits, buckets, timezone: null, previousTotal: Math.round(total * 0.88), }; } function conversionFrom(buckets: ReportBucket[], total: number): ConversionReport { const purchases = buckets.reduce((a, b) => a + b.purchases, 0); const revenue = buckets.reduce((a, b) => a + b.revenue, 0); return { purchases, revenue, conversionPct: Number(((purchases / total) * 100).toFixed(1)), basketSize: Math.round(revenue / purchases), buckets, timezone: null, previousPurchases: Math.round(purchases * 0.91), previousRevenue: Math.round(revenue * 0.86), }; } export const MOCK_FOOTFALL_DAILY = footfallFrom(DAYS); export const MOCK_FOOTFALL_WEEKLY = footfallFrom(WEEKS); export const MOCK_CONVERSION_DAILY = conversionFrom(DAYS, MOCK_FOOTFALL_DAILY.total); export const MOCK_CONVERSION_WEEKLY = conversionFrom(WEEKS, MOCK_FOOTFALL_WEEKLY.total); /** True when the report carries at least one non-zero value for `key`. */ export function hasSignal( report: {buckets: ReportBucket[]} | undefined, key: keyof Omit, ): boolean { return !!report && report.buckets.some((b) => b[key] > 0); } export {withSample} from '@/shared/mocks/withSample';