@withpotter/pages-admin 0.13.0-next.96 → 0.13.0-next.97

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.
@@ -0,0 +1,32 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { CountUp, Reveal } from "../_fx";
3
+ import { StatCard } from "../_ui";
4
+ import { methodCards, minorToMoney } from "./ledger-model";
5
+ const integer = new Intl.NumberFormat("en-US");
6
+ function MethodCards({
7
+ now,
8
+ before,
9
+ currency
10
+ }) {
11
+ const cards = methodCards(now, before, currency);
12
+ return /* @__PURE__ */ jsx("section", { "aria-label": "How customers paid", className: "adm-grid", children: cards.map((card, index) => /* @__PURE__ */ jsx(Reveal, { index, className: "adm-span-3", children: /* @__PURE__ */ jsx(
13
+ StatCard,
14
+ {
15
+ label: card.label,
16
+ value: /* @__PURE__ */ jsx(
17
+ CountUp,
18
+ {
19
+ value: Number(card.amountMinor) / 100,
20
+ format: (v) => minorToMoney(Math.round(v * 100), currency)
21
+ }
22
+ ),
23
+ sub: card.payments === 0 ? "No payments this way yet" : `${integer.format(card.payments)} ${card.payments === 1 ? "payment" : "payments"} \xB7 ${card.share}% of the money received`,
24
+ hint: `Money customers paid by ${card.label.toLowerCase()}, before fees.`,
25
+ delta: card.change === null ? void 0 : { pct: card.change, caption: "vs the period before" }
26
+ }
27
+ ) }, card.key)) });
28
+ }
29
+ export {
30
+ MethodCards
31
+ };
32
+ //# sourceMappingURL=method-cards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/method-cards.tsx"],"sourcesContent":["import type { GatewayChannelTotal } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal } from '../_fx';\nimport { StatCard } from '../_ui';\nimport { methodCards, minorToMoney } from './ledger-model';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * One card for each way customers pay (Card, USSD, Bank transfer, Opay): how\n * much came in that way, how many payments, its share of all the money\n * received, and how it moved since the period before. A quiet method shows\n * zero, so it is seen rather than missing.\n */\nexport function MethodCards({\n now,\n before,\n currency,\n}: {\n now: readonly GatewayChannelTotal[];\n before: readonly GatewayChannelTotal[] | null;\n currency: string;\n}) {\n const cards = methodCards(now, before, currency);\n\n return (\n <section aria-label=\"How customers paid\" className=\"adm-grid\">\n {cards.map((card, index) => (\n <Reveal key={card.key} index={index} className=\"adm-span-3\">\n <StatCard\n label={card.label}\n value={\n <CountUp\n value={Number(card.amountMinor) / 100}\n format={(v) => minorToMoney(Math.round(v * 100), currency)}\n />\n }\n sub={\n card.payments === 0\n ? 'No payments this way yet'\n : `${integer.format(card.payments)} ${card.payments === 1 ? 'payment' : 'payments'} · ${card.share}% of the money received`\n }\n hint={`Money customers paid by ${card.label.toLowerCase()}, before fees.`}\n delta={\n card.change === null\n ? undefined\n : { pct: card.change, caption: 'vs the period before' }\n }\n />\n </Reveal>\n ))}\n </section>\n );\n}\n"],"mappings":"AA+Bc;AA9Bd,SAAS,SAAS,cAAc;AAChC,SAAS,gBAAgB;AACzB,SAAS,aAAa,oBAAoB;AAE1C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAQtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,YAAY,KAAK,QAAQ,QAAQ;AAE/C,SACE,oBAAC,aAAQ,cAAW,sBAAqB,WAAU,YAChD,gBAAM,IAAI,CAAC,MAAM,UAChB,oBAAC,UAAsB,OAAc,WAAU,cAC7C;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,KAAK;AAAA,MACZ,OACE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,OAAO,KAAK,WAAW,IAAI;AAAA,UAClC,QAAQ,CAAC,MAAM,aAAa,KAAK,MAAM,IAAI,GAAG,GAAG,QAAQ;AAAA;AAAA,MAC3D;AAAA,MAEF,KACE,KAAK,aAAa,IACd,6BACA,GAAG,QAAQ,OAAO,KAAK,QAAQ,CAAC,IAAI,KAAK,aAAa,IAAI,YAAY,UAAU,SAAM,KAAK,KAAK;AAAA,MAEtG,MAAM,2BAA2B,KAAK,MAAM,YAAY,CAAC;AAAA,MACzD,OACE,KAAK,WAAW,OACZ,SACA,EAAE,KAAK,KAAK,QAAQ,SAAS,uBAAuB;AAAA;AAAA,EAE5D,KApBW,KAAK,GAqBlB,CACD,GACH;AAEJ;","names":[]}
@@ -13,42 +13,42 @@ function RevenueKpis({
13
13
  }) {
14
14
  const { recurring } = data;
15
15
  const tracked = recurring.tracked !== false;
16
- const notTracked = "Not tracked";
17
- const noSource = "No billing source connected";
16
+ const notTracked = "Not set up yet";
17
+ const noSource = "The plan payments are not connected yet";
18
18
  const gmv = rowFor(data.gmvByCurrency, focus);
19
19
  const lifetimeShare = gmv && gmv.lifetime > 0 ? `${Math.round(gmv.inRange / gmv.lifetime * 100)}% of lifetime` : void 0;
20
20
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Revenue indicators", className: "adm-grid", children: [
21
21
  /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
22
22
  StatCard,
23
23
  {
24
- label: "Monthly recurring revenue (MRR)",
24
+ label: "Plan income each month",
25
25
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
26
- hint: `Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || "NGN"}. Yearly plans are divided by 12.`,
27
- sub: tracked ? `${integer.format(recurring.activeSubscriptions)} paying` : noSource
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
28
  }
29
29
  ) }),
