ligth theme design fix

This commit is contained in:
2026-08-10 15:33:13 +05:30
parent 6cc3c9a0b7
commit 5ce580dced
34 changed files with 2399 additions and 87 deletions

View File

@@ -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.

View File

@@ -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. */}
<KpiRow resource={kpis} />
{/* 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.
*/}
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
<ChartCard title="Footfall" subtitle="Visitors per day" resource={series}>
{(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},
]}
/>
)}
</ChartCard>
@@ -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},
]}
/>
)}
</ChartCard>
</Grid>
{/* 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. */}
<CustomerActivity resource={activityMetrics} />
{/* 6 — where that engagement progresses, and where it stops. */}
<CustomerJourney resource={journey} />
{/* 7 — which deliberate campaigns produced the movement above. */}
<CampaignPerformance resource={campaigns} />
{/*
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.
*/}
<Collapsible
isOpen={isSupportingOpen}
onOpenChange={setSupportingOpen}
trigger={
<VStack gap={0.5} hAlign="start">
{/* Text, not Heading: Collapsible renders the trigger inside a
<button>, and a heading in button content is invalid markup.
The panels within carry their own h2s when expanded. */}
<Text size="base" weight="semibold">
Supporting analytics
</Text>
<Text size="sm" color="secondary">
Conversion, peak hours, reward usage, the period rollup and store
comparison
</Text>
</VStack>
}
>
<VStack gap={5} className="pt-4">
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
<ChartCard
title="Visitors vs purchases"
@@ -160,7 +249,7 @@ export default function DashboardPage() {
</ChartCard>
</Grid>
{/* 6 — operational analytics: when traffic lands, what it redeems. */}
{/* 9 — operational analytics: when traffic lands, what it redeems. */}
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
<ChartCard
title="Peak hours"
@@ -174,19 +263,25 @@ export default function DashboardPage() {
<RewardUsageChart resource={rewards} />
</Grid>
{/* 7 — period rollup. */}
{/* 10 — period rollup. */}
<PerformancePanel
granularity={granularity}
onGranularityChange={setGranularity}
/>
{/* 8 — comparing stores is meaningless when scoped to one of them, which
is why Compare is disabled rather than merely off in that case. */}
{/* 11 — comparing stores is meaningless when scoped to one of them, which
is why Compare is disabled rather than merely off in that case. */}
{isAllStores && isComparing ? (
<StoreComparisonPanel resource={comparison} />
) : null}
</VStack>
</Collapsible>
{/* 9 — bounded feed: six rows, fixed box, full history on /activity. */}
{/* 12 — what to do next. Last because it is the conclusion: every claim
it makes is checkable against a panel above it. */}
<StoreInsights resource={insights} />
{/* 13 — bounded feed: six rows, fixed box, full history on /activity. */}
<ActivityTimeline
resource={activity}
subtitle="Latest events across the selected store and period"

View File

@@ -14,6 +14,7 @@ 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 {
@@ -32,14 +33,23 @@ import {
} from '@/shared/utils/format';
/**
* The LYT programme.
* 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 the catalogue, then the analysis. A merchant opening
* this page is usually here because something needs extending or pausing.
* 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();
@@ -63,7 +73,7 @@ export default function LytsPage() {
<VStack gap={5}>
<PageHeader
title="Lyts"
description="Reward performance, redemption and outstanding LYT liability. 1 LYT = ₹1."
description="Customer activities and rewards — what earns LYTs, what spends them, and what is still outstanding. 1 LYT = ₹1."
controls={<ScopeControls />}
/>
@@ -136,6 +146,12 @@ export default function LytsPage() {
}}
</AsyncBoundary>
{/*
The earning side, before the spending side. All ten activities, their
status and their LYT cost — the same model the dashboard summarises.
*/}
<ActivityProgramme />
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
<ChartCard
title="Redemption"