@withpotter/pages-admin 0.13.0-next.123 → 0.13.0-next.58

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 (262) hide show
  1. package/dist/admin/_money/cash-postings.js +3 -20
  2. package/dist/admin/_money/cash-postings.js.map +1 -1
  3. package/dist/admin/_money/cash.module.css +0 -10
  4. package/dist/admin/_money/finder-format.js +0 -1
  5. package/dist/admin/_money/finder-format.js.map +1 -1
  6. package/dist/admin/_money/format.js +0 -1
  7. package/dist/admin/_money/format.js.map +1 -1
  8. package/dist/admin/_overview/activation-panel.js +6 -6
  9. package/dist/admin/_overview/activation-panel.js.map +1 -1
  10. package/dist/admin/_overview/attention-groups.js +8 -8
  11. package/dist/admin/_overview/attention-groups.js.map +1 -1
  12. package/dist/admin/_overview/attention-panel.js +33 -37
  13. package/dist/admin/_overview/attention-panel.js.map +1 -1
  14. package/dist/admin/_overview/attention.module.css +3 -0
  15. package/dist/admin/_overview/control-room.js +13 -41
  16. package/dist/admin/_overview/control-room.js.map +1 -1
  17. package/dist/admin/_overview/kpi-row.js +7 -7
  18. package/dist/admin/_overview/kpi-row.js.map +1 -1
  19. package/dist/admin/_overview/leaders-panel.js +1 -1
  20. package/dist/admin/_overview/leaders-panel.js.map +1 -1
  21. package/dist/admin/_overview/overdue-outstanding.js +1 -2
  22. package/dist/admin/_overview/overdue-outstanding.js.map +1 -1
  23. package/dist/admin/_overview/portfolio-panel.js +5 -5
  24. package/dist/admin/_overview/portfolio-panel.js.map +1 -1
  25. package/dist/admin/_overview/position-panel.js +12 -8
  26. package/dist/admin/_overview/position-panel.js.map +1 -1
  27. package/dist/admin/_pos/pos-figures.js +5 -18
  28. package/dist/admin/_pos/pos-figures.js.map +1 -1
  29. package/dist/admin/_pos/pos-rules.js +0 -2
  30. package/dist/admin/_pos/pos-rules.js.map +1 -1
  31. package/dist/admin/_pos/pos-words.js +1 -6
  32. package/dist/admin/_pos/pos-words.js.map +1 -1
  33. package/dist/admin/_pos/use-pos.js +1 -70
  34. package/dist/admin/_pos/use-pos.js.map +1 -1
  35. package/dist/admin/_shell/nav-config.js +3 -3
  36. package/dist/admin/_shell/nav-config.js.map +1 -1
  37. package/dist/admin/_shell/nav-filter.js +2 -4
  38. package/dist/admin/_shell/nav-filter.js.map +1 -1
  39. package/dist/admin/_shell/nav-row.js +2 -5
  40. package/dist/admin/_shell/nav-row.js.map +1 -1
  41. package/dist/admin/_shell/sidebar-footer.js +0 -20
  42. package/dist/admin/_shell/sidebar-footer.js.map +1 -1
  43. package/dist/admin/_shell/sidebar-footer.module.css +0 -17
  44. package/dist/admin/_shell/sidebar.js +15 -25
  45. package/dist/admin/_shell/sidebar.js.map +1 -1
  46. package/dist/admin/_shell/sidebar.module.css +0 -69
  47. package/dist/admin/_shell/use-fluid-hover.js +6 -37
  48. package/dist/admin/_shell/use-fluid-hover.js.map +1 -1
  49. package/dist/admin/_ui/admin.css +0 -7
  50. package/dist/admin/_ui/data-table.js +1 -1
  51. package/dist/admin/_ui/data-table.js.map +1 -1
  52. package/dist/admin/_ui/list-card.js +1 -1
  53. package/dist/admin/_ui/list-card.js.map +1 -1
  54. package/dist/admin/_ui/list-card.module.css +16 -2
  55. package/dist/admin/_ui/money-view.js +0 -1
  56. package/dist/admin/_ui/money-view.js.map +1 -1
  57. package/dist/admin/_ui/page-header.js +4 -31
  58. package/dist/admin/_ui/page-header.js.map +1 -1
  59. package/dist/admin/_ui/panel.module.css +10 -0
  60. package/dist/admin/_ui/stat-card.js +2 -11
  61. package/dist/admin/_ui/stat-card.js.map +1 -1
  62. package/dist/admin/_ui/trend.js +2 -2
  63. package/dist/admin/_ui/trend.js.map +1 -1
  64. package/dist/admin/_ui/workspace-tab.js +1 -11
  65. package/dist/admin/_ui/workspace-tab.js.map +1 -1
  66. package/dist/admin/accounts/account-panel.js +6 -49
  67. package/dist/admin/accounts/account-panel.js.map +1 -1
  68. package/dist/admin/accounts/account-panel.module.css +0 -73
  69. package/dist/admin/accounts/account-row.js +2 -11
  70. package/dist/admin/accounts/account-row.js.map +1 -1
  71. package/dist/admin/accounts/account-vitals.js +13 -15
  72. package/dist/admin/accounts/account-vitals.js.map +1 -1
  73. package/dist/admin/accounts/accounts-table.js +8 -39
  74. package/dist/admin/accounts/accounts-table.js.map +1 -1
  75. package/dist/admin/accounts/accounts-workspace.js +2 -2
  76. package/dist/admin/accounts/accounts-workspace.js.map +1 -1
  77. package/dist/admin/accounts/owner-session.js +6 -0
  78. package/dist/admin/accounts/owner-session.js.map +1 -1
  79. package/dist/admin/accounts/rows.module.css +0 -17
  80. package/dist/admin/audit/status-pill.js +0 -6
  81. package/dist/admin/audit/status-pill.js.map +1 -1
  82. package/dist/admin/audit/status-pill.module.css +0 -4
  83. package/dist/admin/commerce/commerce-overview.js +2 -2
  84. package/dist/admin/commerce/commerce-overview.js.map +1 -1
  85. package/dist/admin/commerce/merchant-meta.js +0 -1
  86. package/dist/admin/commerce/merchant-meta.js.map +1 -1
  87. package/dist/admin/commerce/merchant-view.js +2 -2
  88. package/dist/admin/commerce/merchant-view.js.map +1 -1
  89. package/dist/admin/feedback/prompt-operations.module.css +2 -0
  90. package/dist/admin/feedback/prompt-preview.module.css +2 -0
  91. package/dist/admin/hooks.js +1 -57
  92. package/dist/admin/hooks.js.map +1 -1
  93. package/dist/admin/layout.js +2 -8
  94. package/dist/admin/layout.js.map +1 -1
  95. package/dist/admin/page.js +8 -28
  96. package/dist/admin/page.js.map +1 -1
  97. package/dist/admin/payout-tiers/_tier-dialog.js +21 -31
  98. package/dist/admin/payout-tiers/_tier-dialog.js.map +1 -1
  99. package/dist/admin/payout-tiers/tier-preview-result.js +9 -10
  100. package/dist/admin/payout-tiers/tier-preview-result.js.map +1 -1
  101. package/dist/admin/payout-tiers/tiers-table.js +17 -40
  102. package/dist/admin/payout-tiers/tiers-table.js.map +1 -1
  103. package/dist/admin/payout-tiers/tiers-workspace.js +17 -20
  104. package/dist/admin/payout-tiers/tiers-workspace.js.map +1 -1
  105. package/dist/admin/pos/pos-workspace.js +2 -6
  106. package/dist/admin/pos/pos-workspace.js.map +1 -1
  107. package/dist/admin/pos/shifts-tab.js +9 -16
  108. package/dist/admin/pos/shifts-tab.js.map +1 -1
  109. package/dist/admin/revenue/cohort-panel.js +6 -6
  110. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  111. package/dist/admin/revenue/health-panel.js +21 -22
  112. package/dist/admin/revenue/health-panel.js.map +1 -1
  113. package/dist/admin/revenue/plan-mix-panel.js +3 -3
  114. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  115. package/dist/admin/revenue/quality-panel.js +19 -19
  116. package/dist/admin/revenue/quality-panel.js.map +1 -1
  117. package/dist/admin/revenue/revenue-kpis.js +50 -111
  118. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  119. package/dist/admin/revenue/revenue-overview.js +46 -39
  120. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  121. package/dist/admin/revenue/revenue-retention.js +13 -17
  122. package/dist/admin/revenue/revenue-retention.js.map +1 -1
  123. package/dist/admin/revenue/revenue-workspace.js +6 -53
  124. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  125. package/dist/admin/revenue/subscriptions-panel.js +14 -14
  126. package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
  127. package/dist/admin/revenue/top-tenants-panel.js +4 -4
  128. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  129. package/dist/admin/settings/account-tab.js +25 -1
  130. package/dist/admin/settings/account-tab.js.map +1 -1
  131. package/dist/admin/settings/general-tab.js +2 -2
  132. package/dist/admin/settings/general-tab.js.map +1 -1
  133. package/dist/admin/settings/system-status-card.js +30 -0
  134. package/dist/admin/settings/system-status-card.js.map +1 -0
  135. package/dist/admin/support/queue-card.module.css +4 -1
  136. package/dist/admin/tenants/[id]/commerce/format.js +1 -2
  137. package/dist/admin/tenants/[id]/commerce/format.js.map +1 -1
  138. package/dist/admin/tenants/[id]/commerce/recent-orders.js +1 -1
  139. package/dist/admin/tenants/[id]/commerce/recent-orders.js.map +1 -1
  140. package/dist/admin/tenants/[id]/page.js +10 -37
  141. package/dist/admin/tenants/[id]/page.js.map +1 -1
  142. package/dist/admin/tenants/[id]/tenant-layout.module.css +4 -1
  143. package/dist/admin/tenants/[id]/tenant-overview.js +2 -3
  144. package/dist/admin/tenants/[id]/tenant-overview.js.map +1 -1
  145. package/dist/admin/tenants/[id]/tenant-overview.module.css +1 -1
  146. package/dist/admin/tenants/[id]/tenant-pos.js +16 -64
  147. package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
  148. package/dist/admin/tenants/[id]/tenant-wallet-card.js +2 -3
  149. package/dist/admin/tenants/[id]/tenant-wallet-card.js.map +1 -1
  150. package/dist/admin/tenants/[id]/verification/document-viewer.js +52 -3
  151. package/dist/admin/tenants/[id]/verification/document-viewer.js.map +1 -1
  152. package/dist/admin/tenants/[id]/verification/document-viewer.module.css +0 -51
  153. package/dist/admin/tenants/[id]/verification/tenant-verification.js +0 -10
  154. package/dist/admin/tenants/[id]/verification/tenant-verification.js.map +1 -1
  155. package/dist/admin/tenants/tenant-list-row.js +1 -2
  156. package/dist/admin/tenants/tenant-list-row.js.map +1 -1
  157. package/dist/admin/tenants/tenants-table.js +2 -3
  158. package/dist/admin/tenants/tenants-table.js.map +1 -1
  159. package/dist/admin/tenants/tenants-workspace.js +2 -2
  160. package/dist/admin/tenants/tenants-workspace.js.map +1 -1
  161. package/dist/admin/verification/verification-page.js +3 -19
  162. package/dist/admin/verification/verification-page.js.map +1 -1
  163. package/dist/admin/verification/verification.module.css +0 -10
  164. package/dist/admin/verification/waiting-case-actions.js +68 -0
  165. package/dist/admin/verification/waiting-case-actions.js.map +1 -0
  166. package/dist/admin/verification/waiting-cases.js +48 -66
  167. package/dist/admin/verification/waiting-cases.js.map +1 -1
  168. package/package.json +5 -5
  169. package/dist/admin/_pos/olive-balance.js +0 -40
  170. package/dist/admin/_pos/olive-balance.js.map +0 -1
  171. package/dist/admin/_pos/olive-state.js +0 -23
  172. package/dist/admin/_pos/olive-state.js.map +0 -1
  173. package/dist/admin/_pos/payments-model.js +0 -55
  174. package/dist/admin/_pos/payments-model.js.map +0 -1
  175. package/dist/admin/_pos/tenant-operators.js +0 -63
  176. package/dist/admin/_pos/tenant-operators.js.map +0 -1
  177. package/dist/admin/_pos/tenant-outlets.js +0 -77
  178. package/dist/admin/_pos/tenant-outlets.js.map +0 -1
  179. package/dist/admin/_pos/tenant-pos-figures.js +0 -82
  180. package/dist/admin/_pos/tenant-pos-figures.js.map +0 -1
  181. package/dist/admin/_pos/tenant-requests.js +0 -64
  182. package/dist/admin/_pos/tenant-requests.js.map +0 -1
  183. package/dist/admin/_pos/tenant-transactions.js +0 -145
  184. package/dist/admin/_pos/tenant-transactions.js.map +0 -1
  185. package/dist/admin/_shell/nav-fold.js +0 -14
  186. package/dist/admin/_shell/nav-fold.js.map +0 -1
  187. package/dist/admin/_shell/nav-group.js +0 -78
  188. package/dist/admin/_shell/nav-group.js.map +0 -1
  189. package/dist/admin/_shell/use-open-groups.js +0 -64
  190. package/dist/admin/_shell/use-open-groups.js.map +0 -1
  191. package/dist/admin/_ui/page-header.module.css +0 -45
  192. package/dist/admin/accounts/account-sessions.js +0 -70
  193. package/dist/admin/accounts/account-sessions.js.map +0 -1
  194. package/dist/admin/accounts/account-sessions.module.css +0 -55
  195. package/dist/admin/accounts/account-stage-filter.js +0 -29
  196. package/dist/admin/accounts/account-stage-filter.js.map +0 -1
  197. package/dist/admin/accounts/account-stage.js +0 -81
  198. package/dist/admin/accounts/account-stage.js.map +0 -1
  199. package/dist/admin/accounts/hint.js +0 -12
  200. package/dist/admin/accounts/hint.js.map +0 -1
  201. package/dist/admin/payout-tiers/requirement-labels.js +0 -70
  202. package/dist/admin/payout-tiers/requirement-labels.js.map +0 -1
  203. package/dist/admin/pos/transactions-tab.js +0 -214
  204. package/dist/admin/pos/transactions-tab.js.map +0 -1
  205. package/dist/admin/revenue/channel-panel.js +0 -49
  206. package/dist/admin/revenue/channel-panel.js.map +0 -1
  207. package/dist/admin/revenue/channel-rows.js +0 -17
  208. package/dist/admin/revenue/channel-rows.js.map +0 -1
  209. package/dist/admin/revenue/download-button.js +0 -40
  210. package/dist/admin/revenue/download-button.js.map +0 -1
  211. package/dist/admin/revenue/earnings/earnings-colors.js +0 -25
  212. package/dist/admin/revenue/earnings/earnings-colors.js.map +0 -1
  213. package/dist/admin/revenue/earnings/earnings-legend.js +0 -75
  214. package/dist/admin/revenue/earnings/earnings-legend.js.map +0 -1
  215. package/dist/admin/revenue/earnings/earnings-summary.js +0 -296
  216. package/dist/admin/revenue/earnings/earnings-summary.js.map +0 -1
  217. package/dist/admin/revenue/earnings/earnings-table.js +0 -143
  218. package/dist/admin/revenue/earnings/earnings-table.js.map +0 -1
  219. package/dist/admin/revenue/earnings/earnings.module.css +0 -201
  220. package/dist/admin/revenue/earnings/use-earnings.js +0 -27
  221. package/dist/admin/revenue/earnings/use-earnings.js.map +0 -1
  222. package/dist/admin/revenue/ledger-model.js +0 -104
  223. package/dist/admin/revenue/ledger-model.js.map +0 -1
  224. package/dist/admin/revenue/ledger.module.css +0 -144
  225. package/dist/admin/revenue/merchant-headline.js +0 -37
  226. package/dist/admin/revenue/merchant-headline.js.map +0 -1
  227. package/dist/admin/revenue/merchant-payments-view.js +0 -234
  228. package/dist/admin/revenue/merchant-payments-view.js.map +0 -1
  229. package/dist/admin/revenue/method-cards.js +0 -40
  230. package/dist/admin/revenue/method-cards.js.map +0 -1
  231. package/dist/admin/revenue/payment-drawer.js +0 -53
  232. package/dist/admin/revenue/payment-drawer.js.map +0 -1
  233. package/dist/admin/revenue/plan-lines.js +0 -36
  234. package/dist/admin/revenue/plan-lines.js.map +0 -1
  235. package/dist/admin/revenue/plan-payment-drawer.js +0 -42
  236. package/dist/admin/revenue/plan-payment-drawer.js.map +0 -1
  237. package/dist/admin/revenue/plans-panel.js +0 -48
  238. package/dist/admin/revenue/plans-panel.js.map +0 -1
  239. package/dist/admin/revenue/platform-fees.js +0 -9
  240. package/dist/admin/revenue/platform-fees.js.map +0 -1
  241. package/dist/admin/revenue/print-summary-button.js +0 -27
  242. package/dist/admin/revenue/print-summary-button.js.map +0 -1
  243. package/dist/admin/revenue/subscriptions-view.js +0 -213
  244. package/dist/admin/revenue/subscriptions-view.js.map +0 -1
  245. package/dist/admin/revenue/summary-download.js +0 -38
  246. package/dist/admin/revenue/summary-download.js.map +0 -1
  247. package/dist/admin/revenue/summary-print.js +0 -82
  248. package/dist/admin/revenue/summary-print.js.map +0 -1
  249. package/dist/admin/revenue/summary-rows.js +0 -44
  250. package/dist/admin/revenue/summary-rows.js.map +0 -1
  251. package/dist/admin/revenue/url-filters.js +0 -36
  252. package/dist/admin/revenue/url-filters.js.map +0 -1
  253. package/dist/admin/revenue/use-url-filters.js +0 -24
  254. package/dist/admin/revenue/use-url-filters.js.map +0 -1
  255. package/dist/admin/tenants/[id]/use-tenant-wallet.js +0 -23
  256. package/dist/admin/tenants/[id]/use-tenant-wallet.js.map +0 -1
  257. package/dist/admin/tenants/[id]/verification/document-file.js +0 -78
  258. package/dist/admin/tenants/[id]/verification/document-file.js.map +0 -1
  259. package/dist/admin/tenants/[id]/verification/sent-files.js +0 -33
  260. package/dist/admin/tenants/[id]/verification/sent-files.js.map +0 -1
  261. package/dist/admin/verification/preview-dialog.js +0 -111
  262. package/dist/admin/verification/preview-dialog.js.map +0 -1
