import type { ReactNode } from 'react'; import { Ban, CloudUpload, ListChecks, Receipt, ShoppingCart, TrendingDown, TrendingUp } from 'lucide-react'; import { count, money } from '@/features/store-admin/format'; import type { ConsoleTotals } from '../consoleModel'; import type { DaySeries, Trend } from '../useConsoleSeries'; /** * The five figures the board opens with. * * Each card carries the number, the shape of how it got there, and whether that * is better or worse than the period before. The sparkline is not decoration: * ₹12,480 taken evenly across a week and ₹12,480 taken in one afternoon are the * same number and completely different news, and the line is the only part of * the card that can tell them apart. * * Trend arrows are drawn only where a real comparison exists. `Trend.isNew` * marks the case where the previous period was zero — "↑ ∞%" is not a fact, so * it reads "new" instead. */ export interface KpiStripProps { totals: ConsoleTotals; days: readonly DaySeries[]; trends: { online: Trend; counter: Trend; orders: Trend; cancelled: Trend; }; comparisonLabel: string; isLoading: boolean; } export function KpiStrip({ totals, days, trends, comparisonLabel, isLoading }: KpiStripProps) { if (isLoading) { return (
{[0, 1, 2, 3, 4].map((i) => (
))}
); } return (
} label="Online Sales" value={money(totals.onlineRevenue)} trend={trends.online} comparisonLabel={comparisonLabel} series={days.map((d) => d.online)} /> } label="Counter Sales" value={money(totals.counterRevenue)} trend={trends.counter} comparisonLabel={comparisonLabel} series={days.map((d) => d.counter)} /> } label="Total Orders" value={count(totals.totalOrders)} trend={trends.orders} comparisonLabel={comparisonLabel} series={days.map((d) => d.onlineOrders + d.counterBills)} /> } label="Cancelled Orders" value={count(totals.cancelled)} trend={trends.cancelled} comparisonLabel={comparisonLabel} series={days.map(() => 0)} // The only card where a rise is bad news, so the colours invert. isInverted /> } label="Unsynced Bills" value={count(totals.pendingBills)} // A live queue depth, not a period figure: there is nothing last week // to compare it against, so it states what it is instead. note={totals.pendingBills === 0 ? 'all bills are in the books' : 'waiting on a till'} series={days.map(() => 0)} isInverted />
); } function Kpi({ icon, label, value, trend, comparisonLabel, series, note, isInverted, }: { icon: ReactNode; label: string; value: string; trend?: Trend; comparisonLabel?: string; series: readonly number[]; note?: string; isInverted?: boolean; }) { // Up is green unless a rise is the bad outcome — cancellations, unsent bills. const isGood = trend ? (isInverted ? trend.direction === 'down' : trend.direction === 'up') : true; // A flat line on an empty period is not bad news, so it is not drawn in red. const hasShape = series.some((value) => value > 0); return (
{/* Icon and figure only. The sparkline sits on the row below rather than beside the label: at the widths this strip actually gets — five cards sharing the page with Nearle Buddy — a 52px chart alongside truncates "Cancelled Orders" to "Cance…", and a label that has stopped naming its number is worse than no chart. */}
{icon}
{label} {value}
{trend && !trend.isNew && trend.percent !== undefined ? (
{trend.direction === 'down' ? : } {trend.percent}% vs. {comparisonLabel}
) : (
{note ?? (trend?.isNew ? 'new this period' : `vs. ${comparisonLabel}`)}
)}
); } /** * The shape of the period, in about forty pixels. * * A polyline over a normalised series. Flat or empty data draws a flat line * rather than nothing, because an absent sparkline reads as a broken card while * a flat one correctly says "nothing moved". */ function Sparkline({ values, tone }: { values: readonly number[]; tone: 'good' | 'bad' | 'none' }) { const points = values.length >= 2 ? values : [0, 0]; const peak = Math.max(...points); const floor = Math.min(...points); const span = peak - floor || 1; const path = points .map((value, i) => { const x = (i / (points.length - 1)) * 60; const y = 20 - ((value - floor) / span) * 18; return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }) .join(' '); return ( ); }