30
30
  /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
31
31
  StatCard,
32
32
  {
33
- label: "Annual recurring revenue (ARR)",
33
+ label: "Plan income over a year",
34
34
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
35
- hint: "Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR \xD7 12).",
36
- sub: tracked ? "MRR \xD7 12" : noSource
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
37
  }
38
38
  ) }),
39
39
  /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
40
40
  StatCard,
41
41
  {
42
- label: "Average revenue per account (ARPA)",
42
+ label: "Average per paying merchant",
43
43
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
44
- hint: "What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.",
45
- sub: tracked ? "Per paying account" : noSource
44
+ hint: "What one paying merchant pays in a month, on average. (ARPA)",
45
+ sub: tracked ? "Each month" : noSource
46
46
  }
47
47
  ) }),
48
48
  /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
49
49
  StatCard,
50
50
  {
51
- label: "Merchant sales (GMV)",
51
+ label: "Merchant sales",
52
52
  value: /* @__PURE__ */ jsx(
53
53
  MoneyLines,
54
54
  {
@@ -60,7 +60,7 @@ function RevenueKpis({
60
60
  ),
61
61
  href: "/admin/tenants",
62
62
  sub: lifetimeShare,
63
- hint: `The merchant sales ${ADMIN_NAME}'s recurring revenue sits on, not ${ADMIN_NAME}'s own income (that's MRR). One line per currency; currencies are never added together or converted.`,
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
64
  spark: focus ? gmvSpark : void 0,
65
65
  sparkColor: TONE.money.fg
66
66
  }
@@ -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 a billing source the API sends 0s; show that nothing is measured.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not tracked';\n const noSource = 'No billing source connected';\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=\"Monthly recurring revenue (MRR)\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || 'NGN'}. Yearly plans are divided by 12.`}\n sub={\n tracked\n ? `${integer.format(recurring.activeSubscriptions)} paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Annual recurring revenue (ARR)\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR × 12).\"\n sub={tracked ? 'MRR × 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average revenue per account (ARPA)\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.\"\n sub={tracked ? 'Per paying account' : noSource}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales (GMV)\"\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={`The merchant sales ${ADMIN_NAME}'s recurring revenue sits on, not ${ADMIN_NAME}'s own income (that's MRR). One line per currency; currencies are never added together or converted.`}\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,4EAA4E,KAAK,YAAY,KAAK;AAAA,QACxG,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,CAAC,YAChD;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,gBAAa;AAAA;AAAA,IAC9B,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,uBAAuB;AAAA;AAAA,IACxC,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,sBAAsB,UAAU,qCAAqC,UAAU;AAAA,QACrF,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 { 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":[]}
@@ -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 { MethodCards } from "./method-cards";
9
10
  import { HealthPanel } from "./health-panel";
10
11
  import { PlanMixPanel } from "./plan-mix-panel";
11
12
  import { platformFeeRows } from "./platform-fees";
@@ -13,6 +14,7 @@ import { RevenueKpis } from "./revenue-kpis";
13
14
  import { SubscriptionsPanel } from "./subscriptions-panel";
14
15
  import { TopTenantsPanel } from "./top-tenants-panel";
15
16
  import styles from "./revenue.module.css";
17
+ import ledgerStyles from "./ledger.module.css";
16
18
  function RevenueOverview({
17
19
  data,
18
20
  health,
@@ -23,7 +25,8 @@ function RevenueOverview({
23
25
  period,
24
26
  span,
25
27
  recurringCaption,
26
- channels
28
+ channels,
29
+ ledger
27
30
  }) {
28
31
  const gmv = rowFor(data.gmvByCurrency, focus);
29
32
  const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
@@ -77,7 +80,21 @@ function RevenueOverview({
77
80
  ) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
78
81
  }
79
82
  ) }) }),