@@ -14,56 +14,56 @@ function QualityPanel({
14
14
  const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));
15
15
  const rows = [
16
16
  {
17
- label: "Income kept from existing merchants",
18
- note: "Plan income from merchants who were here at the start, after upgrades, downgrades and those who left",
17
+ label: "Net revenue retention",
18
+ note: "Starting cohort MRR, net of churn",
19
19
  value: quality.nrrPct == null ? "\u2013" : `${quality.nrrPct}%`,
20
- hint: `+${money(quality.expansionMrr)} upgrades \xB7 \u2212${money(
20
+ hint: `+${money(quality.expansionMrr)} exp \xB7 \u2212${money(
21
21
  quality.contractionMrr
22
- )} downgrades`
22
+ )} contr`
23
23
  },
24
24
  {
25
- label: "Growth against loss",
26
- note: "Plan income gained (new merchants and upgrades) for every \u20A61 lost (merchants who left and downgrades)",
25
+ label: "Quick ratio",
26
+ note: "New + expansion vs churn + contraction",
27
27
  value: quality.quickRatio == null ? "\u2013" : `${quality.quickRatio}\xD7`
28
28
  },
29
29
  {
30
- label: "Plan income lost",
31
- note: "Share of plan income lost to merchants who left",
30
+ label: "Churn rate",
31
+ note: "MRR lost as a share of the base",
32
32
  value: quality.mrrChurnRatePct == null ? "\u2013" : `${quality.mrrChurnRatePct}%`,
33
33
  tone: quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0 ? "danger" : "default"
34
34
  },
35
35
  {
36
- label: "Merchant sales after refunds",
37
- note: "What merchants sold, less what was refunded",
36
+ label: "Net GMV",
37
+ note: "Merchant GMV after refunds",
38
38
  value: lines((q) => q.netGmv),
39
39
  hint: `Gross ${lines((q) => q.grossGmv)}`
40
40
  },
41
41
  {
42
- label: "Refunded sales",
43
- note: "Value of sales that were refunded in this period",
42
+ label: "Refunded",
43
+ note: "Refunded GMV in the window",
44
44
  value: lines((q) => q.refundedGmv)
45
45
  },
46
46
  {
47
- label: "Share refunded",
48
- note: "Refunded sales as a share of all sales",
47
+ label: "Refund rate",
48
+ note: "Refunds as a share of gross GMV",
49
49
  value: one ? `${one.refundRatePct}%` : "\u2013",
50
50
  hint: one ? one.currency : "Pick a currency"
51
51
  },
52
52
  {
53
- label: "Orders refunded",
54
- note: "How many orders were refunded",
53
+ label: "Refunded orders",
54
+ note: "Order count, not GMV",
55
55
  value: integer.format(
56
56
  (gmv ?? []).filter((q) => !currency || q.currency === currency).reduce((n, q) => n + q.refundedOrders, 0)
57
57
  )
58
58
  },
59
59
  {
60
- label: "Paying merchants lost",
61
- note: "Share of paying merchants who stopped paying in this period",
60
+ label: "Logo churn rate",
61
+ note: "Paying merchants lost in the window",
62
62
  value: quality.logoChurnRatePct == null ? "\u2013" : `${quality.logoChurnRatePct}%`,
63
63
  tone: quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0 ? "danger" : "default"
64
64
  }
65
65
  ];
