diff --git a/AGENTS.md b/AGENTS.md index f97cfcf..cf6a1e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,50 @@ Gray is the accent. The **only** colour permitted is semantic: success, warning, - `/tokens` (dev-only route) is the audit page. After any theme change, load it and run the chromatic sweep in the browser console — it should report **0** non-semantic colour nodes. +## The two brand accents (exception to the above) +Store intelligence carries exactly **two** non-semantic hues, and nothing else may add a third. + +| | token | light | dark | used for | +|---|---|---|---|---| +| warm | `--color-brand-warm` | `#F4C430` | `#F4C430` | brand, rewards, the activities a merchant runs | +| cool | `--color-brand-cool` | `#7C3AED` | `#A78BFA` | analytics, journeys, AI insight | + +- Defined in `src/app/globals.css` in a Tailwind `@theme` block, **not** in `loyalyTheme.ts`. + They are not Astryx tokens: no component variant resolves through them, and putting them in + the theme would let Badge/Banner pick them up as part of the semantic language. +- Three slots per hue. `-ink` is the readable one for text and icons (light mode darkens warm + to `#8A6300`; raw `#F4C430` on white is ~1.7:1). `-soft` is an icon-chip tint only — never a + card background. The base is for non-text marks: chart fills, dots, bars. +- `src/shared/utils/accent.ts` is the only place a hue becomes a class name. Feature files use + `ACCENT[accent].ink` / `.soft`, never `text-brand-warm-ink` by hand. +- Charts stay monochrome by default. `CHART.brand.{warm,cool}` is opt-in per series and is used + on exactly two charts (dashboard Footfall = cool, Revenue = warm). Never make it a ramp + default — `Sparkline` reads `seriesAt(0)`, so that would turn every KPI card's trend line. +- An activity's accent travels on its API payload (`ActivityMetric.accent`), not from grid + position, so an activity is the same colour on every screen. + +## Attribution is estimated — say so +Everything downstream of `ActivityImpact.customers` (repeat visits, purchases, revenue) is +**modelled**, not measured. No purchase is joined to a specific spin, selfie or challenge. + +- The field is `attributedRevenueInr`, never `revenueInr`, and every payload carries + `attribution: 'estimated' | 'observed'`. +- Copy says *attributed*. Never "generated", "earned" or "drove". A merchant who reads + "Selfie generated ₹3.4L" and spends against it is the failure this rule prevents. +- `` sits in the header of every panel that shows an attributed + figure, and renders **nothing** when the basis is `'observed'` — so a real attribution + backend retires the disclosure with no copy edit. Read the basis off the payload, never + hardcode it. +- The single sentence lives in `ATTRIBUTION_NOTE` (types/intelligence.ts) so three panels + cannot drift. + +## Dashboard supporting analytics are collapsed by default +The six preserved supporting panels (conversion pair, peak hours, reward usage, period rollup, +store comparison) sit inside a `Collapsible`, closed by default, state in +`loyaly.dashboard.supporting-analytics`. Expanded they add ~1,700px desktop / ~3,100px mobile +and push "What needs your attention" — the only actionable section — to 81% of the scroll. +They are evidence, not the finding. Do not re-expand by default; do not delete them either. + ## Known upstream issues (Astryx 0.2.0) - **`useEntryAnimation` breaks SSR hydration.** Its source assumes `'use client'` modules never run on the server; in the App Router they do. Any `FieldStatus` (i.e. `TextInput`/`TextArea` diff --git a/src/app/(workspace)/activity/page.tsx b/src/app/(workspace)/activity/page.tsx index eac1f24..940caa0 100644 --- a/src/app/(workspace)/activity/page.tsx +++ b/src/app/(workspace)/activity/page.tsx @@ -8,13 +8,19 @@ import {useDashboardActivity} from '@/features/dashboard/hooks/useDashboard'; import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory'; /** - * The complete event history — where the dashboard's "View all" leads. + * The complete EVENT history — where "View all" on the dashboard's recent + * activity feed leads. + * + * Deliberately narrow. The grouped activity ecosystem — all ten activities, + * their status, their LYT cost and their impact chains — lives on /lyts, + * because that is where a merchant manages the programme. It briefly lived + * here too; two homes for one dataset is exactly the duplication that lets + * two screens drift apart, so this page kept the half that is genuinely its + * own: the raw, chronological log of individual events. * * Not in the sidebar on purpose. It is a drill-down from one panel, the same * relationship /stores/[storeId] has to the store roster, and the nav is the - * four modules the product is organised around. Adding a fifth entry for a log - * would put an operational archive at the same level as Lyts and Staff, which - * is the weighting this whole change is correcting. + * modules the product is organised around. * * Reads the same resource and the same component the dashboard panel does, * uncapped: one feed implementation, two budgets. diff --git a/src/app/(workspace)/dashboard/page.tsx b/src/app/(workspace)/dashboard/page.tsx index 0271e45..5474cac 100644 --- a/src/app/(workspace)/dashboard/page.tsx +++ b/src/app/(workspace)/dashboard/page.tsx @@ -3,11 +3,10 @@ import {useState} from 'react'; import {VStack} from '@astryxdesign/core/Layout'; import {Grid} from '@astryxdesign/core/Grid'; -import {Button} from '@astryxdesign/core/Button'; -import {Icon} from '@astryxdesign/core/Icon'; +import {Collapsible} from '@astryxdesign/core/Collapsible'; +import {Text} from '@astryxdesign/core/Text'; import {PageHeader} from '@/shared/components/primitives/PageHeader'; import {ScopeControls} from '@/shared/components/scope/ScopeControls'; -import {ICONS} from '@/shared/utils/icons'; import {ChartCard} from '@/shared/components/charts/ChartCard'; import {AreaChartView} from '@/shared/components/charts/AreaChartView'; import {LineChartView} from '@/shared/components/charts/LineChartView'; @@ -18,14 +17,24 @@ import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline import {RewardUsageChart} from '@/features/dashboard/components/RewardUsageChart'; import {StoreComparisonPanel} from '@/features/dashboard/components/StoreComparison'; import {PerformancePanel} from '@/features/dashboard/components/PerformancePanel'; +import {CustomerActivity} from '@/features/dashboard/components/CustomerActivity'; +import {CustomerJourney} from '@/features/dashboard/components/CustomerJourney'; +import {CampaignPerformance} from '@/features/dashboard/components/CampaignPerformance'; +import {StoreInsights} from '@/features/dashboard/components/StoreInsights'; +import {CHART} from '@/shared/components/charts/palette'; import { + useActivityMetrics, + useCampaigns, + useCustomerJourney, useDashboardActivity, useDashboardKpis, useDashboardPeakHours, useDashboardRewardUsage, useDashboardStoreComparison, useDashboardTimeseries, + useStoreInsights, } from '@/features/dashboard/hooks/useDashboard'; +import {usePersistentFlag} from '@/shared/hooks/usePersistentFlag'; import {useWorkspace} from '@/shared/providers/WorkspaceProvider'; import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory'; import {greetingFor} from '@/features/dashboard/services/dashboardService'; @@ -38,25 +47,39 @@ import { } from '@/shared/utils/format'; /** - * An analytics workspace. Charts are the subject, not evidence for a to-do list. + * Store intelligence, in the order a merchant actually asks the questions: + * what happened → why is it happening → what should I do next. * - * The page reads top-down as one narrowing question: headline numbers → the two - * trends that drive them → the conversion story behind those → when and on what - * it happens → the period rollup → which store → what just happened. + * what happened the four headline metrics, then the two trends behind them + * why what customers DID (activity), where they stopped + * (journey), which campaigns moved them (campaigns), and the + * supporting analytics for all three + * what next findings with one action each, then the raw event feed * - * Operational widgets — quick actions, tasks, the AI briefing, top store/reward, - * staff status — deliberately do NOT live here. The briefing endpoint they were - * built on is still live and feeds Loyaly AI's AI tab, which is where that - * class of content belongs: a panel you open to be told what to do, beside a - * dashboard you read to work it out yourself. + * The activity band is the change that makes this a Merchant OS rather than an + * analytics page. Purchases are not the only thing that creates customer + * value: a spin, a challenge, a referral or an event all produce engagement, + * and every one of them is rendered here WITH its downstream chain — customers, + * return visits, purchases — so no activity can be read as a vanity count. * - * Recent activity keeps its compact form — six rows in a fixed box, full history - * on /activity. The uncapped version reached 784px, taller than any chart on the - * page, which is an operational log outweighing the analytics it sits among. + * What did not change, on purpose: the sticky header, the store and range + * controls, the KPI row, the Footfall and Revenue charts, the conversion pair, + * peak hours, reward usage, the period rollup and store comparison. This is a + * restructure of an already-sound page, not a rebuild — the new sections were + * inserted at the points in the hierarchy where they answer the next question, + * and the existing analytics moved below them because they are the evidence + * for the activity story rather than the story itself. * - * Every panel is scoped by the same {storeId, range} from WorkspaceProvider, set - * from this page's header. `series` is fetched once and shared by the four - * charts drawn from it, so they cannot disagree. + * Operational widgets — quick actions, tasks, the AI briefing — still do NOT + * live here. "What needs your attention" is not that: it is four findings + * computed from this page's own numbers, each with a single action, which is a + * different thing from a to-do list a merchant has to maintain. + * + * Every panel is scoped by the same {storeId, range} from WorkspaceProvider, + * set from this page's header. `series` is fetched once and shared by the four + * charts drawn from it, and the activity, journey, campaign and insight + * fixtures are all derived from that same series server-side, so no two panels + * on this page can report different numbers for the same fact. */ export default function DashboardPage() { @@ -66,6 +89,13 @@ export default function DashboardPage() { // dashboard, so Compare starts pressed and switches the panel off, rather // than hiding a panel that used to be there until someone finds the button. const [isComparing, setIsComparing] = useState(true); + // Persisted, not component state: a merchant who wants the charts expanded + // wants them expanded tomorrow too, and re-collapsing them on every visit is + // the fastest way to make a disclosure control feel like an obstacle. + const [isSupportingOpen, setSupportingOpen] = usePersistentFlag( + 'loyaly.dashboard.supporting-analytics', + false, + ); const kpis = useDashboardKpis(); const series = useDashboardTimeseries(); @@ -73,6 +103,10 @@ export default function DashboardPage() { const activity = useDashboardActivity(); const rewards = useDashboardRewardUsage(); const comparison = useDashboardStoreComparison(); + const activityMetrics = useActivityMetrics(); + const journey = useCustomerJourney(); + const campaigns = useCampaigns(); + const insights = useStoreInsights(); const isAllStores = storeId === 'all'; const scopeLabel = useScopeLabel(); @@ -92,7 +126,16 @@ export default function DashboardPage() { {/* 3 — headline numbers. */} - {/* 4 — primary analytics: the two series everything else explains. */} + {/* + 4 — primary analytics: the two series everything else explains. + + These two carry the brand hues and nothing else on the page does at + chart scale. Footfall is cool because it is the system measuring the + store; revenue is warm because it is the outcome the brand is for. One + of each, on the two charts that matter most — that is the whole colour + budget for analytics, and it is why the four charts further down stay + monochrome. + */} {(d) => ( @@ -101,7 +144,9 @@ export default function DashboardPage() { xKey="t" xFormat={formatDayLabel} yFormat={formatCompact} - series={[{key: 'visitors', label: 'Visitors'}]} + series={[ + {key: 'visitors', label: 'Visitors', color: CHART.brand.cool}, + ]} /> )} @@ -113,13 +158,57 @@ export default function DashboardPage() { xKey="t" xFormat={formatDayLabel} yFormat={formatInrCompact} - series={[{key: 'revenue', label: 'Revenue'}]} + series={[ + {key: 'revenue', label: 'Revenue', color: CHART.brand.warmBar}, + ]} /> )} - {/* 5 — secondary analytics: the conversion story, in two readings. */} + {/* 5 — what customers did, and what each activity was worth. Six of ten; + the full ecosystem is on /activity. */} + + + {/* 6 — where that engagement progresses, and where it stops. */} + + + {/* 7 — which deliberate campaigns produced the movement above. */} + + + {/* + 8 — supporting analytics, COLLAPSED BY DEFAULT. + + These six panels are preserved exactly as they were; what changed is + their rank. Left expanded they run 1,724px on desktop and roughly + 3,000px on a phone, which put "What needs your attention" — the only + section on the page a merchant can act on — at 81% of the scroll, + behind four charts that explain rather than prompt. Evidence should be + available on demand, not stand between the finding and the action. + + Collapsed, not deleted, and the choice is remembered: a merchant who + opens it once gets it open every visit thereafter. Nothing is + unreachable and nothing was redesigned. + */} + + {/* Text, not Heading: Collapsible renders the trigger inside a +