80
- 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 }) }) }),
83
+ 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
+ ] }),
89
+ /* @__PURE__ */ jsx(
90
+ MethodCards,
91
+ {
92
+ now: ledger.byChannel.current,
93
+ before: ledger.byChannel.previous,
94
+ currency: focus
95
+ }
96
+ )
97
+ ] }) : 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 }) }) }),
81
98
  recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
82
99
  /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
83
100
  HealthPanel,
@@ -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 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 { 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';\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}: {\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}) {\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 {channels && 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 {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 { 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":";AAqEM,SAwDE,UAxDF,KA0DI,YA1DJ;AAnEN,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAOhC,SAAS,YAAY,eAAe,OAAO,UAAU,SAAS,QAAQ,eAAe;AACrF,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,OAAO,YAAY;AACnB,OAAO,kBAAkB;AAQlB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAiBG;AACD,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,YAAY,QAAQ,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG,CAAC,GAAG,CAAC;AAClE,QAAM,aAAa,QAAQ,GAAG,MAAM,SAAM,KAAK,KAAK;AACpD,QAAM,mBAAmB,KAAK,UAAU,YAAY;AAEpD,QAAM,eAAe,gBAAgB,KAAK,8BAA8B,QAAQ;AAEhF,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK;AAAA;AAAA,IACxC;AAAA,IAEC,gBACC,oBAAC,aAAQ,cAAW,iBAAgB,WAAU,YAC5C,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,UAAS;AAAA,YACT,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,UAAU;AAAA;AAAA,QACZ;AAAA,QAEF,MAAK;AAAA;AAAA,IACP,GACF,GACF;AAAA,IAGF,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SAAS;AAAA,QACT,QAAO;AAAA,QAEN,kBACC,oBAAC,SAAI,WAAW,OAAO,WACrB,8BAAC,UAAO,MAAK,SACX;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,SAAS;AAAA,YACT,cAAc,YAAY,IAAI;AAAA,YAC9B,aAAa,QAAQ,KAAK;AAAA,YAC1B,cAAc,WAAW,KAAK;AAAA,YAC9B,QAAQ;AAAA;AAAA,QACV,GACF,GACF,IAEA,oBAAC,OAAE,WAAW,OAAO,OAAQ,yBAAc;AAAA;AAAA,IAE/C,GACF,GACF;AAAA,IAEC,UAAU,QACT,iCACE;AAAA,0BAAC,QAAG,WAAW,aAAa,cAAc,gCAAkB;AAAA,MAC5D,qBAAC,OAAE,WAAW,aAAa,aAAa;AAAA;AAAA,QACgB;AAAA,SACxD;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,OAAO,UAAU;AAAA,UACtB,QAAQ,OAAO,UAAU;AAAA,UACzB,UAAU;AAAA;AAAA,MACZ;AAAA,OACF,IAEA,YACA,SAAS,SAAS,KAChB,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb,8BAAC,gBAAa,QAAQ,UAAU,OAAc,SAAS,YAAY,GACrE,GACF;AAAA,IAIH,oBACC,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,MACA,oBAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,KAAK,UAAU;AAAA,UAC9B;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,mBAAmB,eAAe,eAChD;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,cAAc,CAAC;AAAA,UAC7B,UAAU,KAAK,YAAY;AAAA,UAC3B;AAAA;AAAA,MACF,GACF;AAAA,MACC,oBACC,oBAAC,SAAI,WAAU,cACb,8BAAC,sBAAmB,WAAW,KAAK,WAAW,MAAY,GAC7D;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
@@ -4,6 +4,7 @@ import { useMemo } from "react";
4
4
  import { ADMIN_NAME } from "../_shell/admin-brand";
5
5
  import {
6
6
  useGatewayChannels,
7
+ useMerchantPayments,
7
8
  usePlatformCockpit,
8
9
  usePlatformRevenue
9
10
  } from "../hooks";
@@ -23,10 +24,14 @@ import {
23
24
  usePersistedDateRange,
24
25
  useTabParam
25
26
  } from "../_ui";
27
+ import { MerchantPaymentsView } from "./merchant-payments-view";
26
28
  import { RevenueOverview } from "./revenue-overview";
27
29
  import { RevenueRetention } from "./revenue-retention";
30
+ import { SubscriptionsView } from "./subscriptions-view";
28
31
  const TABS = [
29
32
  { id: "overview", label: "Overview" },
33
+ { id: "merchants", label: "Merchant payments" },
34
+ { id: "subscriptions", label: "Subscriptions" },
30
35
  { id: "retention", label: "Retention" }
31
36
  ];
32
37
  const KEYS = TABS.map((t) => t.id);
@@ -56,6 +61,11 @@ function RevenueWorkspace() {
56
61
  startDate: range.startDate,
57
62
  endDate: range.endDate
58
63
  });
