@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
@@ -1,6 +1,7 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { EmptyBlock } from "../audit/empty-block";
4
+ import ledgerStyles from "./ledger.module.css";
4
5
  import { CohortPanel } from "./cohort-panel";
5
6
  import { QualityPanel } from "./quality-panel";
6
7
  function RevenueRetention({
@@ -10,19 +11,22 @@ function RevenueRetention({
10
11
  caption
11
12
  }) {
12
13
  if (!quality) {
13
- return /* @__PURE__ */ jsx(EmptyBlock, { message: "Retention appears once there is enough billing history." });
14
+ return /* @__PURE__ */ jsx(EmptyBlock, { message: "Retention shows here once there is enough history to compare." });
14
15
  }
15
- return /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
16
- /* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
17
- QualityPanel,
18
- {
19
- quality,
20
- money,
21
- currency,
22
- caption
23
- }
24
- ) }),
25
- /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(CohortPanel, { cohorts: quality.cohorts }) })
16
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ /* @__PURE__ */ jsx("p", { className: ledgerStyles.lede, children: "This shows how well Olive keeps the income it earns, and whether the merchants it signs up carry on selling." }),
18
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
19
+ /* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
20
+ QualityPanel,
21
+ {
22
+ quality,
23
+ money,
24
+ currency,
25
+ caption
26
+ }
27
+ ) }),
28
+ /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(CohortPanel, { cohorts: quality.cohorts }) })
29
+ ] })
26
30
  ] });
27
31
  }
28
32
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-retention.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyBlock } from '../audit/empty-block';\nimport { CohortPanel } from './cohort-panel';\nimport { QualityPanel } from './quality-panel';\n\n/** Retention tab: revenue quality beside logo cohort retention. */\nexport function RevenueRetention({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: PlatformRevenue['quality'];\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 if (!quality) {\n return (\n <EmptyBlock message=\"Retention appears once there is enough billing history.\" />\n );\n }\n\n return (\n <div className=\"adm-grid\">\n <div className=\"adm-span-5\">\n <QualityPanel\n quality={quality}\n money={money}\n currency={currency}\n caption={caption}\n />\n </div>\n <div className=\"adm-span-7\">\n <CohortPanel cohorts={quality.cohorts} />\n </div>\n </div>\n );\n}\n"],"mappings":";AAsBM,cAKF,YALE;AAnBN,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAGtB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,MAAI,CAAC,SAAS;AACZ,WACE,oBAAC,cAAW,SAAQ,2DAA0D;AAAA,EAElF;AAEA,SACE,qBAAC,SAAI,WAAU,YACb;AAAA,wBAAC,SAAI,WAAU,cACb;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF,GACF;AAAA,IACA,oBAAC,SAAI,WAAU,cACb,8BAAC,eAAY,SAAS,QAAQ,SAAS,GACzC;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-retention.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyBlock } from '../audit/empty-block';\nimport ledgerStyles from './ledger.module.css';\nimport { CohortPanel } from './cohort-panel';\nimport { QualityPanel } from './quality-panel';\n\n/** Retention tab: revenue quality beside logo cohort retention. */\nexport function RevenueRetention({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: PlatformRevenue['quality'];\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 if (!quality) {\n return (\n <EmptyBlock message=\"Retention shows here once there is enough history to compare.\" />\n );\n }\n\n return (\n <>\n <p className={ledgerStyles.lede}>\n This shows how well Olive keeps the income it earns, and whether the merchants it signs up carry on selling.\n </p>\n <div className=\"adm-grid\">\n <div className=\"adm-span-5\">\n <QualityPanel\n quality={quality}\n money={money}\n currency={currency}\n caption={caption}\n />\n </div>\n <div className=\"adm-span-7\">\n <CohortPanel cohorts={quality.cohorts} />\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAuBM,SAKF,UALE,KASA,YATA;AApBN,SAAS,kBAAkB;AAC3B,OAAO,kBAAkB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAGtB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,MAAI,CAAC,SAAS;AACZ,WACE,oBAAC,cAAW,SAAQ,iEAAgE;AAAA,EAExF;AAEA,SACE,iCACE;AAAA,wBAAC,OAAE,WAAW,aAAa,MAAM,0HAEjC;AAAA,IACA,qBAAC,SAAI,WAAU,YACf;AAAA,0BAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,MACF,GACF;AAAA,MACA,oBAAC,SAAI,WAAU,cACb,8BAAC,eAAY,SAAS,QAAQ,SAAS,GACzC;AAAA,OACA;AAAA,KACF;AAEJ;","names":[]}
@@ -25,12 +25,15 @@ import {
25
25
  useTabParam
26
26
  } from "../_ui";
27
27
  import { MerchantPaymentsView } from "./merchant-payments-view";
28
+ import { MerchantsView } from "./merchants-view";
28
29
  import { RevenueOverview } from "./revenue-overview";
29
30
  import { RevenueRetention } from "./revenue-retention";
30
31
  import { SubscriptionsView } from "./subscriptions-view";
32
+ import { SummaryDownload } from "./summary-download";
31
33
  const TABS = [
32
34
  { id: "overview", label: "Overview" },
33
35
  { id: "merchants", label: "Merchant payments" },
36
+ { id: "by-merchant", label: "Merchants" },
34
37
  { id: "subscriptions", label: "Subscriptions" },
35
38
  { id: "retention", label: "Retention" }
36
39
  ];
@@ -96,6 +99,8 @@ function RevenueWorkspace() {
96
99
  currency
97
100
  }
98
101
  );
102
+ } else if (tab === "by-merchant") {
103
+ body = /* @__PURE__ */ jsx(MerchantsView, { startDate: range.startDate, endDate: range.endDate });
99
104
  } else if (tab === "subscriptions") {
100
105
  body = /* @__PURE__ */ jsx(
101
106
  SubscriptionsView,
@@ -140,6 +145,17 @@ function RevenueWorkspace() {
140
145
  title: "Revenue",
141
146
  description: `All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`,
142
147
  actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
148
+ tab === "overview" ? /* @__PURE__ */ jsx(
149
+ SummaryDownload,
150
+ {
151
+ ledger: ledger.data,
152
+ revenue: data,
153
+ currency: focus ?? "NGN",
154
+ startDate: range.startDate,
155
+ endDate: range.endDate,
156
+ period
157
+ }
158
+ ) : null,
143
159
  /* @__PURE__ */ jsx(
144
160
  CurrencyFilter,
145
161
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { 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":";AA0GM,cAwDI,YAxDJ;AAxGN,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAElC,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,YAAY,OAAO,WAAW;AAAA,EACpC,EAAE,IAAI,aAAa,OAAO,oBAAoB;AAAA,EAC9C,EAAE,IAAI,iBAAiB,OAAO,gBAAgB;AAAA,EAC9C,EAAE,IAAI,aAAa,OAAO,YAAY;AACxC;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAO1B,SAAS,mBAAmB;AACjC,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,sBAAsB;AAE1B,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAEjD,QAAM,QAAQ;AAAA,IACZ,MACE,aAAa,WACT,YAAY,aAAa,SAAS,IAClC,aAAa,QAAQ;AAAA,IAC3B,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,mBAAmB;AAAA,IAC/D,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAID,QAAM,UAAU,mBAAmB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAED,QAAM,WAAW,mBAAmB;AAAA,IAClC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,SAAS,oBAAoB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,IACf,OAAO;AAAA,EACT,CAAC;AAED,QAAM,aAAa,QAAQ,MAAM,MAAM,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;AAC/D,QAAM,QAAQ,cAAc,YAAY,QAAQ;AAGhD,QAAM,oBAAoB,MAAM,YAAY;AAC5C,QAAM,QAAQ,QAAQ,MAAM,QAAQ,iBAAiB,GAAG,CAAC,iBAAiB,CAAC;AAC3E,QAAM,SAAS;AAAA,IACb,MAAM,QAAQ,mBAAmB,CAAC;AAAA,IAClC,CAAC,iBAAiB;AAAA,EACpB;AAEA,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,SAAS,GAAG,KAAK,CAAC,EAAE,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AAEvD,MAAI;AACJ,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAAA,EAEJ,WAAW,aAAa,CAAC,MAAM;AAC7B,WAAO,oBAAC,cAAW,OAAM,iCAA2B;AAAA,EACtD,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf;AAAA;AAAA,IACF;AAAA,EAEJ,WAAW,QAAQ,iBAAiB;AAClC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,SAAS;AAAA;AAAA,IACX;AAAA,EAEJ,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,KAAK;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,WAAW,GAAG,MAAM,SAAM,QAAQ,KAAK;AAAA;AAAA,IAClD;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,QAAQ,MAAM;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,GAAG,MAAM,SAAM,iBAAiB;AAAA,QAClD,UAAU,SAAS,MAAM;AAAA,QACzB,QAAQ,OAAO;AAAA;AAAA,IACjB;AAAA,EAEJ;AAEA,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,aAAa,gCAAgC,UAAU,0DAA0D,UAAU;AAAA,QAC3H,SACE,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,WACF;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD;AAAA,KACH;AAEJ;","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 { MerchantsView } from './merchants-view';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'by-merchant', label: 'Merchants' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'by-merchant') {\n body = <MerchantsView startDate={range.startDate} endDate={range.endDate} />;\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":";AA6GM,cA0DI,YA1DJ;AA3GN,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAEhC,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,YAAY,OAAO,WAAW;AAAA,EACpC,EAAE,IAAI,aAAa,OAAO,oBAAoB;AAAA,EAC9C,EAAE,IAAI,eAAe,OAAO,YAAY;AAAA,EACxC,EAAE,IAAI,iBAAiB,OAAO,gBAAgB;AAAA,EAC9C,EAAE,IAAI,aAAa,OAAO,YAAY;AACxC;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAO1B,SAAS,mBAAmB;AACjC,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,sBAAsB;AAE1B,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAEjD,QAAM,QAAQ;AAAA,IACZ,MACE,aAAa,WACT,YAAY,aAAa,SAAS,IAClC,aAAa,QAAQ;AAAA,IAC3B,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,mBAAmB;AAAA,IAC/D,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAID,QAAM,UAAU,mBAAmB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAED,QAAM,WAAW,mBAAmB;AAAA,IAClC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,SAAS,oBAAoB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,IACf,OAAO;AAAA,EACT,CAAC;AAED,QAAM,aAAa,QAAQ,MAAM,MAAM,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;AAC/D,QAAM,QAAQ,cAAc,YAAY,QAAQ;AAGhD,QAAM,oBAAoB,MAAM,YAAY;AAC5C,QAAM,QAAQ,QAAQ,MAAM,QAAQ,iBAAiB,GAAG,CAAC,iBAAiB,CAAC;AAC3E,QAAM,SAAS;AAAA,IACb,MAAM,QAAQ,mBAAmB,CAAC;AAAA,IAClC,CAAC,iBAAiB;AAAA,EACpB;AAEA,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,SAAS,GAAG,KAAK,CAAC,EAAE,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AAEvD,MAAI;AACJ,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAAA,EAEJ,WAAW,aAAa,CAAC,MAAM;AAC7B,WAAO,oBAAC,cAAW,OAAM,iCAA2B;AAAA,EACtD,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf;AAAA;AAAA,IACF;AAAA,EAEJ,WAAW,QAAQ,eAAe;AAChC,WAAO,oBAAC,iBAAc,WAAW,MAAM,WAAW,SAAS,MAAM,SAAS;AAAA,EAC5E,WAAW,QAAQ,iBAAiB;AAClC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,SAAS;AAAA;AAAA,IACX;AAAA,EAEJ,WAAW,QAAQ,aAAa;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,KAAK;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,WAAW,GAAG,MAAM,SAAM,QAAQ,KAAK;AAAA;AAAA,IAClD;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,QAAQ,MAAM;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,GAAG,MAAM,SAAM,iBAAiB;AAAA,QAClD,UAAU,SAAS,MAAM;AAAA,QACzB,QAAQ,OAAO;AAAA;AAAA,IACjB;AAAA,EAEJ;AAEA,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,aAAa,gCAAgC,UAAU,0DAA0D,UAAU;AAAA,QAC3H,SACE,qBAAC,SAAI,WAAU,eACZ;AAAA,kBAAQ,aACP;AAAA,YAAC;AAAA;AAAA,cACC,QAAQ,OAAO;AAAA,cACf,SAAS;AAAA,cACT,UAAU,SAAS;AAAA,cACnB,WAAW,MAAM;AAAA,cACjB,SAAS,MAAM;AAAA,cACf;AAAA;AAAA,UACF,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,WACF;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD;AAAA,KACH;AAEJ;","names":[]}
@@ -8,49 +8,49 @@ function SubscriptionsPanel({
8
8
  }) {
9
9
  const rows = [
10
10
  {
11
- label: "Past due",
11
+ label: "Payment missed",
12
12
  status: "past_due",
13
- note: "Payment failed, at risk of churning",
13
+ note: "Their last payment failed, so they may leave",
14
14
  value: integer.format(recurring.pastDueSubscriptions),
15
15
  tone: recurring.pastDueSubscriptions > 0 ? "danger" : "default"
16
16
  },
17
17
  {
18
- label: "Non-renewing",
18
+ label: "Set to end",
19
19
  status: "non_renewing",
20
- note: "Set to cancel at period end",
20
+ note: "Will not renew when the paid period ends",
21
21
  value: integer.format(recurring.nonRenewingSubscriptions),
22
22
  tone: recurring.nonRenewingSubscriptions > 0 ? "danger" : "default"
23
23
  },
24
24
  {
25
25
  label: "Active",
26
26
  status: "active",
27
- note: "Paying on a live billing cycle",
27
+ note: "Paying for their plan",
28
28
  value: integer.format(recurring.activeSubscriptions)
29
29
  },
30
30
  {
31
- label: "Trialing",
31
+ label: "On a trial",
32
32
  status: "trialing",
33
- note: "In trial, not yet billed",
33
+ note: "Trying a plan, not yet paying",
34
34
  value: integer.format(recurring.trialingSubscriptions)
35
35
  },
36
36
  {
37
- label: "New this period",
38
- note: `Started in ${span}`,
37
+ label: "Joined this period",
38
+ note: `Started paying in ${span}`,
39
39
  value: integer.format(recurring.newInRange)
40
40
  },
41
41
  {
42
- label: "Churned this period",
43
- note: `Cancelled in ${span}`,
42
+ label: "Left this period",
43
+ note: `Stopped paying in ${span}`,
44
44
  value: integer.format(recurring.churnedInRange),
45
45
  tone: recurring.churnedInRange > 0 ? "danger" : "default"
46
46
  },
47
47
  {
48
- label: "Cancelled",
49
- note: "Lifetime total",
48
+ label: "Cancelled in total",
49
+ note: "Since the start",
50
50
  value: integer.format(recurring.cancelledSubscriptions)
51
51
  }
52
52
  ];
53
- return /* @__PURE__ */ jsx(Panel, { title: "Subscriptions", caption: "Today", height: "var(--adm-panel-h)", children: /* @__PURE__ */ jsx(FigureList, { rows }) });
53
+ return /* @__PURE__ */ jsx(Panel, { title: "Plan subscriptions today", caption: "Right now", height: "var(--adm-panel-h)", children: /* @__PURE__ */ jsx(FigureList, { rows }) });
54
54
  }
55
55
  export {
56
56
  SubscriptionsPanel
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Past due',\n status: 'past_due',\n note: 'Payment failed, at risk of churning',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Non-renewing',\n status: 'non_renewing',\n note: 'Set to cancel at period end',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying on a live billing cycle',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'Trialing',\n status: 'trialing',\n note: 'In trial, not yet billed',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'New this period',\n note: `Started in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Churned this period',\n note: `Cancelled in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled',\n note: 'Lifetime total',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Subscriptions\" caption=\"Today\" height=\"var(--adm-panel-h)\">\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6DM;AA5DN,SAAS,aAAa;AACtB,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGG;AACD,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,oBAAoB;AAAA,MACpD,MAAM,UAAU,uBAAuB,IAAI,WAAW;AAAA,IACxD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,wBAAwB;AAAA,MACxD,MAAM,UAAU,2BAA2B,IAAI,WAAW;AAAA,IAC5D;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,mBAAmB;AAAA,IACrD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,qBAAqB;AAAA,IACvD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,cAAc,IAAI;AAAA,MACxB,OAAO,QAAQ,OAAO,UAAU,UAAU;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,gBAAgB,IAAI;AAAA,MAC1B,OAAO,QAAQ,OAAO,UAAU,cAAc;AAAA,MAC9C,MAAM,UAAU,iBAAiB,IAAI,WAAW;AAAA,IAClD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,sBAAsB;AAAA,IACxD;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,iBAAgB,SAAQ,SAAQ,QAAO,sBAClD,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Payment missed',\n status: 'past_due',\n note: 'Their last payment failed, so they may leave',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Set to end',\n status: 'non_renewing',\n note: 'Will not renew when the paid period ends',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying for their plan',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'On a trial',\n status: 'trialing',\n note: 'Trying a plan, not yet paying',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'Joined this period',\n note: `Started paying in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Left this period',\n note: `Stopped paying in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled in total',\n note: 'Since the start',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Plan subscriptions today\" caption=\"Right now\" height=\"var(--adm-panel-h)\">\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6DM;AA5DN,SAAS,aAAa;AACtB,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGG;AACD,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,oBAAoB;AAAA,MACpD,MAAM,UAAU,uBAAuB,IAAI,WAAW;AAAA,IACxD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,wBAAwB;AAAA,MACxD,MAAM,UAAU,2BAA2B,IAAI,WAAW;AAAA,IAC5D;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,mBAAmB;AAAA,IACrD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,qBAAqB;AAAA,IACvD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,UAAU;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,cAAc;AAAA,MAC9C,MAAM,UAAU,iBAAiB,IAAI,WAAW;AAAA,IAClD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,sBAAsB;AAAA,IACxD;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,4BAA2B,SAAQ,aAAY,QAAO,sBACjE,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
@@ -18,9 +18,12 @@ import {
18
18
  word
19
19
  } from "./ledger-model";
20
20
  import { PlanMixPanel } from "./plan-mix-panel";
21
+ import { PlanPaymentDrawer } from "./plan-payment-drawer";
22
+ import { useUrlFilters } from "./use-url-filters";
21
23
  import styles from "./ledger.module.css";
22
24
  import table from "./revenue.module.css";
23
25
  const PAGE_SIZE = 25;
26
+ const FILTERS = { q: "", status: "", method: "" };
24
27
  const integer = new Intl.NumberFormat("en-US");
25
28
  const STATUSES = [
26
29
  { value: "", label: "Every result" },
@@ -40,23 +43,19 @@ function SubscriptionsView({
40
43
  endDate,
41
44
  revenue
42
45
  }) {
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);
46
+ const { values, page, set } = useUrlFilters("sp", FILTERS);
47
+ const [search, setSearch] = useState(values.q);
48
+ const [open, setOpen] = useState(null);
48
49
  useEffect(() => {
49
- const timer = setTimeout(() => setQ(search.trim()), 300);
50
+ const timer = setTimeout(() => {
51
+ if (search.trim() !== values.q) set({ q: search.trim() });
52
+ }, 300);
50
53
  return () => clearTimeout(timer);
51
54
  }, [search]);
52
55
  const filters = useMemo(
53
- () => ({ startDate, endDate, status, method, q, page, limit: PAGE_SIZE }),
54
- [startDate, endDate, status, method, q, page]
56
+ () => ({ startDate, endDate, status: values.status, method: values.method, q: values.q, page, limit: PAGE_SIZE }),
57
+ [startDate, endDate, values, page]
55
58
  );
56
- const change = (set) => (value) => {
57
- set(value);
58
- setPage(1);
59
- };
60
59
  const { recurring } = revenue;
61
60
  const tracked = recurring.tracked !== false;
62
61
  const currency = recurring.currency ?? revenue.currency ?? "NGN";
@@ -118,13 +117,13 @@ function SubscriptionsView({
118
117
  FilterSearch,
119
118
  {
120
119
  value: search,
121
- onChange: change(setSearch),
120
+ onChange: setSearch,
122
121
  placeholder: "Search a merchant's name or email, or a reference",
123
122
  label: "Search plan payments"
124
123
  }
125
124
  ),
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" })
125
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.status, onChange: (status) => set({ status }), options: STATUSES, label: "Payment result" }),
126
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.method, onChange: (method) => set({ method }), options: METHODS, label: "Way of paying" })
128
127
  ] }),
129
128
  /* @__PURE__ */ jsx(
130
129
  DownloadButton,
@@ -148,7 +147,7 @@ function SubscriptionsView({
148
147
  /* @__PURE__ */ jsx(Th, { children: "Result" }),
149
148
  /* @__PURE__ */ jsx(Th, { align: "right", children: "Amount" })
150
149
  ] }),
151
- children: payments.data.items.map((row) => /* @__PURE__ */ jsx(PlanPaymentRow, { row }, row.id))
150
+ children: payments.data.items.map((row) => /* @__PURE__ */ jsx(PlanPaymentRow, { row, onOpen: setOpen }, row.id))
152
151
  }
153
152
  ),
154
153
  /* @__PURE__ */ jsx(
@@ -158,15 +157,16 @@ function SubscriptionsView({
158
157
  totalPages: Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1),
159
158
  total: payments.data.total,
160
159
  noun: "plan payments",
161
- onPage: setPage
160
+ onPage: (next) => set({ page: next })
162
161
  }
163
- )
162
+ ),
163
+ /* @__PURE__ */ jsx(PlanPaymentDrawer, { row: open, onClose: () => setOpen(null) })
164
164
  ] })
