@withpotter/pages-admin 0.13.0-next.101 → 0.13.0-next.103

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.
Files changed (60) hide show
  1. package/dist/admin/_pos/tenant-operators.js +73 -0
  2. package/dist/admin/_pos/tenant-operators.js.map +1 -0
  3. package/dist/admin/_pos/tenant-pos-figures.js +82 -0
  4. package/dist/admin/_pos/tenant-pos-figures.js.map +1 -0
  5. package/dist/admin/_pos/use-pos.js +12 -0
  6. package/dist/admin/_pos/use-pos.js.map +1 -1
  7. package/dist/admin/hooks.js +19 -0
  8. package/dist/admin/hooks.js.map +1 -1
  9. package/dist/admin/pos/shifts-tab.js +15 -8
  10. package/dist/admin/pos/shifts-tab.js.map +1 -1
  11. package/dist/admin/revenue/cohort-panel.js +5 -5
  12. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  13. package/dist/admin/revenue/health-panel.js +17 -17
  14. package/dist/admin/revenue/health-panel.js.map +1 -1
  15. package/dist/admin/revenue/ledger-model.js +7 -0
  16. package/dist/admin/revenue/ledger-model.js.map +1 -1
  17. package/dist/admin/revenue/ledger.module.css +43 -0
  18. package/dist/admin/revenue/merchant-headline.js +37 -0
  19. package/dist/admin/revenue/merchant-headline.js.map +1 -0
  20. package/dist/admin/revenue/merchant-payments-view.js +46 -22
  21. package/dist/admin/revenue/merchant-payments-view.js.map +1 -1
  22. package/dist/admin/revenue/merchants-view.js +99 -0
  23. package/dist/admin/revenue/merchants-view.js.map +1 -0
  24. package/dist/admin/revenue/payment-drawer.js +53 -0
  25. package/dist/admin/revenue/payment-drawer.js.map +1 -0
  26. package/dist/admin/revenue/plan-mix-panel.js +1 -1
  27. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  28. package/dist/admin/revenue/plan-payment-drawer.js +42 -0
  29. package/dist/admin/revenue/plan-payment-drawer.js.map +1 -0
  30. package/dist/admin/revenue/print-summary-button.js +34 -0
  31. package/dist/admin/revenue/print-summary-button.js.map +1 -0
  32. package/dist/admin/revenue/quality-panel.js +19 -19
  33. package/dist/admin/revenue/quality-panel.js.map +1 -1
  34. package/dist/admin/revenue/revenue-kpis.js +64 -50
  35. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  36. package/dist/admin/revenue/revenue-overview.js +68 -50
  37. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  38. package/dist/admin/revenue/revenue-retention.js +17 -13
  39. package/dist/admin/revenue/revenue-retention.js.map +1 -1
  40. package/dist/admin/revenue/revenue-workspace.js +16 -0
  41. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  42. package/dist/admin/revenue/subscriptions-panel.js +14 -14
  43. package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
  44. package/dist/admin/revenue/subscriptions-view.js +20 -20
  45. package/dist/admin/revenue/subscriptions-view.js.map +1 -1
  46. package/dist/admin/revenue/summary-download.js +34 -0
  47. package/dist/admin/revenue/summary-download.js.map +1 -0
  48. package/dist/admin/revenue/summary-print.js +82 -0
  49. package/dist/admin/revenue/summary-print.js.map +1 -0
  50. package/dist/admin/revenue/summary-rows.js +44 -0
  51. package/dist/admin/revenue/summary-rows.js.map +1 -0
  52. package/dist/admin/revenue/top-tenants-panel.js +1 -1
  53. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  54. package/dist/admin/revenue/url-filters.js +36 -0
  55. package/dist/admin/revenue/url-filters.js.map +1 -0
  56. package/dist/admin/revenue/use-url-filters.js +24 -0
  57. package/dist/admin/revenue/use-url-filters.js.map +1 -0
  58. package/dist/admin/tenants/[id]/tenant-pos.js +53 -16
  59. package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
  60. package/package.json +4 -4
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import { Fact, FactList } from "@withpotter/ui-kit";
5
+ import { StatusPill } from "../audit/status-pill";
6
+ import { Drawer } from "../_ui";
7
+ import { PURPOSE_WORDS, STATUS_WORDS, SUBSCRIPTION_METHOD_WORDS, minorToMoney, word } from "./ledger-model";
8
+ import styles from "./ledger.module.css";
9
+ const when = (iso) => new Date(iso).toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
10
+ const TONE = { succeeded: "success", pending: "pending", failed: "failed" };
11
+ function PlanPaymentDrawer({ row, onClose }) {
12
+ return /* @__PURE__ */ jsx(
13
+ Drawer,
14
+ {
15
+ open: Boolean(row),
16
+ onClose,
17
+ title: row ? minorToMoney(row.amountKobo, "NGN") : "Plan payment",
18
+ subtitle: row ? row.merchantName ?? "Unknown merchant" : void 0,
19
+ meta: row ? /* @__PURE__ */ jsx(StatusPill, { status: TONE[row.status] ?? "neutral", label: word(STATUS_WORDS, row.status) }) : void 0,
20
+ children: row ? /* @__PURE__ */ jsx("div", { className: styles.drawerBody, children: /* @__PURE__ */ jsxs(FactList, { columns: 2, children: [
21
+ /* @__PURE__ */ jsx(Fact, { label: "Paid on", value: when(row.paidAt ?? row.createdAt) }),
22
+ /* @__PURE__ */ jsx(Fact, { label: "Plan", value: row.plan.charAt(0).toUpperCase() + row.plan.slice(1) }),
23
+ /* @__PURE__ */ jsx(Fact, { label: "What it was for", value: word(PURPOSE_WORDS, row.purpose) }),
24
+ /* @__PURE__ */ jsx(Fact, { label: "How they paid", value: word(SUBSCRIPTION_METHOD_WORDS, row.method) }),
25
+ /* @__PURE__ */ jsx(
26
+ Fact,
27
+ {
28
+ label: "Merchant",
29
+ value: /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.tenantId}`, children: row.merchantName ?? "Open merchant" })
30
+ }
31
+ ),
32
+ /* @__PURE__ */ jsx(Fact, { label: "Email", value: row.merchantEmail ?? "\u2013" }),
33
+ /* @__PURE__ */ jsx(Fact, { label: "Reference", value: row.reference }),
34
+ row.status === "failed" ? /* @__PURE__ */ jsx(Fact, { label: "Why it failed", value: row.failureReason ?? "Not recorded" }) : null
35
+ ] }) }) : null
36
+ }
37
+ );
38
+ }
39
+ export {
40
+ PlanPaymentDrawer
41
+ };
42
+ //# sourceMappingURL=plan-payment-drawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/plan-payment-drawer.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { Fact, FactList } from '@withpotter/ui-kit';\nimport type { SubscriptionPaymentRow } from '@withpotter/lib/admin-api';\nimport { StatusPill } from '../audit/status-pill';\nimport { Drawer } from '../_ui';\nimport { PURPOSE_WORDS, STATUS_WORDS, SUBSCRIPTION_METHOD_WORDS, minorToMoney, word } from './ledger-model';\nimport styles from './ledger.module.css';\n\nconst when = (iso: string): string =>\n new Date(iso).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' });\n\nconst TONE: Record<string, string> = { succeeded: 'success', pending: 'pending', failed: 'failed' };\n\n/** One plan payment in full: which merchant paid, for which plan, and how it went. */\nexport function PlanPaymentDrawer({ row, onClose }: { row: SubscriptionPaymentRow | null; onClose: () => void }) {\n return (\n <Drawer\n open={Boolean(row)}\n onClose={onClose}\n title={row ? minorToMoney(row.amountKobo, 'NGN') : 'Plan payment'}\n subtitle={row ? (row.merchantName ?? 'Unknown merchant') : undefined}\n meta={\n row ? <StatusPill status={TONE[row.status] ?? 'neutral'} label={word(STATUS_WORDS, row.status)} /> : undefined\n }\n >\n {row ? (\n <div className={styles.drawerBody}>\n <FactList columns={2}>\n <Fact label=\"Paid on\" value={when(row.paidAt ?? row.createdAt)} />\n <Fact label=\"Plan\" value={row.plan.charAt(0).toUpperCase() + row.plan.slice(1)} />\n <Fact label=\"What it was for\" value={word(PURPOSE_WORDS, row.purpose)} />\n <Fact label=\"How they paid\" value={word(SUBSCRIPTION_METHOD_WORDS, row.method)} />\n <Fact\n label=\"Merchant\"\n value={<Link href={`/admin/tenants/${row.tenantId}`}>{row.merchantName ?? 'Open merchant'}</Link>}\n />\n <Fact label=\"Email\" value={row.merchantEmail ?? '–'} />\n <Fact label=\"Reference\" value={row.reference} />\n {row.status === 'failed' ? <Fact label=\"Why it failed\" value={row.failureReason ?? 'Not recorded'} /> : null}\n </FactList>\n </div>\n ) : null}\n </Drawer>\n );\n}\n"],"mappings":";AAwBc,cAKJ,YALI;AAtBd,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAE/B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,eAAe,cAAc,2BAA2B,cAAc,YAAY;AAC3F,OAAO,YAAY;AAEnB,MAAM,OAAO,CAAC,QACZ,IAAI,KAAK,GAAG,EAAE,eAAe,SAAS,EAAE,WAAW,UAAU,WAAW,QAAQ,CAAC;AAEnF,MAAM,OAA+B,EAAE,WAAW,WAAW,SAAS,WAAW,QAAQ,SAAS;AAG3F,SAAS,kBAAkB,EAAE,KAAK,QAAQ,GAAgE;AAC/G,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,QAAQ,GAAG;AAAA,MACjB;AAAA,MACA,OAAO,MAAM,aAAa,IAAI,YAAY,KAAK,IAAI;AAAA,MACnD,UAAU,MAAO,IAAI,gBAAgB,qBAAsB;AAAA,MAC3D,MACE,MAAM,oBAAC,cAAW,QAAQ,KAAK,IAAI,MAAM,KAAK,WAAW,OAAO,KAAK,cAAc,IAAI,MAAM,GAAG,IAAK;AAAA,MAGtG,gBACC,oBAAC,SAAI,WAAW,OAAO,YACrB,+BAAC,YAAS,SAAS,GACjB;AAAA,4BAAC,QAAK,OAAM,WAAU,OAAO,KAAK,IAAI,UAAU,IAAI,SAAS,GAAG;AAAA,QAChE,oBAAC,QAAK,OAAM,QAAO,OAAO,IAAI,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,KAAK,MAAM,CAAC,GAAG;AAAA,QAChF,oBAAC,QAAK,OAAM,mBAAkB,OAAO,KAAK,eAAe,IAAI,OAAO,GAAG;AAAA,QACvE,oBAAC,QAAK,OAAM,iBAAgB,OAAO,KAAK,2BAA2B,IAAI,MAAM,GAAG;AAAA,QAChF;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO,oBAAC,QAAK,MAAM,kBAAkB,IAAI,QAAQ,IAAK,cAAI,gBAAgB,iBAAgB;AAAA;AAAA,QAC5F;AAAA,QACA,oBAAC,QAAK,OAAM,SAAQ,OAAO,IAAI,iBAAiB,UAAK;AAAA,QACrD,oBAAC,QAAK,OAAM,aAAY,OAAO,IAAI,WAAW;AAAA,QAC7C,IAAI,WAAW,WAAW,oBAAC,QAAK,OAAM,iBAAgB,OAAO,IAAI,iBAAiB,gBAAgB,IAAK;AAAA,SAC1G,GACF,IACE;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Button, useToast } from "@withpotter/ui-kit";
4
+ import { ADMIN_NAME } from "../_shell/admin-brand";
5
+ import { summaryHtml } from "./summary-print";
6
+ function PrintSummaryButton({ model }) {
7
+ const { toast } = useToast();
8
+ const run = () => {
9
+ const frame = document.createElement("iframe");
10
+ frame.setAttribute("aria-hidden", "true");
11
+ frame.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
12
+ document.body.appendChild(frame);
13
+ const doc = frame.contentDocument;
14
+ const view = frame.contentWindow;
15
+ if (!doc || !view) {
16
+ frame.remove();
17
+ toast({ title: "The summary could not be opened. Please try again." });
18
+ return;
19
+ }
20
+ doc.open();
21
+ doc.write(summaryHtml(model, ADMIN_NAME, (/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { dateStyle: "long" })));
22
+ doc.close();
23
+ setTimeout(() => {
24
+ view.focus();
25
+ view.print();
26
+ setTimeout(() => frame.remove(), 1e3);
27
+ }, 150);
28
+ };
29
+ return /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: run, children: "Save as PDF" });
30
+ }
31
+ export {
32
+ PrintSummaryButton
33
+ };
34
+ //# sourceMappingURL=print-summary-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["'use client';\n\nimport { Button, useToast } from '@withpotter/ui-kit';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport { summaryHtml, type SummaryModel } from './summary-print';\n\n/**\n * Opens the one-page summary in the browser's print window, where \"Save as PDF\" makes the file. The page is drawn in a\n * hidden frame, so the admin itself is never printed and no pop-up is needed.\n */\nexport function PrintSummaryButton({ model }: { model: SummaryModel }) {\n const { toast } = useToast();\n\n const run = () => {\n const frame = document.createElement('iframe');\n frame.setAttribute('aria-hidden', 'true');\n frame.style.cssText = 'position:fixed;width:0;height:0;border:0;visibility:hidden';\n document.body.appendChild(frame);\n\n const doc = frame.contentDocument;\n const view = frame.contentWindow;\n\n if (!doc || !view) {\n frame.remove();\n toast({ title: 'The summary could not be opened. Please try again.' });\n\n return;\n }\n\n doc.open();\n doc.write(summaryHtml(model, ADMIN_NAME, new Date().toLocaleDateString('en-GB', { dateStyle: 'long' })));\n doc.close();\n\n // Print once the page has laid out, and tidy the frame up when the print window closes.\n setTimeout(() => {\n view.focus();\n view.print();\n setTimeout(() => frame.remove(), 1000);\n }, 150);\n };\n\n return (\n <Button variant=\"secondary\" size=\"sm\" onClick={run}>\n Save as PDF\n </Button>\n );\n}\n"],"mappings":";AA0CI;AAxCJ,SAAS,QAAQ,gBAAgB;AACjC,SAAS,kBAAkB;AAC3B,SAAS,mBAAsC;AAMxC,SAAS,mBAAmB,EAAE,MAAM,GAA4B;AACrE,QAAM,EAAE,MAAM,IAAI,SAAS;AAE3B,QAAM,MAAM,MAAM;AAChB,UAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,UAAM,aAAa,eAAe,MAAM;AACxC,UAAM,MAAM,UAAU;AACtB,aAAS,KAAK,YAAY,KAAK;AAE/B,UAAM,MAAM,MAAM;AAClB,UAAM,OAAO,MAAM;AAEnB,QAAI,CAAC,OAAO,CAAC,MAAM;AACjB,YAAM,OAAO;AACb,YAAM,EAAE,OAAO,qDAAqD,CAAC;AAErE;AAAA,IACF;AAEA,QAAI,KAAK;AACT,QAAI,MAAM,YAAY,OAAO,aAAY,oBAAI,KAAK,GAAE,mBAAmB,SAAS,EAAE,WAAW,OAAO,CAAC,CAAC,CAAC;AACvG,QAAI,MAAM;AAGV,eAAW,MAAM;AACf,WAAK,MAAM;AACX,WAAK,MAAM;AACX,iBAAW,MAAM,MAAM,OAAO,GAAG,GAAI;AAAA,IACvC,GAAG,GAAG;AAAA,EACR;AAEA,SACE,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,KAAK,yBAEpD;AAEJ;","names":[]}
@@ -14,56 +14,56 @@ function QualityPanel({
14
14
  const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));
15
15
  const rows = [
16
16
  {
17
- label: "Net revenue retention",
18
- note: "Starting cohort MRR, net of churn",
17
+ label: "Income kept from existing merchants",
18
+ note: "Plan income from merchants who were here at the start, after upgrades, downgrades and those who left",
19
19
  value: quality.nrrPct == null ? "\u2013" : `${quality.nrrPct}%`,
20
- hint: `+${money(quality.expansionMrr)} exp \xB7 \u2212${money(
20
+ hint: `+${money(quality.expansionMrr)} upgrades \xB7 \u2212${money(
21
21
  quality.contractionMrr
22
- )} contr`
22
+ )} downgrades`
23
23
  },
24
24
  {
25
- label: "Quick ratio",
26
- note: "New + expansion vs churn + contraction",
25
+ label: "Growth against loss",
26
+ note: "Plan income gained (new merchants and upgrades) for every \u20A61 lost (merchants who left and downgrades)",
27
27
  value: quality.quickRatio == null ? "\u2013" : `${quality.quickRatio}\xD7`
28
28
  },
29
29
  {
30
- label: "Churn rate",
31
- note: "MRR lost as a share of the base",
30
+ label: "Plan income lost",
31
+ note: "Share of plan income lost to merchants who left",
32
32
  value: quality.mrrChurnRatePct == null ? "\u2013" : `${quality.mrrChurnRatePct}%`,
33
33
  tone: quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0 ? "danger" : "default"
34
34
  },
35
35
  {
36
- label: "Net GMV",
37
- note: "Merchant GMV after refunds",
36
+ label: "Merchant sales after refunds",
37
+ note: "What merchants sold, less what was refunded",
38
38
  value: lines((q) => q.netGmv),
39
39
  hint: `Gross ${lines((q) => q.grossGmv)}`
40
40
  },
41
41
  {
42
- label: "Refunded",
43
- note: "Refunded GMV in the window",
42
+ label: "Refunded sales",
43
+ note: "Value of sales that were refunded in this period",
44
44
  value: lines((q) => q.refundedGmv)
45
45
  },
46
46
  {
47
- label: "Refund rate",
48
- note: "Refunds as a share of gross GMV",
47
+ label: "Share refunded",
48
+ note: "Refunded sales as a share of all sales",
49
49
  value: one ? `${one.refundRatePct}%` : "\u2013",
50
50
  hint: one ? one.currency : "Pick a currency"
51
51
  },
52
52
  {
53
- label: "Refunded orders",
54
- note: "Order count, not GMV",
53
+ label: "Orders refunded",
54
+ note: "How many orders were refunded",
55
55
  value: integer.format(
56
56
  (gmv ?? []).filter((q) => !currency || q.currency === currency).reduce((n, q) => n + q.refundedOrders, 0)
57
57
  )
58
58
  },
59
59
  {
60
- label: "Logo churn rate",
61
- note: "Paying merchants lost in the window",
60
+ label: "Paying merchants lost",
61
+ note: "Share of paying merchants who stopped paying in this period",
62
62
  value: quality.logoChurnRatePct == null ? "\u2013" : `${quality.logoChurnRatePct}%`,
63
63
  tone: quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0 ? "danger" : "default"
64
64
  }
65
65
  ];
66
- return /* @__PURE__ */ jsx(Panel, { title: "Revenue quality", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
66
+ return /* @__PURE__ */ jsx(Panel, { title: "How healthy the income is", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
67
67
  }
68
68
  export {
69
69
  QualityPanel
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: 'Net revenue retention',\n note: 'Starting cohort MRR, net of churn',\n value: quality.nrrPct == null ? '–' : `${quality.nrrPct}%`,\n hint: `+${money(quality.expansionMrr)} exp · −${money(\n quality.contractionMrr,\n )} contr`,\n },\n {\n label: 'Quick ratio',\n note: 'New + expansion vs churn + contraction',\n value: quality.quickRatio == null ? '–' : `${quality.quickRatio}×`,\n },\n {\n label: 'Churn rate',\n note: 'MRR lost as a share of the base',\n value:\n quality.mrrChurnRatePct == null ? '–' : `${quality.mrrChurnRatePct}%`,\n tone:\n quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n {\n label: 'Net GMV',\n note: 'Merchant GMV after refunds',\n value: lines((q) => q.netGmv),\n hint: `Gross ${lines((q) => q.grossGmv)}`,\n },\n {\n label: 'Refunded',\n note: 'Refunded GMV in the window',\n value: lines((q) => q.refundedGmv),\n },\n {\n label: 'Refund rate',\n note: 'Refunds as a share of gross GMV',\n value: one ? `${one.refundRatePct}%` : '–',\n hint: one ? one.currency : 'Pick a currency',\n },\n {\n label: 'Refunded orders',\n note: 'Order count, not GMV',\n value: integer.format(\n (gmv ?? [])\n .filter((q) => !currency || q.currency === currency)\n .reduce((n, q) => n + q.refundedOrders, 0),\n ),\n },\n {\n label: 'Logo churn rate',\n note: 'Paying merchants lost in the window',\n value:\n quality.logoChurnRatePct == null ? '–' : `${quality.logoChurnRatePct}%`,\n tone:\n quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n ];\n\n return (\n <Panel title=\"Revenue quality\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6FM;AA5FN,SAAS,OAAO,YAAY,UAAU,cAAc;AACpD,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAKtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AAED,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,CAAC,WACb,WAAW,KAAK,UAAU,MAAM,EAAE,KAAK,QAAK,KAAK,SAAS,QAAQ;AACpE,QAAM,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ;AAE/C,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK;AAC1E,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,UAAU,OAAO,WAAM,GAAG,QAAQ,MAAM;AAAA,MACvD,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC,mBAAW;AAAA,QAC9C,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,cAAc,OAAO,WAAM,GAAG,QAAQ,UAAU;AAAA,IACjE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,mBAAmB,OAAO,WAAM,GAAG,QAAQ,eAAe;AAAA,MACpE,MACE,QAAQ,mBAAmB,QAAQ,QAAQ,kBAAkB,IACzD,WACA;AAAA,IACR;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM;AAAA,MAC5B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,WAAW;AAAA,IACnC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,GAAG,IAAI,aAAa,MAAM;AAAA,MACvC,MAAM,MAAM,IAAI,WAAW;AAAA,IAC7B;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,SACZ,OAAO,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ,EAClD,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,gBAAgB,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,oBAAoB,OAAO,WAAM,GAAG,QAAQ,gBAAgB;AAAA,MACtE,MACE,QAAQ,oBAAoB,QAAQ,QAAQ,mBAAmB,IAC3D,WACA;AAAA,IACR;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,mBAAkB,SAC7B,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: 'Income kept from existing merchants',\n note: 'Plan income from merchants who were here at the start, after upgrades, downgrades and those who left',\n value: quality.nrrPct == null ? '–' : `${quality.nrrPct}%`,\n hint: `+${money(quality.expansionMrr)} upgrades · −${money(\n quality.contractionMrr,\n )} downgrades`,\n },\n {\n label: 'Growth against loss',\n note: 'Plan income gained (new merchants and upgrades) for every ₦1 lost (merchants who left and downgrades)',\n value: quality.quickRatio == null ? '–' : `${quality.quickRatio}×`,\n },\n {\n label: 'Plan income lost',\n note: 'Share of plan income lost to merchants who left',\n value:\n quality.mrrChurnRatePct == null ? '–' : `${quality.mrrChurnRatePct}%`,\n tone:\n quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n {\n label: 'Merchant sales after refunds',\n note: 'What merchants sold, less what was refunded',\n value: lines((q) => q.netGmv),\n hint: `Gross ${lines((q) => q.grossGmv)}`,\n },\n {\n label: 'Refunded sales',\n note: 'Value of sales that were refunded in this period',\n value: lines((q) => q.refundedGmv),\n },\n {\n label: 'Share refunded',\n note: 'Refunded sales as a share of all sales',\n value: one ? `${one.refundRatePct}%` : '–',\n hint: one ? one.currency : 'Pick a currency',\n },\n {\n label: 'Orders refunded',\n note: 'How many orders were refunded',\n value: integer.format(\n (gmv ?? [])\n .filter((q) => !currency || q.currency === currency)\n .reduce((n, q) => n + q.refundedOrders, 0),\n ),\n },\n {\n label: 'Paying merchants lost',\n note: 'Share of paying merchants who stopped paying in this period',\n value:\n quality.logoChurnRatePct == null ? '–' : `${quality.logoChurnRatePct}%`,\n tone:\n quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n ];\n\n return (\n <Panel title=\"How healthy the income is\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6FM;AA5FN,SAAS,OAAO,YAAY,UAAU,cAAc;AACpD,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAKtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AAED,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,CAAC,WACb,WAAW,KAAK,UAAU,MAAM,EAAE,KAAK,QAAK,KAAK,SAAS,QAAQ;AACpE,QAAM,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ;AAE/C,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK;AAC1E,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,UAAU,OAAO,WAAM,GAAG,QAAQ,MAAM;AAAA,MACvD,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC,wBAAgB;AAAA,QACnD,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,cAAc,OAAO,WAAM,GAAG,QAAQ,UAAU;AAAA,IACjE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,mBAAmB,OAAO,WAAM,GAAG,QAAQ,eAAe;AAAA,MACpE,MACE,QAAQ,mBAAmB,QAAQ,QAAQ,kBAAkB,IACzD,WACA;AAAA,IACR;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM;AAAA,MAC5B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,WAAW;AAAA,IACnC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,GAAG,IAAI,aAAa,MAAM;AAAA,MACvC,MAAM,MAAM,IAAI,WAAW;AAAA,IAC7B;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,SACZ,OAAO,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ,EAClD,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,gBAAgB,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,oBAAoB,OAAO,WAAM,GAAG,QAAQ,gBAAgB;AAAA,MACtE,MACE,QAAQ,oBAAoB,QAAQ,QAAQ,mBAAmB,IAC3D,WACA;AAAA,IACR;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,6BAA4B,SACvC,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
@@ -1,9 +1,11 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ADMIN_NAME } from "../_shell/admin-brand";
3
3
  import { CountUp, Reveal, TONE } from "../_fx";
4
4
  import { MoneyLines, StatCard, rowFor } from "../_ui";
5
5
  const integer = new Intl.NumberFormat("en-US");
6
6
  function RevenueKpis({
7
+ part,
8
+ extra,
7
9
  data,
8
10
  money,
9
11
  money2,
@@ -17,55 +19,67 @@ function RevenueKpis({
17
19
  const noSource = "The plan payments are not connected yet";
18
20
  const gmv = rowFor(data.gmvByCurrency, focus);
19
21
  const lifetimeShare = gmv && gmv.lifetime > 0 ? `${Math.round(gmv.inRange / gmv.lifetime * 100)}% of lifetime` : void 0;
20
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Revenue indicators", className: "adm-grid", children: [
21
- /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
22
- StatCard,
23
- {
24
- label: "Plan income each month",
25
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
26
- 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. (MRR)`,
27
- sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : noSource
28
- }
29
- ) }),
30
- /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
31
- StatCard,
32
- {
33
- label: "Plan income over a year",
34
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
35
- hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)",
36
- sub: tracked ? "Each month times 12" : noSource
37
- }
38
- ) }),
39
- /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
40
- StatCard,
41
- {
42
- label: "Average per paying merchant",
43
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
44
- hint: "What one paying merchant pays in a month, on average. (ARPA)",
45
- sub: tracked ? "Each month" : noSource
46
- }
47
- ) }),
48
- /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
49
- StatCard,
50
- {
51
- label: "Merchant sales",
52
- value: /* @__PURE__ */ jsx(
53
- MoneyLines,
54
- {
55
- rows: data.gmvByCurrency,
56
- currency,
57
- amount: (g) => g.inRange,
58
- empty: "No sales yet"
59
- }
60
- ),
61
- href: "/admin/tenants",
62
- sub: lifetimeShare,
63
- 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)`,
64
- spark: focus ? gmvSpark : void 0,
65
- sparkColor: TONE.money.fg
66
- }
67
- ) })
68
- ] });
22
+ return /* @__PURE__ */ jsxs(
23
+ "section",
24
+ {
25
+ "aria-label": part === "olive" ? "What Olive earns" : "What merchants sold",
26
+ className: "adm-grid",
27
+ children: [
28
+ part === "olive" && /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
30
+ StatCard,
31
+ {
32
+ label: "Plan income each month",
33
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
34
+ 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. (MRR)`,
35
+ sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : noSource
36
+ }
37
+ ) }),
38
+ /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
39
+ StatCard,
40
+ {
41
+ label: "Plan income over a year",
42
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
43
+ hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)",
44
+ sub: tracked ? "Each month times 12" : noSource
45
+ }
46
+ ) }),
47
+ /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
48
+ StatCard,
49
+ {
50
+ label: "Average per paying merchant",
51
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
52
+ hint: "What one paying merchant pays in a month, on average. (ARPA)",
53
+ sub: tracked ? "Each month" : noSource
54
+ }
55
+ ) })
56
+ ] }),
57
+ part === "merchant" && /* @__PURE__ */ jsxs(Fragment, { children: [
58
+ /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
59
+ StatCard,
60
+ {
61
+ label: "Merchant sales",
62
+ value: /* @__PURE__ */ jsx(
63
+ MoneyLines,
64
+ {
65
+ rows: data.gmvByCurrency,
66
+ currency,
67
+ amount: (g) => g.inRange,
68
+ empty: "No sales yet"
69
+ }
70
+ ),
71
+ href: "/admin/tenants",
72
+ sub: lifetimeShare,
73
+ 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)`,
74
+ spark: focus ? gmvSpark : void 0,
75
+ sparkColor: TONE.money.fg
76
+ }
77
+ ) }),
78
+ extra
79
+ ] })
80
+ ]
81
+ }
82
+ );
69
83
  }
70
84
  export {
71
85
  RevenueKpis
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\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/** The four headline figures: recurring revenue first, then merchant GMV. */\nexport function RevenueKpis({\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\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 aria-label=\"Revenue indicators\" className=\"adm-grid\">\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. (MRR)`}\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. (ARR)\"\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. (ARPA)\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\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 </section>\n );\n}\n"],"mappings":"AAsCI,SAMU,KANV;AAtCJ,SAAS,kBAAkB;AAE3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;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,qBAAC,aAAQ,cAAW,sBAAqB,WAAU,YACjD;AAAA,wBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,QAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,IAER,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,IACzC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,eAAe;AAAA;AAAA,IAChC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX;AAAA,YACA,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,OAAM;AAAA;AAAA,QACR;AAAA,QAEF,MAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,QACtG,OAAO,QAAQ,WAAW;AAAA,QAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,IACzB,GACF;AAAA,KACF;AAEJ;","names":[]}
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. (MRR)`}\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. (ARR)\"\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. (ARPA)\"\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,WACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -6,6 +6,7 @@ import { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend }
6
6
  import { Reveal } from "../_fx";
7
7
  import { Tinted } from "../audit/tinted";
8
8
  import { ChannelPanel } from "./channel-panel";
9
+ import { MerchantHeadline } from "./merchant-headline";
9
10
  import { MethodCards } from "./method-cards";
10
11
  import { HealthPanel } from "./health-panel";
11
12
  import { PlanMixPanel } from "./plan-mix-panel";
@@ -34,37 +35,25 @@ function RevenueOverview({
34
35
  const recurringTracked = data.recurring.tracked !== false;
35
36
  const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
36
37
  return /* @__PURE__ */ jsxs(Fragment, { children: [
38
+ /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Money merchants earned" }),
39
+ /* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "What customers paid to merchants. This is the merchants' money, not Olive's own income." }),
37
40
  /* @__PURE__ */ jsx(
38
41
  RevenueKpis,
39
42
  {
43
+ part: "merchant",
40
44
  data,
41
45
  money,
42
46
  money2,
43
47
  currency,
44
48
  focus,
45
- gmvSpark: gmvSeries.map((p) => p.value)
49
+ gmvSpark: gmvSeries.map((p) => p.value),
50
+ extra: ledger && focus ? /* @__PURE__ */ jsx(MerchantHeadline, { ledger, focus }) : null
46
51
  }
47
52
  ),
48
- platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
49
- StatCard,
50
- {
51
- label: "Platform fees earned",
52
- value: /* @__PURE__ */ jsx(
53
- MoneyLines,
54
- {
55
- rows: platformFees,
56
- currency: "",
57
- amount: (r) => r.amount,
58
- decimals: 2
59
- }
60
- ),
61
- hint: "What Olive keeps from payouts, settlements and subscriptions."
62
- }
63
- ) }) }),
64
53
  /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
65
54
  Panel,
66
55
  {
67
- title: "GMV trend",
56
+ title: "Merchant sales over time",
68
57
  caption: gmvCaption,
69
58
  height: "calc(var(--adm-panel-h) + 32px)",
70
59
  children: focus ? /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(Tinted, { tone: "money", children: /* @__PURE__ */ jsx(
@@ -81,11 +70,7 @@ function RevenueOverview({
81
70
  }
82
71
  ) }) }),
83
72
  ledger && focus ? /* @__PURE__ */ jsxs(Fragment, { children: [
84
- /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "How customers paid" }),
85
- /* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
86
- "Money customers paid to merchants, by way of paying. ",
87
- gmvCaption
88
- ] }),
73
+ /* @__PURE__ */ jsx("h3", { className: ledgerStyles.subTitle, children: "How customers paid" }),
89
74
  /* @__PURE__ */ jsx(
90
75
  MethodCards,
91
76
  {
@@ -95,34 +80,67 @@ function RevenueOverview({
95
80
  }
96
81
  )
97
82
  ] }) : channels && channels.length > 0 && /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(ChannelPanel, { totals: channels, focus, caption: gmvCaption }) }) }),
98
- recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
99
- /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
100
- HealthPanel,
101
- {
102
- health,
103
- money,
104
- caption: recurringCaption
105
- }
106
- ) }),
107
- /* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
108
- PlanMixPanel,
109
- {
110
- planBreakdown: data.recurring.planBreakdown,
111
- money,
112
- caption: recurringCaption
113
- }
114
- ) })
83
+ /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
84
+ TopTenantsPanel,
85
+ {
86
+ tenants: gmv?.topTenants ?? [],
87
+ lifetime: gmv?.lifetime ?? 0,
88
+ focus
89
+ }
90
+ ) }) }),
91
+ /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Money Olive earns" }),
92
+ /* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
93
+ "What merchants pay Olive: their plan payments, and the fees Olive keeps.",
94
+ recurringTracked ? "" : " Plan income is not set up on this box yet."
115
95
  ] }),
116
- /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
117
- /* @__PURE__ */ jsx("div", { className: recurringTracked ? "adm-span-8" : "adm-span-12", children: /* @__PURE__ */ jsx(
118
- TopTenantsPanel,
119
- {
120
- tenants: gmv?.topTenants ?? [],
121
- lifetime: gmv?.lifetime ?? 0,
122
- focus
123
- }
124
- ) }),
125
- recurringTracked && /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) })
96
+ /* @__PURE__ */ jsx(
97
+ RevenueKpis,
98
+ {
99
+ part: "olive",
100
+ data,
101
+ money,
102
+ money2,
103
+ currency,
104
+ focus,
105
+ gmvSpark: []
106
+ }
107
+ ),
108
+ platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
109
+ StatCard,
110
+ {
111
+ label: "Fees Olive keeps",
112
+ value: /* @__PURE__ */ jsx(
113
+ MoneyLines,
114
+ {
115
+ rows: platformFees,
116
+ currency: "",
117
+ amount: (r) => r.amount,
118
+ decimals: 2
119
+ }
120
+ ),
121
+ hint: "What Olive keeps from payouts, settlements and plan payments, after costs."
122
+ }
123
+ ) }) }),
124
+ recurringTracked && /* @__PURE__ */ jsxs(Fragment, { children: [
125
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
126
+ /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
127
+ HealthPanel,
128
+ {
129
+ health,
130
+ money,
131
+ caption: recurringCaption
132
+ }
133
+ ) }),
134
+ /* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
135
+ PlanMixPanel,
136
+ {
137
+ planBreakdown: data.recurring.planBreakdown,
138
+ money,
139
+ caption: recurringCaption
140
+ }
141
+ ) })
142
+ ] }),
143
+ /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) })
126
144
  ] })
127
145
  ] });
128
146
  }
@@ -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 { 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 <RevenueKpis\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\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=\"Platform fees earned\"\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, settlements and subscriptions.\"\n />\n </Reveal>\n </section>\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"GMV trend\"\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 <h2 className={ledgerStyles.sectionTitle}>How customers paid</h2>\n <p className={ledgerStyles.sectionNote}>\n Money customers paid to merchants, by way of paying. {gmvCaption}\n </p>\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 {recurringTracked && (\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 )}\n\n <div className=\"adm-grid\">\n <div className={recurringTracked ? 'adm-span-8' : 'adm-span-12'}>\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n {recurringTracked && (\n <div className=\"adm-span-4\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n )}\n </div>\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 { 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, settlements 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":[]}