@withpotter/pages-admin 0.13.0-next.124 → 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 -308
  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,7 +14,6 @@ import {
14
14
  dangerRowBtn
15
15
  } from "../_ui";
16
16
  import { TierDialog } from "./_tier-dialog";
17
- import { requirementLabel } from "./requirement-labels";
18
17
  import { TIERS_KEY, useTiers } from "./use-tiers";
19
18
  const TAG = {
20
19
  padding: "2px 8px",
@@ -49,49 +48,27 @@ function TiersTable({
49
48
  remove.reset();
50
49
  }
51
50
  const columns = [
51
+ { key: "label", header: "Tier", render: (t) => `${t.label} (${t.key})` },
52
+ { key: "currency", header: "Currency", render: (t) => t.currency },
53
+ { key: "position", header: "Position", render: (t) => t.position },
52
54
  {
53
- key: "label",
54
- header: "Level",
55
- render: (t) => /* @__PURE__ */ jsxs("span", { children: [
56
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: t.label }),
57
- !t.enabled && /* @__PURE__ */ jsx("span", { style: { ...TAG, marginLeft: 8 }, children: "Disabled" }),
58
- /* @__PURE__ */ jsxs(
59
- "span",
60
- {
61
- style: {
62
- display: "block",
63
- fontSize: 12,
64
- color: "var(--text-tertiary)",
65
- fontFamily: "var(--font-mono, monospace)"
66
- },
67
- children: [
68
- t.currency,
69
- " \xB7 priority ",
70
- t.position
71
- ]
72
- }
73
- )
74
- ] })
55
+ key: "limits",
56
+ header: "Per transfer \xB7 daily \xB7 monthly",
57
+ render: (t) => [
58
+ `${formatMinor(t.minimumMinor, t.currency)}\u2013${formatMinor(t.maximumMinor, t.currency)}`,
59
+ formatMinor(t.dailyMaximumMinor, t.currency),
60
+ t.monthlyMaximumMinor ? formatMinor(t.monthlyMaximumMinor, t.currency) : "no cap"
61
+ ].join(" \xB7 ")
75
62
  },
76
63
  {
77
64
  key: "requirements",
78
- header: "Must complete",
79
- render: (t) => t.requirements.length === 0 ? "\u2013" : /* @__PURE__ */ jsx("span", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: t.requirements.map((r) => /* @__PURE__ */ jsx("span", { style: TAG, children: requirementLabel(r) }, r)) })
80
- },
81
- {
82
- key: "transfer",
83
- header: "Per transfer",
84
- render: (t) => `${formatMinor(t.minimumMinor, t.currency)} to ${formatMinor(t.maximumMinor, t.currency)}`
85
- },
86
- {
87
- key: "daily",
88
- header: "Per day",
89
- render: (t) => formatMinor(t.dailyMaximumMinor, t.currency)
65
+ header: "Requires",
66
+ render: (t) => t.requirements.length === 0 ? "Nothing" : /* @__PURE__ */ jsx("span", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: t.requirements.map((r) => /* @__PURE__ */ jsx("span", { style: TAG, children: r }, r)) })
90
67
  },
91
68
  {
92
- key: "monthly",
93
- header: "Per month",
94
- render: (t) => t.monthlyMaximumMinor ? formatMinor(t.monthlyMaximumMinor, t.currency) : "No cap"
69
+ key: "enabled",
70
+ header: "Status",
71
+ render: (t) => t.enabled ? "Enabled" : "Disabled"
95
72
  },
96
73
  {
97
74
  key: "actions",
@@ -128,8 +105,8 @@ function TiersTable({
128
105
  getRowKey: (t) => t.id,
129
106
  isLoading,
130
107
  isError,
131
- emptyTitle: currency && data?.length ? `No ${currency} levels` : "No levels yet",
132
- emptyDescription: `Until you add one, every merchant has to verify their identity before they can send money out${currency ? ` in ${currency}` : ""}.`
108
+ emptyTitle: currency && data?.length ? `No ${currency} tiers` : "No tiers yet",
109
+ emptyDescription: `With no tier, every business resolves to verification required${currency ? ` in ${currency}` : ""}.`
133
110
  }
134
111
  ),
135
112
  editing && /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/payout-tiers/tiers-table.tsx"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { Banner } from '@withpotter/ui-kit/banner';\nimport { useConfirm } from '@withpotter/ui-kit/confirm-dialog';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport {\n adminMoneyApi,\n type PayoutTier,\n type PayoutTierInput,\n} from '@withpotter/lib/admin-money-api';\nimport {\n DataTable,\n byCurrency,\n rowBtn,\n dangerRowBtn,\n type Column,\n} from '../_ui';\nimport { TierDialog } from './_tier-dialog';\nimport { requirementLabel } from './requirement-labels';\nimport { TIERS_KEY, useTiers } from './use-tiers';\n\nconst TAG = {\n padding: '2px 8px',\n borderRadius: 999,\n border: '1px solid var(--border-default)',\n fontSize: 12,\n color: 'var(--text-secondary)',\n} as const;\n\ntype Editing = PayoutTier | 'new' | null;\n\n/** The tiers, with edit and remove on each row and the tier dialog. */\nexport function TiersTable({\n editing,\n onEdit,\n currency = '',\n}: {\n editing: Editing;\n onEdit: (next: Editing) => void;\n /** The currency filter's choice; '' lists every currency. */\n currency?: string;\n}) {\n const qc = useQueryClient();\n const confirm = useConfirm();\n const { data, isLoading, isError } = useTiers();\n\n const refresh = () => qc.invalidateQueries({ queryKey: TIERS_KEY });\n const save = useMutation({\n mutationFn: (body: PayoutTierInput) =>\n editing && editing !== 'new'\n ? adminMoneyApi.updateTier(editing.id, body)\n : adminMoneyApi.createTier(body),\n onSuccess: () => {\n close();\n refresh();\n },\n });\n const remove = useMutation({\n mutationFn: (id: string) => adminMoneyApi.removeTier(id),\n onSuccess: refresh,\n });\n // Closing the dialog clears both errors, as opening a fresh one should.\n function close() {\n onEdit(null);\n save.reset();\n remove.reset();\n }\n\n const columns: Column<PayoutTier>[] = [\n {\n key: 'label',\n header: 'Level',\n render: (t) => (\n <span>\n <span style={{ fontWeight: 500 }}>{t.label}</span>\n {!t.enabled && (\n <span style={{ ...TAG, marginLeft: 8 }}>Disabled</span>\n )}\n <span\n style={{\n display: 'block',\n fontSize: 12,\n color: 'var(--text-tertiary)',\n fontFamily: 'var(--font-mono, monospace)',\n }}\n >\n {t.currency} · priority {t.position}\n </span>\n </span>\n ),\n },\n {\n key: 'requirements',\n header: 'Must complete',\n render: (t) =>\n t.requirements.length === 0 ? (\n '–'\n ) : (\n <span style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>\n {t.requirements.map((r) => (\n <span key={r} style={TAG}>\n {requirementLabel(r)}\n </span>\n ))}\n </span>\n ),\n },\n {\n key: 'transfer',\n header: 'Per transfer',\n render: (t) =>\n `${formatMinor(t.minimumMinor, t.currency)} to ${formatMinor(t.maximumMinor, t.currency)}`,\n },\n {\n key: 'daily',\n header: 'Per day',\n render: (t) => formatMinor(t.dailyMaximumMinor, t.currency),\n },\n {\n key: 'monthly',\n header: 'Per month',\n render: (t) =>\n t.monthlyMaximumMinor\n ? formatMinor(t.monthlyMaximumMinor, t.currency)\n : 'No cap',\n },\n {\n key: 'actions',\n header: '',\n render: (t) => (\n <span style={{ display: 'flex', gap: 6 }}>\n <button type=\"button\" style={rowBtn} onClick={() => onEdit(t)}>\n Edit\n </button>\n <button\n type=\"button\"\n style={dangerRowBtn}\n onClick={async () => {\n const ok = await confirm({\n title: `Remove ${t.label}?`,\n description:\n 'Businesses on this tier fall back to the next tier they qualify for.',\n confirmLabel: 'Remove',\n tone: 'danger',\n });\n if (ok) remove.mutate(t.id);\n }}\n >\n Remove\n </button>\n </span>\n ),\n },\n ];\n\n return (\n <>\n {remove.error && !editing && (\n <Banner tone=\"error\">{remove.error.message}</Banner>\n )}\n <DataTable\n columns={columns}\n rows={data && byCurrency(data, currency, (t) => t.currency)}\n getRowKey={(t) => t.id}\n isLoading={isLoading}\n isError={isError}\n emptyTitle={\n currency && data?.length ? `No ${currency} levels` : 'No levels yet'\n }\n emptyDescription={`Until you add one, every merchant has to verify their identity before they can send money out${currency ? ` in ${currency}` : ''}.`}\n />\n {editing && (\n <TierDialog\n tier={editing === 'new' ? null : editing}\n saving={save.isPending}\n error={save.error?.message ?? null}\n onSave={(b) => save.mutate(b)}\n onClose={close}\n />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/payout-tiers/tiers-table.tsx"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { Banner } from '@withpotter/ui-kit/banner';\nimport { useConfirm } from '@withpotter/ui-kit/confirm-dialog';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport {\n adminMoneyApi,\n type PayoutTier,\n type PayoutTierInput,\n} from '@withpotter/lib/admin-money-api';\nimport {\n DataTable,\n byCurrency,\n rowBtn,\n dangerRowBtn,\n type Column,\n} from '../_ui';\nimport { TierDialog } from './_tier-dialog';\nimport { TIERS_KEY, useTiers } from './use-tiers';\n\nconst TAG = {\n padding: '2px 8px',\n borderRadius: 999,\n border: '1px solid var(--border-default)',\n fontSize: 12,\n color: 'var(--text-secondary)',\n} as const;\n\ntype Editing = PayoutTier | 'new' | null;\n\n/** The tiers, with edit and remove on each row and the tier dialog. */\nexport function TiersTable({\n editing,\n onEdit,\n currency = '',\n}: {\n editing: Editing;\n onEdit: (next: Editing) => void;\n /** The currency filter's choice; '' lists every currency. */\n currency?: string;\n}) {\n const qc = useQueryClient();\n const confirm = useConfirm();\n const { data, isLoading, isError } = useTiers();\n\n const refresh = () => qc.invalidateQueries({ queryKey: TIERS_KEY });\n const save = useMutation({\n mutationFn: (body: PayoutTierInput) =>\n editing && editing !== 'new'\n ? adminMoneyApi.updateTier(editing.id, body)\n : adminMoneyApi.createTier(body),\n onSuccess: () => {\n close();\n refresh();\n },\n });\n const remove = useMutation({\n mutationFn: (id: string) => adminMoneyApi.removeTier(id),\n onSuccess: refresh,\n });\n // Closing the dialog clears both errors, as opening a fresh one should.\n function close() {\n onEdit(null);\n save.reset();\n remove.reset();\n }\n\n const columns: Column<PayoutTier>[] = [\n { key: 'label', header: 'Tier', render: (t) => `${t.label} (${t.key})` },\n { key: 'currency', header: 'Currency', render: (t) => t.currency },\n { key: 'position', header: 'Position', render: (t) => t.position },\n {\n key: 'limits',\n header: 'Per transfer · daily · monthly',\n render: (t) =>\n [\n `${formatMinor(t.minimumMinor, t.currency)}–${formatMinor(t.maximumMinor, t.currency)}`,\n formatMinor(t.dailyMaximumMinor, t.currency),\n t.monthlyMaximumMinor\n ? formatMinor(t.monthlyMaximumMinor, t.currency)\n : 'no cap',\n ].join(' · '),\n },\n {\n key: 'requirements',\n header: 'Requires',\n render: (t) =>\n t.requirements.length === 0 ? (\n 'Nothing'\n ) : (\n <span style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>\n {t.requirements.map((r) => (\n <span key={r} style={TAG}>\n {r}\n </span>\n ))}\n </span>\n ),\n },\n {\n key: 'enabled',\n header: 'Status',\n render: (t) => (t.enabled ? 'Enabled' : 'Disabled'),\n },\n {\n key: 'actions',\n header: '',\n render: (t) => (\n <span style={{ display: 'flex', gap: 6 }}>\n <button type=\"button\" style={rowBtn} onClick={() => onEdit(t)}>\n Edit\n </button>\n <button\n type=\"button\"\n style={dangerRowBtn}\n onClick={async () => {\n const ok = await confirm({\n title: `Remove ${t.label}?`,\n description:\n 'Businesses on this tier fall back to the next tier they qualify for.',\n confirmLabel: 'Remove',\n tone: 'danger',\n });\n if (ok) remove.mutate(t.id);\n }}\n >\n Remove\n </button>\n </span>\n ),\n },\n ];\n\n return (\n <>\n {remove.error && !editing && (\n <Banner tone=\"error\">{remove.error.message}</Banner>\n )}\n <DataTable\n columns={columns}\n rows={data && byCurrency(data, currency, (t) => t.currency)}\n getRowKey={(t) => t.id}\n isLoading={isLoading}\n isError={isError}\n emptyTitle={\n currency && data?.length ? `No ${currency} tiers` : 'No tiers yet'\n }\n emptyDescription={`With no tier, every business resolves to verification required${currency ? ` in ${currency}` : ''}.`}\n />\n {editing && (\n <TierDialog\n tier={editing === 'new' ? null : editing}\n saving={save.isPending}\n error={save.error?.message ?? null}\n onSave={(b) => save.mutate(b)}\n onClose={close}\n />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -4,16 +4,22 @@ import { useState } from "react";
4
4
  import { Button } from "@withpotter/ui-kit/button";
5
5
  import {
6
6
  CurrencyFilter,
7
- Drawer,
8
7
  PageHeader,
8
+ PageTabs,
9
9
  currenciesIn,
10
- useCurrencyParam
10
+ useCurrencyParam,
11
+ useTabParam
11
12
  } from "../_ui";
12
13
  import { TierPreviewTab } from "./tier-preview-tab";
13
14
  import { TiersTable } from "./tiers-table";
14
15
  import { useTiers } from "./use-tiers";
16
+ const TABS = [
17
+ { id: "tiers", label: "Tiers" },
18
+ { id: "preview", label: "Preview a business" }
19
+ ];
20
+ const KEYS = TABS.map((t) => t.id);
15
21
  function TiersWorkspace() {
16
- const [previewing, setPreviewing] = useState(false);
22
+ const [tab, setTab] = useTabParam(KEYS);
17
23
  const [editing, setEditing] = useState(null);
18
24
  const [currency, setCurrency] = useCurrencyParam();
19
25
  const tiers = useTiers();
@@ -21,9 +27,9 @@ function TiersWorkspace() {
21
27
  /* @__PURE__ */ jsx(
22
28
  PageHeader,
23
29
  {
24
- title: "Payout limits",
25
- description: "How much money a merchant can send out, based on how fully their identity has been checked.",
26
- actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
30
+ title: "Payout tiers",
31
+ description: "Per-currency limits by verification level.",
32
+ actions: tab === "tiers" ? /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
27
33
  /* @__PURE__ */ jsx(
28
34
  CurrencyFilter,
29
35
  {
@@ -32,22 +38,13 @@ function TiersWorkspace() {
32
38
  currencies: currenciesIn(tiers.data?.map((t) => t.currency))
33
39
  }
34
40
  ),
35
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setPreviewing(true), children: "Preview a business" }),
36
- /* @__PURE__ */ jsx(Button, { onClick: () => setEditing("new"), children: "New level" })
37
- ] })
41
+ /* @__PURE__ */ jsx(Button, { onClick: () => setEditing("new"), children: "New tier" })
42
+ ] }) : void 0
38
43
  }
