@withpotter/pages-admin 0.13.0-next.109 → 0.13.0-next.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/_pos/pos-figures.js +18 -5
- package/dist/admin/_pos/pos-figures.js.map +1 -1
- package/dist/admin/_pos/pos-words.js +6 -1
- package/dist/admin/_pos/pos-words.js.map +1 -1
- package/dist/admin/_pos/use-pos.js +4 -2
- package/dist/admin/_pos/use-pos.js.map +1 -1
- package/dist/admin/pos/pos-workspace.js +6 -2
- package/dist/admin/pos/pos-workspace.js.map +1 -1
- package/dist/admin/pos/shifts-tab.js +2 -2
- package/dist/admin/pos/shifts-tab.js.map +1 -1
- package/dist/admin/pos/transactions-tab.js +71 -0
- package/dist/admin/pos/transactions-tab.js.map +1 -0
- package/dist/admin/revenue/plan-lines.js +35 -0
- package/dist/admin/revenue/plan-lines.js.map +1 -0
- package/dist/admin/revenue/plans-panel.js +48 -0
- package/dist/admin/revenue/plans-panel.js.map +1 -0
- package/dist/admin/revenue/revenue-kpis.js +45 -1
- package/dist/admin/revenue/revenue-kpis.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +12 -2
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +4 -1
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/package.json +4 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { StatCard } from "../_ui";
|
|
4
4
|
import { amountLines } from "./pos-money";
|
|
5
|
-
import { inPeriod } from "./pos-words";
|
|
5
|
+
import { ALL_TIME, givenOut, inPeriod } from "./pos-words";
|
|
6
6
|
import { usePosSummary } from "./use-pos";
|
|
7
7
|
import styles from "./pos.module.css";
|
|
8
8
|
function PosFigures({
|
|
@@ -11,6 +11,7 @@ function PosFigures({
|
|
|
11
11
|
currency = ""
|
|
12
12
|
}) {
|
|
13
13
|
const { data, isLoading, isError } = usePosSummary(period);
|
|
14
|
+
const total = usePosSummary(ALL_TIME);
|
|
14
15
|
const value = (n) => isError || n === void 0 ? "\u2013" : n.toLocaleString("en-US");
|
|
15
16
|
const money = data ? amountLines(data.paymentsAmount, currency) : [];
|
|
16
17
|
const countNote = currency ? ". The count is every currency" : "";
|
|
@@ -19,10 +20,10 @@ function PosFigures({
|
|
|
19
20
|
StatCard,
|
|
20
21
|
{
|
|
21
22
|
compact: true,
|
|
22
|
-
label: "
|
|
23
|
+
label: "POS given out",
|
|
23
24
|
isLoading,
|
|
24
|
-
value: value(data
|
|
25
|
-
sub: data ? `${data.revoked} revoked` : void 0
|
|
25
|
+
value: value(data ? givenOut(data) : void 0),
|
|
26
|
+
sub: data ? `${data.terminals} in use \xB7 ${data.revoked} revoked` : void 0
|
|
26
27
|
}
|
|
27
28
|
),
|
|
28
29
|
/* @__PURE__ */ jsx(
|
|
@@ -71,7 +72,19 @@ function PosFigures({
|
|
|
71
72
|
label: "Card payments",
|
|
72
73
|
isLoading,
|
|
73
74
|
value: value(data?.paymentsCount),
|
|
74
|
-
sub: data ? money.length ? `${money.join(" \xB7 ")}, ${phrase}${countNote}` : `None ${currency ? `in ${currency} ` : ""}${inPeriod(
|
|
75
|
+
sub: data ? money.length ? `${money.join(" \xB7 ")}, ${phrase}${countNote}` : `None ${currency ? `in ${currency} ` : ""}${inPeriod(
|
|
76
|
+
phrase
|
|
77
|
+
)}${countNote}` : void 0
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
StatCard,
|
|
82
|
+
{
|
|
83
|
+
compact: true,
|
|
84
|
+
label: "All-time transactions",
|
|
85
|
+
isLoading: total.isLoading,
|
|
86
|
+
value: value(total.data?.paymentsCount),
|
|
87
|
+
sub: total.data ? amountLines(total.data.paymentsAmount, currency).join(" \xB7 ") || "None yet" : void 0
|
|
75
88
|
}
|
|
76
89
|
)
|
|
77
90
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_pos/pos-figures.tsx"],"sourcesContent":["'use client';\n\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { StatCard } from '../_ui';\nimport { amountLines } from './pos-money';\nimport { inPeriod } from './pos-words';\nimport { usePosSummary } from './use-pos';\nimport styles from './pos.module.css';\n\n/**\n * The fleet at a glance: one number per card. Payments are counted in the\n * period and listed per currency, never added across currencies.\n */\nexport function PosFigures({\n period,\n phrase,\n currency = '',\n}: {\n period: PosWindow;\n phrase: string;\n /** The currency filter's choice; '' lists every currency. */\n currency?: string;\n}) {\n const { data, isLoading, isError } = usePosSummary(period);\n const value = (n: number | undefined) =>\n isError || n === undefined ? '–' : n.toLocaleString('en-US');\n const money = data ? amountLines(data.paymentsAmount, currency) : [];\n // The API counts payments across every currency, not per currency.\n const countNote = currency ? '. The count is every currency' : '';\n\n return (\n <section className={styles.figures} aria-label=\"POS at a glance\">\n <StatCard\n compact\n label=\"
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/pos-figures.tsx"],"sourcesContent":["'use client';\n\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { StatCard } from '../_ui';\nimport { amountLines } from './pos-money';\nimport { ALL_TIME, givenOut, inPeriod } from './pos-words';\nimport { usePosSummary } from './use-pos';\nimport styles from './pos.module.css';\n\n/**\n * The fleet at a glance: one number per card. Payments are counted in the\n * period and listed per currency, never added across currencies.\n */\nexport function PosFigures({\n period,\n phrase,\n currency = '',\n}: {\n period: PosWindow;\n phrase: string;\n /** The currency filter's choice; '' lists every currency. */\n currency?: string;\n}) {\n const { data, isLoading, isError } = usePosSummary(period);\n // Not the period's: the running total since the first terminal, whatever the control says.\n const total = usePosSummary(ALL_TIME);\n const value = (n: number | undefined) =>\n isError || n === undefined ? '–' : n.toLocaleString('en-US');\n const money = data ? amountLines(data.paymentsAmount, currency) : [];\n // The API counts payments across every currency, not per currency.\n const countNote = currency ? '. The count is every currency' : '';\n\n return (\n <section className={styles.figures} aria-label=\"POS at a glance\">\n <StatCard\n compact\n label=\"POS given out\"\n isLoading={isLoading}\n value={value(data ? givenOut(data) : undefined)}\n sub={\n data\n ? `${data.terminals} in use · ${data.revoked} revoked`\n : undefined\n }\n />\n <StatCard\n compact\n label=\"Online now\"\n isLoading={isLoading}\n value={value(data?.online)}\n sub={data ? `Of ${data.terminals}` : undefined}\n />\n <StatCard\n compact\n label=\"Offline over a day\"\n isLoading={isLoading}\n value={value(data?.offlineOverDay)}\n alert={!!data?.offlineOverDay}\n />\n <StatCard\n compact\n label=\"Suspended\"\n isLoading={isLoading}\n value={value(data?.suspended)}\n />\n <StatCard\n compact\n label=\"Shifts open over 12 h\"\n isLoading={isLoading}\n value={value(data?.openShiftsOverTwelveHours)}\n alert={!!data?.openShiftsOverTwelveHours}\n />\n <StatCard\n compact\n label=\"Card payments\"\n isLoading={isLoading}\n value={value(data?.paymentsCount)}\n sub={\n data\n ? money.length\n ? `${money.join(' · ')}, ${phrase}${countNote}`\n : `None ${currency ? `in ${currency} ` : ''}${inPeriod(\n phrase,\n )}${countNote}`\n : undefined\n }\n />\n <StatCard\n compact\n label=\"All-time transactions\"\n isLoading={total.isLoading}\n value={value(total.data?.paymentsCount)}\n sub={\n total.data\n ? amountLines(total.data.paymentsAmount, currency).join(' · ') ||\n 'None yet'\n : undefined\n }\n />\n </section>\n );\n}\n"],"mappings":";AAiCI,SACE,KADF;AA9BJ,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,UAAU,UAAU,gBAAgB;AAC7C,SAAS,qBAAqB;AAC9B,OAAO,YAAY;AAMZ,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAKG;AACD,QAAM,EAAE,MAAM,WAAW,QAAQ,IAAI,cAAc,MAAM;AAEzD,QAAM,QAAQ,cAAc,QAAQ;AACpC,QAAM,QAAQ,CAAC,MACb,WAAW,MAAM,SAAY,WAAM,EAAE,eAAe,OAAO;AAC7D,QAAM,QAAQ,OAAO,YAAY,KAAK,gBAAgB,QAAQ,IAAI,CAAC;AAEnE,QAAM,YAAY,WAAW,kCAAkC;AAE/D,SACE,qBAAC,aAAQ,WAAW,OAAO,SAAS,cAAW,mBAC7C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,OAAO,SAAS,IAAI,IAAI,MAAS;AAAA,QAC9C,KACE,OACI,GAAG,KAAK,SAAS,gBAAa,KAAK,OAAO,aAC1C;AAAA;AAAA,IAER;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,MAAM,MAAM;AAAA,QACzB,KAAK,OAAO,MAAM,KAAK,SAAS,KAAK;AAAA;AAAA,IACvC;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,MAAM,cAAc;AAAA,QACjC,OAAO,CAAC,CAAC,MAAM;AAAA;AAAA,IACjB;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,MAAM,SAAS;AAAA;AAAA,IAC9B;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,MAAM,yBAAyB;AAAA,QAC5C,OAAO,CAAC,CAAC,MAAM;AAAA;AAAA,IACjB;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN;AAAA,QACA,OAAO,MAAM,MAAM,aAAa;AAAA,QAChC,KACE,OACI,MAAM,SACJ,GAAG,MAAM,KAAK,QAAK,CAAC,KAAK,MAAM,GAAG,SAAS,KAC3C,QAAQ,WAAW,MAAM,QAAQ,MAAM,EAAE,GAAG;AAAA,UAC1C;AAAA,QACF,CAAC,GAAG,SAAS,KACf;AAAA;AAAA,IAER;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN,WAAW,MAAM;AAAA,QACjB,OAAO,MAAM,MAAM,MAAM,aAAa;AAAA,QACtC,KACE,MAAM,OACF,YAAY,MAAM,KAAK,gBAAgB,QAAQ,EAAE,KAAK,QAAK,KAC3D,aACA;AAAA;AAAA,IAER;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -69,7 +69,8 @@ const ADMIN_EVENT = {
|
|
|
69
69
|
"pos_terminal.revoke": "Revoked"
|
|
70
70
|
};
|
|
71
71
|
function historyLine(e) {
|
|
72
|
-
if (e.source === "admin_action")
|
|
72
|
+
if (e.source === "admin_action")
|
|
73
|
+
return ADMIN_EVENT[e.event] ?? words(e.event);
|
|
73
74
|
return e.summary?.trim() || words(e.event);
|
|
74
75
|
}
|
|
75
76
|
function historyBy(e) {
|
|
@@ -82,7 +83,10 @@ function inPeriod(phrase) {
|
|
|
82
83
|
return `in ${phrase.startsWith("last") ? "the " : ""}${phrase}`;
|
|
83
84
|
}
|
|
84
85
|
const accessLine = (t) => t.operatorAccess === "any_member" ? "Anyone on the till roster can sign in" : "Only the people listed can sign in";
|
|
86
|
+
const givenOut = (s) => s.terminals + s.revoked;
|
|
87
|
+
const ALL_TIME = { from: "2020-01-01T00:00:00.000Z" };
|
|
85
88
|
export {
|
|
89
|
+
ALL_TIME,
|
|
86
90
|
SHIFT_STATUS_LABEL,
|
|
87
91
|
TERMINAL_STATUS_LABEL,
|
|
88
92
|
accessLine,
|
|
@@ -91,6 +95,7 @@ export {
|
|
|
91
95
|
cardLine,
|
|
92
96
|
closedLine,
|
|
93
97
|
count,
|
|
98
|
+
givenOut,
|
|
94
99
|
historyBy,
|
|
95
100
|
historyLine,
|
|
96
101
|
inPeriod,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_pos/pos-words.ts"],"sourcesContent":["import type {\n PosHistoryEntry,\n PosShift,\n PosShiftStatus,\n PosTerminal,\n PosTerminalStatus,\n} from '@withpotter/lib/admin-pos-types';\n\n/** The words the POS screens use for what the API sends. Pure and tested. */\n\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\nexport const TERMINAL_STATUS_LABEL: Record<PosTerminalStatus, string> = {\n active: 'Active',\n suspended: 'Suspended',\n revoked: 'Revoked',\n};\n\nexport const SHIFT_STATUS_LABEL: Record<PosShiftStatus, string> = {\n open: 'Open',\n closed: 'Closed',\n};\n\nconst CLOSED_REASON: Record<string, string> = {\n operator_sign_in_elsewhere: 'Signed in at another till',\n shift_expired: 'Ran out of time',\n manual_revoke: 'Closed from a dashboard',\n};\n\n/** Why a shift ended, in words; null while it is open. */\nexport function closedLine(s: Pick<PosShift, 'status' | 'closedReason'>) {\n if (s.status === 'open') return null;\n\n return (s.closedReason && CLOSED_REASON[s.closedReason]) ?? 'Closed';\n}\n\n/** \"15 Oct 2026\". */\nexport function onDay(iso: string): string {\n return new Date(iso).toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"15 Oct 2026, 14:03\". */\nexport function atTime(iso: string): string {\n const d = new Date(iso);\n\n return `${onDay(iso)}, ${d.toLocaleTimeString('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n })}`;\n}\n\n/** \"just now\", \"4 min ago\", \"3 h ago\", \"2 days ago\". */\nexport function ago(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n\n if (ms < MINUTE) return 'just now';\n\n if (ms < HOUR) return `${Math.floor(ms / MINUTE)} min ago`;\n\n if (ms < DAY) return `${Math.floor(ms / HOUR)} h ago`;\n\n const days = Math.floor(ms / DAY);\n\n return days === 1 ? '1 day ago' : `${days} days ago`;\n}\n\n/**\n * How a terminal's connection reads. Online is green; offline for over a\n * day is amber, because someone should look; a stopped terminal (revoked\n * or suspended) and one never heard from are grey.\n */\nexport type OnlineTone = 'online' | 'offline' | 'stale' | 'never' | 'stopped';\n\ntype Seen = Pick<PosTerminal, 'online' | 'lastSeenAt' | 'status'>;\n\nexport function onlineTone(t: Seen, now = Date.now()): OnlineTone {\n if (t.status !== 'active') return 'stopped';\n\n if (t.online) return 'online';\n\n if (!t.lastSeenAt) return 'never';\n\n return now - new Date(t.lastSeenAt).getTime() > DAY ? 'stale' : 'offline';\n}\n\n/** \"Seen 4 min ago\", \"Offline · seen 3 days ago\", \"Never seen\". */\nexport function seenLine(t: Seen, now = Date.now()): string {\n if (!t.lastSeenAt) return 'Never seen';\n\n const when = ago(t.lastSeenAt, now);\n\n if (t.online) return `Seen ${when}`;\n\n return t.status === 'active' ? `Offline · seen ${when}` : `Last seen ${when}`;\n}\n\n/** \"Urovo · TID T100\", \"Needs a TID\", or nothing for a revoked terminal. */\nexport function cardLine(\n t: Pick<PosTerminal, 'provider' | 'tid' | 'status'>,\n): string | null {\n if (t.status === 'revoked') return null;\n\n if (!t.tid) return 'Needs a TID';\n\n const vendor = t.provider\n ? `${t.provider.charAt(0).toUpperCase()}${t.provider.slice(1)} · `\n : '';\n\n return `${vendor}TID ${t.tid}`;\n}\n\nconst words = (s: string) => {\n const w = s.replace(/[._-]+/g, ' ').trim();\n\n return w.charAt(0).toUpperCase() + w.slice(1);\n};\n\nconst ADMIN_EVENT: Record<string, string> = {\n 'pos_terminal.suspend': 'Suspended',\n 'pos_terminal.resume': 'Resumed',\n 'pos_terminal.revoke': 'Revoked',\n};\n\n/** One history entry as a sentence: what happened. */\nexport function historyLine(e: PosHistoryEntry): string {\n if (e.source === 'admin_action')
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/pos-words.ts"],"sourcesContent":["import type {\n PosHistoryEntry,\n PosShift,\n PosShiftStatus,\n PosTerminal,\n PosTerminalStatus,\n} from '@withpotter/lib/admin-pos-types';\n\n/** The words the POS screens use for what the API sends. Pure and tested. */\n\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\nexport const TERMINAL_STATUS_LABEL: Record<PosTerminalStatus, string> = {\n active: 'Active',\n suspended: 'Suspended',\n revoked: 'Revoked',\n};\n\nexport const SHIFT_STATUS_LABEL: Record<PosShiftStatus, string> = {\n open: 'Open',\n closed: 'Closed',\n};\n\nconst CLOSED_REASON: Record<string, string> = {\n operator_sign_in_elsewhere: 'Signed in at another till',\n shift_expired: 'Ran out of time',\n manual_revoke: 'Closed from a dashboard',\n};\n\n/** Why a shift ended, in words; null while it is open. */\nexport function closedLine(s: Pick<PosShift, 'status' | 'closedReason'>) {\n if (s.status === 'open') return null;\n\n return (s.closedReason && CLOSED_REASON[s.closedReason]) ?? 'Closed';\n}\n\n/** \"15 Oct 2026\". */\nexport function onDay(iso: string): string {\n return new Date(iso).toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"15 Oct 2026, 14:03\". */\nexport function atTime(iso: string): string {\n const d = new Date(iso);\n\n return `${onDay(iso)}, ${d.toLocaleTimeString('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n })}`;\n}\n\n/** \"just now\", \"4 min ago\", \"3 h ago\", \"2 days ago\". */\nexport function ago(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n\n if (ms < MINUTE) return 'just now';\n\n if (ms < HOUR) return `${Math.floor(ms / MINUTE)} min ago`;\n\n if (ms < DAY) return `${Math.floor(ms / HOUR)} h ago`;\n\n const days = Math.floor(ms / DAY);\n\n return days === 1 ? '1 day ago' : `${days} days ago`;\n}\n\n/**\n * How a terminal's connection reads. Online is green; offline for over a\n * day is amber, because someone should look; a stopped terminal (revoked\n * or suspended) and one never heard from are grey.\n */\nexport type OnlineTone = 'online' | 'offline' | 'stale' | 'never' | 'stopped';\n\ntype Seen = Pick<PosTerminal, 'online' | 'lastSeenAt' | 'status'>;\n\nexport function onlineTone(t: Seen, now = Date.now()): OnlineTone {\n if (t.status !== 'active') return 'stopped';\n\n if (t.online) return 'online';\n\n if (!t.lastSeenAt) return 'never';\n\n return now - new Date(t.lastSeenAt).getTime() > DAY ? 'stale' : 'offline';\n}\n\n/** \"Seen 4 min ago\", \"Offline · seen 3 days ago\", \"Never seen\". */\nexport function seenLine(t: Seen, now = Date.now()): string {\n if (!t.lastSeenAt) return 'Never seen';\n\n const when = ago(t.lastSeenAt, now);\n\n if (t.online) return `Seen ${when}`;\n\n return t.status === 'active' ? `Offline · seen ${when}` : `Last seen ${when}`;\n}\n\n/** \"Urovo · TID T100\", \"Needs a TID\", or nothing for a revoked terminal. */\nexport function cardLine(\n t: Pick<PosTerminal, 'provider' | 'tid' | 'status'>,\n): string | null {\n if (t.status === 'revoked') return null;\n\n if (!t.tid) return 'Needs a TID';\n\n const vendor = t.provider\n ? `${t.provider.charAt(0).toUpperCase()}${t.provider.slice(1)} · `\n : '';\n\n return `${vendor}TID ${t.tid}`;\n}\n\nconst words = (s: string) => {\n const w = s.replace(/[._-]+/g, ' ').trim();\n\n return w.charAt(0).toUpperCase() + w.slice(1);\n};\n\nconst ADMIN_EVENT: Record<string, string> = {\n 'pos_terminal.suspend': 'Suspended',\n 'pos_terminal.resume': 'Resumed',\n 'pos_terminal.revoke': 'Revoked',\n};\n\n/** One history entry as a sentence: what happened. */\nexport function historyLine(e: PosHistoryEntry): string {\n if (e.source === 'admin_action')\n return ADMIN_EVENT[e.event] ?? words(e.event);\n\n return e.summary?.trim() || words(e.event);\n}\n\n/** Who did it: an admin by name, or the business itself. */\nexport function historyBy(e: PosHistoryEntry): string {\n if (e.by.type === 'admin') return `${e.by.name ?? 'An admin'} · Admin`;\n\n return 'The business';\n}\n\n/** \"2 terminals\", \"1 terminal\". */\nexport const count = (n: number, one: string, many = `${one}s`) =>\n `${n} ${n === 1 ? one : many}`;\n\n/**\n * A period phrase after a noun: \"in the last 30 days\", \"in Sep 30 – Oct 1\",\n * and \"yet\" for all time (\"No payments yet\").\n */\nexport function inPeriod(phrase: string): string {\n if (phrase === 'all time') return 'yet';\n\n return `in ${phrase.startsWith('last') ? 'the ' : ''}${phrase}`;\n}\n\n/** Who may sign in at a terminal, in a line. */\nexport const accessLine = (t: Pick<PosTerminal, 'operatorAccess'>) =>\n t.operatorAccess === 'any_member'\n ? 'Anyone on the till roster can sign in'\n : 'Only the people listed can sign in';\n\n/** Every terminal ever handed out: the ones in use (not revoked) and the ones taken back. */\nexport const givenOut = (s: { terminals: number; revoked: number }) =>\n s.terminals + s.revoked;\n\n/** The window that reaches back past the first terminal: for figures that must not follow the period control. */\nexport const ALL_TIME = { from: '2020-01-01T00:00:00.000Z' } as const;\n"],"mappings":"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","names":[]}
|
|
@@ -69,9 +69,9 @@ function useTenantTillRequests(tenantId, enabled = true) {
|
|
|
69
69
|
retry
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
function usePosPayments(tenantId, enabled = true) {
|
|
73
73
|
return useInfiniteQuery({
|
|
74
|
-
queryKey: key("payments", tenantId),
|
|
74
|
+
queryKey: key("payments", tenantId ?? "all"),
|
|
75
75
|
queryFn: ({ pageParam }) => adminPosApi.payments({ tenantId, cursor: pageParam, limit: 25 }),
|
|
76
76
|
initialPageParam: void 0,
|
|
77
77
|
getNextPageParam: (last) => last.nextCursor ?? void 0,
|
|
@@ -79,6 +79,7 @@ function useTenantPayments(tenantId, enabled = true) {
|
|
|
79
79
|
retry
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
const useTenantPayments = (tenantId, enabled = true) => usePosPayments(tenantId, enabled);
|
|
82
83
|
const oliveRetry = (count, error) => !isForbidden(error) && !isNotFound(error) && !isNotLinked(error) && count < 1;
|
|
83
84
|
function useOliveAccount(tenantId, enabled = true) {
|
|
84
85
|
return useQuery({
|
|
@@ -137,6 +138,7 @@ export {
|
|
|
137
138
|
useOliveAccountTransactions,
|
|
138
139
|
useOliveOutlets,
|
|
139
140
|
usePosHistory,
|
|
141
|
+
usePosPayments,
|
|
140
142
|
usePosShifts,
|
|
141
143
|
usePosSummary,
|
|
142
144
|
usePosTerminal,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_pos/use-pos.ts"],"sourcesContent":["'use client';\n\nimport {\n keepPreviousData,\n useMutation,\n useInfiniteQuery,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { adminPosApi, type TerminalAct } from '@withpotter/lib/admin-pos-api';\nimport type {\n ShiftFilters,\n TerminalFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { isForbidden, isNotFound, isNotLinked } from './pos-rules';\n\n/** Every POS query sits under this, so one action refreshes them all. */\nexport const POS_KEY = ['platform-admin', 'pos'] as const;\n\nconst key = (...parts: unknown[]) => [...POS_KEY, ...parts];\n\n/** A 403 or 404 will not change on a retry; anything else gets one more go. */\nconst retry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && count < 1;\n\nexport function usePosSummary(w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('summary', w),\n queryFn: () => adminPosApi.summary(w),\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminals(filters: TerminalFilters, enabled = true) {\n return useQuery({\n queryKey: key('terminals', filters),\n queryFn: () => adminPosApi.terminals(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminal(id: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('terminal', id, w),\n queryFn: () => adminPosApi.terminal(id, w),\n enabled,\n retry,\n });\n}\n\nexport function usePosHistory(id: string, enabled = true) {\n return useQuery({\n queryKey: key('history', id),\n queryFn: () => adminPosApi.history(id),\n enabled,\n retry,\n });\n}\n\nexport function usePosShifts(filters: ShiftFilters, enabled = true) {\n return useQuery({\n queryKey: key('shifts', filters),\n queryFn: () => adminPosApi.shifts(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function useTenantRoster(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('roster', tenantId),\n queryFn: () => adminPosApi.roster(tenantId),\n enabled,\n retry,\n });\n}\n\nexport function useTenantTillRequests(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('till-requests', tenantId),\n queryFn: () => adminPosApi.tillRequests(tenantId),\n enabled,\n retry,\n });\n}\n\n/** Card payments, newest first, a page at a time. */\nexport function
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_pos/use-pos.ts"],"sourcesContent":["'use client';\n\nimport {\n keepPreviousData,\n useMutation,\n useInfiniteQuery,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { adminPosApi, type TerminalAct } from '@withpotter/lib/admin-pos-api';\nimport type {\n ShiftFilters,\n TerminalFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosWindow } from '@withpotter/lib/admin-pos-types';\nimport { isForbidden, isNotFound, isNotLinked } from './pos-rules';\n\n/** Every POS query sits under this, so one action refreshes them all. */\nexport const POS_KEY = ['platform-admin', 'pos'] as const;\n\nconst key = (...parts: unknown[]) => [...POS_KEY, ...parts];\n\n/** A 403 or 404 will not change on a retry; anything else gets one more go. */\nconst retry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && count < 1;\n\nexport function usePosSummary(w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('summary', w),\n queryFn: () => adminPosApi.summary(w),\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminals(filters: TerminalFilters, enabled = true) {\n return useQuery({\n queryKey: key('terminals', filters),\n queryFn: () => adminPosApi.terminals(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function usePosTerminal(id: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('terminal', id, w),\n queryFn: () => adminPosApi.terminal(id, w),\n enabled,\n retry,\n });\n}\n\nexport function usePosHistory(id: string, enabled = true) {\n return useQuery({\n queryKey: key('history', id),\n queryFn: () => adminPosApi.history(id),\n enabled,\n retry,\n });\n}\n\nexport function usePosShifts(filters: ShiftFilters, enabled = true) {\n return useQuery({\n queryKey: key('shifts', filters),\n queryFn: () => adminPosApi.shifts(filters),\n placeholderData: keepPreviousData,\n enabled,\n retry,\n });\n}\n\nexport function useTenantRoster(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('roster', tenantId),\n queryFn: () => adminPosApi.roster(tenantId),\n enabled,\n retry,\n });\n}\n\nexport function useTenantTillRequests(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('till-requests', tenantId),\n queryFn: () => adminPosApi.tillRequests(tenantId),\n enabled,\n retry,\n });\n}\n\n/** Card payments, newest first, a page at a time: one business's, or every business's when none is given. */\nexport function usePosPayments(tenantId?: string, enabled = true) {\n return useInfiniteQuery({\n queryKey: key('payments', tenantId ?? 'all'),\n queryFn: ({ pageParam }) =>\n adminPosApi.payments({ tenantId, cursor: pageParam, limit: 25 }),\n initialPageParam: undefined as string | undefined,\n getNextPageParam: (last) => last.nextCursor ?? undefined,\n enabled,\n retry,\n });\n}\n\nexport const useTenantPayments = (tenantId: string, enabled = true) =>\n usePosPayments(tenantId, enabled);\n\n/**\n * What the business sees from Olive. Read live and audited on the server, so these are fetched only\n * when their tab or card is on screen, never in the background, and a 400 (Olive not linked) is not retried.\n */\nconst oliveRetry = (count: number, error: unknown) =>\n !isForbidden(error) && !isNotFound(error) && !isNotLinked(error) && count < 1;\n\nexport function useOliveAccount(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'account'),\n queryFn: () => adminPosApi.olive.account(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useOliveAccountTransactions(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'account-transactions'),\n queryFn: () => adminPosApi.olive.accountTransactions(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useOliveOutlets(tenantId: string, enabled = true) {\n return useQuery({\n queryKey: key('olive', tenantId, 'outlets'),\n queryFn: () => adminPosApi.olive.outlets(tenantId),\n enabled,\n retry: oliveRetry,\n staleTime: 60_000,\n });\n}\n\nexport function useTenantPos(tenantId: string, w: PosWindow, enabled = true) {\n return useQuery({\n queryKey: key('tenant', tenantId, w),\n queryFn: () => adminPosApi.tenant(tenantId, w),\n enabled,\n retry,\n });\n}\n\n/**\n * Every action can move the figures, the lists and a business's tab, so\n * all of POS is refetched after it, a 409 included: that means someone\n * else acted first. The refetch is not awaited, so the caller's own\n * `onSuccess` (the toast, closing the form) is not held back by it.\n */\nfunction usePosWrite<V, R>(fn: (v: V) => Promise<R>) {\n const client = useQueryClient();\n\n return useMutation({\n mutationFn: fn,\n onSettled: () => {\n void client.invalidateQueries({ queryKey: POS_KEY });\n },\n });\n}\n\nexport const useTerminalStatus = () =>\n usePosWrite((v: { id: string; act: TerminalAct; reason: string }) =>\n adminPosApi.setTerminalStatus(v.id, v.act, v.reason),\n );\n\nexport const useCloseShift = () =>\n usePosWrite((v: { id: string; reason: string }) =>\n adminPosApi.closeShift(v.id, v.reason),\n );\n"],"mappings":"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","names":[]}
|
|
@@ -16,9 +16,12 @@ import { usePosPeriod } from "../_pos/use-pos-period";
|
|
|
16
16
|
import { usePosSummary } from "../_pos/use-pos";
|
|
17
17
|
import { ShiftsTab } from "./shifts-tab";
|
|
18
18
|
import { TerminalsTab } from "./terminals-tab";
|
|
19
|
+
import { TransactionsTab } from "./transactions-tab";
|
|
19
20
|
const TABS = [
|
|
20
21
|
{ id: "terminals", label: "Terminals" },
|
|
21
|
-
|
|
22
|
+
// The merchant's POS page calls these sessions, here POS sessions; the address keeps `shifts` so old links still open.
|
|
23
|
+
{ id: "shifts", label: "POS sessions" },
|
|
24
|
+
{ id: "transactions", label: "Transactions" }
|
|
22
25
|
];
|
|
23
26
|
const KEYS = TABS.map((t) => t.id);
|
|
24
27
|
function PosWorkspace() {
|
|
@@ -57,7 +60,8 @@ function PosWorkspace() {
|
|
|
57
60
|
/* @__PURE__ */ jsx(PosFigures, { period, phrase, currency }),
|
|
58
61
|
/* @__PURE__ */ jsx(PageTabs, { tabs: TABS, active: tab, onChange: setTab }),
|
|
59
62
|
tab === "terminals" && /* @__PURE__ */ jsx(TerminalsTab, { period, currency }),
|
|
60
|
-
tab === "shifts" && /* @__PURE__ */ jsx(ShiftsTab, { currency })
|
|
63
|
+
tab === "shifts" && /* @__PURE__ */ jsx(ShiftsTab, { currency }),
|
|
64
|
+
tab === "transactions" && /* @__PURE__ */ jsx(TransactionsTab, {})
|
|
61
65
|
] });
|
|
62
66
|
}
|
|
63
67
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/pos/pos-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../_ui';\nimport { PosFigures } from '../_pos/pos-figures';\nimport { usePosAccess } from '../_pos/use-pos-access';\nimport { usePosPeriod } from '../_pos/use-pos-period';\nimport { usePosSummary } from '../_pos/use-pos';\nimport { ShiftsTab } from './shifts-tab';\nimport { TerminalsTab } from './terminals-tab';\n\n/**\n * Box's POS page has an Orders tab for hardware orders. Olive sells no\n * terminals through the admin, so it has no such tab.\n */\nconst TABS = [\n { id: 'terminals', label: 'Terminals' },\n { id: 'shifts', label: '
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pos/pos-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../_ui';\nimport { PosFigures } from '../_pos/pos-figures';\nimport { usePosAccess } from '../_pos/use-pos-access';\nimport { usePosPeriod } from '../_pos/use-pos-period';\nimport { usePosSummary } from '../_pos/use-pos';\nimport { ShiftsTab } from './shifts-tab';\nimport { TerminalsTab } from './terminals-tab';\nimport { TransactionsTab } from './transactions-tab';\n\n/**\n * Box's POS page has an Orders tab for hardware orders. Olive sells no\n * terminals through the admin, so it has no such tab.\n */\nconst TABS = [\n { id: 'terminals', label: 'Terminals' },\n // The merchant's POS page calls these sessions, here POS sessions; the address keeps `shifts` so old links still open.\n { id: 'shifts', label: 'POS sessions' },\n { id: 'transactions', label: 'Transactions' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * POS across every business: the fleet's figures, then every terminal and\n * every till shift. A terminal opens on its own page.\n */\nexport function PosWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const { isLoading, canRead } = usePosAccess();\n const { period, phrase, control } = usePosPeriod();\n const [currency, setCurrency] = useCurrencyParam();\n // The figures' own request (one shared query): its currencies are the\n // filter's choices.\n const summary = usePosSummary(period, canRead);\n const currencies = currenciesIn(\n summary.data?.paymentsAmount.map((a) => a.currency),\n );\n\n if (isLoading) return <AdmLoading label=\"Loading POS…\" />;\n\n if (!canRead)\n return (\n <div>\n <PageHeader title=\"POS\" />\n <EmptyNote well>{\"You don't have permission to see POS.\"}</EmptyNote>\n </div>\n );\n\n return (\n <div>\n <PageHeader\n title=\"POS\"\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n {control}\n </div>\n }\n />\n <PosFigures period={period} phrase={phrase} currency={currency} />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {tab === 'terminals' && (\n <TerminalsTab period={period} currency={currency} />\n )}\n {tab === 'shifts' && <ShiftsTab currency={currency} />}\n {tab === 'transactions' && <TransactionsTab />}\n </div>\n );\n}\n"],"mappings":";AAiDwB,cAIlB,YAJkB;AA/CxB,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAMhC,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,aAAa,OAAO,YAAY;AAAA;AAAA,EAEtC,EAAE,IAAI,UAAU,OAAO,eAAe;AAAA,EACtC,EAAE,IAAI,gBAAgB,OAAO,eAAe;AAC9C;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAM1B,SAAS,eAAe;AAC7B,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM,EAAE,WAAW,QAAQ,IAAI,aAAa;AAC5C,QAAM,EAAE,QAAQ,QAAQ,QAAQ,IAAI,aAAa;AACjD,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAGjD,QAAM,UAAU,cAAc,QAAQ,OAAO;AAC7C,QAAM,aAAa;AAAA,IACjB,QAAQ,MAAM,eAAe,IAAI,CAAC,MAAM,EAAE,QAAQ;AAAA,EACpD;AAEA,MAAI,UAAW,QAAO,oBAAC,cAAW,OAAM,qBAAe;AAEvD,MAAI,CAAC;AACH,WACE,qBAAC,SACC;AAAA,0BAAC,cAAW,OAAM,OAAM;AAAA,MACxB,oBAAC,aAAU,MAAI,MAAE,mDAAwC;AAAA,OAC3D;AAGJ,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SACE,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACC;AAAA,WACH;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,cAAW,QAAgB,QAAgB,UAAoB;AAAA,IAChE,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD,QAAQ,eACP,oBAAC,gBAAa,QAAgB,UAAoB;AAAA,IAEnD,QAAQ,YAAY,oBAAC,aAAU,UAAoB;AAAA,IACnD,QAAQ,kBAAkB,oBAAC,mBAAgB;AAAA,KAC9C;AAEJ;","names":[]}
|
|
@@ -22,7 +22,7 @@ const STATUS_OPTIONS = [
|
|
|
22
22
|
function ShiftsTab({
|
|
23
23
|
currency = "",
|
|
24
24
|
tenantId: fixedTenantId,
|
|
25
|
-
title = "
|
|
25
|
+
title = "POS sessions"
|
|
26
26
|
}) {
|
|
27
27
|
const [paramTenantId, setTenantId] = useTenantParam();
|
|
28
28
|
const tenantId = fixedTenantId ?? paramTenantId;
|
|
@@ -54,7 +54,7 @@ function ShiftsTab({
|
|
|
54
54
|
isError,
|
|
55
55
|
error,
|
|
56
56
|
onRetry: () => void refetch(),
|
|
57
|
-
emptyTitle: narrowed ? "No
|
|
57
|
+
emptyTitle: narrowed ? "No POS sessions match." : "No POS sessions yet.",
|
|
58
58
|
controls: /* @__PURE__ */ jsxs(Filters, { label: "Filter shifts", children: [
|
|
59
59
|
tenantId && !fixedTenantId && /* @__PURE__ */ jsx(
|
|
60
60
|
RemovableChip,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport {\n POS_PAGE_SIZE,\n type ShiftFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosShiftStatus } from '@withpotter/lib/admin-pos-types';\nimport { FilterPress, FilterSelect, Filters } from '../_ui';\nimport { ListPager } from '../_ui/list-card';\nimport { LONG_OPEN_HOURS } from '../_pos/pos-rules';\nimport { SHIFT_STATUS_LABEL } from '../_pos/pos-words';\nimport { ShiftsTable } from '../_pos/shifts-table';\nimport { usePosShifts } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\n\nconst STATUS_OPTIONS = [\n { value: '', label: 'Any status' },\n ...Object.entries(SHIFT_STATUS_LABEL).map(([value, label]) => ({\n value,\n label,\n })),\n];\n\n/** Open and recent shifts across every business, newest first. */\nexport function ShiftsTab({\n currency = '',\n tenantId: fixedTenantId,\n title = '
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport {\n POS_PAGE_SIZE,\n type ShiftFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosShiftStatus } from '@withpotter/lib/admin-pos-types';\nimport { FilterPress, FilterSelect, Filters } from '../_ui';\nimport { ListPager } from '../_ui/list-card';\nimport { LONG_OPEN_HOURS } from '../_pos/pos-rules';\nimport { SHIFT_STATUS_LABEL } from '../_pos/pos-words';\nimport { ShiftsTable } from '../_pos/shifts-table';\nimport { usePosShifts } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\n\nconst STATUS_OPTIONS = [\n { value: '', label: 'Any status' },\n ...Object.entries(SHIFT_STATUS_LABEL).map(([value, label]) => ({\n value,\n label,\n })),\n];\n\n/** Open and recent shifts across every business, newest first. */\nexport function ShiftsTab({\n currency = '',\n tenantId: fixedTenantId,\n title = 'POS sessions',\n}: {\n currency?: string;\n /** Fixes the list to one business (its own page); otherwise `?tenant=` narrows it. */\n tenantId?: string;\n title?: string;\n}) {\n const [paramTenantId, setTenantId] = useTenantParam();\n const tenantId = fixedTenantId ?? paramTenantId;\n const [status, setStatus] = useState<PosShiftStatus | ''>('');\n const [longOpen, setLongOpen] = useState(false);\n const [page, setPage] = useState(1);\n const filters: ShiftFilters = {\n status: longOpen ? 'open' : status,\n openOverHours: longOpen ? LONG_OPEN_HOURS : undefined,\n tenantId: tenantId || undefined,\n page,\n limit: POS_PAGE_SIZE,\n };\n const { data, isLoading, isError, error, refetch } = usePosShifts(filters);\n const narrowed = !!(status || longOpen || (tenantId && !fixedTenantId));\n const reset = (fn: () => void) => {\n fn();\n setPage(1);\n };\n\n return (\n <ShiftsTable\n title={title}\n rows={data?.data}\n total={data?.total}\n withBusiness={!fixedTenantId}\n currency={currency}\n isLoading={isLoading}\n isError={isError}\n error={error}\n onRetry={() => void refetch()}\n emptyTitle={narrowed ? 'No POS sessions match.' : 'No POS sessions yet.'}\n controls={\n <Filters label=\"Filter shifts\">\n {tenantId && !fixedTenantId && (\n <RemovableChip\n onRemove={() => reset(() => setTenantId(''))}\n removeLabel=\"Show every business\"\n >\n One business\n </RemovableChip>\n )}\n <FilterPress\n on={longOpen}\n onToggle={() => reset(() => setLongOpen((v) => !v))}\n >\n Open over {LONG_OPEN_HOURS} hours\n </FilterPress>\n {!longOpen && (\n <FilterSelect\n label=\"Status\"\n value={status}\n onChange={(v) => reset(() => setStatus(v as PosShiftStatus | ''))}\n options={STATUS_OPTIONS}\n />\n )}\n {narrowed && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() =>\n reset(() => {\n setStatus('');\n setLongOpen(false);\n if (!fixedTenantId) setTenantId('');\n })\n }\n >\n Clear filters\n </Button>\n )}\n </Filters>\n }\n footer={\n data && data.total > 0 ? (\n <ListPager\n page={page}\n totalPages={Math.max(1, data.totalPages)}\n total={data.total}\n noun={data.total === 1 ? 'shift' : 'shifts'}\n onPage={setPage}\n />\n ) : undefined\n }\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Badge, Button, TableRow, Th } from "@withpotter/ui-kit";
|
|
5
|
+
import { formatMinor } from "@withpotter/lib/money-minor";
|
|
6
|
+
import { ListCard } from "../_ui/list-card";
|
|
7
|
+
import list from "../_ui/list-card.module.css";
|
|
8
|
+
import { isForbidden, posErrorText } from "../_pos/pos-rules";
|
|
9
|
+
import { atTime } from "../_pos/pos-words";
|
|
10
|
+
import { usePosPayments, usePosTerminals } from "../_pos/use-pos";
|
|
11
|
+
import pos from "../_pos/pos.module.css";
|
|
12
|
+
function TransactionsTab() {
|
|
13
|
+
const q = usePosPayments();
|
|
14
|
+
const terminals = usePosTerminals({ limit: 100 });
|
|
15
|
+
const rows = useMemo(
|
|
16
|
+
() => q.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
17
|
+
[q.data]
|
|
18
|
+
);
|
|
19
|
+
const names = useMemo(
|
|
20
|
+
() => new Map(terminals.data?.data.map((t) => [t.tenant.id, t.tenant.name])),
|
|
21
|
+
[terminals.data]
|
|
22
|
+
);
|
|
23
|
+
return /* @__PURE__ */ jsx(
|
|
24
|
+
ListCard,
|
|
25
|
+
{
|
|
26
|
+
title: "Transactions",
|
|
27
|
+
count: q.hasNextPage ? void 0 : rows.length,
|
|
28
|
+
columns: 6,
|
|
29
|
+
minWidth: 900,
|
|
30
|
+
isLoading: q.isLoading,
|
|
31
|
+
isError: q.isError && !q.data,
|
|
32
|
+
onRetry: () => void q.refetch(),
|
|
33
|
+
errorTitle: isForbidden(q.error) ? "You don't have permission to see transactions." : posErrorText(q.error, "Transactions did not load."),
|
|
34
|
+
isEmpty: rows.length === 0,
|
|
35
|
+
emptyTitle: "No card payments yet.",
|
|
36
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
37
|
+
/* @__PURE__ */ jsx(Th, { children: "Business" }),
|
|
38
|
+
/* @__PURE__ */ jsx(Th, { children: "Terminal" }),
|
|
39
|
+
/* @__PURE__ */ jsx(Th, { children: "Reference" }),
|
|
40
|
+
/* @__PURE__ */ jsx(Th, { children: "Status" }),
|
|
41
|
+
/* @__PURE__ */ jsx(Th, { children: "Amount" }),
|
|
42
|
+
/* @__PURE__ */ jsx(Th, { children: "When" })
|
|
43
|
+
] }),
|
|
44
|
+
footer: q.hasNextPage ? /* @__PURE__ */ jsx("div", { className: pos.stack, children: /* @__PURE__ */ jsx(
|
|
45
|
+
Button,
|
|
46
|
+
{
|
|
47
|
+
size: "sm",
|
|
48
|
+
variant: "secondary",
|
|
49
|
+
disabled: q.isFetchingNextPage,
|
|
50
|
+
onClick: () => void q.fetchNextPage(),
|
|
51
|
+
children: q.isFetchingNextPage ? "Loading\u2026" : "Show more"
|
|
52
|
+
}
|
|
53
|
+
) }) : void 0,
|
|
54
|
+
children: rows.map((p) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
55
|
+
/* @__PURE__ */ jsx("td", { children: names.get(p.tenantId) ?? "\u2013" }),
|
|
56
|
+
/* @__PURE__ */ jsx("td", { children: p.deviceLabel ?? "\u2013" }),
|
|
57
|
+
/* @__PURE__ */ jsx("td", { className: list.muted, children: p.reference ?? "\u2013" }),
|
|
58
|
+
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(Badge, { variant: p.status === "SUCCEEDED" ? "success" : "secondary", children: p.status.charAt(0) + p.status.slice(1).toLowerCase() }) }),
|
|
59
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${pos.num}`, children: `${p.direction === "out" ? "-" : ""}${formatMinor(
|
|
60
|
+
p.amountMinor,
|
|
61
|
+
p.currency
|
|
62
|
+
)}` }),
|
|
63
|
+
/* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: atTime(p.at) })
|
|
64
|
+
] }, p.id))
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
TransactionsTab
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=transactions-tab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pos/transactions-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { Badge, Button, TableRow, Th } from '@withpotter/ui-kit';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { ListCard } from '../_ui/list-card';\nimport list from '../_ui/list-card.module.css';\nimport { isForbidden, posErrorText } from '../_pos/pos-rules';\nimport { atTime } from '../_pos/pos-words';\nimport { usePosPayments, usePosTerminals } from '../_pos/use-pos';\nimport pos from '../_pos/pos.module.css';\n\n/**\n * Card payments taken on every business's terminals, newest first. The payments carry the business's\n * id, not its name, so the names come from the terminals (the API's page is as large as it allows).\n */\nexport function TransactionsTab() {\n const q = usePosPayments();\n const terminals = usePosTerminals({ limit: 100 });\n const rows = useMemo(\n () => q.data?.pages.flatMap((p) => p.rows) ?? [],\n [q.data],\n );\n const names = useMemo(\n () =>\n new Map(terminals.data?.data.map((t) => [t.tenant.id, t.tenant.name])),\n [terminals.data],\n );\n\n return (\n <ListCard\n title=\"Transactions\"\n count={q.hasNextPage ? undefined : rows.length}\n columns={6}\n minWidth={900}\n isLoading={q.isLoading}\n isError={q.isError && !q.data}\n onRetry={() => void q.refetch()}\n errorTitle={\n isForbidden(q.error)\n ? \"You don't have permission to see transactions.\"\n : posErrorText(q.error, 'Transactions did not load.')\n }\n isEmpty={rows.length === 0}\n emptyTitle=\"No card payments yet.\"\n head={\n <tr>\n <Th>Business</Th>\n <Th>Terminal</Th>\n <Th>Reference</Th>\n <Th>Status</Th>\n <Th>Amount</Th>\n <Th>When</Th>\n </tr>\n }\n footer={\n q.hasNextPage ? (\n <div className={pos.stack}>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n disabled={q.isFetchingNextPage}\n onClick={() => void q.fetchNextPage()}\n >\n {q.isFetchingNextPage ? 'Loading…' : 'Show more'}\n </Button>\n </div>\n ) : undefined\n }\n >\n {rows.map((p) => (\n <TableRow key={p.id}>\n <td>{names.get(p.tenantId) ?? '–'}</td>\n <td>{p.deviceLabel ?? '–'}</td>\n <td className={list.muted}>{p.reference ?? '–'}</td>\n <td className={list.nowrap}>\n <Badge variant={p.status === 'SUCCEEDED' ? 'success' : 'secondary'}>\n {p.status.charAt(0) + p.status.slice(1).toLowerCase()}\n </Badge>\n </td>\n <td className={`${list.nowrap} ${pos.num}`}>\n {`${p.direction === 'out' ? '-' : ''}${formatMinor(\n p.amountMinor,\n p.currency,\n )}`}\n </td>\n <td className={`${list.nowrap} ${list.muted}`}>{atTime(p.at)}</td>\n </TableRow>\n ))}\n </ListCard>\n );\n}\n"],"mappings":";AA8CQ,SACE,KADF;AA5CR,SAAS,eAAe;AACxB,SAAS,OAAO,QAAQ,UAAU,UAAU;AAC5C,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,aAAa,oBAAoB;AAC1C,SAAS,cAAc;AACvB,SAAS,gBAAgB,uBAAuB;AAChD,OAAO,SAAS;AAMT,SAAS,kBAAkB;AAChC,QAAM,IAAI,eAAe;AACzB,QAAM,YAAY,gBAAgB,EAAE,OAAO,IAAI,CAAC;AAChD,QAAM,OAAO;AAAA,IACX,MAAM,EAAE,MAAM,MAAM,QAAQ,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC;AAAA,IAC/C,CAAC,EAAE,IAAI;AAAA,EACT;AACA,QAAM,QAAQ;AAAA,IACZ,MACE,IAAI,IAAI,UAAU,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC;AAAA,IACvE,CAAC,UAAU,IAAI;AAAA,EACjB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,EAAE,cAAc,SAAY,KAAK;AAAA,MACxC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW,EAAE;AAAA,MACb,SAAS,EAAE,WAAW,CAAC,EAAE;AAAA,MACzB,SAAS,MAAM,KAAK,EAAE,QAAQ;AAAA,MAC9B,YACE,YAAY,EAAE,KAAK,IACf,mDACA,aAAa,EAAE,OAAO,4BAA4B;AAAA,MAExD,SAAS,KAAK,WAAW;AAAA,MACzB,YAAW;AAAA,MACX,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,sBAAQ;AAAA,QACZ,oBAAC,MAAG,uBAAS;AAAA,QACb,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,oBAAM;AAAA,QACV,oBAAC,MAAG,kBAAI;AAAA,SACV;AAAA,MAEF,QACE,EAAE,cACA,oBAAC,SAAI,WAAW,IAAI,OAClB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,UAAU,EAAE;AAAA,UACZ,SAAS,MAAM,KAAK,EAAE,cAAc;AAAA,UAEnC,YAAE,qBAAqB,kBAAa;AAAA;AAAA,MACvC,GACF,IACE;AAAA,MAGL,eAAK,IAAI,CAAC,MACT,qBAAC,YACC;AAAA,4BAAC,QAAI,gBAAM,IAAI,EAAE,QAAQ,KAAK,UAAI;AAAA,QAClC,oBAAC,QAAI,YAAE,eAAe,UAAI;AAAA,QAC1B,oBAAC,QAAG,WAAW,KAAK,OAAQ,YAAE,aAAa,UAAI;AAAA,QAC/C,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,SAAM,SAAS,EAAE,WAAW,cAAc,YAAY,aACpD,YAAE,OAAO,OAAO,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC,EAAE,YAAY,GACtD,GACF;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,IAAI,GAAG,IACrC,aAAG,EAAE,cAAc,QAAQ,MAAM,EAAE,GAAG;AAAA,UACrC,EAAE;AAAA,UACF,EAAE;AAAA,QACJ,CAAC,IACH;AAAA,QACA,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IAAK,iBAAO,EAAE,EAAE,GAAE;AAAA,WAfhD,EAAE,EAgBjB,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { byCurrency, formatPlanPrice } from "@withpotter/lib/plan-price";
|
|
2
|
+
function priceList(prices, currencies) {
|
|
3
|
+
return Object.keys(prices).sort(byCurrency).map(
|
|
4
|
+
(code) => formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent)
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
function planLines(plans, currencies, breakdown) {
|
|
8
|
+
const earned = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const row of breakdown) {
|
|
10
|
+
const seen = earned.get(row.planId);
|
|
11
|
+
earned.set(row.planId, {
|
|
12
|
+
subscribers: (seen?.subscribers ?? 0) + row.subscribers,
|
|
13
|
+
mrr: (seen?.mrr ?? 0) + row.mrr
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return plans.map((plan) => {
|
|
17
|
+
const version = plan.live ?? plan.open;
|
|
18
|
+
const prices = version?.prices ?? {};
|
|
19
|
+
const hit = earned.get(plan.id);
|
|
20
|
+
return {
|
|
21
|
+
id: plan.id,
|
|
22
|
+
name: version?.name ?? plan.slug,
|
|
23
|
+
slug: plan.slug,
|
|
24
|
+
prices: version ? priceList(prices, currencies) : [],
|
|
25
|
+
free: Object.keys(prices).length > 0 && Object.values(prices).every((minor) => /^0*$/.test(minor)),
|
|
26
|
+
draft: plan.live === null,
|
|
27
|
+
subscribers: hit?.subscribers ?? plan.subscribers,
|
|
28
|
+
mrr: hit?.mrr ?? 0
|
|
29
|
+
};
|
|
30
|
+
}).sort((a, b) => b.mrr - a.mrr || b.subscribers - a.subscribers || a.name.localeCompare(b.name));
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
planLines
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=plan-lines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/plan-lines.ts"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PriceCurrency } from '@withpotter/lib/admin-pricing-api';\nimport { byCurrency, formatPlanPrice } from '@withpotter/lib/plan-price';\nimport type { PlanSummary } from '@withpotter/lib/pricing-plans-types';\n\n/** One line per currency, at that currency's exponent. */\nfunction priceList(prices: Record<string, string>, currencies: PriceCurrency[]): string[] {\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) =>\n formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent),\n );\n}\n\n/** What one plan shows: its catalogue entry joined with what it earns. */\nexport interface PlanLine {\n id: string;\n name: string;\n slug: string;\n prices: string[];\n /** Nothing to pay: every price is zero. */\n free: boolean;\n /** Created but not live yet. */\n draft: boolean;\n subscribers: number;\n mrr: number;\n}\n\n/**\n * Every plan an admin has created, one line each, with the name it was given\n * (Free, Basic, Premium and so on). The plans list is the source of the rows, so\n * a plan nobody is on yet still shows, with 0 subscribers; the revenue figures\n * for it are joined in by plan id.\n */\nexport function planLines(\n plans: readonly PlanSummary[],\n currencies: PriceCurrency[],\n breakdown: PlatformRevenue['recurring']['planBreakdown'],\n): PlanLine[] {\n const earned = new Map<string, { subscribers: number; mrr: number }>();\n\n for (const row of breakdown) {\n const seen = earned.get(row.planId);\n\n // A plan billed monthly and yearly comes as two rows: add them up.\n earned.set(row.planId, {\n subscribers: (seen?.subscribers ?? 0) + row.subscribers,\n mrr: (seen?.mrr ?? 0) + row.mrr,\n });\n }\n\n return plans\n .map((plan): PlanLine => {\n const version = plan.live ?? plan.open;\n const prices: Record<string, string> = version?.prices ?? {};\n const hit = earned.get(plan.id);\n\n return {\n id: plan.id,\n name: version?.name ?? plan.slug,\n slug: plan.slug,\n prices: version ? priceList(prices, currencies) : [],\n free:\n Object.keys(prices).length > 0 &&\n Object.values(prices).every((minor) => /^0*$/.test(minor)),\n draft: plan.live === null,\n subscribers: hit?.subscribers ?? plan.subscribers,\n mrr: hit?.mrr ?? 0,\n };\n })\n .sort((a, b) => b.mrr - a.mrr || b.subscribers - a.subscribers || a.name.localeCompare(b.name));\n}\n"],"mappings":"AAEA,SAAS,YAAY,uBAAuB;AAI5C,SAAS,UAAU,QAAgC,YAAuC;AACxF,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,UAAU,EACf;AAAA,IAAI,CAAC,SACJ,gBAAgB,OAAO,IAAI,GAAG,MAAM,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,QAAQ;AAAA,EACvF;AACJ;AAsBO,SAAS,UACd,OACA,YACA,WACY;AACZ,QAAM,SAAS,oBAAI,IAAkD;AAErE,aAAW,OAAO,WAAW;AAC3B,UAAM,OAAO,OAAO,IAAI,IAAI,MAAM;AAGlC,WAAO,IAAI,IAAI,QAAQ;AAAA,MACrB,cAAc,MAAM,eAAe,KAAK,IAAI;AAAA,MAC5C,MAAM,MAAM,OAAO,KAAK,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO,MACJ,IAAI,CAAC,SAAmB;AACvB,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,SAAiC,SAAS,UAAU,CAAC;AAC3D,UAAM,MAAM,OAAO,IAAI,KAAK,EAAE;AAE9B,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B,MAAM,KAAK;AAAA,MACX,QAAQ,UAAU,UAAU,QAAQ,UAAU,IAAI,CAAC;AAAA,MACnD,MACE,OAAO,KAAK,MAAM,EAAE,SAAS,KAC7B,OAAO,OAAO,MAAM,EAAE,MAAM,CAAC,UAAU,OAAO,KAAK,KAAK,CAAC;AAAA,MAC3D,OAAO,KAAK,SAAS;AAAA,MACrB,aAAa,KAAK,eAAe,KAAK;AAAA,MACtC,KAAK,KAAK,OAAO;AAAA,IACnB;AAAA,EACF,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAClG;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
|
|
3
|
+
import { Panel } from "../_ui";
|
|
4
|
+
import styles from "./revenue.module.css";
|
|
5
|
+
const integer = new Intl.NumberFormat("en-US");
|
|
6
|
+
function PlansPanel({
|
|
7
|
+
lines,
|
|
8
|
+
money,
|
|
9
|
+
caption
|
|
10
|
+
}) {
|
|
11
|
+
const totalMrr = lines.reduce((sum, p) => sum + p.mrr, 0);
|
|
12
|
+
return /* @__PURE__ */ jsx(Panel, { title: "Plans", count: lines.length || void 0, caption, flush: true, children: lines.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "No plans created yet." }) : /* @__PURE__ */ jsx(
|
|
13
|
+
TableShell,
|
|
14
|
+
{
|
|
15
|
+
bare: true,
|
|
16
|
+
minWidth: 520,
|
|
17
|
+
density: "compact",
|
|
18
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
19
|
+
/* @__PURE__ */ jsx(Th, { children: "Plan" }),
|
|
20
|
+
/* @__PURE__ */ jsx(Th, { align: "right", children: "Price a month" }),
|
|
21
|
+
/* @__PURE__ */ jsx(Th, { align: "right", children: "Subscribers" }),
|
|
22
|
+
/* @__PURE__ */ jsx(Th, { align: "right", children: "Monthly income" }),
|
|
23
|
+
/* @__PURE__ */ jsx(Th, { align: "right", children: "Share" })
|
|
24
|
+
] }),
|
|
25
|
+
children: lines.map((p) => {
|
|
26
|
+
const share = totalMrr > 0 ? p.mrr / totalMrr * 100 : 0;
|
|
27
|
+
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
28
|
+
/* @__PURE__ */ jsx("td", { className: styles.name, children: /* @__PURE__ */ jsxs("span", { className: styles.tenantText, children: [
|
|
29
|
+
p.name,
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: styles.slug, children: p.draft ? `${p.slug} \xB7 not live` : p.slug })
|
|
31
|
+
] }) }),
|
|
32
|
+
/* @__PURE__ */ jsx("td", { className: styles.num, children: p.free ? "Free" : p.prices.length ? p.prices.map((l) => /* @__PURE__ */ jsx("div", { children: l }, l)) : "\u2013" }),
|
|
33
|
+
/* @__PURE__ */ jsx("td", { className: styles.num, children: integer.format(p.subscribers) }),
|
|
34
|
+
/* @__PURE__ */ jsx("td", { className: styles.numStrong, children: money(p.mrr) }),
|
|
35
|
+
/* @__PURE__ */ jsx("td", { className: styles.num, children: /* @__PURE__ */ jsxs("span", { className: styles.share, children: [
|
|
36
|
+
/* @__PURE__ */ jsx("span", { className: styles.shareTrack, "aria-hidden": true, children: /* @__PURE__ */ jsx("span", { className: styles.shareFill, style: { width: `${Math.min(share, 100)}%` } }) }),
|
|
37
|
+
share.toFixed(1),
|
|
38
|
+
"%"
|
|
39
|
+
] }) })
|
|
40
|
+
] }, p.id);
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
) });
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
PlansPanel
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=plans-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/plans-panel.tsx"],"sourcesContent":["import { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { Panel } from '../_ui';\nimport type { PlanLine } from './plan-lines';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\nexport function PlansPanel({\n lines,\n money,\n caption,\n}: {\n lines: readonly PlanLine[];\n money: (value: number) => string;\n caption: string;\n}) {\n const totalMrr = lines.reduce((sum, p) => sum + p.mrr, 0);\n\n return (\n <Panel title=\"Plans\" count={lines.length || undefined} caption={caption} flush>\n {lines.length === 0 ? (\n <EmptyNote className={styles.empty}>No plans created yet.</EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={520}\n density=\"compact\"\n head={\n <tr>\n <Th>Plan</Th>\n <Th align=\"right\">Price a month</Th>\n <Th align=\"right\">Subscribers</Th>\n <Th align=\"right\">Monthly income</Th>\n <Th align=\"right\">Share</Th>\n </tr>\n }\n >\n {lines.map((p) => {\n const share = totalMrr > 0 ? (p.mrr / totalMrr) * 100 : 0;\n\n return (\n <TableRow key={p.id}>\n <td className={styles.name}>\n <span className={styles.tenantText}>\n {p.name}\n <span className={styles.slug}>{p.draft ? `${p.slug} · not live` : p.slug}</span>\n </span>\n </td>\n <td className={styles.num}>\n {p.free ? 'Free' : p.prices.length ? p.prices.map((l) => <div key={l}>{l}</div>) : '–'}\n </td>\n <td className={styles.num}>{integer.format(p.subscribers)}</td>\n <td className={styles.numStrong}>{money(p.mrr)}</td>\n <td className={styles.num}>\n <span className={styles.share}>\n <span className={styles.shareTrack} aria-hidden>\n <span className={styles.shareFill} style={{ width: `${Math.min(share, 100)}%` }} />\n </span>\n {share.toFixed(1)}%\n </span>\n </td>\n </TableRow>\n );\n })}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":"AAqBQ,cAOI,YAPJ;AArBR,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,aAAa;AAEtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAEtC,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,WAAW,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAExD,SACE,oBAAC,SAAM,OAAM,SAAQ,OAAO,MAAM,UAAU,QAAW,SAAkB,OAAK,MAC3E,gBAAM,WAAW,IAChB,oBAAC,aAAU,WAAW,OAAO,OAAO,mCAAqB,IAEzD;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ,UAAU;AAAA,MACV,SAAQ;AAAA,MACR,MACE,qBAAC,QACC;AAAA,4BAAC,MAAG,kBAAI;AAAA,QACR,oBAAC,MAAG,OAAM,SAAQ,2BAAa;AAAA,QAC/B,oBAAC,MAAG,OAAM,SAAQ,yBAAW;AAAA,QAC7B,oBAAC,MAAG,OAAM,SAAQ,4BAAc;AAAA,QAChC,oBAAC,MAAG,OAAM,SAAQ,mBAAK;AAAA,SACzB;AAAA,MAGD,gBAAM,IAAI,CAAC,MAAM;AAChB,cAAM,QAAQ,WAAW,IAAK,EAAE,MAAM,WAAY,MAAM;AAExD,eACE,qBAAC,YACC;AAAA,8BAAC,QAAG,WAAW,OAAO,MACpB,+BAAC,UAAK,WAAW,OAAO,YACrB;AAAA,cAAE;AAAA,YACH,oBAAC,UAAK,WAAW,OAAO,MAAO,YAAE,QAAQ,GAAG,EAAE,IAAI,mBAAgB,EAAE,MAAK;AAAA,aAC3E,GACF;AAAA,UACA,oBAAC,QAAG,WAAW,OAAO,KACnB,YAAE,OAAO,SAAS,EAAE,OAAO,SAAS,EAAE,OAAO,IAAI,CAAC,MAAM,oBAAC,SAAa,eAAJ,CAAM,CAAM,IAAI,UACrF;AAAA,UACA,oBAAC,QAAG,WAAW,OAAO,KAAM,kBAAQ,OAAO,EAAE,WAAW,GAAE;AAAA,UAC1D,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,UAC/C,oBAAC,QAAG,WAAW,OAAO,KACpB,+BAAC,UAAK,WAAW,OAAO,OACtB;AAAA,gCAAC,UAAK,WAAW,OAAO,YAAY,eAAW,MAC7C,8BAAC,UAAK,WAAW,OAAO,WAAW,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,GAAG,CAAC,IAAI,GAAG,GACnF;AAAA,YACC,MAAM,QAAQ,CAAC;AAAA,YAAE;AAAA,aACpB,GACF;AAAA,aAnBa,EAAE,EAoBjB;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH,GAEJ;AAEJ;","names":[]}
|
|
@@ -27,6 +27,15 @@ function RevenueKpis({
|
|
|
27
27
|
children: [
|
|
28
28
|
part === "olive" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
29
|
/* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
30
|
+
StatCard,
|
|
31
|
+
{
|
|
32
|
+
label: "Plan income collected",
|
|
33
|
+
value: tracked && recurring.collected !== void 0 ? /* @__PURE__ */ jsx(CountUp, { value: recurring.collected, format: money2 }) : notTracked,
|
|
34
|
+
hint: `Plan payments that actually went through in this window, in ${data.currency || "NGN"}. The monthly figure beside it is what is expected, this is what came in.`,
|
|
35
|
+
sub: tracked ? "In this window" : noSource
|
|
36
|
+
}
|
|
37
|
+
) }),
|
|
38
|
+
/* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
30
39
|
StatCard,
|
|
31
40
|
{
|
|
32
41
|
label: "Plan income each month",
|
|
@@ -75,7 +84,42 @@ function RevenueKpis({
|
|
|
75
84
|
sparkColor: TONE.money.fg
|
|
76
85
|
}
|
|
77
86
|
) }),
|
|
78
|
-
extra
|
|
87
|
+
extra,
|
|
88
|
+
data.merchantWalletsByCurrency && /* @__PURE__ */ jsx(Reveal, { index: 4, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
89
|
+
StatCard,
|
|
90
|
+
{
|
|
91
|
+
label: "In merchants' wallets",
|
|
92
|
+
value: /* @__PURE__ */ jsx(
|
|
93
|
+
MoneyLines,
|
|
94
|
+
{
|
|
95
|
+
rows: data.merchantWalletsByCurrency,
|
|
96
|
+
currency,
|
|
97
|
+
amount: (r) => r.amount,
|
|
98
|
+
decimals: 2,
|
|
99
|
+
empty: "Nothing held"
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
hint: "What merchants hold in their wallets right now: pending, available, held back and on its way to a bank, added together. A balance today, so it does not follow the dates above.",
|
|
103
|
+
sub: "Right now"
|
|
104
|
+
}
|
|
105
|
+
) }),
|
|
106
|
+
data.merchantPayoutsByCurrency && /* @__PURE__ */ jsx(Reveal, { index: 5, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
107
|
+
StatCard,
|
|
108
|
+
{
|
|
109
|
+
label: "Merchants sent out",
|
|
110
|
+
value: /* @__PURE__ */ jsx(
|
|
111
|
+
MoneyLines,
|
|
112
|
+
{
|
|
113
|
+
rows: data.merchantPayoutsByCurrency,
|
|
114
|
+
currency,
|
|
115
|
+
amount: (r) => r.amount,
|
|
116
|
+
decimals: 2,
|
|
117
|
+
empty: "Nothing sent out"
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
hint: "What merchants withdrew to their bank accounts in this window. Payouts the bank sent back are taken off."
|
|
121
|
+
}
|
|
122
|
+
) })
|
|
79
123
|
] })
|
|
80
124
|
]
|
|
81
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { ReactNode } from 'react';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * The headline figures, in two groups that are never added together: what\n * merchants sold (`merchant`), and what Olive itself earns from plans (`olive`).\n */\nexport function RevenueKpis({\n part,\n extra,\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n part: 'merchant' | 'olive';\n /** More cards for the merchant group, in the same row. */\n extra?: ReactNode;\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without the plan payments connected the API sends 0s; say so rather than show a zero.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not set up yet';\n const noSource = 'The plan payments are not connected yet';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section\n aria-label={part === 'olive' ? 'What Olive earns' : 'What merchants sold'}\n className=\"adm-grid\"\n >\n {part === 'olive' && (\n <>\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted.`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12.\"\n sub={tracked ? 'Each month times 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What one paying merchant pays in a month, on average.\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\n </>\n )}\n {part === 'merchant' && (\n <>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n {extra}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"AAoDQ,mBAMM,KANN;AApDR,SAAS,kBAAkB;AAG3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAMtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,UAAU,qBAAqB;AAAA,MACpD,WAAU;AAAA,MAET;AAAA,iBAAS,WACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,cAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,UAER,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,UACzC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,eAAe;AAAA;AAAA,UAChC,GACF;AAAA,WACE;AAAA,QAED,SAAS,cACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,cACtG,OAAO,QAAQ,WAAW;AAAA,cAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,UACzB,GACF;AAAA,UACK;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { ReactNode } from 'react';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * The headline figures, in two groups that are never added together: what\n * merchants sold (`merchant`), and what Olive itself earns from plans (`olive`).\n */\nexport function RevenueKpis({\n part,\n extra,\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n part: 'merchant' | 'olive';\n /** More cards for the merchant group, in the same row. */\n extra?: ReactNode;\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without the plan payments connected the API sends 0s; say so rather than show a zero.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not set up yet';\n const noSource = 'The plan payments are not connected yet';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section\n aria-label={part === 'olive' ? 'What Olive earns' : 'What merchants sold'}\n className=\"adm-grid\"\n >\n {part === 'olive' && (\n <>\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income collected\"\n value={\n tracked && recurring.collected !== undefined ? (\n <CountUp value={recurring.collected} format={money2} />\n ) : (\n notTracked\n )\n }\n hint={`Plan payments that actually went through in this window, in ${data.currency || 'NGN'}. The monthly figure beside it is what is expected, this is what came in.`}\n sub={tracked ? 'In this window' : noSource}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted.`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12.\"\n sub={tracked ? 'Each month times 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What one paying merchant pays in a month, on average.\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\n </>\n )}\n {part === 'merchant' && (\n <>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n {extra}\n {data.merchantWalletsByCurrency && (\n <Reveal index={4} className=\"adm-span-3\">\n <StatCard\n label=\"In merchants' wallets\"\n value={\n <MoneyLines\n rows={data.merchantWalletsByCurrency}\n currency={currency}\n amount={(r) => r.amount}\n decimals={2}\n empty=\"Nothing held\"\n />\n }\n hint=\"What merchants hold in their wallets right now: pending, available, held back and on its way to a bank, added together. A balance today, so it does not follow the dates above.\"\n sub=\"Right now\"\n />\n </Reveal>\n )}\n {data.merchantPayoutsByCurrency && (\n <Reveal index={5} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants sent out\"\n value={\n <MoneyLines\n rows={data.merchantPayoutsByCurrency}\n currency={currency}\n amount={(r) => r.amount}\n decimals={2}\n empty=\"Nothing sent out\"\n />\n }\n hint=\"What merchants withdrew to their bank accounts in this window. Payouts the bank sent back are taken off.\"\n />\n </Reveal>\n )}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -10,6 +10,8 @@ import { MerchantHeadline } from "./merchant-headline";
|
|
|
10
10
|
import { MethodCards } from "./method-cards";
|
|
11
11
|
import { HealthPanel } from "./health-panel";
|
|
12
12
|
import { PlanMixPanel } from "./plan-mix-panel";
|
|
13
|
+
import { planLines } from "./plan-lines";
|
|
14
|
+
import { PlansPanel } from "./plans-panel";
|
|
13
15
|
import { platformFeeRows } from "./platform-fees";
|
|
14
16
|
import { RevenueKpis } from "./revenue-kpis";
|
|
15
17
|
import { SubscriptionsPanel } from "./subscriptions-panel";
|
|
@@ -27,7 +29,8 @@ function RevenueOverview({
|
|
|
27
29
|
span,
|
|
28
30
|
recurringCaption,
|
|
29
31
|
channels,
|
|
30
|
-
ledger
|
|
32
|
+
ledger,
|
|
33
|
+
plans
|
|
31
34
|
}) {
|
|
32
35
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
33
36
|
const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
|
|
@@ -131,7 +134,14 @@ function RevenueOverview({
|
|
|
131
134
|
caption: recurringCaption
|
|
132
135
|
}
|
|
133
136
|
) }),
|
|
134
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
|
|
137
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
138
|
+
PlansPanel,
|
|
139
|
+
{
|
|
140
|
+
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
141
|
+
money,
|
|
142
|
+
caption: recurringCaption
|
|
143
|
+
}
|
|
144
|
+
) : /* @__PURE__ */ jsx(
|
|
135
145
|
PlanMixPanel,
|
|
136
146
|
{
|
|
137
147
|
planBreakdown: data.recurring.planBreakdown,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n channels.length > 0 && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n )\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n\n <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n channels.length > 0 && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n )\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n\n <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "../_ui";
|
|
27
27
|
import { MerchantPaymentsView } from "./merchant-payments-view";
|
|
28
28
|
import { MerchantsView } from "./merchants-view";
|
|
29
|
+
import { usePlanList } from "../pricing/use-plans";
|
|
29
30
|
import { RevenueOverview } from "./revenue-overview";
|
|
30
31
|
import { RevenueRetention } from "./revenue-retention";
|
|
31
32
|
import { SubscriptionsView } from "./subscriptions-view";
|
|
@@ -56,6 +57,7 @@ function RevenueWorkspace() {
|
|
|
56
57
|
startDate: range.startDate,
|
|
57
58
|
endDate: range.endDate
|
|
58
59
|
});
|
|
60
|
+
const plans = usePlanList();
|
|
59
61
|
const cockpit = usePlatformCockpit({
|
|
60
62
|
startDate: range.startDate,
|
|
61
63
|
endDate: range.endDate
|
|
@@ -134,7 +136,8 @@ function RevenueWorkspace() {
|
|
|
134
136
|
span,
|
|
135
137
|
recurringCaption: `${period} \xB7 ${recurringCurrency}`,
|
|
136
138
|
channels: channels.data?.byChannel,
|
|
137
|
-
ledger: ledger.data
|
|
139
|
+
ledger: ledger.data,
|
|
140
|
+
plans: plans.data
|
|
138
141
|
}
|
|
139
142
|
);
|
|
140
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { MerchantsView } from './merchants-view';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'by-merchant', label: 'Merchants' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'by-merchant') {\n body = <MerchantsView startDate={range.startDate} endDate={range.endDate} />;\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { MerchantsView } from './merchants-view';\nimport { usePlanList } from '../pricing/use-plans';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'by-merchant', label: 'Merchants' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const plans = usePlanList();\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'by-merchant') {\n body = <MerchantsView startDate={range.startDate} endDate={range.endDate} />;\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n plans={plans.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/pages-admin",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.112",
|
|
4
4
|
"description": "Box console platform-admin routes: overview, revenue, accounts, tenants, admins, roles, audit.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@radix-ui/react-popover": "^1.1.23",
|
|
33
33
|
"@tanstack/react-query": "^5.90.20",
|
|
34
|
-
"@withpotter/lib": "^0.13.0-next.
|
|
35
|
-
"@withpotter/platform-admin": "^0.13.0-next.
|
|
36
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
34
|
+
"@withpotter/lib": "^0.13.0-next.112",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.112",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.112",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|