diff --git a/src/app/(workspace)/commerce/page.tsx b/src/app/(workspace)/commerce/page.tsx
index 6a86142..c975ca6 100644
--- a/src/app/(workspace)/commerce/page.tsx
+++ b/src/app/(workspace)/commerce/page.tsx
@@ -57,13 +57,7 @@ export default function CommercePage() {
);
diff --git a/src/app/(workspace)/dashboard/page.tsx b/src/app/(workspace)/dashboard/page.tsx
index 02ac35d..7edc1b1 100644
--- a/src/app/(workspace)/dashboard/page.tsx
+++ b/src/app/(workspace)/dashboard/page.tsx
@@ -121,13 +121,7 @@ export default function DashboardPage() {
);
diff --git a/src/app/(workspace)/lyts/page.tsx b/src/app/(workspace)/lyts/page.tsx
index 6437dd7..01ceffa 100644
--- a/src/app/(workspace)/lyts/page.tsx
+++ b/src/app/(workspace)/lyts/page.tsx
@@ -7,31 +7,27 @@ import {FeatureUnavailable} from '@/shared/components/patterns/FeatureUnavailabl
/**
* LYTs.
*
- * The entire loyalty programme — reward catalogue, claims, redemptions,
- * outstanding LYT liability, expiry windows, accrual rate — has no counterpart
- * in the Loyaly platform contract. Every figure this page used to show was
- * generated locally, including the "outstanding liability" a merchant would
- * reasonably read as money they owe.
+ * The merchant-app specification (§2.1) states the product direction does NOT
+ * use loyalty points, LYT balances, redemption or tier calculation — so this
+ * is not a panel waiting on an endpoint, it is a feature the product dropped.
+ * The copy says that, rather than implying a reward catalogue is on its way.
*
- * The page is kept, and states what it needs. Nothing is simulated.
+ * Every figure this page used to show was generated locally, including an
+ * "outstanding liability" in rupees that a merchant would reasonably read as
+ * money they owe. The route is kept so an existing bookmark still lands
+ * somewhere that explains itself. Nothing is simulated.
*/
export default function LytsPage() {
return (
);
diff --git a/src/app/(workspace)/settings/profile/page.tsx b/src/app/(workspace)/settings/profile/page.tsx
index 69df6fc..baa414c 100644
--- a/src/app/(workspace)/settings/profile/page.tsx
+++ b/src/app/(workspace)/settings/profile/page.tsx
@@ -25,8 +25,7 @@ export default async function MerchantProfilePage() {
) : (
)}
diff --git a/src/app/(workspace)/staff/page.tsx b/src/app/(workspace)/staff/page.tsx
index b00f7e1..c6c9400 100644
--- a/src/app/(workspace)/staff/page.tsx
+++ b/src/app/(workspace)/staff/page.tsx
@@ -33,12 +33,7 @@ export default function LeaderboardPage() {
);
diff --git a/src/services/api/reportMapper.ts b/src/services/api/reportMapper.ts
index d3d7b92..ced5559 100644
--- a/src/services/api/reportMapper.ts
+++ b/src/services/api/reportMapper.ts
@@ -86,11 +86,18 @@ export function toConversion(
revenue: current.revenue ?? 0,
conversionPct: current.conversion ?? null,
basketSize: current.average_basket ?? null,
- // The server's conversion report is a scalar summary with no time series,
- // so there is nothing to chart. Empty rather than fabricated.
- buckets: [],
- // SalesReport carries no timezone either; the footfall report names it.
- timezone: null,
+ buckets: (current.points ?? []).map((p) => ({
+ label: p.bucket,
+ // The conversion series carries no visitor split; those belong to the
+ // footfall report and are 0 here so the bucket shape stays uniform.
+ visitors: 0,
+ newVisitors: 0,
+ returningVisitors: 0,
+ purchases: p.baskets ?? 0,
+ revenue: p.revenue ?? 0,
+ conversion: 0,
+ })),
+ timezone: current.timezone ?? null,
previousPurchases: previous?.purchasers ?? null,
previousRevenue: previous?.revenue ?? null,
};
diff --git a/src/services/api/types.ts b/src/services/api/types.ts
index 22a6d05..0c37792 100644
--- a/src/services/api/types.ts
+++ b/src/services/api/types.ts
@@ -242,10 +242,19 @@ export interface ApiFootfallReport {
}
/**
- * The conversion report is a SCALAR SUMMARY. The server's SalesReport carries
- * no time series at all, so there is no `buckets`/`points` field to read and
- * anything charting this over time needs a backend change first.
+ * The conversion report carries both the window totals and a `points` series.
+ * The totals are NOT the sum of the series: `purchasers` is unique people over
+ * the whole window, while a per-bucket `purchasers` counts somebody once per
+ * bucket they bought in.
*/
+export interface ApiSalesPoint {
+ bucket: string;
+ /** Distinct people who bought in THIS bucket. Not summable across buckets. */
+ purchasers: number;
+ baskets: number;
+ revenue: number;
+}
+
export interface ApiConversionReport {
visitors: number;
/** People who bought — named `purchasers`, not `purchases`. Reading the
@@ -255,6 +264,9 @@ export interface ApiConversionReport {
revenue: number;
average_basket: number;
currency?: string;
+ /** The time series, named `points` like the footfall report. */
+ points?: ApiSalesPoint[];
+ timezone?: string;
}
// ---------------------------------------------------------------------------
diff --git a/src/shared/components/patterns/FeatureUnavailable.tsx b/src/shared/components/patterns/FeatureUnavailable.tsx
index fa7b733..5c47d2d 100644
--- a/src/shared/components/patterns/FeatureUnavailable.tsx
+++ b/src/shared/components/patterns/FeatureUnavailable.tsx
@@ -7,33 +7,44 @@ import {Icon} from '@astryxdesign/core/Icon';
import {ICONS} from '@/shared/utils/icons';
/**
- * "This needs a backend resource that does not exist yet."
+ * "This section has nothing to show yet, and here is why."
*
* ── Why this exists instead of fixture data ──────────────────────────────
- * Several screens in this console were built against a loyalty domain — LYT
- * balances, reward catalogues, engagement activities, product inventory — that
- * the Loyaly platform does not expose. The choice was to keep inventing
- * numbers or to say so. Invented numbers are worse than a blank panel: a
- * merchant cannot tell them from real ones, and the first time they act on one
- * the whole product loses its credibility.
+ * Several screens were built against a domain the platform does not record.
+ * The choice was to keep inventing numbers or to say so. Invented numbers are
+ * worse than a blank panel: a merchant cannot tell them from real ones, and
+ * the first time they act on one the whole product loses its credibility.
*
- * So the screen stays, its layout stays, and the panel states plainly what it
- * is waiting for. When the endpoint ships, one hook replaces one component.
+ * ── Why there is no `endpoints` prop any more ────────────────────────────
+ * There was. Every panel printed "Required backend resources" and then
+ * `GET /api/staff — shop-floor roster, distinct from console accounts`, in a
+ * monospace font, on a screen a shop owner opens to run their business. That
+ * is an engineering backlog rendered as product copy, and it breaks the same
+ * rule the merchant-app spec sets for the API itself (§35): expose business
+ * operations, not implementation details. A merchant cannot act on a route
+ * name — being shown one only says the product is unfinished.
+ *
+ * Gating it on `NODE_ENV` was tried and is NOT sufficient, which is worth
+ * recording because it looks like it should be: the arrays were passed as
+ * props from each page, so the literals were still constructed and still
+ * shipped in the production bundle — verified by grepping `.next/static` after
+ * a clean build. Not rendering is not the same as not shipping. The prop is
+ * gone instead, and what each section needs is recorded in the page's own
+ * doc comment, where the people who can act on it actually work.
*
* This is NOT an error state. The platform's own vocabulary has a status for
* exactly this — 501, "the feature is off for this deployment, not an error" —
* and the treatment is deliberately calm: secondary text, no red, no alarm.
+ *
+ * `description` is merchant copy. Write it as a sentence a shop owner would
+ * accept: what is missing, why, and what would change it — never a route.
*/
export function FeatureUnavailable({
title,
description,
- endpoints,
}: {
title: string;
description: string;
- /** The API resources this section needs. Naming them turns a dead end into
- * a work item somebody can pick up. */
- endpoints?: string[];
}) {
return (
@@ -46,19 +57,6 @@ export function FeatureUnavailable({
{description}
-
- {endpoints?.length ? (
-
-
- Required backend resources
-
- {endpoints.map((e) => (
-
- {e}
-
- ))}
-
- ) : null}
);