39
44
  ),
40
- /* @__PURE__ */ jsx(TiersTable, { editing, onEdit: setEditing, currency }),
41
- /* @__PURE__ */ jsx(
42
- Drawer,
43
- {
44
- open: previewing,
45
- onClose: () => setPreviewing(false),
46
- title: "Preview a business",
47
- subtitle: "Which level a merchant gets today, and what they still need to complete.",
48
- children: /* @__PURE__ */ jsx(TierPreviewTab, {})
49
- }
50
- )
45
+ /* @__PURE__ */ jsx(PageTabs, { tabs: TABS, active: tab, onChange: setTab }),
46
+ tab === "tiers" && /* @__PURE__ */ jsx(TiersTable, { editing, onEdit: setEditing, currency }),
47
+ tab === "preview" && /* @__PURE__ */ jsx(TierPreviewTab, {})
51
48
  ] });
52
49
  }
53
50
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/payout-tiers/tiers-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button } from '@withpotter/ui-kit/button';\nimport type { PayoutTier } from '@withpotter/lib/admin-money-api';\nimport {\n CurrencyFilter,\n Drawer,\n PageHeader,\n currenciesIn,\n useCurrencyParam,\n} from '../_ui';\nimport { TierPreviewTab } from './tier-preview-tab';\nimport { TiersTable } from './tiers-table';\nimport { useTiers } from './use-tiers';\n\n/**\n * How much a business may send out, by how far it is verified.\n *\n * This is configuration, not approved policy: nothing is seeded, and with no\n * tier a business resolves to \"verification required\". The preview tab\n * answers the question an admin actually asks: which tier would THIS\n * business get today, and what is it missing?\n */\nexport function TiersWorkspace() {\n const [previewing, setPreviewing] = useState(false);\n const [editing, setEditing] = useState<PayoutTier | 'new' | null>(null);\n const [currency, setCurrency] = useCurrencyParam();\n const tiers = useTiers();\n\n return (\n <>\n <PageHeader\n title=\"Payout limits\"\n description=\"How much money a merchant can send out, based on how fully their identity has been checked.\"\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currenciesIn(tiers.data?.map((t) => t.currency))}\n />\n <Button variant=\"secondary\" onClick={() => setPreviewing(true)}>\n Preview a business\n </Button>\n <Button onClick={() => setEditing('new')}>New level</Button>\n </div>\n }\n />\n <TiersTable editing={editing} onEdit={setEditing} currency={currency} />\n <Drawer\n open={previewing}\n onClose={() => setPreviewing(false)}\n title=\"Preview a business\"\n subtitle=\"Which level a merchant gets today, and what they still need to complete.\"\n >\n <TierPreviewTab />\n </Drawer>\n </>\n );\n}\n"],"mappings":";AA+BI,mBAMQ,KADF,YALN;AA7BJ,SAAS,gBAAgB;AACzB,SAAS,cAAc;AAEvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AAUlB,SAAS,iBAAiB;AAC/B,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoC,IAAI;AACtE,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AACjD,QAAM,QAAQ,SAAS;AAEvB,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,aAAY;AAAA,QACZ,SACE,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV,YAAY,aAAa,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA;AAAA,UAC7D;AAAA,UACA,oBAAC,UAAO,SAAQ,aAAY,SAAS,MAAM,cAAc,IAAI,GAAG,gCAEhE;AAAA,UACA,oBAAC,UAAO,SAAS,MAAM,WAAW,KAAK,GAAG,uBAAS;AAAA,WACrD;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,cAAW,SAAkB,QAAQ,YAAY,UAAoB;AAAA,IACtE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,SAAS,MAAM,cAAc,KAAK;AAAA,QAClC,OAAM;AAAA,QACN,UAAS;AAAA,QAET,8BAAC,kBAAe;AAAA;AAAA,IAClB;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/payout-tiers/tiers-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button } from '@withpotter/ui-kit/button';\nimport type { PayoutTier } from '@withpotter/lib/admin-money-api';\nimport {\n CurrencyFilter,\n PageHeader,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../_ui';\nimport { TierPreviewTab } from './tier-preview-tab';\nimport { TiersTable } from './tiers-table';\nimport { useTiers } from './use-tiers';\n\nconst TABS = [\n { id: 'tiers', label: 'Tiers' },\n { id: 'preview', label: 'Preview a business' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * How much a business may send out, by how far it is verified.\n *\n * This is configuration, not approved policy: nothing is seeded, and with no\n * tier a business resolves to \"verification required\". The preview tab\n * answers the question an admin actually asks: which tier would THIS\n * business get today, and what is it missing?\n */\nexport function TiersWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const [editing, setEditing] = useState<PayoutTier | 'new' | null>(null);\n const [currency, setCurrency] = useCurrencyParam();\n const tiers = useTiers();\n\n return (\n <>\n <PageHeader\n title=\"Payout tiers\"\n description=\"Per-currency limits by verification level.\"\n actions={\n tab === 'tiers' ? (\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currenciesIn(tiers.data?.map((t) => t.currency))}\n />\n <Button onClick={() => setEditing('new')}>New tier</Button>\n </div>\n ) : undefined\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {tab === 'tiers' && (\n <TiersTable editing={editing} onEdit={setEditing} currency={currency} />\n )}\n {tab === 'preview' && <TierPreviewTab />}\n </>\n );\n}\n"],"mappings":";AAuCI,mBAOU,KADF,YANR;AArCJ,SAAS,gBAAgB;AACzB,SAAS,cAAc;AAEvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AAEzB,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,SAAS,OAAO,QAAQ;AAAA,EAC9B,EAAE,IAAI,WAAW,OAAO,qBAAqB;AAC/C;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAU1B,SAAS,iBAAiB;AAC/B,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoC,IAAI;AACtE,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AACjD,QAAM,QAAQ,SAAS;AAEvB,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,aAAY;AAAA,QACZ,SACE,QAAQ,UACN,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV,YAAY,aAAa,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA;AAAA,UAC7D;AAAA,UACA,oBAAC,UAAO,SAAS,MAAM,WAAW,KAAK,GAAG,sBAAQ;AAAA,WACpD,IACE;AAAA;AAAA,IAER;AAAA,IACA,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD,QAAQ,WACP,oBAAC,cAAW,SAAkB,QAAQ,YAAY,UAAoB;AAAA,IAEvE,QAAQ,aAAa,oBAAC,kBAAe;AAAA,KACxC;AAEJ;","names":[]}
@@ -16,12 +16,9 @@ import { usePosPeriod } from "../_pos/use-pos-period";
16
16
  import { usePosSummary } from "../_pos/use-pos";
