first commit
This commit is contained in:
124
src/features/dashboard/mocks/dashboardMock.ts
Normal file
124
src/features/dashboard/mocks/dashboardMock.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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';
|
||||
Reference in New Issue
Block a user