66
- return /* @__PURE__ */ jsx(Panel, { title: "How healthy the income is", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
66
+ return /* @__PURE__ */ jsx(Panel, { title: "Revenue quality", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
67
67
  }
68
68
  export {
69
69
  QualityPanel
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: 'Income kept from existing merchants',\n note: 'Plan income from merchants who were here at the start, after upgrades, downgrades and those who left',\n value: quality.nrrPct == null ? '–' : `${quality.nrrPct}%`,\n hint: `+${money(quality.expansionMrr)} upgrades · −${money(\n quality.contractionMrr,\n )} downgrades`,\n },\n {\n label: 'Growth against loss',\n note: 'Plan income gained (new merchants and upgrades) for every ₦1 lost (merchants who left and downgrades)',\n value: quality.quickRatio == null ? '–' : `${quality.quickRatio}×`,\n },\n {\n label: 'Plan income lost',\n note: 'Share of plan income lost to merchants who left',\n value:\n quality.mrrChurnRatePct == null ? '–' : `${quality.mrrChurnRatePct}%`,\n tone:\n quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n {\n label: 'Merchant sales after refunds',\n note: 'What merchants sold, less what was refunded',\n value: lines((q) => q.netGmv),\n hint: `Gross ${lines((q) => q.grossGmv)}`,\n },\n {\n label: 'Refunded sales',\n note: 'Value of sales that were refunded in this period',\n value: lines((q) => q.refundedGmv),\n },\n {\n label: 'Share refunded',\n note: 'Refunded sales as a share of all sales',\n value: one ? `${one.refundRatePct}%` : '–',\n hint: one ? one.currency : 'Pick a currency',\n },\n {\n label: 'Orders refunded',\n note: 'How many orders were refunded',\n value: integer.format(\n (gmv ?? [])\n .filter((q) => !currency || q.currency === currency)\n .reduce((n, q) => n + q.refundedOrders, 0),\n ),\n },\n {\n label: 'Paying merchants lost',\n note: 'Share of paying merchants who stopped paying in this period',\n value:\n quality.logoChurnRatePct == null ? '–' : `${quality.logoChurnRatePct}%`,\n tone:\n quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n ];\n\n return (\n <Panel title=\"How healthy the income is\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6FM;AA5FN,SAAS,OAAO,YAAY,UAAU,cAAc;AACpD,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAKtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AAED,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,CAAC,WACb,WAAW,KAAK,UAAU,MAAM,EAAE,KAAK,QAAK,KAAK,SAAS,QAAQ;AACpE,QAAM,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ;AAE/C,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK;AAC1E,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,UAAU,OAAO,WAAM,GAAG,QAAQ,MAAM;AAAA,MACvD,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC,wBAAgB;AAAA,QACnD,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,cAAc,OAAO,WAAM,GAAG,QAAQ,UAAU;AAAA,IACjE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,mBAAmB,OAAO,WAAM,GAAG,QAAQ,eAAe;AAAA,MACpE,MACE,QAAQ,mBAAmB,QAAQ,QAAQ,kBAAkB,IACzD,WACA;AAAA,IACR;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM;AAAA,MAC5B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,WAAW;AAAA,IACnC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,GAAG,IAAI,aAAa,MAAM;AAAA,MACvC,MAAM,MAAM,IAAI,WAAW;AAAA,IAC7B;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,SACZ,OAAO,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ,EAClD,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,gBAAgB,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,oBAAoB,OAAO,WAAM,GAAG,QAAQ,gBAAgB;AAAA,MACtE,MACE,QAAQ,oBAAoB,QAAQ,QAAQ,mBAAmB,IAC3D,WACA;AAAA,IACR;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,6BAA4B,SACvC,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: 'Net revenue retention',\n note: 'Starting cohort MRR, net of churn',\n value: quality.nrrPct == null ? '–' : `${quality.nrrPct}%`,\n hint: `+${money(quality.expansionMrr)} exp · −${money(\n quality.contractionMrr,\n )} contr`,\n },\n {\n label: 'Quick ratio',\n note: 'New + expansion vs churn + contraction',\n value: quality.quickRatio == null ? '–' : `${quality.quickRatio}×`,\n },\n {\n label: 'Churn rate',\n note: 'MRR lost as a share of the base',\n value:\n quality.mrrChurnRatePct == null ? '–' : `${quality.mrrChurnRatePct}%`,\n tone:\n quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n {\n label: 'Net GMV',\n note: 'Merchant GMV after refunds',\n value: lines((q) => q.netGmv),\n hint: `Gross ${lines((q) => q.grossGmv)}`,\n },\n {\n label: 'Refunded',\n note: 'Refunded GMV in the window',\n value: lines((q) => q.refundedGmv),\n },\n {\n label: 'Refund rate',\n note: 'Refunds as a share of gross GMV',\n value: one ? `${one.refundRatePct}%` : '–',\n hint: one ? one.currency : 'Pick a currency',\n },\n {\n label: 'Refunded orders',\n note: 'Order count, not GMV',\n value: integer.format(\n (gmv ?? [])\n .filter((q) => !currency || q.currency === currency)\n .reduce((n, q) => n + q.refundedOrders, 0),\n ),\n },\n {\n label: 'Logo churn rate',\n note: 'Paying merchants lost in the window',\n value:\n quality.logoChurnRatePct == null ? '–' : `${quality.logoChurnRatePct}%`,\n tone:\n quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n ];\n\n return (\n <Panel title=\"Revenue quality\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6FM;AA5FN,SAAS,OAAO,YAAY,UAAU,cAAc;AACpD,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAKtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AAED,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,CAAC,WACb,WAAW,KAAK,UAAU,MAAM,EAAE,KAAK,QAAK,KAAK,SAAS,QAAQ;AACpE,QAAM,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ;AAE/C,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK;AAC1E,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,UAAU,OAAO,WAAM,GAAG,QAAQ,MAAM;AAAA,MACvD,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC,mBAAW;AAAA,QAC9C,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,cAAc,OAAO,WAAM,GAAG,QAAQ,UAAU;AAAA,IACjE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,mBAAmB,OAAO,WAAM,GAAG,QAAQ,eAAe;AAAA,MACpE,MACE,QAAQ,mBAAmB,QAAQ,QAAQ,kBAAkB,IACzD,WACA;AAAA,IACR;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM;AAAA,MAC5B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,WAAW;AAAA,IACnC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,GAAG,IAAI,aAAa,MAAM;AAAA,MACvC,MAAM,MAAM,IAAI,WAAW;AAAA,IAC7B;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,SACZ,OAAO,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ,EAClD,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,gBAAgB,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,oBAAoB,OAAO,WAAM,GAAG,QAAQ,gBAAgB;AAAA,MACtE,MACE,QAAQ,oBAAoB,QAAQ,QAAQ,mBAAmB,IAC3D,WACA;AAAA,IACR;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,mBAAkB,SAC7B,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
@@ -1,11 +1,9 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ADMIN_NAME } from "../_shell/admin-brand";
3
3
  import { CountUp, Reveal, TONE } from "../_fx";
4
4
  import { MoneyLines, StatCard, rowFor } from "../_ui";
5
5
  const integer = new Intl.NumberFormat("en-US");
6
6
  function RevenueKpis({
7
- part,
8
- extra,
9
7
  data,
10
8
  money,
11
9
  money2,
@@ -14,116 +12,57 @@ function RevenueKpis({
14
12
  gmvSpark
15
13
  }) {
16
14
  const { recurring } = data;
17
- const tracked = recurring.tracked !== false;
18
- const notTracked = "Not set up yet";
19
- const noSource = "The plan payments are not connected yet";
20
15
  const gmv = rowFor(data.gmvByCurrency, focus);
21
16
  const lifetimeShare = gmv && gmv.lifetime > 0 ? `${Math.round(gmv.inRange / gmv.lifetime * 100)}% of lifetime` : void 0;
22
- return /* @__PURE__ */ jsxs(
23
- "section",
24
- {
25
- "aria-label": part === "olive" ? "What Olive earns" : "What merchants sold",
26
- className: "adm-grid",
27
- children: [
28
- part === "olive" && /* @__PURE__ */ jsxs(Fragment, { children: [
29
- /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
30
- StatCard,
31
- {
32
- label: "Plan income collected",
33
- value: tracked && recurring.collected !== void 0 ? /* @__PURE__ */ jsx(CountUp, { value: recurring.collected, format: money2 }) : notTracked,
34
- hint: `Plan payments that actually went through in this window, in ${data.currency || "NGN"}. The monthly figure beside it is what is expected, this is what came in.`,
35
- sub: tracked ? "In this window" : noSource
36
- }
37
- ) }),
38
- /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
39
- StatCard,
40
- {
41
- label: "Plan income each month",
42
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
43
- 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.`,
44
- sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} ${(recurring.payingMerchants ?? recurring.activeSubscriptions) === 1 ? "merchant" : "merchants"} paying` : noSource
45
- }
46
- ) }),
47
- /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
48
- StatCard,
49
- {
50
- label: "Plan income over a year",
51
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
52
- hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12.",
53
- sub: tracked ? "Each month times 12" : noSource
54
- }
55
- ) }),
56
- /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
57
- StatCard,
58
- {
59
- label: "Average per paying merchant",
60
- value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
61
- hint: "What one paying merchant pays in a month, on average.",
62
- sub: tracked ? "Each month" : noSource
63
- }
64
- ) })
65
- ] }),
66
- part === "merchant" && /* @__PURE__ */ jsxs(Fragment, { children: [
67
- /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
68
- StatCard,
69
- {
70
- label: "Merchant sales",
71
- value: /* @__PURE__ */ jsx(
72
- MoneyLines,
73
- {
74
- rows: data.gmvByCurrency,
75
- currency,
76
- amount: (g) => g.inRange,
77
- empty: "No sales yet"
78
- }
79
- ),
80
- href: "/admin/tenants",
81
- sub: lifetimeShare,
82
- 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)`,
83
- spark: focus ? gmvSpark : void 0,
84
- sparkColor: TONE.money.fg
85
- }
86
- ) }),
87
- extra,
88
- data.merchantWalletsByCurrency && /* @__PURE__ */ jsx(Reveal, { index: 4, className: "adm-span-3", children: /* @__PURE__ */ jsx(
89
- StatCard,
90
- {
91
- label: "In merchants' wallets",
92
- value: /* @__PURE__ */ jsx(
93
- MoneyLines,
94
- {
95
- rows: data.merchantWalletsByCurrency,
96
- currency,
97
- amount: (r) => r.amount,
98
- decimals: 2,
99
- empty: "Nothing held"
100
- }
101
- ),
102
- hint: "What merchants hold in their wallets right now: pending, available, held back and on its way to a bank, added together. A balance today, so it does not follow the dates above.",
103
- sub: "Right now"
104
- }
105
- ) }),
106
- data.merchantPayoutsByCurrency && /* @__PURE__ */ jsx(Reveal, { index: 5, className: "adm-span-3", children: /* @__PURE__ */ jsx(
107
- StatCard,
108
- {
109
- label: "Merchants sent out",
110
- value: /* @__PURE__ */ jsx(
111
- MoneyLines,
112
- {
113
- rows: data.merchantPayoutsByCurrency,
114
- currency,
115
- amount: (r) => r.amount,
116
- decimals: 2,
117
- empty: "Nothing sent out"
118
- }
119
- ),
120
- hint: "What merchants withdrew to their bank accounts in this window. Payouts the bank sent back are taken off."
121
- }
122
- ) })
123
- ] })
124
- ]
125
- }
126
- );
17
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Revenue indicators", className: "adm-grid", children: [
18
+ /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
19
+ StatCard,
20
+ {
21
+ label: "Monthly recurring revenue (MRR)",
22
+ value: /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }),
23
+ hint: `Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || "USD"}. Yearly plans are divided by 12.`,
24
+ sub: `${integer.format(recurring.activeSubscriptions)} paying`
25
+ }
26
+ ) }),
27
+ /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
28
+ StatCard,
29
+ {
30
+ label: "Annual recurring revenue (ARR)",
31
+ value: /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }),
32
+ hint: "Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR \xD7 12).",
33
+ sub: "MRR \xD7 12"
34
+ }
35
+ ) }),
36
+ /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
37
+ StatCard,
38
+ {
39
+ label: "Average revenue per account (ARPA)",
40
+ value: /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }),
41
+ hint: "What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.",
42
+ sub: "Per paying account"
43
+ }
44
+ ) }),
45
+ /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
46
+ StatCard,
47
+ {
48
+ label: "Merchant sales (GMV)",
49
+ value: /* @__PURE__ */ jsx(
50
+ MoneyLines,
51
+ {
52
+ rows: data.gmvByCurrency,
53
+ currency,
54
+ amount: (g) => g.inRange,
55
+ empty: "No sales yet"
56
+ }
57
+ ),
58
+ href: "/admin/tenants",
59
+ sub: lifetimeShare,
60
+ 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.`,
61
+ spark: focus ? gmvSpark : void 0,
62
+ sparkColor: TONE.money.fg
63
+ }
64
+ ) })
65
+ ] });
127
66
  }
