125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
/**
|
|
* 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<ReportBucket, 'label'>,
|
|
): boolean {
|
|
return !!report && report.buckets.some((b) => b[key] > 0);
|
|
}
|
|
|
|
export {withSample} from '@/shared/mocks/withSample';
|