165
165
  ] });
166
166
  }
167
- function PlanPaymentRow({ row }) {
167
+ function PlanPaymentRow({ row, onOpen }) {
168
168
  const when = row.paidAt ?? row.createdAt;
169
- return /* @__PURE__ */ jsxs(TableRow, { children: [
169
+ return /* @__PURE__ */ jsxs(TableRow, { onClick: () => onOpen(row), children: [
170
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
171
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
172
172
  /* @__PURE__ */ jsx("span", { className: styles.whoName, children: row.merchantName || "Unknown merchant" }),
@@ -1 +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":[]}
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 { PlanPaymentDrawer } from './plan-payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', status: '', method: '' };\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 { values, page, set } = useUrlFilters('sp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<SubscriptionPaymentRow | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, status: values.status, method: values.method, q: values.q, page, limit: PAGE_SIZE }),\n [startDate, endDate, values, page],\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={setSearch}\n placeholder=\"Search a merchant's name or email, or a reference\"\n label=\"Search plan payments\"\n />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} 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} onOpen={setOpen} />\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={(next) => set({ page: next })}\n />\n <PlanPaymentDrawer row={open} onClose={() => setOpen(null)} />\n </>\n )}\n </div>\n );\n}\n\nfunction PlanPaymentRow({ row, onOpen }: { row: SubscriptionPaymentRow; onOpen: (row: SubscriptionPaymentRow) => void }) {\n const when = row.paidAt ?? row.createdAt;\n\n return (\n <TableRow onClick={() => onOpen(row)}>\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":";AAwFM,SAwCE,UAxCF,KAIA,YAJA;AAtFN,SAAS,WAAW,SAAS,gBAAgB;AAC7C,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD;AAAA,EACE;AAAA,OAGK;AACP,SAAS,+BAA+B;AAExC,SAAS,SAAS,cAAc;AAChC,SAAS,YAAY,cAAc,cAAc,SAAS,YAAY,UAAU,eAAe;AAC/F,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,OAAO,YAAY;AACnB,OAAO,WAAW;AAElB,MAAM,YAAY;AAClB,MAAM,UAAU,EAAE,GAAG,IAAI,QAAQ,IAAI,QAAQ,GAAG;AAChD,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAE7C,MAAM,WAAW;AAAA,EACf,EAAE,OAAO,IAAI,OAAO,eAAe;AAAA,EACnC,EAAE,OAAO,aAAa,OAAO,OAAO;AAAA,EACpC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,WAAW,OAAO,UAAU;AACvC;AACA,MAAM,UAAU;AAAA,EACd,EAAE,OAAO,IAAI,OAAO,sBAAsB;AAAA,EAC1C,EAAE,OAAO,WAAW,OAAO,eAAe;AAAA,EAC1C,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,UAAU,OAAO,mBAAmB;AAC/C;AAEA,MAAM,OAAO,CAAC,WACZ,WAAW,cAAc,OAAO,aAAa,WAAW,WAAW,OAAO,YAAY,OAAO;AAGxF,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,EAAE,QAAQ,MAAM,IAAI,IAAI,cAAc,MAAM,OAAO;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,OAAO,CAAC;AAC7C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwC,IAAI;AAEpE,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW,MAAM;AAC7B,UAAI,OAAO,KAAK,MAAM,OAAO,EAAG,KAAI,EAAE,GAAG,OAAO,KAAK,EAAE,CAAC;AAAA,IAC1D,GAAG,GAAG;AAEN,WAAO,MAAM,aAAa,KAAK;AAAA,EAGjC,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU;AAAA,IACd,OAAO,EAAE,WAAW,SAAS,QAAQ,OAAO,QAAQ,QAAQ,OAAO,QAAQ,GAAG,OAAO,GAAG,MAAM,OAAO,UAAU;AAAA,IAC/G,CAAC,WAAW,SAAS,QAAQ,IAAI;AAAA,EACnC;AAEA,QAAM,EAAE,UAAU,IAAI;AACtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,WAAW,UAAU,YAAY,QAAQ,YAAY;AAC3D,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,SAAS,QAAQ,UAAU,CAAC;AAClC,QAAM,SAAS;AACf,QAAM,MAAM;AACZ,QAAM,WAAW,wBAAwB,OAAO;AAEhD,SACE,qBAAC,SACC;AAAA,wBAAC,OAAE,WAAW,OAAO,MAAM,6HAE3B;AAAA,IAEA,qBAAC,aAAQ,cAAW,eAAc,WAAU,YAC1C;AAAA,0BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,UAAU,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAAK;AAAA,UACpE,KAAK,UAAU,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAAsB;AAAA,UAClH,MAAK;AAAA;AAAA,MACP,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,UAAU,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAAK;AAAA,UACpE,KAAK,UAAU,wBAAwB;AAAA,UACvC,MAAK;AAAA;AAAA,MACP,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,UAAU,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAAK;AAAA,UACtE,KAAK,UAAU,eAAe;AAAA,UAC9B,MAAK;AAAA;AAAA,MACP,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,UAAU,GAAG,QAAQ,OAAO,UAAU,UAAU,CAAC,YAAY;AAAA,UACpE,KAAK,UAAU,GAAG,QAAQ,OAAO,UAAU,cAAc,CAAC,sCAAsC;AAAA,UAChG,MAAK;AAAA;AAAA,MACP,GACF;AAAA,OACF;AAAA,IAEC,WAAW,UAAU,cAAc,SAAS,IAC3C,iCACE;AAAA,0BAAC,QAAG,WAAW,OAAO,cAAc,2BAAa;AAAA,MACjD,oBAAC,OAAE,WAAW,OAAO,aAAa,8DAAgD;AAAA,MAClF,oBAAC,gBAAa,eAAe,UAAU,eAAe,OAAc,SAAS,mBAAgB,QAAQ,IAAI;AAAA,OAC3G,IACE;AAAA,IAEJ,oBAAC,QAAG,WAAW,OAAO,cAAc,gCAAkB;AAAA,IACrD,SAAS,UACR,oBAAC,SAAI,WAAW,OAAO,QAAQ,4IAG/B,IACE,SAAS,aAAa,CAAC,SAAS,OAClC,oBAAC,cAAW,OAAM,+BAAyB,IAE3C,iCACE;AAAA,2BAAC,SAAI,WAAW,OAAO,SACrB;AAAA,6BAAC,WAAQ,OAAM,wBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV,aAAY;AAAA,cACZ,OAAM;AAAA;AAAA,UACR;AAAA,UACA,oBAAC,gBAAa,OAAO,OAAO,QAAQ,UAAU,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,OAAM,kBAAiB;AAAA,UACrH,oBAAC,gBAAa,OAAO,OAAO,QAAQ,UAAU,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,SAAS,OAAM,iBAAgB;AAAA,WACrH;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,UAAU,aAAa,iBAAiB,WAAW,OAAO;AAAA,YAC1D,MAAM,MAAM,iBAAiB,wBAAwB,EAAE,GAAG,SAAS,MAAM,QAAW,OAAO,OAAU,CAAC;AAAA;AAAA,QACxG;AAAA,SACF;AAAA,MAEC,SAAS,KAAK,MAAM,WAAW,IAC9B,oBAAC,aAAU,WAAW,MAAM,OAAO,+EAAiE,IAEpG;AAAA,QAAC;AAAA;AAAA,UACC,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,sBAAQ;AAAA,YACZ,oBAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,iBAAG;AAAA,YACP,oBAAC,MAAG,iBAAG;AAAA,YACP,oBAAC,MAAG,oBAAM;AAAA,YACV,oBAAC,MAAG,OAAM,SAAQ,oBAAM;AAAA,aAC1B;AAAA,UAGD,mBAAS,KAAK,MAAM,IAAI,CAAC,QACxB,oBAAC,kBAA4B,KAAU,QAAQ,WAA1B,IAAI,EAA+B,CACzD;AAAA;AAAA,MACH;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,YAAY,KAAK,IAAI,KAAK,KAAK,SAAS,KAAK,QAAQ,SAAS,GAAG,CAAC;AAAA,UAClE,OAAO,SAAS,KAAK;AAAA,UACrB,MAAK;AAAA,UACL,QAAQ,CAAC,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC;AAAA;AAAA,MACtC;AAAA,MACA,oBAAC,qBAAkB,KAAK,MAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AAAA,OAC9D;AAAA,KAEJ;AAEJ;AAEA,SAAS,eAAe,EAAE,KAAK,OAAO,GAAmF;AACvH,QAAM,OAAO,IAAI,UAAU,IAAI;AAE/B,SACE,qBAAC,YAAS,SAAS,MAAM,OAAO,GAAG,GACjC;AAAA,wBAAC,QAAG,WAAW,MAAM,KAClB,cAAI,KAAK,IAAI,EAAE,eAAe,QAAW,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,WAAW,MAAM,WAAW,QAAQ,UAAU,CAAC,GACnI;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,SAAU,cAAI,gBAAgB,oBAAmB;AAAA,MACxE,IAAI,gBAAgB,oBAAC,UAAK,WAAW,OAAO,QAAS,cAAI,eAAc,IAAU;AAAA,OACpF,GACF;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,MAAO,cAAI,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,KAAK,MAAM,CAAC,GAAE;AAAA,IACjF,oBAAC,QAAI,eAAK,eAAe,IAAI,OAAO,GAAE;AAAA,IACtC,oBAAC,QAAI,eAAK,2BAA2B,IAAI,MAAM,GAAE;AAAA,IACjD,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,GAAG,OAAO,MAAM,IAAI,KAAK,IAAI,MAAM,CAAC,IAAK,eAAK,cAAc,IAAI,MAAM,GAAE;AAAA,MACxF,IAAI,WAAW,YAAY,IAAI,gBAAgB,oBAAC,UAAK,WAAW,OAAO,QAAS,cAAI,eAAc,IAAU;AAAA,OAC/G,GACF;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,WAAY,uBAAa,IAAI,YAAY,KAAK,GAAE;AAAA,KACvE;AAEJ;","names":[]}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { toCsv } from "@withpotter/lib/csv";
4
+ import { DownloadButton } from "./download-button";
5
+ import { downloadName } from "./ledger-model";
6
+ import { PrintSummaryButton } from "./print-summary-button";
7
+ import { SUMMARY_HEAD, summaryRows } from "./summary-rows";
8
+ import { summaryModel } from "./summary-print";
9
+ function SummaryDownload({
10
+ ledger,
11
+ revenue,
12
+ currency,
13
+ startDate,
14
+ endDate,
15
+ period
16
+ }) {
17
+ const rows = summaryRows(ledger, revenue, currency);
18
+ if (rows.length === 0) return null;
19
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
20
+ /* @__PURE__ */ jsx(PrintSummaryButton, { model: summaryModel(ledger, revenue, currency, period) }),
21
+ /* @__PURE__ */ jsx(
22
+ DownloadButton,
23
+ {
24
+ label: "Download summary",
25
+ filename: downloadName("revenue-summary", startDate, endDate),
26
+ load: async () => toCsv([SUMMARY_HEAD, ...rows])
27
+ }
28
+ )
29
+ ] });
30
+ }
31
+ export {
32
+ SummaryDownload
33
+ };
34
+ //# sourceMappingURL=summary-download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/summary-download.tsx"],"sourcesContent":["'use client';\n\nimport { toCsv } from '@withpotter/lib/csv';\nimport type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { DownloadButton } from './download-button';\nimport { downloadName } from './ledger-model';\nimport { PrintSummaryButton } from './print-summary-button';\nimport { SUMMARY_HEAD, summaryRows } from './summary-rows';\nimport { summaryModel } from './summary-print';\n\n/** The figures on the Overview as a one-page PDF (through the print window) or a small spreadsheet. */\nexport function SummaryDownload({\n ledger,\n revenue,\n currency,\n startDate,\n endDate,\n period,\n}: {\n ledger?: MerchantPaymentsPage;\n revenue?: PlatformRevenue;\n currency: string;\n startDate?: string;\n endDate?: string;\n /** The period in words, such as \"last 30 days\". */\n period: string;\n}) {\n const rows = summaryRows(ledger, revenue, currency);\n\n if (rows.length === 0) return null;\n\n return (\n <>\n <PrintSummaryButton model={summaryModel(ledger, revenue, currency, period)} />\n <DownloadButton\n label=\"Download summary\"\n filename={downloadName('revenue-summary', startDate, endDate)}\n load={async () => toCsv([SUMMARY_HEAD, ...rows])}\n />\n </>\n );\n}\n"],"mappings":";AAgCI,mBACE,KADF;AA9BJ,SAAS,aAAa;AAEtB,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,cAAc,mBAAmB;AAC1C,SAAS,oBAAoB;AAGtB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,OAAO,YAAY,QAAQ,SAAS,QAAQ;AAElD,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,iCACE;AAAA,wBAAC,sBAAmB,OAAO,aAAa,QAAQ,SAAS,UAAU,MAAM,GAAG;AAAA,IAC5E;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,UAAU,aAAa,mBAAmB,WAAW,OAAO;AAAA,QAC5D,MAAM,YAAY,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;AAAA;AAAA,IACjD;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,82 @@
1
+ import { methodCards, minorToMoney, pctChange } from "./ledger-model";
2
+ const integer = new Intl.NumberFormat("en-US");
3
+ const change = (now, before) => {
4
+ const pct = pctChange(now, before);
5
+ return pct === null ? void 0 : `${pct > 0 ? "+" : ""}${pct}% since the period before`;
6
+ };
7
+ function summaryModel(ledger, revenue, currency, period) {
8
+ const groups = [];
9
+ const now = ledger?.cards.current.find((c) => c.currency === currency);
10
+ const before = ledger?.cards.previous?.find((c) => c.currency === currency);
11
+ const money = (minor) => minorToMoney(minor, currency);
12
+ if (ledger && now) {
13
+ groups.push({
14
+ heading: "Money merchants earned",
15
+ note: "What customers paid to merchants. This is the merchants' money, not Olive's own income.",
16
+ rows: [
17
+ { label: "Money received", value: money(now.grossMinor), detail: change(now.grossMinor, before?.grossMinor) },
18
+ { label: "Fees taken", value: money(now.feeMinor), detail: change(now.feeMinor, before?.feeMinor) },
19
+ { label: "Merchants keep", value: money(now.netMinor), detail: change(now.netMinor, before?.netMinor) },
20
+ { label: "Refunded", value: money(now.refundsMinor) },
21
+ { label: "Payments that went through", value: integer.format(now.payments) },
22
+ { label: "Payments that worked", value: now.successRate === null ? "\u2013" : `${now.successRate}%` }
23
+ ]
24
+ });
25
+ groups.push({
26
+ heading: "How customers paid",
27
+ note: "Money received, by way of paying.",
28
+ rows: methodCards(ledger.byChannel.current, ledger.byChannel.previous, currency).map((card) => ({
29
+ label: card.label,
30
+ value: money(card.amountMinor),
31
+ detail: card.payments === 0 ? "No payments this way" : [
32
+ `${integer.format(card.payments)} payments`,
33
+ `${card.share}% of the money received`,
34
+ card.change === null ? "" : `${card.change > 0 ? "+" : ""}${card.change}% since the period before`
35
+ ].filter(Boolean).join(" \xB7 ")
36
+ }))
37
+ });
38
+ }
39
+ const plan = revenue?.recurring;
40
+ const naira = (value) => minorToMoney(Math.round(value * 100), plan?.currency ?? "NGN");
41
+ groups.push({
42
+ heading: "Money Olive earns",
43
+ note: "What merchants pay Olive for their plan.",
44
+ rows: plan && plan.tracked !== false ? [
45
+ { label: "Plan income each month", value: naira(plan.mrr) },
46
+ { label: "Plan income over a year", value: naira(plan.arr) },
47
+ { label: "Average per paying merchant", value: naira(plan.arpa) },
48
+ { label: "Merchants paying", value: integer.format(plan.payingMerchants ?? plan.activeSubscriptions) },
49
+ { label: "Joined this period", value: integer.format(plan.newInRange) },
50
+ { label: "Left this period", value: integer.format(plan.churnedInRange) }
51
+ ] : [{ label: "Plan income", value: "Not set up yet", detail: "The plan payments are not connected on this box." }]
52
+ });
53
+ return { title: "Revenue summary", period, groups };
54
+ }
55
+ const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
56
+ function summaryHtml(model, brand, printedOn) {
57
+ const groups = model.groups.map(
58
+ (group) => `<section><h2>${esc(group.heading)}</h2><p class="note">${esc(group.note)}</p><table>${group.rows.map(
59
+ (row) => `<tr><td class="label">${esc(row.label)}${row.detail ? `<div class="detail">${esc(row.detail)}</div>` : ""}</td><td class="value">${esc(row.value)}</td></tr>`
60
+ ).join("")}</table></section>`
61
+ ).join("");
62
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${esc(model.title)}</title><style>
63
+ @page { size: A4; margin: 14mm; }
64
+ * { box-sizing: border-box; }
65
+ body { margin: 0; font: 12px/1.45 -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; color: #1f2a14; }
66
+ h1 { margin: 0 0 2px; font-size: 22px; color: #3d5a1e; }
67
+ .period { margin: 0 0 14px; color: #5b6356; font-size: 13px; }
68
+ section { margin: 0 0 14px; break-inside: avoid; }
69
+ h2 { margin: 0 0 2px; font-size: 14px; color: #3d5a1e; }
70
+ .note { margin: 0 0 6px; color: #5b6356; }
71
+ table { width: 100%; border-collapse: collapse; }
72
+ td { padding: 5px 0; border-bottom: 1px solid #d9ddd3; vertical-align: top; }
73
+ .value { text-align: right; font-weight: 600; white-space: nowrap; padding-left: 12px; }
74
+ .detail { color: #5b6356; font-size: 11px; }
75
+ footer { margin-top: 10px; color: #5b6356; font-size: 10px; }
76
+ </style></head><body><h1>${esc(model.title)}</h1><p class="period">${esc(brand)} \xB7 ${esc(model.period)}</p>${groups}<footer>Printed ${esc(printedOn)}. Amounts are in the currency shown; different currencies are never added together.</footer></body></html>`;
77
+ }
78
+ export {
79
+ summaryHtml,
80
+ summaryModel
81
+ };
82
+ //# sourceMappingURL=summary-print.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/summary-print.ts"],"sourcesContent":["import type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { methodCards, minorToMoney, pctChange } from './ledger-model';\n\nexport interface SummaryRow {\n label: string;\n value: string;\n /** A short second line: the change since the period before, or a count. */\n detail?: string;\n}\n\nexport interface SummaryGroup {\n heading: string;\n note: string;\n rows: SummaryRow[];\n}\n\nexport interface SummaryModel {\n title: string;\n period: string;\n groups: SummaryGroup[];\n}\n\nconst integer = new Intl.NumberFormat('en-US');\n\nconst change = (now: string | number, before?: string | number): string | undefined => {\n const pct = pctChange(now, before);\n\n return pct === null ? undefined : `${pct > 0 ? '+' : ''}${pct}% since the period before`;\n};\n\n/** The Overview's figures as words and amounts a person can read on one printed page. */\nexport function summaryModel(\n ledger: MerchantPaymentsPage | undefined,\n revenue: PlatformRevenue | undefined,\n currency: string,\n period: string,\n): SummaryModel {\n const groups: SummaryGroup[] = [];\n const now = ledger?.cards.current.find((c) => c.currency === currency);\n const before = ledger?.cards.previous?.find((c) => c.currency === currency);\n const money = (minor: string) => minorToMoney(minor, currency);\n\n if (ledger && now) {\n groups.push({\n heading: 'Money merchants earned',\n note: \"What customers paid to merchants. This is the merchants' money, not Olive's own income.\",\n rows: [\n { label: 'Money received', value: money(now.grossMinor), detail: change(now.grossMinor, before?.grossMinor) },\n { label: 'Fees taken', value: money(now.feeMinor), detail: change(now.feeMinor, before?.feeMinor) },\n { label: 'Merchants keep', value: money(now.netMinor), detail: change(now.netMinor, before?.netMinor) },\n { label: 'Refunded', value: money(now.refundsMinor) },\n { label: 'Payments that went through', value: integer.format(now.payments) },\n { label: 'Payments that worked', value: now.successRate === null ? '–' : `${now.successRate}%` },\n ],\n });\n\n groups.push({\n heading: 'How customers paid',\n note: 'Money received, by way of paying.',\n rows: methodCards(ledger.byChannel.current, ledger.byChannel.previous, currency).map((card) => ({\n label: card.label,\n value: money(card.amountMinor),\n detail:\n card.payments === 0\n ? 'No payments this way'\n : [\n `${integer.format(card.payments)} payments`,\n `${card.share}% of the money received`,\n card.change === null ? '' : `${card.change > 0 ? '+' : ''}${card.change}% since the period before`,\n ]\n .filter(Boolean)\n .join(' · '),\n })),\n });\n }\n\n const plan = revenue?.recurring;\n const naira = (value: number) => minorToMoney(Math.round(value * 100), plan?.currency ?? 'NGN');\n\n groups.push({\n heading: 'Money Olive earns',\n note: 'What merchants pay Olive for their plan.',\n rows:\n plan && plan.tracked !== false\n ? [\n { label: 'Plan income each month', value: naira(plan.mrr) },\n { label: 'Plan income over a year', value: naira(plan.arr) },\n { label: 'Average per paying merchant', value: naira(plan.arpa) },\n { label: 'Merchants paying', value: integer.format(plan.payingMerchants ?? plan.activeSubscriptions) },\n { label: 'Joined this period', value: integer.format(plan.newInRange) },\n { label: 'Left this period', value: integer.format(plan.churnedInRange) },\n ]\n : [{ label: 'Plan income', value: 'Not set up yet', detail: 'The plan payments are not connected on this box.' }],\n });\n\n return { title: 'Revenue summary', period, groups };\n}\n\nconst esc = (text: string): string =>\n text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n\n/** A self-contained page that prints on one A4 sheet. Every value is escaped. */\nexport function summaryHtml(model: SummaryModel, brand: string, printedOn: string): string {\n const groups = model.groups\n .map(\n (group) => `<section><h2>${esc(group.heading)}</h2><p class=\"note\">${esc(group.note)}</p><table>${group.rows\n .map(\n (row) =>\n `<tr><td class=\"label\">${esc(row.label)}${row.detail ? `<div class=\"detail\">${esc(row.detail)}</div>` : ''}</td><td class=\"value\">${esc(row.value)}</td></tr>`,\n )\n .join('')}</table></section>`,\n )\n .join('');\n\n return `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>${esc(model.title)}</title><style>\n@page { size: A4; margin: 14mm; }\n* { box-sizing: border-box; }\nbody { margin: 0; font: 12px/1.45 -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif; color: #1f2a14; }\nh1 { margin: 0 0 2px; font-size: 22px; color: #3d5a1e; }\n.period { margin: 0 0 14px; color: #5b6356; font-size: 13px; }\nsection { margin: 0 0 14px; break-inside: avoid; }\nh2 { margin: 0 0 2px; font-size: 14px; color: #3d5a1e; }\n.note { margin: 0 0 6px; color: #5b6356; }\ntable { width: 100%; border-collapse: collapse; }\ntd { padding: 5px 0; border-bottom: 1px solid #d9ddd3; vertical-align: top; }\n.value { text-align: right; font-weight: 600; white-space: nowrap; padding-left: 12px; }\n.detail { color: #5b6356; font-size: 11px; }\nfooter { margin-top: 10px; color: #5b6356; font-size: 10px; }\n</style></head><body><h1>${esc(model.title)}</h1><p class=\"period\">${esc(brand)} · ${esc(model.period)}</p>${groups}<footer>Printed ${esc(printedOn)}. Amounts are in the currency shown; different currencies are never added together.</footer></body></html>`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,44 @@
1
+ import { methodCards, pctChange, plainAmount } from "./ledger-model";
2
+ const SUMMARY_HEAD = ["Section", "Figure", "This period", "Period before", "Change (%)"];
3
+ const naira = (kobo) => plainAmount(Math.round(kobo * 100));
4
+ function summaryRows(ledger, revenue, currency) {
5
+ const rows = [];
6
+ const now = ledger?.cards.current.find((c) => c.currency === currency);
7
+ const before = ledger?.cards.previous?.find((c) => c.currency === currency);
8
+ if (now) {
9
+ const money = (label, a, b) => rows.push(["Merchant payments", label, plainAmount(a), b === void 0 ? "" : plainAmount(b), String(pctChange(a, b) ?? "")]);
10
+ money("Money received", now.grossMinor, before?.grossMinor);
11
+ money("Fees taken", now.feeMinor, before?.feeMinor);
12
+ money("Merchants keep", now.netMinor, before?.netMinor);
13
+ money("Refunded", now.refundsMinor, before?.refundsMinor);
14
+ rows.push(["Merchant payments", "Payments that went through", String(now.payments), before ? String(before.payments) : "", String(pctChange(now.payments, before?.payments) ?? "")]);
15
+ rows.push(["Merchant payments", "Payments that worked (%)", String(now.successRate ?? ""), String(before?.successRate ?? ""), ""]);
16
+ }
17
+ if (ledger) {
18
+ for (const card of methodCards(ledger.byChannel.current, ledger.byChannel.previous, currency)) {
19
+ const prior = ledger.byChannel.previous?.find((r) => r.currency === currency && r.channel.key === card.key);
20
+ rows.push([
21
+ "How customers paid",
22
+ card.label,
23
+ plainAmount(card.amountMinor),
24
+ ledger.byChannel.previous ? plainAmount(prior?.amountMinor ?? "0") : "",
25
+ String(card.change ?? "")
26
+ ]);
27
+ }
28
+ }
29
+ const plan = revenue?.recurring;
30
+ if (plan && plan.tracked !== false) {
31
+ rows.push(["Plan income", "Income each month", naira(plan.mrr), "", ""]);
32
+ rows.push(["Plan income", "Income over a year", naira(plan.arr), "", ""]);
33
+ rows.push(["Plan income", "Average per paying merchant", naira(plan.arpa), "", ""]);
34
+ rows.push(["Plan income", "Merchants paying", String(plan.payingMerchants ?? plan.activeSubscriptions), "", ""]);
35
+ rows.push(["Plan income", "Joined in this period", String(plan.newInRange), "", ""]);
36
+ rows.push(["Plan income", "Left in this period", String(plan.churnedInRange), "", ""]);
37
+ }
38
+ return rows;
39
+ }
40
+ export {
41
+ SUMMARY_HEAD,
42
+ summaryRows
43
+ };
44
+ //# sourceMappingURL=summary-rows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/summary-rows.ts"],"sourcesContent":["import type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { methodCards, pctChange, plainAmount } from './ledger-model';\n\nexport const SUMMARY_HEAD = ['Section', 'Figure', 'This period', 'Period before', 'Change (%)'];\n\nconst naira = (kobo: number): string => plainAmount(Math.round(kobo * 100));\n\n/**\n * The Revenue figures as rows for a spreadsheet: what customers paid merchants (with the period before), the ways\n * of paying, and Olive's plan income. Amounts are plain numbers in one currency.\n */\nexport function summaryRows(\n ledger: MerchantPaymentsPage | undefined,\n revenue: PlatformRevenue | undefined,\n currency: string,\n): string[][] {\n const rows: string[][] = [];\n const now = ledger?.cards.current.find((c) => c.currency === currency);\n const before = ledger?.cards.previous?.find((c) => c.currency === currency);\n\n if (now) {\n const money = (label: string, a: string, b?: string) =>\n rows.push(['Merchant payments', label, plainAmount(a), b === undefined ? '' : plainAmount(b), String(pctChange(a, b) ?? '')]);\n\n money('Money received', now.grossMinor, before?.grossMinor);\n money('Fees taken', now.feeMinor, before?.feeMinor);\n money('Merchants keep', now.netMinor, before?.netMinor);\n money('Refunded', now.refundsMinor, before?.refundsMinor);\n rows.push(['Merchant payments', 'Payments that went through', String(now.payments), before ? String(before.payments) : '', String(pctChange(now.payments, before?.payments) ?? '')]);\n rows.push(['Merchant payments', 'Payments that worked (%)', String(now.successRate ?? ''), String(before?.successRate ?? ''), '']);\n }\n\n if (ledger) {\n for (const card of methodCards(ledger.byChannel.current, ledger.byChannel.previous, currency)) {\n const prior = ledger.byChannel.previous?.find((r) => r.currency === currency && r.channel.key === card.key);\n\n rows.push([\n 'How customers paid',\n card.label,\n plainAmount(card.amountMinor),\n ledger.byChannel.previous ? plainAmount(prior?.amountMinor ?? '0') : '',\n String(card.change ?? ''),\n ]);\n }\n }\n\n const plan = revenue?.recurring;\n\n if (plan && plan.tracked !== false) {\n rows.push(['Plan income', 'Income each month', naira(plan.mrr), '', '']);\n rows.push(['Plan income', 'Income over a year', naira(plan.arr), '', '']);\n rows.push(['Plan income', 'Average per paying merchant', naira(plan.arpa), '', '']);\n rows.push(['Plan income', 'Merchants paying', String(plan.payingMerchants ?? plan.activeSubscriptions), '', '']);\n rows.push(['Plan income', 'Joined in this period', String(plan.newInRange), '', '']);\n rows.push(['Plan income', 'Left in this period', String(plan.churnedInRange), '', '']);\n }\n\n return rows;\n}\n"],"mappings":"AACA,SAAS,aAAa,WAAW,mBAAmB;AAE7C,MAAM,eAAe,CAAC,WAAW,UAAU,eAAe,iBAAiB,YAAY;AAE9F,MAAM,QAAQ,CAAC,SAAyB,YAAY,KAAK,MAAM,OAAO,GAAG,CAAC;AAMnE,SAAS,YACd,QACA,SACA,UACY;AACZ,QAAM,OAAmB,CAAC;AAC1B,QAAM,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ;AACrE,QAAM,SAAS,QAAQ,MAAM,UAAU,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ;AAE1E,MAAI,KAAK;AACP,UAAM,QAAQ,CAAC,OAAe,GAAW,MACvC,KAAK,KAAK,CAAC,qBAAqB,OAAO,YAAY,CAAC,GAAG,MAAM,SAAY,KAAK,YAAY,CAAC,GAAG,OAAO,UAAU,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;AAE9H,UAAM,kBAAkB,IAAI,YAAY,QAAQ,UAAU;AAC1D,UAAM,cAAc,IAAI,UAAU,QAAQ,QAAQ;AAClD,UAAM,kBAAkB,IAAI,UAAU,QAAQ,QAAQ;AACtD,UAAM,YAAY,IAAI,cAAc,QAAQ,YAAY;AACxD,SAAK,KAAK,CAAC,qBAAqB,8BAA8B,OAAO,IAAI,QAAQ,GAAG,SAAS,OAAO,OAAO,QAAQ,IAAI,IAAI,OAAO,UAAU,IAAI,UAAU,QAAQ,QAAQ,KAAK,EAAE,CAAC,CAAC;AACnL,SAAK,KAAK,CAAC,qBAAqB,4BAA4B,OAAO,IAAI,eAAe,EAAE,GAAG,OAAO,QAAQ,eAAe,EAAE,GAAG,EAAE,CAAC;AAAA,EACnI;AAEA,MAAI,QAAQ;AACV,eAAW,QAAQ,YAAY,OAAO,UAAU,SAAS,OAAO,UAAU,UAAU,QAAQ,GAAG;AAC7F,YAAM,QAAQ,OAAO,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,aAAa,YAAY,EAAE,QAAQ,QAAQ,KAAK,GAAG;AAE1G,WAAK,KAAK;AAAA,QACR;AAAA,QACA,KAAK;AAAA,QACL,YAAY,KAAK,WAAW;AAAA,QAC5B,OAAO,UAAU,WAAW,YAAY,OAAO,eAAe,GAAG,IAAI;AAAA,QACrE,OAAO,KAAK,UAAU,EAAE;AAAA,MAC1B,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,OAAO,SAAS;AAEtB,MAAI,QAAQ,KAAK,YAAY,OAAO;AAClC,SAAK,KAAK,CAAC,eAAe,qBAAqB,MAAM,KAAK,GAAG,GAAG,IAAI,EAAE,CAAC;AACvE,SAAK,KAAK,CAAC,eAAe,sBAAsB,MAAM,KAAK,GAAG,GAAG,IAAI,EAAE,CAAC;AACxE,SAAK,KAAK,CAAC,eAAe,+BAA+B,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,CAAC;AAClF,SAAK,KAAK,CAAC,eAAe,oBAAoB,OAAO,KAAK,mBAAmB,KAAK,mBAAmB,GAAG,IAAI,EAAE,CAAC;AAC/G,SAAK,KAAK,CAAC,eAAe,yBAAyB,OAAO,KAAK,UAAU,GAAG,IAAI,EAAE,CAAC;AACnF,SAAK,KAAK,CAAC,eAAe,uBAAuB,OAAO,KAAK,cAAc,GAAG,IAAI,EAAE,CAAC;AAAA,EACvF;AAEA,SAAO;AACT;","names":[]}
@@ -17,7 +17,7 @@ function TopTenantsPanel({
17
17
  return /* @__PURE__ */ jsx(
18
18
  Panel,
19
19
  {
20
- title: "Top merchants by GMV",
20
+ title: "Biggest merchants by sales",
21
21
  count: tenants.length || void 0,
22
22
  caption: focus ?? void 0,
23
23
  action: /* @__PURE__ */ jsx(Link, { href: "/admin/tenants", children: "View all" }),