128
67
  export {
129
68
  RevenueKpis
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { ReactNode } from 'react';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * The headline figures, in two groups that are never added together: what\n * merchants sold (`merchant`), and what Olive itself earns from plans (`olive`).\n */\nexport function RevenueKpis({\n part,\n extra,\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n part: 'merchant' | 'olive';\n /** More cards for the merchant group, in the same row. */\n extra?: ReactNode;\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without the plan payments connected the API sends 0s; say so rather than show a zero.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not set up yet';\n const noSource = 'The plan payments are not connected yet';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section\n aria-label={part === 'olive' ? 'What Olive earns' : 'What merchants sold'}\n className=\"adm-grid\"\n >\n {part === 'olive' && (\n <>\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income collected\"\n value={\n tracked && recurring.collected !== undefined ? (\n <CountUp value={recurring.collected} format={money2} />\n ) : (\n notTracked\n )\n }\n hint={`Plan payments that actually went through in this window, in ${data.currency || 'NGN'}. The monthly figure beside it is what is expected, this is what came in.`}\n sub={tracked ? 'In this window' : noSource}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted.`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} ${(recurring.payingMerchants ?? recurring.activeSubscriptions) === 1 ? 'merchant' : 'merchants'} paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12.\"\n sub={tracked ? 'Each month times 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What one paying merchant pays in a month, on average.\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\n </>\n )}\n {part === 'merchant' && (\n <>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n {extra}\n {data.merchantWalletsByCurrency && (\n <Reveal index={4} className=\"adm-span-3\">\n <StatCard\n label=\"In merchants' wallets\"\n value={\n <MoneyLines\n rows={data.merchantWalletsByCurrency}\n currency={currency}\n amount={(r) => r.amount}\n decimals={2}\n empty=\"Nothing held\"\n />\n }\n hint=\"What merchants hold in their wallets right now: pending, available, held back and on its way to a bank, added together. A balance today, so it does not follow the dates above.\"\n sub=\"Right now\"\n />\n </Reveal>\n )}\n {data.merchantPayoutsByCurrency && (\n <Reveal index={5} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants sent out\"\n value={\n <MoneyLines\n rows={data.merchantPayoutsByCurrency}\n currency={currency}\n amount={(r) => r.amount}\n decimals={2}\n empty=\"Nothing sent out\"\n />\n }\n hint=\"What merchants withdrew to their bank accounts in this window. Payouts the bank sent back are taken off.\"\n />\n </Reveal>\n )}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"AAoDQ,mBAMM,KANN;AApDR,SAAS,kBAAkB;AAG3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAMtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,UAAU,qBAAqB;AAAA,MACpD,WAAU;AAAA,MAET;AAAA,iBAAS,WACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,WAAW,UAAU,cAAc,SACjC,oBAAC,WAAQ,OAAO,UAAU,WAAW,QAAQ,QAAQ,IAErD;AAAA,cAGJ,MAAM,+DAA+D,KAAK,YAAY,KAAK;AAAA,cAC3F,KAAK,UAAU,mBAAmB;AAAA;AAAA,UACpC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,cAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,KAAK,UAAU,mBAAmB,UAAU,yBAAyB,IAAI,aAAa,WAAW,YAC9K;AAAA;AAAA,UAER,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,UACzC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,eAAe;AAAA;AAAA,UAChC,GACF;AAAA,WACE;AAAA,QAED,SAAS,cACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,cACtG,OAAO,QAAQ,WAAW;AAAA,cAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,UACzB,GACF;AAAA,UACK;AAAA,UACA,KAAK,6BACJ,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,UAAU;AAAA,kBACV,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAI;AAAA;AAAA,UACN,GACF;AAAA,UAED,KAAK,6BACJ,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,UAAU;AAAA,kBACV,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA;AAAA,UACP,GACF;AAAA,WAEJ;AAAA;AAAA;AAAA,EAEJ;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 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={<CountUp value={recurring.mrr} format={money} />}\n hint={`Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || 'USD'}. Yearly plans are divided by 12.`}\n sub={`${integer.format(recurring.activeSubscriptions)} paying`}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Annual recurring revenue (ARR)\"\n value={<CountUp value={recurring.arr} format={money} />}\n hint=\"Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR × 12).\"\n sub=\"MRR × 12\"\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average revenue per account (ARPA)\"\n value={<CountUp value={recurring.arpa} format={money2} />}\n hint=\"What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.\"\n sub=\"Per paying account\"\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":"AAkCI,SAIa,KAJb;AAlCJ,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;AACtB,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,OAAO,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO;AAAA,QACrD,MAAM,4EAA4E,KAAK,YAAY,KAAK;AAAA,QACxG,KAAK,GAAG,QAAQ,OAAO,UAAU,mBAAmB,CAAC;AAAA;AAAA,IACvD,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO;AAAA,QACrD,MAAK;AAAA,QACL,KAAI;AAAA;AAAA,IACN,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ;AAAA,QACvD,MAAK;AAAA,QACL,KAAI;AAAA;AAAA,IACN,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,65 +1,63 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { EarningsSummary } from "./earnings/earnings-summary";
4
- import { EarningsTable } from "./earnings/earnings-table";
3
+ import { useMemo } from "react";
4
+ import { TimeSeriesChart } from "@withpotter/components/charts";
5
+ import { PICK_CURRENCY, Panel, moneyIn, rowFor, toTrend } from "../_ui";
6
+ import { Tinted } from "../audit/tinted";
5
7
  import { HealthPanel } from "./health-panel";
6
8
  import { PlanMixPanel } from "./plan-mix-panel";
7
- import { planLines } from "./plan-lines";
8
- import { PlansPanel } from "./plans-panel";
9
9
  import { RevenueKpis } from "./revenue-kpis";
10
- import ledgerStyles from "./ledger.module.css";
10
+ import { SubscriptionsPanel } from "./subscriptions-panel";
11
+ import { TopTenantsPanel } from "./top-tenants-panel";
12
+ import styles from "./revenue.module.css";
11
13
  function RevenueOverview({
12
- startDate,
13
- endDate,
14
14
  data,
15
15
  health,
16
16
  money,
17
17
  money2,
18
18
  currency,
19
19
  focus,
20
+ period,
20
21
  span,
21
- recurringCaption,
22
- plans
22
+ recurringCaption
23
23
  }) {
24
- const recurringTracked = data.recurring.tracked !== false;
24
+ const gmv = rowFor(data.gmvByCurrency, focus);
25
+ const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
26
+ const gmvCaption = focus ? `${period} \xB7 ${focus}` : period;
25
27
  return /* @__PURE__ */ jsxs(Fragment, { children: [
26
- /* @__PURE__ */ jsx(
27
- EarningsSummary,
28
- {
29
- startDate,
30
- endDate,
31
- currency,
32
- recurring: data.recurring,
33
- span
34
- }
35
- ),
36
- /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Plan income" }),
37
- /* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
38
- "What merchants pay Olive for their plans, each month and over a year.",
39
- recurringTracked ? "" : " Plan income is not set up on this box yet."
40
- ] }),
41
28
  /* @__PURE__ */ jsx(
42
29
  RevenueKpis,
43
30
  {
44
- part: "olive",
45
31
  data,
46
32
  money,
47
33
  money2,
48
34
  currency,
49
35
  focus,
50
- gmvSpark: []
36
+ gmvSpark: gmvSeries.map((p) => p.value)
51
37
  }
52
38
  ),
53
- recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
54
- /* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
55
- /* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
56
- PlansPanel,
57
- {
58
- lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
59
- money,
60
- caption: recurringCaption
61
- }
62
- ) : /* @__PURE__ */ jsx(
39
+ /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
40
+ Panel,
41
+ {
42
+ title: "GMV trend",
43
+ caption: gmvCaption,
44
+ height: "calc(var(--adm-panel-h) + 32px)",
45
+ children: focus ? /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(Tinted, { tone: "money", children: /* @__PURE__ */ jsx(
46
+ TimeSeriesChart,
47
+ {
48
+ data: gmvSeries,
49
+ compare: null,
50
+ compareLabel: `Previous ${span}`,
51
+ formatValue: moneyIn(focus),
52
+ emptyMessage: `No paid ${focus} orders in this window yet`,
53
+ height: 240
54
+ }
55
+ ) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
56
+ }
57
+ ) }) }),
58
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
59
+ /* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
60
+ /* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
63
61
  PlanMixPanel,
64
62
  {
65
63
  planBreakdown: data.recurring.planBreakdown,
@@ -68,8 +66,17 @@ function RevenueOverview({
68
66
  }
69
67
  ) })
70
68
  ] }),
71
- /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Every earning" }),
72
- /* @__PURE__ */ jsx(EarningsTable, { startDate, endDate })
69
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
70
+ /* @__PURE__ */ jsx("div", { className: "adm-span-8", children: /* @__PURE__ */ jsx(
71
+ TopTenantsPanel,
72
+ {
73
+ tenants: gmv?.topTenants ?? [],
74
+ lifetime: gmv?.lifetime ?? 0,
75
+ focus
76
+ }
77
+ ) }),
78
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) })
79
+ ] })
73
80
  ] });
