ligth theme design fix
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user