'use client'; import {VStack} from '@astryxdesign/core/Layout'; import {Grid} from '@astryxdesign/core/Grid'; import {Text} from '@astryxdesign/core/Text'; import {PageHeader} from '@/shared/components/primitives/PageHeader'; import {MetricCard} from '@/shared/components/patterns/MetricCard'; import {SkeletonMetricGrid} from '@/shared/components/patterns/LoadingState'; import {AsyncBoundary} from '@/shared/components/data/AsyncBoundary'; import {ChartCard} from '@/shared/components/charts/ChartCard'; import {AreaChartView} from '@/shared/components/charts/AreaChartView'; import {LineChartView} from '@/shared/components/charts/LineChartView'; import {BarChartView} from '@/shared/components/charts/BarChartView'; import {RewardGrid} from '@/features/lyts/components/RewardGrid'; import {ExpiryAlerts} from '@/features/lyts/components/ExpiryAlerts'; import {RewardPerformanceTable} from '@/features/lyts/components/RewardPerformanceTable'; import {ActivityProgramme} from '@/features/lyts/components/ActivityProgramme'; import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline'; import {useMetricColumns} from '@/features/dashboard/components/KpiRow'; import { useLytsActivity, useRedemptions, useRewards, } from '@/features/lyts/hooks/useLyts'; import {ScopeControls} from '@/shared/components/scope/ScopeControls'; import {ICONS} from '@/shared/utils/icons'; import {usageRate} from '@/features/lyts/services/lytsService'; import { formatCompact, formatDayLabel, formatLyt, formatPct, } from '@/shared/utils/format'; /** * The LYT programme — activities and the rewards they pay out in. * * Ordered by urgency rather than by data type: what's expiring, then the * headline numbers, then what customers can DO to earn, then the catalogue of * what they spend on, then the analysis. A merchant opening this page is * usually here because something needs extending or pausing. * * 1 LYT = ₹1, so "outstanding" figures are simultaneously a count and a * rupee liability — which is why they lead. * * ── The activity half ───────────────────────────────────────────────────── * Activities sit here, above rewards, because they are the earning side of * the same ledger: an activity issues LYTs, a reward spends them, and a * merchant tuning the programme is trading one against the other. The * dashboard shows six activities as a summary of engagement; this page is * where all ten are managed, and it reads the SAME activity model — see * ActivityProgramme. */ export default function LytsPage() { const rewards = useRewards(); const redemptions = useRedemptions(); const activity = useLytsActivity(); const columns = useMetricColumns(); // "Now" comes from the SERVER's clock, carried on every response. // // Date.now() here would be impure during render, would disagree between the // SSR and hydration passes, and would report the wrong countdown to anyone // whose device clock is off — on a panel whose entire job is telling a // merchant an offer expires in two days. Falls back to 0 only before the // first response lands, when no expiry panel is rendered anyway. const nowMs = rewards.meta ? new Date(rewards.meta.generatedAt).getTime() : 0; return ( } /> } > {(rows) => { const active = rows.filter( (r) => r.status === 'active' || r.status === 'expiring', ); const claimed = rows.reduce((a, r) => a + r.claimed, 0); const used = rows.reduce((a, r) => a + r.used, 0); const outstanding = rows.reduce( (a, r) => a + (r.claimed - r.used) * r.costLyt, 0, ); const mostClaimed = rows.reduce((a, b) => b.claimed > a.claimed ? b : a, ); const leastUsed = rows.reduce((a, b) => usageRate(b.claimed, b.used) < usageRate(a.claimed, a.used) ? b : a, ); return ( String(Math.round(v))} icon={ICONS.activeRewards} footer={ } /> formatPct(v, 0)} icon={ICONS.lyts} deltaPct={usageRate(claimed, used) - 50} footer={ } /> } /> } /> ); }} {/* The earning side, before the spending side. All ten activities, their status and their LYT cost — the same model the dashboard summarises. */} {(d) => ( )} {(d) => ( )} {(rows) => ( b.claimed - a.claimed).slice(0, 6)} xKey="name" yFormat={formatCompact} series={[ {key: 'claimed', label: 'Claimed'}, {key: 'used', label: 'Redeemed'}, ]} /> )} ); } /** * Stands in for the sparkline on metrics with no meaningful time series — * "most claimed" is a name, not a trend. MetricCard's `footer` slot exists * for exactly this so the card keeps its height and the row stays aligned. */ function Caption({text}: {text: string}) { return ( {text} ); }