74
81
  }
75
82
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformCockpit, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport { EarningsSummary } from './earnings/earnings-summary';\nimport { EarningsTable } from './earnings/earnings-table';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { RevenueKpis } from './revenue-kpis';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Money Olive earns: one page, no sub-tabs, read top to bottom. What Olive made and kept (cards, pies, graphs, the\n * merchants it came from), then Olive's own plan income, then every earning in a table. It is Olive's money only: what\n * customers pay merchants lives on the Merchant payments tab, and the plan subscriptions on the Subscriptions tab.\n */\nexport function RevenueOverview({\n startDate,\n endDate,\n data,\n health,\n money,\n money2,\n currency,\n focus,\n span,\n recurringCaption,\n plans,\n}: {\n /** The window, for the earnings views, which read their own data. */\n startDate?: string;\n endDate?: string;\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 follows the billing currency. */\n currency: string;\n /** The one currency the plan figures show, or null. */\n focus: string | null;\n span: string;\n recurringCaption: string;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const recurringTracked = data.recurring.tracked !== false;\n\n return (\n <>\n <EarningsSummary\n startDate={startDate}\n endDate={endDate}\n currency={currency}\n recurring={data.recurring}\n span={span}\n />\n\n <h2 className={ledgerStyles.sectionTitle}>Plan income</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive for their plans, each month and over a year.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className={plans ? 'adm-span-12' : 'adm-span-7'}>\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n {/* The Plans table has five columns, so it gets a full row; the older plan mix keeps its narrow slot. */}\n <div className={plans ? 'adm-span-12' : 'adm-span-5'}>\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n\n <h2 className={ledgerStyles.sectionTitle}>Every earning</h2>\n <EarningsTable startDate={startDate} endDate={endDate} />\n </>\n );\n}\n"],"mappings":";AAmDI,mBACE,KASA,YAVF;AA/CJ,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,OAAO,kBAAkB;AAOlB,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,mBAAmB,KAAK,UAAU,YAAY;AAEpD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,KAAK;AAAA,QAChB;AAAA;AAAA,IACF;AAAA,IAEA,oBAAC,QAAG,WAAW,aAAa,cAAc,yBAAW;AAAA,IACrD,qBAAC,OAAE,WAAW,aAAa,aAAa;AAAA;AAAA,MAErC,mBAAmB,KAAK;AAAA,OAC3B;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,CAAC;AAAA;AAAA,IACb;AAAA,IAEC,oBACC,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,QAAQ,gBAAgB,cACtC,8BAAC,eAAY,QAAgB,OAAc,SAAS,kBAAkB,GACxE;AAAA,MAEA,oBAAC,SAAI,WAAW,QAAQ,gBAAgB,cACrC,kBACC;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,UAAU,MAAM,OAAO,MAAM,YAAY,KAAK,UAAU,aAAa;AAAA,UAC5E;AAAA,UACA,SAAS;AAAA;AAAA,MACX,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,KAAK,UAAU;AAAA,UAC9B;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GAEJ;AAAA,OACF;AAAA,IAGF,oBAAC,QAAG,WAAW,aAAa,cAAc,2BAAa;AAAA,IACvD,oBAAC,iBAAc,WAAsB,SAAkB;AAAA,KACzD;AAEJ;","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 { PlatformCockpit, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { PICK_CURRENCY, Panel, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Tinted } from '../audit/tinted';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\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}: {\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}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\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 <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 <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel health={health} money={money} caption={recurringCaption} />\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 <div className=\"adm-grid\">\n <div className=\"adm-span-8\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n <div className=\"adm-span-4\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAiDI,mBACE,KAoCA,YArCF;AA/CJ,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAEhC,SAAS,eAAe,OAAO,SAAS,QAAQ,eAAe;AAC/D,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,OAAO,YAAY;AAQZ,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;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;AAEpD,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,IAEA,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,IAEA,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAU,cACb,8BAAC,eAAY,QAAgB,OAAc,SAAS,kBAAkB,GACxE;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,IAEA,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,cAAc,CAAC;AAAA,UAC7B,UAAU,KAAK,YAAY;AAAA,UAC3B;AAAA;AAAA,MACF,GACF;AAAA,MACA,oBAAC,SAAI,WAAU,cACb,8BAAC,sBAAmB,WAAW,KAAK,WAAW,MAAY,GAC7D;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { EmptyBlock } from "../audit/empty-block";
4
- import ledgerStyles from "./ledger.module.css";
5
4
  import { CohortPanel } from "./cohort-panel";
6
5
  import { QualityPanel } from "./quality-panel";
7
6
  function RevenueRetention({
@@ -11,22 +10,19 @@ function RevenueRetention({
11
10
  caption
12
11
  }) {
13
12
  if (!quality) {
14
- return /* @__PURE__ */ jsx(EmptyBlock, { message: "Retention shows here once there is enough history to compare." });
13
+ return /* @__PURE__ */ jsx(EmptyBlock, { message: "Retention appears once there is enough billing history." });
15
14
  }
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
- ] })
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 }) })
30
26
  ] });
31
27
  }
32
28
  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 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":[]}
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":[]}