64
+ const ledger = useMerchantPayments({
65
+ startDate: range.startDate,
66
+ endDate: range.endDate,
67
+ limit: 1
68
+ });
59
69
  const currencies = useMemo(() => data?.currencies ?? [], [data]);
60
70
  const focus = focusCurrency(currencies, currency);
61
71
  const recurringCurrency = data?.currency || "NGN";
@@ -77,6 +87,24 @@ function RevenueWorkspace() {
77
87
  );
78
88
  } else if (isLoading || !data) {
79
89
  body = /* @__PURE__ */ jsx(AdmLoading, { label: "Loading revenue metrics\u2026" });
90
+ } else if (tab === "merchants") {
91
+ body = /* @__PURE__ */ jsx(
92
+ MerchantPaymentsView,
93
+ {
94
+ startDate: range.startDate,
95
+ endDate: range.endDate,
96
+ currency
97
+ }
98
+ );
99
+ } else if (tab === "subscriptions") {
100
+ body = /* @__PURE__ */ jsx(
101
+ SubscriptionsView,
102
+ {
103
+ startDate: range.startDate,
104
+ endDate: range.endDate,
105
+ revenue: data
106
+ }
107
+ );
80
108
  } else if (tab === "retention") {
81
109
  body = /* @__PURE__ */ jsx(
82
110
  RevenueRetention,
@@ -100,7 +128,8 @@ function RevenueWorkspace() {
100
128
  period,
101
129
  span,
102
130
  recurringCaption: `${period} \xB7 ${recurringCurrency}`,
103
- channels: channels.data?.byChannel
131
+ channels: channels.data?.byChannel,
132
+ ledger: ledger.data
104
133
  }
105
134
  );
106
135
  }