17
17
  import { ShiftsTab } from "./shifts-tab";
18
18
  import { TerminalsTab } from "./terminals-tab";
19
- import { TransactionsTab } from "./transactions-tab";
20
19
  const TABS = [
21
20
  { id: "terminals", label: "Terminals" },
22
- // The merchant's POS page calls these sessions, here POS sessions; the address keeps `shifts` so old links still open.
23
- { id: "shifts", label: "POS sessions" },
24
- { id: "transactions", label: "Transactions" }
21
+ { id: "shifts", label: "Shifts" }
25
22
  ];
26
23
  const KEYS = TABS.map((t) => t.id);
27
24
  function PosWorkspace() {
@@ -60,8 +57,7 @@ function PosWorkspace() {
60
57
  /* @__PURE__ */ jsx(PosFigures, { period, phrase, currency }),
61
58
  /* @__PURE__ */ jsx(PageTabs, { tabs: TABS, active: tab, onChange: setTab }),
62
59
  tab === "terminals" && /* @__PURE__ */ jsx(TerminalsTab, { period, currency }),
63
- tab === "shifts" && /* @__PURE__ */ jsx(ShiftsTab, { currency }),
64
- tab === "transactions" && /* @__PURE__ */ jsx(TransactionsTab, {})
60
+ tab === "shifts" && /* @__PURE__ */ jsx(ShiftsTab, { currency })
65
61
  ] });
66
62
  }
67
63
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/pos/pos-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../_ui';\nimport { PosFigures } from '../_pos/pos-figures';\nimport { usePosAccess } from '../_pos/use-pos-access';\nimport { usePosPeriod } from '../_pos/use-pos-period';\nimport { usePosSummary } from '../_pos/use-pos';\nimport { ShiftsTab } from './shifts-tab';\nimport { TerminalsTab } from './terminals-tab';\nimport { TransactionsTab } from './transactions-tab';\n\n/**\n * Box's POS page has an Orders tab for hardware orders. Olive sells no\n * terminals through the admin, so it has no such tab.\n */\nconst TABS = [\n { id: 'terminals', label: 'Terminals' },\n // The merchant's POS page calls these sessions, here POS sessions; the address keeps `shifts` so old links still open.\n { id: 'shifts', label: 'POS sessions' },\n { id: 'transactions', label: 'Transactions' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * POS across every business: the fleet's figures, then every terminal and\n * every till shift. A terminal opens on its own page.\n */\nexport function PosWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const { isLoading, canRead } = usePosAccess();\n const { period, phrase, control } = usePosPeriod();\n const [currency, setCurrency] = useCurrencyParam();\n // The figures' own request (one shared query): its currencies are the\n // filter's choices.\n const summary = usePosSummary(period, canRead);\n const currencies = currenciesIn(\n summary.data?.paymentsAmount.map((a) => a.currency),\n );\n\n if (isLoading) return <AdmLoading label=\"Loading POS…\" />;\n\n if (!canRead)\n return (\n <div>\n <PageHeader title=\"POS\" />\n <EmptyNote well>{\"You don't have permission to see POS.\"}</EmptyNote>\n </div>\n );\n\n return (\n <div>\n <PageHeader\n title=\"POS\"\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n {control}\n </div>\n }\n />\n <PosFigures period={period} phrase={phrase} currency={currency} />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {tab === 'terminals' && (\n <TerminalsTab period={period} currency={currency} />\n )}\n {tab === 'shifts' && <ShiftsTab currency={currency} />}\n {tab === 'transactions' && <TransactionsTab />}\n </div>\n );\n}\n"],"mappings":";AAiDwB,cAIlB,YAJkB;AA/CxB,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAMhC,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,aAAa,OAAO,YAAY;AAAA;AAAA,EAEtC,EAAE,IAAI,UAAU,OAAO,eAAe;AAAA,EACtC,EAAE,IAAI,gBAAgB,OAAO,eAAe;AAC9C;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAM1B,SAAS,eAAe;AAC7B,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM,EAAE,WAAW,QAAQ,IAAI,aAAa;AAC5C,QAAM,EAAE,QAAQ,QAAQ,QAAQ,IAAI,aAAa;AACjD,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAGjD,QAAM,UAAU,cAAc,QAAQ,OAAO;AAC7C,QAAM,aAAa;AAAA,IACjB,QAAQ,MAAM,eAAe,IAAI,CAAC,MAAM,EAAE,QAAQ;AAAA,EACpD;AAEA,MAAI,UAAW,QAAO,oBAAC,cAAW,OAAM,qBAAe;AAEvD,MAAI,CAAC;AACH,WACE,qBAAC,SACC;AAAA,0BAAC,cAAW,OAAM,OAAM;AAAA,MACxB,oBAAC,aAAU,MAAI,MAAE,mDAAwC;AAAA,OAC3D;AAGJ,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SACE,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACC;AAAA,WACH;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,cAAW,QAAgB,QAAgB,UAAoB;AAAA,IAChE,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD,QAAQ,eACP,oBAAC,gBAAa,QAAgB,UAAoB;AAAA,IAEnD,QAAQ,YAAY,oBAAC,aAAU,UAAoB;AAAA,IACnD,QAAQ,kBAAkB,oBAAC,mBAAgB;AAAA,KAC9C;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/pos/pos-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n currenciesIn,\n useCurrencyParam,\n useTabParam,\n} from '../_ui';\nimport { PosFigures } from '../_pos/pos-figures';\nimport { usePosAccess } from '../_pos/use-pos-access';\nimport { usePosPeriod } from '../_pos/use-pos-period';\nimport { usePosSummary } from '../_pos/use-pos';\nimport { ShiftsTab } from './shifts-tab';\nimport { TerminalsTab } from './terminals-tab';\n\n/**\n * Box's POS page has an Orders tab for hardware orders. Olive sells no\n * terminals through the admin, so it has no such tab.\n */\nconst TABS = [\n { id: 'terminals', label: 'Terminals' },\n { id: 'shifts', label: 'Shifts' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * POS across every business: the fleet's figures, then every terminal and\n * every till shift. A terminal opens on its own page.\n */\nexport function PosWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const { isLoading, canRead } = usePosAccess();\n const { period, phrase, control } = usePosPeriod();\n const [currency, setCurrency] = useCurrencyParam();\n // The figures' own request (one shared query): its currencies are the\n // filter's choices.\n const summary = usePosSummary(period, canRead);\n const currencies = currenciesIn(\n summary.data?.paymentsAmount.map((a) => a.currency),\n );\n\n if (isLoading) return <AdmLoading label=\"Loading POS…\" />;\n\n if (!canRead)\n return (\n <div>\n <PageHeader title=\"POS\" />\n <EmptyNote well>{\"You don't have permission to see POS.\"}</EmptyNote>\n </div>\n );\n\n return (\n <div>\n <PageHeader\n title=\"POS\"\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n {control}\n </div>\n }\n />\n <PosFigures period={period} phrase={phrase} currency={currency} />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {tab === 'terminals' && (\n <TerminalsTab period={period} currency={currency} />\n )}\n {tab === 'shifts' && <ShiftsTab currency={currency} />}\n </div>\n );\n}\n"],"mappings":";AA8CwB,cAIlB,YAJkB;AA5CxB,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAM7B,MAAM,OAAO;AAAA,EACX,EAAE,IAAI,aAAa,OAAO,YAAY;AAAA,EACtC,EAAE,IAAI,UAAU,OAAO,SAAS;AAClC;AAEA,MAAM,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE;AAM1B,SAAS,eAAe;AAC7B,QAAM,CAAC,KAAK,MAAM,IAAI,YAAY,IAAI;AACtC,QAAM,EAAE,WAAW,QAAQ,IAAI,aAAa;AAC5C,QAAM,EAAE,QAAQ,QAAQ,QAAQ,IAAI,aAAa;AACjD,QAAM,CAAC,UAAU,WAAW,IAAI,iBAAiB;AAGjD,QAAM,UAAU,cAAc,QAAQ,OAAO;AAC7C,QAAM,aAAa;AAAA,IACjB,QAAQ,MAAM,eAAe,IAAI,CAAC,MAAM,EAAE,QAAQ;AAAA,EACpD;AAEA,MAAI,UAAW,QAAO,oBAAC,cAAW,OAAM,qBAAe;AAEvD,MAAI,CAAC;AACH,WACE,qBAAC,SACC;AAAA,0BAAC,cAAW,OAAM,OAAM;AAAA,MACxB,oBAAC,aAAU,MAAI,MAAE,mDAAwC;AAAA,OAC3D;AAGJ,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SACE,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV;AAAA;AAAA,UACF;AAAA,UACC;AAAA,WACH;AAAA;AAAA,IAEJ;AAAA,IACA,oBAAC,cAAW,QAAgB,QAAgB,UAAoB;AAAA,IAChE,oBAAC,YAAS,MAAM,MAAM,QAAQ,KAAK,UAAU,QAAQ;AAAA,IACpD,QAAQ,eACP,oBAAC,gBAAa,QAAgB,UAAoB;AAAA,IAEnD,QAAQ,YAAY,oBAAC,aAAU,UAAoB;AAAA,KACtD;AAEJ;","names":[]}
@@ -2,9 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { Button, RemovableChip } from "@withpotter/ui-kit";
5
- import {
6
- POS_PAGE_SIZE
7
- } from "@withpotter/lib/admin-pos-query";
5
+ import { POS_PAGE_SIZE } from "@withpotter/lib/admin-pos-query";
8
6
  import { FilterPress, FilterSelect, Filters } from "../_ui";
9
7
  import { ListPager } from "../_ui/list-card";
10
8
  import { LONG_OPEN_HOURS } from "../_pos/pos-rules";
@@ -19,13 +17,8 @@ const STATUS_OPTIONS = [
19
17
  label
20
18
  }))
