Files
loyaly_cutomerweb/src/features/dashboard/mocks/dashboardMock.ts
2026-09-25 16:31:10 +05:30

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';