@@ -109,7 +138,7 @@ function RevenueWorkspace() {
109
138
  PageHeader,
110
139
  {
111
140
  title: "Revenue",
112
- description: `Every revenue on ${ADMIN_NAME} is reported here: its subscription income each month and year, and the merchant sales (GMV) it is based on.`,
141
+ description: `All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`,
113
142
  actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
114
143
  /* @__PURE__ */ jsx(
115
144
  CurrencyFilter,
@@ -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 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 { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/** Revenue: headline money on Overview, revenue quality and cohorts on Retention. */\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 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 === '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 />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`Every revenue on ${ADMIN_NAME} is reported here: its subscription income each month and year, and the merchant sales (GMV) it is based on.`}\n actions={\n <div className=\"adm-toolbar\">\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":[]}
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 { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\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 === '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 <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":[]}
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
5
+ import {
6
+ platformAdminApi
7
+ } from "@withpotter/lib/admin-api";
8
+ import { useSubscriptionPayments } from "../hooks";
9
+ import { CountUp, Reveal } from "../_fx";
10
+ import { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from "../_ui";
11
+ import { DownloadButton } from "./download-button";
12
+ import {
13
+ PURPOSE_WORDS,
14
+ STATUS_WORDS,
15
+ SUBSCRIPTION_METHOD_WORDS,
16
+ downloadName,
17
+ minorToMoney,
18
+ word
19
+ } from "./ledger-model";
20
+ import { PlanMixPanel } from "./plan-mix-panel";
21
+ import styles from "./ledger.module.css";
22
+ import table from "./revenue.module.css";
23
+ const PAGE_SIZE = 25;
24
+ const integer = new Intl.NumberFormat("en-US");
25
+ const STATUSES = [
26
+ { value: "", label: "Every result" },
27
+ { value: "succeeded", label: "Paid" },
28
+ { value: "failed", label: "Failed" },
29
+ { value: "pending", label: "Waiting" }
30
+ ];
31
+ const METHODS = [
32
+ { value: "", label: "Every way of paying" },
33
+ { value: "gateway", label: "Card or bank" },
34
+ { value: "wallet", label: "Wallet" },
35
+ { value: "manual", label: "Recorded by hand" }
36
+ ];
37
+ const tone = (status) => status === "succeeded" ? styles.statusGood : status === "failed" ? styles.statusBad : styles.statusWait;
38
+ function SubscriptionsView({
39
+ startDate,
40
+ endDate,
41
+ revenue
42
+ }) {
43
+ const [search, setSearch] = useState("");
44
+ const [q, setQ] = useState("");
45
+ const [status, setStatus] = useState("");
46
+ const [method, setMethod] = useState("");
47
+ const [page, setPage] = useState(1);
48
+ useEffect(() => {
49
+ const timer = setTimeout(() => setQ(search.trim()), 300);
50
+ return () => clearTimeout(timer);
51
+ }, [search]);
52
+ const filters = useMemo(
53
+ () => ({ startDate, endDate, status, method, q, page, limit: PAGE_SIZE }),
54
+ [startDate, endDate, status, method, q, page]
55
+ );
56
+ const change = (set) => (value) => {
57
+ set(value);
58
+ setPage(1);
59
+ };
60
+ const { recurring } = revenue;
61
+ const tracked = recurring.tracked !== false;
62
+ const currency = recurring.currency ?? revenue.currency ?? "NGN";
63
+ const money = moneyIn(currency);
64
+ const money2 = moneyIn(currency, 2);
65
+ const notSet = "Not set up yet";
66
+ const why = "The plan payments have not been connected to this page yet.";
67
+ const payments = useSubscriptionPayments(filters);
68
+ return /* @__PURE__ */ jsxs("div", { children: [
69
+ /* @__PURE__ */ jsx("p", { className: styles.lede, children: "This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants." }),
70
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Plan income", className: "adm-grid", children: [
71
+ /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
72
+ StatCard,
73
+ {
74
+ label: "Plan income each month",
75
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notSet,
76
+ sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why,
77
+ hint: "What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)"
78
+ }
79
+ ) }),
80
+ /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
81
+ StatCard,
82
+ {
83
+ label: "Plan income over a year",
84
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notSet,
85
+ sub: tracked ? "Each month times 12" : why,
86
+ hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)"
87
+ }
88
+ ) }),
89
+ /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
90
+ StatCard,
91
+ {
92
+ label: "Average per paying merchant",
93
+ value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notSet,
94
+ sub: tracked ? "Each month" : why,
95
+ hint: "What one paying merchant pays Olive in a month, on average. (ARPA)"
96
+ }
97
+ ) }),
98
+ /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
99
+ StatCard,
100
+ {
101
+ label: "Joined and left",
102
+ value: tracked ? `${integer.format(recurring.newInRange)} joined` : notSet,
103
+ sub: tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why,
104
+ hint: "Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off."
105
+ }
106
+ ) })
107
+ ] }),
108
+ tracked && recurring.planBreakdown.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
109
+ /* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "Who pays what" }),
110
+ /* @__PURE__ */ jsx("p", { className: styles.sectionNote, children: "How the monthly plan income splits across plans." }),
111
+ /* @__PURE__ */ jsx(PlanMixPanel, { planBreakdown: recurring.planBreakdown, money, caption: `Each month \xB7 ${currency}` })
112
+ ] }) : null,
113
+ /* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "Every plan payment" }),
114
+ payments.isError ? /* @__PURE__ */ jsx("div", { className: styles.notice, children: "Plan payments are not available on this box yet. Once the subscription module is connected, every payment will be listed here." }) : payments.isLoading || !payments.data ? /* @__PURE__ */ jsx(AdmLoading, { label: "Loading plan payments\u2026" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
116
+ /* @__PURE__ */ jsxs(Filters, { label: "Filter plan payments", children: [
117
+ /* @__PURE__ */ jsx(
118
+ FilterSearch,
119
+ {
120
+ value: search,
121
+ onChange: change(setSearch),
122
+ placeholder: "Search a merchant's name or email, or a reference",
123
+ label: "Search plan payments"
124
+ }
125
+ ),
126
+ /* @__PURE__ */ jsx(FilterSelect, { value: status, onChange: change(setStatus), options: STATUSES, label: "Payment result" }),
127
+ /* @__PURE__ */ jsx(FilterSelect, { value: method, onChange: change(setMethod), options: METHODS, label: "Way of paying" })
128
+ ] }),
129
+ /* @__PURE__ */ jsx(
130
+ DownloadButton,
131
+ {
132
+ filename: downloadName("plan-payments", startDate, endDate),
133
+ load: () => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: void 0, limit: void 0 })
134
+ }
135
+ )
136
+ ] }),
137
+ payments.data.items.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No plan payments match. Try a wider date range or clear a filter." }) : /* @__PURE__ */ jsx(
138
+ TableShell,
139
+ {
140
+ minWidth: 860,
141
+ density: "compact",
142
+ head: /* @__PURE__ */ jsxs("tr", { children: [
143
+ /* @__PURE__ */ jsx(Th, { children: "When" }),
144
+ /* @__PURE__ */ jsx(Th, { children: "Merchant" }),
145
+ /* @__PURE__ */ jsx(Th, { children: "Plan" }),
146
+ /* @__PURE__ */ jsx(Th, { children: "For" }),
147
+ /* @__PURE__ */ jsx(Th, { children: "How" }),
148
+ /* @__PURE__ */ jsx(Th, { children: "Result" }),
149
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Amount" })
150
+ ] }),
151
+ children: payments.data.items.map((row) => /* @__PURE__ */ jsx(PlanPaymentRow, { row }, row.id))
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(
155
+ Pagination,
156
+ {
157
+ page,
158
+ totalPages: Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1),
159
+ total: payments.data.total,
160
+ noun: "plan payments",
161
+ onPage: setPage
162
+ }
163
+ )
164
+ ] })
165
+ ] });
166
+ }
167
+ function PlanPaymentRow({ row }) {
168
+ const when = row.paidAt ?? row.createdAt;
169
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
170
+ /* @__PURE__ */ jsx("td", { className: table.num, children: new Date(when).toLocaleString(void 0, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) }),
171
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
172
+ /* @__PURE__ */ jsx("span", { className: styles.whoName, children: row.merchantName || "Unknown merchant" }),
173
+ row.merchantEmail ? /* @__PURE__ */ jsx("span", { className: styles.whoSub, children: row.merchantEmail }) : null
174
+ ] }) }),
175
+ /* @__PURE__ */ jsx("td", { className: table.name, children: row.plan.charAt(0).toUpperCase() + row.plan.slice(1) }),
176
+ /* @__PURE__ */ jsx("td", { children: word(PURPOSE_WORDS, row.purpose) }),
177
+ /* @__PURE__ */ jsx("td", { children: word(SUBSCRIPTION_METHOD_WORDS, row.method) }),
178
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
179
+ /* @__PURE__ */ jsx("span", { className: `${styles.status} ${tone(row.status)}`, children: word(STATUS_WORDS, row.status) }),
180
+ row.status === "failed" && row.failureReason ? /* @__PURE__ */ jsx("span", { className: styles.whoSub, children: row.failureReason }) : null
181
+ ] }) }),
182
+ /* @__PURE__ */ jsx("td", { className: table.numStrong, children: minorToMoney(row.amountKobo, "NGN") })
183
+ ] });
184
+ }
185
+ export {
186
+ SubscriptionsView
187
+ };
188
+ //# sourceMappingURL=subscriptions-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/subscriptions-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport {\n platformAdminApi,\n type PlatformRevenue,\n type SubscriptionPaymentRow,\n} from '@withpotter/lib/admin-api';\nimport { useSubscriptionPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n PURPOSE_WORDS,\n STATUS_WORDS,\n SUBSCRIPTION_METHOD_WORDS,\n downloadName,\n minorToMoney,\n word,\n} from './ledger-model';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst integer = new Intl.NumberFormat('en-US');\n\nconst STATUSES = [\n { value: '', label: 'Every result' },\n { value: 'succeeded', label: 'Paid' },\n { value: 'failed', label: 'Failed' },\n { value: 'pending', label: 'Waiting' },\n];\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'gateway', label: 'Card or bank' },\n { value: 'wallet', label: 'Wallet' },\n { value: 'manual', label: 'Recorded by hand' },\n];\n\nconst tone = (status: string) =>\n status === 'succeeded' ? styles.statusGood : status === 'failed' ? styles.statusBad : styles.statusWait;\n\n/** What merchants pay Olive for their plan: the income figures, who pays what, and every plan payment. */\nexport function SubscriptionsView({\n startDate,\n endDate,\n revenue,\n}: {\n startDate?: string;\n endDate?: string;\n revenue: PlatformRevenue;\n}) {\n const [search, setSearch] = useState('');\n const [q, setQ] = useState('');\n const [status, setStatus] = useState('');\n const [method, setMethod] = useState('');\n const [page, setPage] = useState(1);\n\n useEffect(() => {\n const timer = setTimeout(() => setQ(search.trim()), 300);\n\n return () => clearTimeout(timer);\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, status, method, q, page, limit: PAGE_SIZE }),\n [startDate, endDate, status, method, q, page],\n );\n const change = <T,>(set: (v: T) => void) => (value: T) => {\n set(value);\n setPage(1);\n };\n\n const { recurring } = revenue;\n const tracked = recurring.tracked !== false;\n const currency = recurring.currency ?? revenue.currency ?? 'NGN';\n const money = moneyIn(currency);\n const money2 = moneyIn(currency, 2);\n const notSet = 'Not set up yet';\n const why = 'The plan payments have not been connected to this page yet.';\n const payments = useSubscriptionPayments(filters);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants.\n </p>\n\n <section aria-label=\"Plan income\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={tracked ? <CountUp value={recurring.mrr} format={money} /> : notSet}\n sub={tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why}\n hint=\"What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)\"\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={tracked ? <CountUp value={recurring.arr} format={money} /> : notSet}\n sub={tracked ? 'Each month times 12' : why}\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)\"\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={tracked ? <CountUp value={recurring.arpa} format={money2} /> : notSet}\n sub={tracked ? 'Each month' : why}\n hint=\"What one paying merchant pays Olive in a month, on average. (ARPA)\"\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Joined and left\"\n value={tracked ? `${integer.format(recurring.newInRange)} joined` : notSet}\n sub={tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why}\n hint=\"Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off.\"\n />\n </Reveal>\n </section>\n\n {tracked && recurring.planBreakdown.length > 0 ? (\n <>\n <h2 className={styles.sectionTitle}>Who pays what</h2>\n <p className={styles.sectionNote}>How the monthly plan income splits across plans.</p>\n <PlanMixPanel planBreakdown={recurring.planBreakdown} money={money} caption={`Each month · ${currency}`} />\n </>\n ) : null}\n\n <h2 className={styles.sectionTitle}>Every plan payment</h2>\n {payments.isError ? (\n <div className={styles.notice}>\n Plan payments are not available on this box yet. Once the subscription module is connected, every payment will\n be listed here.\n </div>\n ) : payments.isLoading || !payments.data ? (\n <AdmLoading label=\"Loading plan payments…\" />\n ) : (\n <>\n <div className={styles.toolbar}>\n <Filters label=\"Filter plan payments\">\n <FilterSearch\n value={search}\n onChange={change(setSearch)}\n placeholder=\"Search a merchant's name or email, or a reference\"\n label=\"Search plan payments\"\n />\n <FilterSelect value={status} onChange={change(setStatus)} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={method} onChange={change(setMethod)} options={METHODS} label=\"Way of paying\" />\n </Filters>\n <DownloadButton\n filename={downloadName('plan-payments', startDate, endDate)}\n load={() => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {payments.data.items.length === 0 ? (\n <EmptyNote className={table.empty}>No plan payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={860}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Merchant</Th>\n <Th>Plan</Th>\n <Th>For</Th>\n <Th>How</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n </tr>\n }\n >\n {payments.data.items.map((row) => (\n <PlanPaymentRow key={row.id} row={row} />\n ))}\n </TableShell>\n )}\n <Pagination\n page={page}\n totalPages={Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1)}\n total={payments.data.total}\n noun=\"plan payments\"\n onPage={setPage}\n />\n </>\n )}\n </div>\n );\n}\n\nfunction PlanPaymentRow({ row }: { row: SubscriptionPaymentRow }) {\n const when = row.paidAt ?? row.createdAt;\n\n return (\n <TableRow>\n <td className={table.num}>\n {new Date(when).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.merchantName || 'Unknown merchant'}</span>\n {row.merchantEmail ? <span className={styles.whoSub}>{row.merchantEmail}</span> : null}\n </span>\n </td>\n <td className={table.name}>{row.plan.charAt(0).toUpperCase() + row.plan.slice(1)}</td>\n <td>{word(PURPOSE_WORDS, row.purpose)}</td>\n <td>{word(SUBSCRIPTION_METHOD_WORDS, row.method)}</td>\n <td>\n <span className={styles.who}>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n {row.status === 'failed' && row.failureReason ? <span className={styles.whoSub}>{row.failureReason}</span> : null}\n </span>\n </td>\n <td className={table.numStrong}>{minorToMoney(row.amountKobo, 'NGN')}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2240,6 +2240,7 @@ function ActivityTable({
2240
2240
  /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Customer" }),