21
19
  ];
22
- function ShiftsTab({
23
- currency = "",
24
- tenantId: fixedTenantId,
25
- title = "POS sessions"
26
- }) {
27
- const [paramTenantId, setTenantId] = useTenantParam();
28
- const tenantId = fixedTenantId ?? paramTenantId;
20
+ function ShiftsTab({ currency = "" }) {
21
+ const [tenantId, setTenantId] = useTenantParam();
29
22
  const [status, setStatus] = useState("");
30
23
  const [longOpen, setLongOpen] = useState(false);
31
24
  const [page, setPage] = useState(1);
@@ -37,7 +30,7 @@ function ShiftsTab({
37
30
  limit: POS_PAGE_SIZE
38
31
  };
39
32
  const { data, isLoading, isError, error, refetch } = usePosShifts(filters);
40
- const narrowed = !!(status || longOpen || tenantId && !fixedTenantId);
33
+ const narrowed = !!(status || longOpen || tenantId);
41
34
  const reset = (fn) => {
42
35
  fn();
43
36
  setPage(1);
@@ -45,18 +38,18 @@ function ShiftsTab({
45
38
  return /* @__PURE__ */ jsx(
46
39
  ShiftsTable,
47
40
  {
48
- title,
41
+ title: "Shifts",
49
42
  rows: data?.data,
50
43
  total: data?.total,
51
- withBusiness: !fixedTenantId,
44
+ withBusiness: true,
52
45
  currency,
53
46
  isLoading,
54
47
  isError,
55
48
  error,
56
49
  onRetry: () => void refetch(),
57
- emptyTitle: narrowed ? "No POS sessions match." : "No POS sessions yet.",
50
+ emptyTitle: narrowed ? "No shifts match." : "No shifts yet.",
58
51
  controls: /* @__PURE__ */ jsxs(Filters, { label: "Filter shifts", children: [
59
- tenantId && !fixedTenantId && /* @__PURE__ */ jsx(
52
+ tenantId && /* @__PURE__ */ jsx(
60
53
  RemovableChip,
61
54
  {
62
55
  onRemove: () => reset(() => setTenantId("")),
@@ -93,7 +86,7 @@ function ShiftsTab({
93
86
  onClick: () => reset(() => {
94
87
  setStatus("");
95
88
  setLongOpen(false);
96
- if (!fixedTenantId) setTenantId("");
89
+ setTenantId("");
97
90
  }),
98
91
  children: "Clear filters"
99
92
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport {\n POS_PAGE_SIZE,\n type ShiftFilters,\n} from '@withpotter/lib/admin-pos-query';\nimport type { PosShiftStatus } from '@withpotter/lib/admin-pos-types';\nimport { FilterPress, FilterSelect, Filters } from '../_ui';\nimport { ListPager } from '../_ui/list-card';\nimport { LONG_OPEN_HOURS } from '../_pos/pos-rules';\nimport { SHIFT_STATUS_LABEL } from '../_pos/pos-words';\nimport { ShiftsTable } from '../_pos/shifts-table';\nimport { usePosShifts } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\n\nconst STATUS_OPTIONS = [\n { value: '', label: 'Any status' },\n ...Object.entries(SHIFT_STATUS_LABEL).map(([value, label]) => ({\n value,\n label,\n })),\n];\n\n/** Open and recent shifts across every business, newest first. */\nexport function ShiftsTab({\n currency = '',\n tenantId: fixedTenantId,\n title = 'POS sessions',\n}: {\n currency?: string;\n /** Fixes the list to one business (its own page); otherwise `?tenant=` narrows it. */\n tenantId?: string;\n title?: string;\n}) {\n const [paramTenantId, setTenantId] = useTenantParam();\n const tenantId = fixedTenantId ?? paramTenantId;\n const [status, setStatus] = useState<PosShiftStatus | ''>('');\n const [longOpen, setLongOpen] = useState(false);\n const [page, setPage] = useState(1);\n const filters: ShiftFilters = {\n status: longOpen ? 'open' : status,\n openOverHours: longOpen ? LONG_OPEN_HOURS : undefined,\n tenantId: tenantId || undefined,\n page,\n limit: POS_PAGE_SIZE,\n };\n const { data, isLoading, isError, error, refetch } = usePosShifts(filters);\n const narrowed = !!(status || longOpen || (tenantId && !fixedTenantId));\n const reset = (fn: () => void) => {\n fn();\n setPage(1);\n };\n\n return (\n <ShiftsTable\n title={title}\n rows={data?.data}\n total={data?.total}\n withBusiness={!fixedTenantId}\n currency={currency}\n isLoading={isLoading}\n isError={isError}\n error={error}\n onRetry={() => void refetch()}\n emptyTitle={narrowed ? 'No POS sessions match.' : 'No POS sessions yet.'}\n controls={\n <Filters label=\"Filter shifts\">\n {tenantId && !fixedTenantId && (\n <RemovableChip\n onRemove={() => reset(() => setTenantId(''))}\n removeLabel=\"Show every business\"\n >\n One business\n </RemovableChip>\n )}\n <FilterPress\n on={longOpen}\n onToggle={() => reset(() => setLongOpen((v) => !v))}\n >\n Open over {LONG_OPEN_HOURS} hours\n </FilterPress>\n {!longOpen && (\n <FilterSelect\n label=\"Status\"\n value={status}\n onChange={(v) => reset(() => setStatus(v as PosShiftStatus | ''))}\n options={STATUS_OPTIONS}\n />\n )}\n {narrowed && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() =>\n reset(() => {\n setStatus('');\n setLongOpen(false);\n if (!fixedTenantId) setTenantId('');\n })\n }\n >\n Clear filters\n </Button>\n )}\n </Filters>\n }\n footer={\n data && data.total > 0 ? (\n <ListPager\n page={page}\n totalPages={Math.max(1, data.totalPages)}\n total={data.total}\n noun={data.total === 1 ? 'shift' : 'shifts'}\n onPage={setPage}\n />\n ) : undefined\n }\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/pos/shifts-tab.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Button, RemovableChip } from '@withpotter/ui-kit';\nimport { POS_PAGE_SIZE, type ShiftFilters } from '@withpotter/lib/admin-pos-query';\nimport type { PosShiftStatus } from '@withpotter/lib/admin-pos-types';\nimport { FilterPress, FilterSelect, Filters } from '../_ui';\nimport { ListPager } from '../_ui/list-card';\nimport { LONG_OPEN_HOURS } from '../_pos/pos-rules';\nimport { SHIFT_STATUS_LABEL } from '../_pos/pos-words';\nimport { ShiftsTable } from '../_pos/shifts-table';\nimport { usePosShifts } from '../_pos/use-pos';\nimport { useTenantParam } from '../_pos/use-tenant-param';\n\nconst STATUS_OPTIONS = [\n { value: '', label: 'Any status' },\n ...Object.entries(SHIFT_STATUS_LABEL).map(([value, label]) => ({\n value,\n label,\n })),\n];\n\n/** Open and recent shifts across every business, newest first. */\nexport function ShiftsTab({ currency = '' }: { currency?: string }) {\n const [tenantId, setTenantId] = useTenantParam();\n const [status, setStatus] = useState<PosShiftStatus | ''>('');\n const [longOpen, setLongOpen] = useState(false);\n const [page, setPage] = useState(1);\n const filters: ShiftFilters = {\n status: longOpen ? 'open' : status,\n openOverHours: longOpen ? LONG_OPEN_HOURS : undefined,\n tenantId: tenantId || undefined,\n page,\n limit: POS_PAGE_SIZE,\n };\n const { data, isLoading, isError, error, refetch } = usePosShifts(filters);\n const narrowed = !!(status || longOpen || tenantId);\n const reset = (fn: () => void) => {\n fn();\n setPage(1);\n };\n\n return (\n <ShiftsTable\n title=\"Shifts\"\n rows={data?.data}\n total={data?.total}\n withBusiness\n currency={currency}\n isLoading={isLoading}\n isError={isError}\n error={error}\n onRetry={() => void refetch()}\n emptyTitle={narrowed ? 'No shifts match.' : 'No shifts yet.'}\n controls={\n <Filters label=\"Filter shifts\">\n {tenantId && (\n <RemovableChip\n onRemove={() => reset(() => setTenantId(''))}\n removeLabel=\"Show every business\"\n >\n One business\n </RemovableChip>\n )}\n <FilterPress\n on={longOpen}\n onToggle={() => reset(() => setLongOpen((v) => !v))}\n >\n Open over {LONG_OPEN_HOURS} hours\n </FilterPress>\n {!longOpen && (\n <FilterSelect\n label=\"Status\"\n value={status}\n onChange={(v) => reset(() => setStatus(v as PosShiftStatus | ''))}\n options={STATUS_OPTIONS}\n />\n )}\n {narrowed && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() =>\n reset(() => {\n setStatus('');\n setLongOpen(false);\n setTenantId('');\n })\n }\n >\n Clear filters\n </Button>\n )}\n </Filters>\n }\n footer={\n data && data.total > 0 ? (\n <ListPager\n page={page}\n totalPages={Math.max(1, data.totalPages)}\n total={data.total}\n noun={data.total === 1 ? 'shift' : 'shifts'}\n onPage={setPage}\n />\n ) : undefined\n }\n />\n );\n}\n"],"mappings":";AAyDY,cAOF,YAPE;AAvDZ,SAAS,gBAAgB;AACzB,SAAS,QAAQ,qBAAqB;AACtC,SAAS,qBAAwC;AAEjD,SAAS,aAAa,cAAc,eAAe;AACnD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAE/B,MAAM,iBAAiB;AAAA,EACrB,EAAE,OAAO,IAAI,OAAO,aAAa;AAAA,EACjC,GAAG,OAAO,QAAQ,kBAAkB,EAAE,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO;AAAA,IAC7D;AAAA,IACA;AAAA,EACF,EAAE;AACJ;AAGO,SAAS,UAAU,EAAE,WAAW,GAAG,GAA0B;AAClE,QAAM,CAAC,UAAU,WAAW,IAAI,eAAe;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8B,EAAE;AAC5D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,UAAwB;AAAA,IAC5B,QAAQ,WAAW,SAAS;AAAA,IAC5B,eAAe,WAAW,kBAAkB;AAAA,IAC5C,UAAU,YAAY;AAAA,IACtB;AAAA,IACA,OAAO;AAAA,EACT;AACA,QAAM,EAAE,MAAM,WAAW,SAAS,OAAO,QAAQ,IAAI,aAAa,OAAO;AACzE,QAAM,WAAW,CAAC,EAAE,UAAU,YAAY;AAC1C,QAAM,QAAQ,CAAC,OAAmB;AAChC,OAAG;AACH,YAAQ,CAAC;AAAA,EACX;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,MACb,cAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,MAAM,KAAK,QAAQ;AAAA,MAC5B,YAAY,WAAW,qBAAqB;AAAA,MAC5C,UACE,qBAAC,WAAQ,OAAM,iBACZ;AAAA,oBACC;AAAA,UAAC;AAAA;AAAA,YACC,UAAU,MAAM,MAAM,MAAM,YAAY,EAAE,CAAC;AAAA,YAC3C,aAAY;AAAA,YACb;AAAA;AAAA,QAED;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,UAAU,MAAM,MAAM,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;AAAA,YACnD;AAAA;AAAA,cACY;AAAA,cAAgB;AAAA;AAAA;AAAA,QAC7B;AAAA,QACC,CAAC,YACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,MAAM,MAAM,UAAU,CAAwB,CAAC;AAAA,YAChE,SAAS;AAAA;AAAA,QACX;AAAA,QAED,YACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,MACP,MAAM,MAAM;AACV,wBAAU,EAAE;AACZ,0BAAY,KAAK;AACjB,0BAAY,EAAE;AAAA,YAChB,CAAC;AAAA,YAEJ;AAAA;AAAA,QAED;AAAA,SAEJ;AAAA,MAEF,QACE,QAAQ,KAAK,QAAQ,IACnB;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,YAAY,KAAK,IAAI,GAAG,KAAK,UAAU;AAAA,UACvC,OAAO,KAAK;AAAA,UACZ,MAAM,KAAK,UAAU,IAAI,UAAU;AAAA,UACnC,QAAQ;AAAA;AAAA,MACV,IACE;AAAA;AAAA,EAER;AAEJ;","names":[]}
@@ -17,28 +17,28 @@ function CohortPanel({ cohorts }) {
17
17
  return /* @__PURE__ */ jsx(
18
18
  Panel,
19
19
  {
20
- title: "Do new merchants keep selling?",
20
+ title: "Logo cohort retention",
21
21
  action: cohorts.length > 0 && /* @__PURE__ */ jsx(
22
22
  ViewSwitch,
23
23
  {
24
24
  className: "adm-view-switch",
25
- "aria-label": "Show the table as",
25
+ "aria-label": "Show cohort cells as",
26
26
  options: LENSES,
27
27
  value: lens,
28
28
  onChange: setLens
29
29
  }
30
30
  ),
31
- footer: "Of the merchants who signed up in each month, the share still selling (at least one paid order) in each month after. M0 is the sign-up month itself.",
31
+ footer: "Share of each signup-month cohort still transacting (at least 1 paid order) at each month offset. M0 is the signup month itself.",
32
32
  flush: true,
33
- children: cohorts.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "Not enough sign-up history yet." }) : /* @__PURE__ */ jsx(
33
+ children: cohorts.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "Not enough signup history yet." }) : /* @__PURE__ */ jsx(
34
34
  TableShell,
35
35
  {
36
36
  bare: true,
37
37
  minWidth: Math.max(320, 200 + offsets.length * 64),
38
38
  density: "compact",
39
39
  head: /* @__PURE__ */ jsxs("tr", { children: [
40
- /* @__PURE__ */ jsx(Th, { children: "Signed up" }),
41
- /* @__PURE__ */ jsx(Th, { align: "right", children: "Merchants" }),
40
+ /* @__PURE__ */ jsx(Th, { children: "Cohort" }),
41
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Tenants" }),
42
42
  offsets.map((_, i) => /* @__PURE__ */ jsxs(Th, { align: "right", children: [
43
43
  "M",
44
44
  i
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/cohort-panel.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { EmptyNote, TableShell, Th, ViewSwitch } from '@withpotter/ui-kit';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { TONE } from '../_fx';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Cohort = NonNullable<PlatformRevenue['quality']>['cohorts'][number];\ntype Lens = 'share' | 'count';\n\nconst LENSES = [\n { value: 'share' as const, label: 'Share' },\n { value: 'count' as const, label: 'Merchants' },\n];\n\n/**\n * Logo retention by signup month, shaded by the share still transacting.\n * The switch reads each cell as a share of the cohort or as the merchants\n * that share stands for; the shading stays the share either way.\n */\nexport function CohortPanel({ cohorts }: { cohorts: Cohort[] }) {\n const [lens, setLens] = useState<Lens>('share');\n const offsets = cohorts[0]?.retention ?? [];\n const cell = (v: number, size: number) =>\n lens === 'share' ? `${v}%` : integer.format(Math.round((size * v) / 100));\n\n return (\n <Panel\n title=\"Do new merchants keep selling?\"\n action={\n cohorts.length > 0 && (\n <ViewSwitch\n className=\"adm-view-switch\"\n aria-label=\"Show the table as\"\n options={LENSES}\n value={lens}\n onChange={setLens}\n />\n )\n }\n footer=\"Of the merchants who signed up in each month, the share still selling (at least one paid order) in each month after. M0 is the sign-up month itself.\"\n flush\n >\n {cohorts.length === 0 ? (\n <EmptyNote className={styles.empty}>\n Not enough sign-up history yet.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={Math.max(320, 200 + offsets.length * 64)}\n density=\"compact\"\n head={\n <tr>\n <Th>Signed up</Th>\n <Th align=\"right\">Merchants</Th>\n {offsets.map((_, i) => (\n <Th key={i} align=\"right\">\n M{i}\n </Th>\n ))}\n </tr>\n }\n >\n {cohorts.map((c) => (\n <tr key={c.cohort}>\n <td className={styles.cohort}>{c.cohort}</td>\n <td className={styles.num}>{integer.format(c.size)}</td>\n {c.retention.map((v, i) => (\n <td\n key={i}\n className={styles.heat}\n data-empty={v == null}\n style={\n v == null\n ? undefined\n : {\n // 60% at most, so dark figures stay readable on the darkest cell.\n background: `color-mix(in srgb, ${TONE.brand.fg} ${Math.round(\n Math.min(v, 100) * 0.6,\n )}%, transparent)`,\n }\n }\n >\n {v == null ? '–' : cell(v, c.size)}\n </td>\n ))}\n </tr>\n ))}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":";AAmCU,cA0BM,YA1BN;AAjCV,SAAS,gBAAgB;AACzB,SAAS,WAAW,YAAY,IAAI,kBAAkB;AAEtD,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAK7C,MAAM,SAAS;AAAA,EACb,EAAE,OAAO,SAAkB,OAAO,QAAQ;AAAA,EAC1C,EAAE,OAAO,SAAkB,OAAO,YAAY;AAChD;AAOO,SAAS,YAAY,EAAE,QAAQ,GAA0B;AAC9D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe,OAAO;AAC9C,QAAM,UAAU,QAAQ,CAAC,GAAG,aAAa,CAAC;AAC1C,QAAM,OAAO,CAAC,GAAW,SACvB,SAAS,UAAU,GAAG,CAAC,MAAM,QAAQ,OAAO,KAAK,MAAO,OAAO,IAAK,GAAG,CAAC;AAE1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,QACE,QAAQ,SAAS,KACf;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,SAAS;AAAA,UACT,OAAO;AAAA,UACP,UAAU;AAAA;AAAA,MACZ;AAAA,MAGJ,QAAO;AAAA,MACP,OAAK;AAAA,MAEJ,kBAAQ,WAAW,IAClB,oBAAC,aAAU,WAAW,OAAO,OAAO,6CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU,KAAK,IAAI,KAAK,MAAM,QAAQ,SAAS,EAAE;AAAA,UACjD,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,uBAAS;AAAA,YACb,oBAAC,MAAG,OAAM,SAAQ,uBAAS;AAAA,YAC1B,QAAQ,IAAI,CAAC,GAAG,MACf,qBAAC,MAAW,OAAM,SAAQ;AAAA;AAAA,cACtB;AAAA,iBADK,CAET,CACD;AAAA,aACH;AAAA,UAGD,kBAAQ,IAAI,CAAC,MACZ,qBAAC,QACC;AAAA,gCAAC,QAAG,WAAW,OAAO,QAAS,YAAE,QAAO;AAAA,YACxC,oBAAC,QAAG,WAAW,OAAO,KAAM,kBAAQ,OAAO,EAAE,IAAI,GAAE;AAAA,YAClD,EAAE,UAAU,IAAI,CAAC,GAAG,MACnB;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW,OAAO;AAAA,gBAClB,cAAY,KAAK;AAAA,gBACjB,OACE,KAAK,OACD,SACA;AAAA;AAAA,kBAEE,YAAY,sBAAsB,KAAK,MAAM,EAAE,IAAI,KAAK;AAAA,oBACtD,KAAK,IAAI,GAAG,GAAG,IAAI;AAAA,kBACrB,CAAC;AAAA,gBACH;AAAA,gBAGL,eAAK,OAAO,WAAM,KAAK,GAAG,EAAE,IAAI;AAAA;AAAA,cAd5B;AAAA,YAeP,CACD;AAAA,eArBM,EAAE,MAsBX,CACD;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/cohort-panel.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { EmptyNote, TableShell, Th, ViewSwitch } from '@withpotter/ui-kit';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { TONE } from '../_fx';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Cohort = NonNullable<PlatformRevenue['quality']>['cohorts'][number];\ntype Lens = 'share' | 'count';\n\nconst LENSES = [\n { value: 'share' as const, label: 'Share' },\n { value: 'count' as const, label: 'Merchants' },\n];\n\n/**\n * Logo retention by signup month, shaded by the share still transacting.\n * The switch reads each cell as a share of the cohort or as the merchants\n * that share stands for; the shading stays the share either way.\n */\nexport function CohortPanel({ cohorts }: { cohorts: Cohort[] }) {\n const [lens, setLens] = useState<Lens>('share');\n const offsets = cohorts[0]?.retention ?? [];\n const cell = (v: number, size: number) =>\n lens === 'share' ? `${v}%` : integer.format(Math.round((size * v) / 100));\n\n return (\n <Panel\n title=\"Logo cohort retention\"\n action={\n cohorts.length > 0 && (\n <ViewSwitch\n className=\"adm-view-switch\"\n aria-label=\"Show cohort cells as\"\n options={LENSES}\n value={lens}\n onChange={setLens}\n />\n )\n }\n footer=\"Share of each signup-month cohort still transacting (at least 1 paid order) at each month offset. M0 is the signup month itself.\"\n flush\n >\n {cohorts.length === 0 ? (\n <EmptyNote className={styles.empty}>\n Not enough signup history yet.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={Math.max(320, 200 + offsets.length * 64)}\n density=\"compact\"\n head={\n <tr>\n <Th>Cohort</Th>\n <Th align=\"right\">Tenants</Th>\n {offsets.map((_, i) => (\n <Th key={i} align=\"right\">\n M{i}\n </Th>\n ))}\n </tr>\n }\n >\n {cohorts.map((c) => (\n <tr key={c.cohort}>\n <td className={styles.cohort}>{c.cohort}</td>\n <td className={styles.num}>{integer.format(c.size)}</td>\n {c.retention.map((v, i) => (\n <td\n key={i}\n className={styles.heat}\n data-empty={v == null}\n style={\n v == null\n ? undefined\n : {\n // 60% at most, so dark figures stay readable on the darkest cell.\n background: `color-mix(in srgb, ${TONE.brand.fg} ${Math.round(\n Math.min(v, 100) * 0.6,\n )}%, transparent)`,\n }\n }\n >\n {v == null ? '–' : cell(v, c.size)}\n </td>\n ))}\n </tr>\n ))}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":";AAmCU,cA0BM,YA1BN;AAjCV,SAAS,gBAAgB;AACzB,SAAS,WAAW,YAAY,IAAI,kBAAkB;AAEtD,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAK7C,MAAM,SAAS;AAAA,EACb,EAAE,OAAO,SAAkB,OAAO,QAAQ;AAAA,EAC1C,EAAE,OAAO,SAAkB,OAAO,YAAY;AAChD;AAOO,SAAS,YAAY,EAAE,QAAQ,GAA0B;AAC9D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe,OAAO;AAC9C,QAAM,UAAU,QAAQ,CAAC,GAAG,aAAa,CAAC;AAC1C,QAAM,OAAO,CAAC,GAAW,SACvB,SAAS,UAAU,GAAG,CAAC,MAAM,QAAQ,OAAO,KAAK,MAAO,OAAO,IAAK,GAAG,CAAC;AAE1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,QACE,QAAQ,SAAS,KACf;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,SAAS;AAAA,UACT,OAAO;AAAA,UACP,UAAU;AAAA;AAAA,MACZ;AAAA,MAGJ,QAAO;AAAA,MACP,OAAK;AAAA,MAEJ,kBAAQ,WAAW,IAClB,oBAAC,aAAU,WAAW,OAAO,OAAO,4CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU,KAAK,IAAI,KAAK,MAAM,QAAQ,SAAS,EAAE;AAAA,UACjD,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,oBAAM;AAAA,YACV,oBAAC,MAAG,OAAM,SAAQ,qBAAO;AAAA,YACxB,QAAQ,IAAI,CAAC,GAAG,MACf,qBAAC,MAAW,OAAM,SAAQ;AAAA;AAAA,cACtB;AAAA,iBADK,CAET,CACD;AAAA,aACH;AAAA,UAGD,kBAAQ,IAAI,CAAC,MACZ,qBAAC,QACC;AAAA,gCAAC,QAAG,WAAW,OAAO,QAAS,YAAE,QAAO;AAAA,YACxC,oBAAC,QAAG,WAAW,OAAO,KAAM,kBAAQ,OAAO,EAAE,IAAI,GAAE;AAAA,YAClD,EAAE,UAAU,IAAI,CAAC,GAAG,MACnB;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW,OAAO;AAAA,gBAClB,cAAY,KAAK;AAAA,gBACjB,OACE,KAAK,OACD,SACA;AAAA;AAAA,kBAEE,YAAY,sBAAsB,KAAK,MAAM,EAAE,IAAI,KAAK;AAAA,oBACtD,KAAK,IAAI,GAAG,GAAG,IAAI;AAAA,kBACrB,CAAC;AAAA,gBACH;AAAA,gBAGL,eAAK,OAAO,WAAM,KAAK,GAAG,EAAE,IAAI;AAAA;AAAA,cAd5B;AAAA,YAeP,CACD;AAAA,eArBM,EAAE,MAsBX,CACD;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -2,57 +2,56 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { AdmLoading, Panel } from "../_ui";
3
3
  import { FigureList } from "./figure-list";
4
4
  const integer = new Intl.NumberFormat("en-US");
5
- const pct = (value) => value == null ? "\u2013" : `${value}%`;
6
5
  function HealthPanel({
7
6
  health,
8
7
  money,
9
8
  caption
10
9
  }) {
11
10
  if (!health) {
12
- return /* @__PURE__ */ jsx(Panel, { title: "How plan income is changing", caption, children: /* @__PURE__ */ jsx(AdmLoading, { label: "Loading revenue health\u2026", size: "sm" }) });
11
+ return /* @__PURE__ */ jsx(Panel, { title: "Revenue health", caption, children: /* @__PURE__ */ jsx(AdmLoading, { label: "Loading revenue health\u2026", size: "sm" }) });
13
12
  }
14
13
  const rows = [
15
14
  {
16
- label: "Plan income change",
17
- note: "Gained from new merchants, less lost from merchants who left",
15
+ label: "Net new MRR",
16
+ note: "New and expansion, less churned",
18
17
  value: money(health.netNewMrr),
19
- hint: `+${money(health.newMrr)} gained \xB7 \u2212${money(health.churnedMrr)} lost`,
18
+ hint: `+${money(health.newMrr)} new \xB7 \u2212${money(health.churnedMrr)} churned`,
20
19
  tone: health.netNewMrr < 0 ? "danger" : "default"
21
20
  },
22
21
  {
23
- label: "Plan income kept",
24
- note: "Share of the plan income at the start that is still being paid",
25
- value: pct(health.grossRevenueRetentionPct)
22
+ label: "Gross revenue retention",
23
+ note: "Share of starting MRR kept, ignoring expansion",
24
+ value: `${health.grossRevenueRetentionPct}%`
26
25
  },
27
26
  {
28
- label: "Plan income lost",
29
- note: "Share of plan income lost to merchants who left",
30
- value: pct(health.grossMrrChurnPct),
27
+ label: "Gross MRR churn",
28
+ note: "MRR lost as a share of the base",
29
+ value: `${health.grossMrrChurnPct}%`,
31
30
  tone: health.grossMrrChurnPct > 0 ? "danger" : "default"
32
31
  },
33
32
  {
34
- label: "Paying merchants gained",
35
- note: "Merchants who started paying, less merchants who stopped",
33
+ label: "Net new logos",
34
+ note: "Paying merchants gained, less lost",
36
35
  value: integer.format(health.netNewLogos),
37
- hint: `+${integer.format(health.newLogos)} joined \xB7 \u2212${integer.format(
36
+ hint: `+${integer.format(health.newLogos)} new \xB7 \u2212${integer.format(
38
37
  health.churnedLogos
39
- )} left`,
38
+ )} churned`,
40
39
  tone: health.netNewLogos < 0 ? "danger" : "default"
41
40
  },
42
41
  {
43
- label: "Paying merchants lost",
44
- note: "Share of paying merchants who stopped paying",
45
- value: pct(health.logoChurnPct),
42
+ label: "Logo churn",
43
+ note: "Paying merchants lost as a share of the base",
44
+ value: `${health.logoChurnPct}%`,
46
45
  tone: health.logoChurnPct > 0 ? "danger" : "default"
47
46
  },
48
47
  {
49
- label: "Reliance on the top 10",
50
- note: "Share of merchant sales that comes from the 10 biggest merchants",
51
- value: pct(health.gmvConcentrationTop10Pct),
48
+ label: "GMV concentration",
49
+ note: "Share of GMV from the top 10 merchants",
50
+ value: `${health.gmvConcentrationTop10Pct}%`,
52
51
  tone: health.gmvConcentrationTop10Pct > 50 ? "danger" : "default"
53
52
  }
54
53
  ];
55
- return /* @__PURE__ */ jsx(Panel, { title: "How plan income is changing", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
54
+ return /* @__PURE__ */ jsx(Panel, { title: "Revenue health", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
56
55
  }
57
56
  export {
58
57
  HealthPanel
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/health-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { AdmLoading, Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** A share, or a dash when there is no base to take it of (the api sends null then, not 0). */\nconst pct = (value: number | null | undefined) => (value == null ? '–' : `${value}%`);\n\n/** MRR movement, logo churn and concentration risk — how healthy the base is. */\nexport function HealthPanel({\n health,\n money,\n caption,\n}: {\n health: PlatformCockpit['health'] | undefined;\n money: (value: number) => string;\n caption: string;\n}) {\n if (!health) {\n return (\n <Panel title=\"How plan income is changing\" caption={caption}>\n <AdmLoading label=\"Loading revenue health…\" size=\"sm\" />\n </Panel>\n );\n }\n\n const rows: Figure[] = [\n {\n label: 'Plan income change',\n note: 'Gained from new merchants, less lost from merchants who left',\n value: money(health.netNewMrr),\n hint: `+${money(health.newMrr)} gained · −${money(health.churnedMrr)} lost`,\n tone: health.netNewMrr < 0 ? 'danger' : 'default',\n },\n {\n label: 'Plan income kept',\n note: 'Share of the plan income at the start that is still being paid',\n value: pct(health.grossRevenueRetentionPct),\n },\n {\n label: 'Plan income lost',\n note: 'Share of plan income lost to merchants who left',\n value: pct(health.grossMrrChurnPct),\n tone: health.grossMrrChurnPct > 0 ? 'danger' : 'default',\n },\n {\n label: 'Paying merchants gained',\n note: 'Merchants who started paying, less merchants who stopped',\n value: integer.format(health.netNewLogos),\n hint: `+${integer.format(health.newLogos)} joined · −${integer.format(\n health.churnedLogos,\n )} left`,\n tone: health.netNewLogos < 0 ? 'danger' : 'default',\n },\n {\n label: 'Paying merchants lost',\n note: 'Share of paying merchants who stopped paying',\n value: pct(health.logoChurnPct),\n tone: health.logoChurnPct > 0 ? 'danger' : 'default',\n },\n {\n label: 'Reliance on the top 10',\n note: 'Share of merchant sales that comes from the 10 biggest merchants',\n value: pct(health.gmvConcentrationTop10Pct),\n tone: health.gmvConcentrationTop10Pct > 50 ? 'danger' : 'default',\n },\n ];\n\n return (\n <Panel title=\"How plan income is changing\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AAsBQ;AArBR,SAAS,YAAY,aAAa;AAClC,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAG7C,MAAM,MAAM,CAAC,UAAsC,SAAS,OAAO,WAAM,GAAG,KAAK;AAG1E,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,CAAC,QAAQ;AACX,WACE,oBAAC,SAAM,OAAM,+BAA8B,SACzC,8BAAC,cAAW,OAAM,gCAA0B,MAAK,MAAK,GACxD;AAAA,EAEJ;AAEA,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,OAAO,SAAS;AAAA,MAC7B,MAAM,IAAI,MAAM,OAAO,MAAM,CAAC,sBAAc,MAAM,OAAO,UAAU,CAAC;AAAA,MACpE,MAAM,OAAO,YAAY,IAAI,WAAW;AAAA,IAC1C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,IAAI,OAAO,wBAAwB;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,IAAI,OAAO,gBAAgB;AAAA,MAClC,MAAM,OAAO,mBAAmB,IAAI,WAAW;AAAA,IACjD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,OAAO,WAAW;AAAA,MACxC,MAAM,IAAI,QAAQ,OAAO,OAAO,QAAQ,CAAC,sBAAc,QAAQ;AAAA,QAC7D,OAAO;AAAA,MACT,CAAC;AAAA,MACD,MAAM,OAAO,cAAc,IAAI,WAAW;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,IAAI,OAAO,YAAY;AAAA,MAC9B,MAAM,OAAO,eAAe,IAAI,WAAW;AAAA,IAC7C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,IAAI,OAAO,wBAAwB;AAAA,MAC1C,MAAM,OAAO,2BAA2B,KAAK,WAAW;AAAA,IAC1D;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,+BAA8B,SACzC,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/health-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { AdmLoading, Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** MRR movement, logo churn and concentration risk — how healthy the base is. */\nexport function HealthPanel({\n health,\n money,\n caption,\n}: {\n health: PlatformCockpit['health'] | undefined;\n money: (value: number) => string;\n caption: string;\n}) {\n if (!health) {\n return (\n <Panel title=\"Revenue health\" caption={caption}>\n <AdmLoading label=\"Loading revenue health…\" size=\"sm\" />\n </Panel>\n );\n }\n\n const rows: Figure[] = [\n {\n label: 'Net new MRR',\n note: 'New and expansion, less churned',\n value: money(health.netNewMrr),\n hint: `+${money(health.newMrr)} new · −${money(health.churnedMrr)} churned`,\n tone: health.netNewMrr < 0 ? 'danger' : 'default',\n },\n {\n label: 'Gross revenue retention',\n note: 'Share of starting MRR kept, ignoring expansion',\n value: `${health.grossRevenueRetentionPct}%`,\n },\n {\n label: 'Gross MRR churn',\n note: 'MRR lost as a share of the base',\n value: `${health.grossMrrChurnPct}%`,\n tone: health.grossMrrChurnPct > 0 ? 'danger' : 'default',\n },\n {\n label: 'Net new logos',\n note: 'Paying merchants gained, less lost',\n value: integer.format(health.netNewLogos),\n hint: `+${integer.format(health.newLogos)} new · −${integer.format(\n health.churnedLogos,\n )} churned`,\n tone: health.netNewLogos < 0 ? 'danger' : 'default',\n },\n {\n label: 'Logo churn',\n note: 'Paying merchants lost as a share of the base',\n value: `${health.logoChurnPct}%`,\n tone: health.logoChurnPct > 0 ? 'danger' : 'default',\n },\n {\n label: 'GMV concentration',\n note: 'Share of GMV from the top 10 merchants',\n value: `${health.gmvConcentrationTop10Pct}%`,\n tone: health.gmvConcentrationTop10Pct > 50 ? 'danger' : 'default',\n },\n ];\n\n return (\n <Panel title=\"Revenue health\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AAmBQ;AAlBR,SAAS,YAAY,aAAa;AAClC,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,CAAC,QAAQ;AACX,WACE,oBAAC,SAAM,OAAM,kBAAiB,SAC5B,8BAAC,cAAW,OAAM,gCAA0B,MAAK,MAAK,GACxD;AAAA,EAEJ;AAEA,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,OAAO,SAAS;AAAA,MAC7B,MAAM,IAAI,MAAM,OAAO,MAAM,CAAC,mBAAW,MAAM,OAAO,UAAU,CAAC;AAAA,MACjE,MAAM,OAAO,YAAY,IAAI,WAAW;AAAA,IAC1C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,GAAG,OAAO,wBAAwB;AAAA,IAC3C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,GAAG,OAAO,gBAAgB;AAAA,MACjC,MAAM,OAAO,mBAAmB,IAAI,WAAW;AAAA,IACjD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,OAAO,WAAW;AAAA,MACxC,MAAM,IAAI,QAAQ,OAAO,OAAO,QAAQ,CAAC,mBAAW,QAAQ;AAAA,QAC1D,OAAO;AAAA,MACT,CAAC;AAAA,MACD,MAAM,OAAO,cAAc,IAAI,WAAW;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,GAAG,OAAO,YAAY;AAAA,MAC7B,MAAM,OAAO,eAAe,IAAI,WAAW;AAAA,IAC7C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,GAAG,OAAO,wBAAwB;AAAA,MACzC,MAAM,OAAO,2BAA2B,KAAK,WAAW;AAAA,IAC1D;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,kBAAiB,SAC5B,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
@@ -13,7 +13,7 @@ function PlanMixPanel({
13
13
  return /* @__PURE__ */ jsx(
14
14
  Panel,
15
15
  {
16
- title: "Who pays what",
16
+ title: "Plan mix",
17
17
  count: plans.length || void 0,
18
18
  caption,
19
19
  flush: true,
@@ -26,8 +26,8 @@ function PlanMixPanel({
26
26
  head: /* @__PURE__ */ jsxs("tr", { children: [
27
27
  /* @__PURE__ */ jsx(Th, { children: "Plan" }),
28
28
  /* @__PURE__ */ jsx(Th, { align: "right", children: "Subscribers" }),
29
- /* @__PURE__ */ jsx(Th, { align: "right", children: "Monthly income" }),
30
- /* @__PURE__ */ jsx(Th, { align: "right", children: "Share of monthly income" })
29
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "MRR" }),
30
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "% of MRR" })
31
31
  ] }),
32
32
  children: plans.map((p) => {
33
33
  const share = totalMrr > 0 ? p.mrr / totalMrr * 100 : 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/plan-mix-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Subscriber count and MRR per plan, ranked by MRR. */\nexport function PlanMixPanel({\n planBreakdown,\n money,\n caption,\n}: {\n planBreakdown: PlatformRevenue['recurring']['planBreakdown'];\n money: (value: number) => string;\n caption: string;\n}) {\n const plans = [...planBreakdown].sort((a, b) => b.mrr - a.mrr);\n const totalMrr = plans.reduce((sum, p) => sum + p.mrr, 0);\n\n return (\n <Panel\n title=\"Who pays what\"\n count={plans.length || undefined}\n caption={caption}\n flush\n >\n {plans.length === 0 ? (\n <EmptyNote className={styles.empty}>\n No active plans in this period.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={420}\n density=\"compact\"\n head={\n <tr>\n <Th>Plan</Th>\n <Th align=\"right\">Subscribers</Th>\n <Th align=\"right\">Monthly income</Th>\n <Th align=\"right\">Share of monthly income</Th>\n </tr>\n }\n >\n {plans.map((p) => {\n const share = totalMrr > 0 ? (p.mrr / totalMrr) * 100 : 0;\n return (\n <TableRow key={p.planId}>\n <td className={styles.name}>\n <span className={styles.tenantText}>\n {p.name}\n <span className={styles.slug}>{p.interval}</span>\n </span>\n </td>\n <td className={styles.num}>\n {integer.format(p.subscribers)}\n </td>\n <td className={styles.numStrong}>{money(p.mrr)}</td>\n <td className={styles.num}>\n <span className={styles.share}>\n <span className={styles.shareTrack} aria-hidden>\n <span\n className={styles.shareFill}\n style={{ width: `${Math.min(share, 100)}%` }}\n />\n </span>\n {share.toFixed(1)}%\n </span>\n </td>\n </TableRow>\n );\n })}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":"AA4BQ,cASI,YATJ;AA3BR,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC7D,QAAM,WAAW,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM,UAAU;AAAA,MACvB;AAAA,MACA,OAAK;AAAA,MAEJ,gBAAM,WAAW,IAChB,oBAAC,aAAU,WAAW,OAAO,OAAO,6CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,OAAM,SAAQ,yBAAW;AAAA,YAC7B,oBAAC,MAAG,OAAM,SAAQ,4BAAc;AAAA,YAChC,oBAAC,MAAG,OAAM,SAAQ,qCAAuB;AAAA,aAC3C;AAAA,UAGD,gBAAM,IAAI,CAAC,MAAM;AAChB,kBAAM,QAAQ,WAAW,IAAK,EAAE,MAAM,WAAY,MAAM;AACxD,mBACE,qBAAC,YACC;AAAA,kCAAC,QAAG,WAAW,OAAO,MACpB,+BAAC,UAAK,WAAW,OAAO,YACrB;AAAA,kBAAE;AAAA,gBACH,oBAAC,UAAK,WAAW,OAAO,MAAO,YAAE,UAAS;AAAA,iBAC5C,GACF;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,KACnB,kBAAQ,OAAO,EAAE,WAAW,GAC/B;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,cAC/C,oBAAC,QAAG,WAAW,OAAO,KACpB,+BAAC,UAAK,WAAW,OAAO,OACtB;AAAA,oCAAC,UAAK,WAAW,OAAO,YAAY,eAAW,MAC7C;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,OAAO;AAAA,oBAClB,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,GAAG,CAAC,IAAI;AAAA;AAAA,gBAC7C,GACF;AAAA,gBACC,MAAM,QAAQ,CAAC;AAAA,gBAAE;AAAA,iBACpB,GACF;AAAA,iBArBa,EAAE,MAsBjB;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/plan-mix-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Subscriber count and MRR per plan, ranked by MRR. */\nexport function PlanMixPanel({\n planBreakdown,\n money,\n caption,\n}: {\n planBreakdown: PlatformRevenue['recurring']['planBreakdown'];\n money: (value: number) => string;\n caption: string;\n}) {\n const plans = [...planBreakdown].sort((a, b) => b.mrr - a.mrr);\n const totalMrr = plans.reduce((sum, p) => sum + p.mrr, 0);\n\n return (\n <Panel\n title=\"Plan mix\"\n count={plans.length || undefined}\n caption={caption}\n flush\n >\n {plans.length === 0 ? (\n <EmptyNote className={styles.empty}>\n No active plans in this period.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={420}\n density=\"compact\"\n head={\n <tr>\n <Th>Plan</Th>\n <Th align=\"right\">Subscribers</Th>\n <Th align=\"right\">MRR</Th>\n <Th align=\"right\">% of MRR</Th>\n </tr>\n }\n >\n {plans.map((p) => {\n const share = totalMrr > 0 ? (p.mrr / totalMrr) * 100 : 0;\n return (\n <TableRow key={p.planId}>\n <td className={styles.name}>\n <span className={styles.tenantText}>\n {p.name}\n <span className={styles.slug}>{p.interval}</span>\n </span>\n </td>\n <td className={styles.num}>\n {integer.format(p.subscribers)}\n </td>\n <td className={styles.numStrong}>{money(p.mrr)}</td>\n <td className={styles.num}>\n <span className={styles.share}>\n <span className={styles.shareTrack} aria-hidden>\n <span\n className={styles.shareFill}\n style={{ width: `${Math.min(share, 100)}%` }}\n />\n </span>\n {share.toFixed(1)}%\n </span>\n </td>\n </TableRow>\n );\n })}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":"AA4BQ,cASI,YATJ;AA3BR,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC7D,QAAM,WAAW,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM,UAAU;AAAA,MACvB;AAAA,MACA,OAAK;AAAA,MAEJ,gBAAM,WAAW,IAChB,oBAAC,aAAU,WAAW,OAAO,OAAO,6CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,OAAM,SAAQ,yBAAW;AAAA,YAC7B,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,YACrB,oBAAC,MAAG,OAAM,SAAQ,sBAAQ;AAAA,aAC5B;AAAA,UAGD,gBAAM,IAAI,CAAC,MAAM;AAChB,kBAAM,QAAQ,WAAW,IAAK,EAAE,MAAM,WAAY,MAAM;AACxD,mBACE,qBAAC,YACC;AAAA,kCAAC,QAAG,WAAW,OAAO,MACpB,+BAAC,UAAK,WAAW,OAAO,YACrB;AAAA,kBAAE;AAAA,gBACH,oBAAC,UAAK,WAAW,OAAO,MAAO,YAAE,UAAS;AAAA,iBAC5C,GACF;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,KACnB,kBAAQ,OAAO,EAAE,WAAW,GAC/B;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,cAC/C,oBAAC,QAAG,WAAW,OAAO,KACpB,+BAAC,UAAK,WAAW,OAAO,OACtB;AAAA,oCAAC,UAAK,WAAW,OAAO,YAAY,eAAW,MAC7C;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,OAAO;AAAA,oBAClB,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,GAAG,CAAC,IAAI;AAAA;AAAA,gBAC7C,GACF;AAAA,gBACC,MAAM,QAAQ,CAAC;AAAA,gBAAE;AAAA,iBACpB,GACF;AAAA,iBArBa,EAAE,MAsBjB;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}