2241
2241
  /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Total" }),
2242
2242
  /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Payment" }),
2243
+ /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Paid by" }),
2243
2244
  /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Status" }),
2244
2245
  /* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Date" })
2245
2246
  ] }) }),
@@ -2248,7 +2249,10 @@ function ActivityTable({
2248
2249
  "#",
2249
2250
  o.orderNumber
2250
2251
  ] }),
2251
- /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: o.customerName ?? o.customerEmail }),
2252
+ /* @__PURE__ */ jsxs("td", { style: tableStyles.cell, children: [
2253
+ o.customerName ?? o.customerEmail,
2254
+ o.customerName ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "var(--text-tertiary)" }, children: [o.customerEmail, o.customerPhone].filter(Boolean).join(" \xB7 ") }) : null
2255
+ ] }),
2252
2256
  /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: moneyCur(o.total, o.currency) }),
2253
2257
  /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: /* @__PURE__ */ jsx(
2254
2258
  Badge,
@@ -2257,6 +2261,7 @@ function ActivityTable({
2257
2261
  children: o.paymentStatus
2258
2262
  }
2259
2263
  ) }),
2264
+ /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: o.paymentMethod ?? /* @__PURE__ */ jsx("span", { style: { color: "var(--text-tertiary)" }, children: "Not recorded" }) }),
2260
2265
  /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: /* @__PURE__ */ jsx(
2261
2266
  Badge,
2262
2267
  {