@withpotter/pages-admin 0.13.0-next.104 → 0.13.0-next.105

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 (47) hide show
  1. package/dist/admin/_overview/activation-panel.js +6 -6
  2. package/dist/admin/_overview/activation-panel.js.map +1 -1
  3. package/dist/admin/_overview/attention-groups.js +8 -8
  4. package/dist/admin/_overview/attention-groups.js.map +1 -1
  5. package/dist/admin/_overview/attention-panel.js +3 -3
  6. package/dist/admin/_overview/attention-panel.js.map +1 -1
  7. package/dist/admin/_overview/control-room.js +9 -9
  8. package/dist/admin/_overview/control-room.js.map +1 -1
  9. package/dist/admin/_overview/kpi-row.js +7 -7
  10. package/dist/admin/_overview/kpi-row.js.map +1 -1
  11. package/dist/admin/_overview/leaders-panel.js +1 -1
  12. package/dist/admin/_overview/leaders-panel.js.map +1 -1
  13. package/dist/admin/_overview/portfolio-panel.js +5 -5
  14. package/dist/admin/_overview/portfolio-panel.js.map +1 -1
  15. package/dist/admin/_overview/position-panel.js +6 -6
  16. package/dist/admin/_overview/position-panel.js.map +1 -1
  17. package/dist/admin/_ui/data-table.js +1 -1
  18. package/dist/admin/_ui/data-table.js.map +1 -1
  19. package/dist/admin/_ui/list-card.js +1 -1
  20. package/dist/admin/_ui/list-card.js.map +1 -1
  21. package/dist/admin/_ui/trend.js +2 -2
  22. package/dist/admin/_ui/trend.js.map +1 -1
  23. package/dist/admin/page.js +6 -6
  24. package/dist/admin/page.js.map +1 -1
  25. package/dist/admin/payout-tiers/_tier-dialog.js +31 -21
  26. package/dist/admin/payout-tiers/_tier-dialog.js.map +1 -1
  27. package/dist/admin/payout-tiers/requirement-labels.js +70 -0
  28. package/dist/admin/payout-tiers/requirement-labels.js.map +1 -0
  29. package/dist/admin/payout-tiers/tier-preview-result.js +10 -9
  30. package/dist/admin/payout-tiers/tier-preview-result.js.map +1 -1
  31. package/dist/admin/payout-tiers/tiers-table.js +6 -7
  32. package/dist/admin/payout-tiers/tiers-table.js.map +1 -1
  33. package/dist/admin/payout-tiers/tiers-workspace.js +2 -2
  34. package/dist/admin/payout-tiers/tiers-workspace.js.map +1 -1
  35. package/dist/admin/revenue/cohort-panel.js +1 -1
  36. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  37. package/dist/admin/revenue/plan-mix-panel.js +2 -2
  38. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  39. package/dist/admin/revenue/revenue-kpis.js +3 -3
  40. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  41. package/dist/admin/revenue/revenue-overview.js +1 -1
  42. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  43. package/dist/admin/revenue/subscriptions-view.js +3 -3
  44. package/dist/admin/revenue/subscriptions-view.js.map +1 -1
  45. package/dist/admin/revenue/top-tenants-panel.js +3 -3
  46. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  47. package/package.json +4 -4
@@ -22,7 +22,7 @@ function TiersWorkspace() {
22
22
  PageHeader,
23
23
  {
24
24
  title: "Payout limits",
25
- description: "Per-currency limits by verification level.",
25
+ description: "How much money a merchant can send out, based on how fully their identity has been checked.",
26
26
  actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
27
27
  /* @__PURE__ */ jsx(
28
28
  CurrencyFilter,
@@ -44,7 +44,7 @@ function TiersWorkspace() {
44
44
  open: previewing,
45
45
  onClose: () => setPreviewing(false),
46
46
  title: "Preview a business",
47
- subtitle: "Which level this business lands on today, and what it is missing.",
47
+ subtitle: "Which level a merchant gets today, and what they still need to complete.",
48
48
  children: /* @__PURE__ */ jsx(TierPreviewTab, {})
49
49
  }
50
50
  )
@@ -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=\"Per-currency limits by verification level.\"\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 this business lands on today, and what it is missing.\"\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 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":[]}
@@ -22,7 +22,7 @@ function CohortPanel({ cohorts }) {
22
22
  ViewSwitch,
23
23
  {
24
24
  className: "adm-view-switch",
25
- "aria-label": "Show cohort cells as",
25
+ "aria-label": "Show the table as",
26
26
  options: LENSES,
27
27
  value: lens,
28
28
  onChange: setLens
@@ -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 cohort cells 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=\"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":[]}
@@ -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: "MRR" }),
30
- /* @__PURE__ */ jsx(Th, { align: "right", children: "% of MRR" })
29
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Monthly income" }),
30
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Share of monthly income" })
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\">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":[]}
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":[]}
@@ -31,7 +31,7 @@ function RevenueKpis({
31
31
  {
32
32
  label: "Plan income each month",
33
33
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
34
- hint: `What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || "NGN"}. Free plans, trials and pauses are not counted. (MRR)`,
34
+ hint: `What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || "NGN"}. Free plans, trials and pauses are not counted.`,
35
35
  sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : noSource
36
36
  }
37
37
  ) }),
@@ -40,7 +40,7 @@ function RevenueKpis({
40
40
  {
41
41
  label: "Plan income over a year",
42
42
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
43
- hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)",
43
+ hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12.",
44
44
  sub: tracked ? "Each month times 12" : noSource
45
45
  }
46
46
  ) }),
@@ -49,7 +49,7 @@ function RevenueKpis({
49
49
  {
50
50
  label: "Average per paying merchant",
51
51
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
52
- hint: "What one paying merchant pays in a month, on average. (ARPA)",
52
+ hint: "What one paying merchant pays in a month, on average.",
53
53
  sub: tracked ? "Each month" : noSource
54
54
  }
55
55
  ) })
@@ -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 each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted. (MRR)`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)\"\n sub={tracked ? 'Each month times 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What one paying merchant pays in a month, on average. (ARPA)\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\n </>\n )}\n {part === 'merchant' && (\n <>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n {extra}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"AAoDQ,mBAMM,KANN;AApDR,SAAS,kBAAkB;AAG3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAMtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,UAAU,qBAAqB;AAAA,MACpD,WAAU;AAAA,MAET;AAAA,iBAAS,WACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,cAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,UAER,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,UACzC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,eAAe;AAAA;AAAA,UAChC,GACF;AAAA,WACE;AAAA,QAED,SAAS,cACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,cACtG,OAAO,QAAQ,WAAW;AAAA,cAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,UACzB,GACF;AAAA,UACK;AAAA,WACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { ReactNode } from 'react';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * The headline figures, in two groups that are never added together: what\n * merchants sold (`merchant`), and what Olive itself earns from plans (`olive`).\n */\nexport function RevenueKpis({\n part,\n extra,\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n part: 'merchant' | 'olive';\n /** More cards for the merchant group, in the same row. */\n extra?: ReactNode;\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without the plan payments connected the API sends 0s; say so rather than show a zero.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not set up yet';\n const noSource = 'The plan payments are not connected yet';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section\n aria-label={part === 'olive' ? 'What Olive earns' : 'What merchants sold'}\n className=\"adm-grid\"\n >\n {part === 'olive' && (\n <>\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted.`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12.\"\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 </>\n )}\n </section>\n );\n}\n"],"mappings":"AAoDQ,mBAMM,KANN;AApDR,SAAS,kBAAkB;AAG3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAMtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,UAAU,qBAAqB;AAAA,MACpD,WAAU;AAAA,MAET;AAAA,iBAAS,WACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,cAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,UAER,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,UACzC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,eAAe;AAAA;AAAA,UAChC,GACF;AAAA,WACE;AAAA,QAED,SAAS,cACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,cACtG,OAAO,QAAQ,WAAW;AAAA,cAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,UACzB,GACF;AAAA,UACK;AAAA,WACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -118,7 +118,7 @@ function RevenueOverview({
118
118
  decimals: 2
119
119
  }
120
120
  ),
121
- hint: "What Olive keeps from payouts, settlements and plan payments, after costs."
121
+ hint: "What Olive keeps from payouts and plan payments, after costs."
122
122
  }
123
123
  ) }) }),
124
124
  recurringTracked && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n channels.length > 0 && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n )\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n\n <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts, settlements and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n channels.length > 0 && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n )\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n\n <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -73,7 +73,7 @@ function SubscriptionsView({
73
73
  label: "Plan income each month",
74
74
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notSet,
75
75
  sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why,
76
- hint: "What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)"
76
+ hint: "What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted."
77
77
  }
78
78
  ) }),
79
79
  /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
@@ -82,7 +82,7 @@ function SubscriptionsView({
82
82
  label: "Plan income over a year",
83
83
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notSet,
84
84
  sub: tracked ? "Each month times 12" : why,
85
- hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)"
85
+ hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12."
86
86
  }
87
87
  ) }),
88
88
  /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
@@ -91,7 +91,7 @@ function SubscriptionsView({
91
91
  label: "Average per paying merchant",
92
92
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notSet,
93
93
  sub: tracked ? "Each month" : why,
94
- hint: "What one paying merchant pays Olive in a month, on average. (ARPA)"
94
+ hint: "What one paying merchant pays Olive in a month, on average."
95
95
  }
96
96
  ) }),
97
97
  /* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/subscriptions-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport {\n platformAdminApi,\n type PlatformRevenue,\n type SubscriptionPaymentRow,\n} from '@withpotter/lib/admin-api';\nimport { useSubscriptionPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n PURPOSE_WORDS,\n STATUS_WORDS,\n SUBSCRIPTION_METHOD_WORDS,\n downloadName,\n minorToMoney,\n word,\n} from './ledger-model';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { PlanPaymentDrawer } from './plan-payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', status: '', method: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\nconst STATUSES = [\n { value: '', label: 'Every result' },\n { value: 'succeeded', label: 'Paid' },\n { value: 'failed', label: 'Failed' },\n { value: 'pending', label: 'Waiting' },\n];\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'gateway', label: 'Card or bank' },\n { value: 'wallet', label: 'Wallet' },\n { value: 'manual', label: 'Recorded by hand' },\n];\n\nconst tone = (status: string) =>\n status === 'succeeded' ? styles.statusGood : status === 'failed' ? styles.statusBad : styles.statusWait;\n\n/** What merchants pay Olive for their plan: the income figures, who pays what, and every plan payment. */\nexport function SubscriptionsView({\n startDate,\n endDate,\n revenue,\n}: {\n startDate?: string;\n endDate?: string;\n revenue: PlatformRevenue;\n}) {\n const { values, page, set } = useUrlFilters('sp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<SubscriptionPaymentRow | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, status: values.status, method: values.method, q: values.q, page, limit: PAGE_SIZE }),\n [startDate, endDate, values, page],\n );\n\n const { recurring } = revenue;\n const tracked = recurring.tracked !== false;\n const currency = recurring.currency ?? revenue.currency ?? 'NGN';\n const money = moneyIn(currency);\n const money2 = moneyIn(currency, 2);\n const notSet = 'Not set up yet';\n const why = 'The plan payments have not been connected to this page yet.';\n const payments = useSubscriptionPayments(filters);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants.\n </p>\n\n <section aria-label=\"Plan income\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={tracked ? <CountUp value={recurring.mrr} format={money} /> : notSet}\n sub={tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why}\n hint=\"What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)\"\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={tracked ? <CountUp value={recurring.arr} format={money} /> : notSet}\n sub={tracked ? 'Each month times 12' : why}\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)\"\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={tracked ? <CountUp value={recurring.arpa} format={money2} /> : notSet}\n sub={tracked ? 'Each month' : why}\n hint=\"What one paying merchant pays Olive in a month, on average. (ARPA)\"\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Joined and left\"\n value={tracked ? `${integer.format(recurring.newInRange)} joined` : notSet}\n sub={tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why}\n hint=\"Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off.\"\n />\n </Reveal>\n </section>\n\n {tracked && recurring.planBreakdown.length > 0 ? (\n <>\n <h2 className={styles.sectionTitle}>Who pays what</h2>\n <p className={styles.sectionNote}>How the monthly plan income splits across plans.</p>\n <PlanMixPanel planBreakdown={recurring.planBreakdown} money={money} caption={`Each month · ${currency}`} />\n </>\n ) : null}\n\n <h2 className={styles.sectionTitle}>Every plan payment</h2>\n {payments.isError ? (\n <div className={styles.notice}>\n Plan payments are not available on this box yet. Once the subscription module is connected, every payment will\n be listed here.\n </div>\n ) : payments.isLoading || !payments.data ? (\n <AdmLoading label=\"Loading plan payments…\" />\n ) : (\n <>\n <div className={styles.toolbar}>\n <Filters label=\"Filter plan payments\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a merchant's name or email, or a reference\"\n label=\"Search plan payments\"\n />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} options={METHODS} label=\"Way of paying\" />\n </Filters>\n <DownloadButton\n filename={downloadName('plan-payments', startDate, endDate)}\n load={() => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {payments.data.items.length === 0 ? (\n <EmptyNote className={table.empty}>No plan payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={860}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Merchant</Th>\n <Th>Plan</Th>\n <Th>For</Th>\n <Th>How</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n </tr>\n }\n >\n {payments.data.items.map((row) => (\n <PlanPaymentRow key={row.id} row={row} onOpen={setOpen} />\n ))}\n </TableShell>\n )}\n <Pagination\n page={page}\n totalPages={Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1)}\n total={payments.data.total}\n noun=\"plan payments\"\n onPage={(next) => set({ page: next })}\n />\n <PlanPaymentDrawer row={open} onClose={() => setOpen(null)} />\n </>\n )}\n </div>\n );\n}\n\nfunction PlanPaymentRow({ row, onOpen }: { row: SubscriptionPaymentRow; onOpen: (row: SubscriptionPaymentRow) => void }) {\n const when = row.paidAt ?? row.createdAt;\n\n return (\n <TableRow onClick={() => onOpen(row)}>\n <td className={table.num}>\n {new Date(when).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.merchantName || 'Unknown merchant'}</span>\n {row.merchantEmail ? <span className={styles.whoSub}>{row.merchantEmail}</span> : null}\n </span>\n </td>\n <td className={table.name}>{row.plan.charAt(0).toUpperCase() + row.plan.slice(1)}</td>\n <td>{word(PURPOSE_WORDS, row.purpose)}</td>\n <td>{word(SUBSCRIPTION_METHOD_WORDS, row.method)}</td>\n <td>\n <span className={styles.who}>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n {row.status === 'failed' && row.failureReason ? <span className={styles.whoSub}>{row.failureReason}</span> : null}\n </span>\n </td>\n <td className={table.numStrong}>{minorToMoney(row.amountKobo, 'NGN')}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/subscriptions-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport {\n platformAdminApi,\n type PlatformRevenue,\n type SubscriptionPaymentRow,\n} from '@withpotter/lib/admin-api';\nimport { useSubscriptionPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n PURPOSE_WORDS,\n STATUS_WORDS,\n SUBSCRIPTION_METHOD_WORDS,\n downloadName,\n minorToMoney,\n word,\n} from './ledger-model';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { PlanPaymentDrawer } from './plan-payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', status: '', method: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\nconst STATUSES = [\n { value: '', label: 'Every result' },\n { value: 'succeeded', label: 'Paid' },\n { value: 'failed', label: 'Failed' },\n { value: 'pending', label: 'Waiting' },\n];\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'gateway', label: 'Card or bank' },\n { value: 'wallet', label: 'Wallet' },\n { value: 'manual', label: 'Recorded by hand' },\n];\n\nconst tone = (status: string) =>\n status === 'succeeded' ? styles.statusGood : status === 'failed' ? styles.statusBad : styles.statusWait;\n\n/** What merchants pay Olive for their plan: the income figures, who pays what, and every plan payment. */\nexport function SubscriptionsView({\n startDate,\n endDate,\n revenue,\n}: {\n startDate?: string;\n endDate?: string;\n revenue: PlatformRevenue;\n}) {\n const { values, page, set } = useUrlFilters('sp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<SubscriptionPaymentRow | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, status: values.status, method: values.method, q: values.q, page, limit: PAGE_SIZE }),\n [startDate, endDate, values, page],\n );\n\n const { recurring } = revenue;\n const tracked = recurring.tracked !== false;\n const currency = recurring.currency ?? revenue.currency ?? 'NGN';\n const money = moneyIn(currency);\n const money2 = moneyIn(currency, 2);\n const notSet = 'Not set up yet';\n const why = 'The plan payments have not been connected to this page yet.';\n const payments = useSubscriptionPayments(filters);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants.\n </p>\n\n <section aria-label=\"Plan income\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={tracked ? <CountUp value={recurring.mrr} format={money} /> : notSet}\n sub={tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why}\n hint=\"What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted.\"\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={tracked ? <CountUp value={recurring.arr} format={money} /> : notSet}\n sub={tracked ? 'Each month times 12' : why}\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12.\"\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={tracked ? <CountUp value={recurring.arpa} format={money2} /> : notSet}\n sub={tracked ? 'Each month' : why}\n hint=\"What one paying merchant pays Olive in a month, on average.\"\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Joined and left\"\n value={tracked ? `${integer.format(recurring.newInRange)} joined` : notSet}\n sub={tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why}\n hint=\"Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off.\"\n />\n </Reveal>\n </section>\n\n {tracked && recurring.planBreakdown.length > 0 ? (\n <>\n <h2 className={styles.sectionTitle}>Who pays what</h2>\n <p className={styles.sectionNote}>How the monthly plan income splits across plans.</p>\n <PlanMixPanel planBreakdown={recurring.planBreakdown} money={money} caption={`Each month · ${currency}`} />\n </>\n ) : null}\n\n <h2 className={styles.sectionTitle}>Every plan payment</h2>\n {payments.isError ? (\n <div className={styles.notice}>\n Plan payments are not available on this box yet. Once the subscription module is connected, every payment will\n be listed here.\n </div>\n ) : payments.isLoading || !payments.data ? (\n <AdmLoading label=\"Loading plan payments…\" />\n ) : (\n <>\n <div className={styles.toolbar}>\n <Filters label=\"Filter plan payments\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a merchant's name or email, or a reference\"\n label=\"Search plan payments\"\n />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} options={METHODS} label=\"Way of paying\" />\n </Filters>\n <DownloadButton\n filename={downloadName('plan-payments', startDate, endDate)}\n load={() => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {payments.data.items.length === 0 ? (\n <EmptyNote className={table.empty}>No plan payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={860}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Merchant</Th>\n <Th>Plan</Th>\n <Th>For</Th>\n <Th>How</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n </tr>\n }\n >\n {payments.data.items.map((row) => (\n <PlanPaymentRow key={row.id} row={row} onOpen={setOpen} />\n ))}\n </TableShell>\n )}\n <Pagination\n page={page}\n totalPages={Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1)}\n total={payments.data.total}\n noun=\"plan payments\"\n onPage={(next) => set({ page: next })}\n />\n <PlanPaymentDrawer row={open} onClose={() => setOpen(null)} />\n </>\n )}\n </div>\n );\n}\n\nfunction PlanPaymentRow({ row, onOpen }: { row: SubscriptionPaymentRow; onOpen: (row: SubscriptionPaymentRow) => void }) {\n const when = row.paidAt ?? row.createdAt;\n\n return (\n <TableRow onClick={() => onOpen(row)}>\n <td className={table.num}>\n {new Date(when).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.merchantName || 'Unknown merchant'}</span>\n {row.merchantEmail ? <span className={styles.whoSub}>{row.merchantEmail}</span> : null}\n </span>\n </td>\n <td className={table.name}>{row.plan.charAt(0).toUpperCase() + row.plan.slice(1)}</td>\n <td>{word(PURPOSE_WORDS, row.purpose)}</td>\n <td>{word(SUBSCRIPTION_METHOD_WORDS, row.method)}</td>\n <td>\n <span className={styles.who}>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n {row.status === 'failed' && row.failureReason ? <span className={styles.whoSub}>{row.failureReason}</span> : null}\n </span>\n </td>\n <td className={table.numStrong}>{minorToMoney(row.amountKobo, 'NGN')}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
@@ -31,9 +31,9 @@ function TopTenantsPanel({
31
31
  head: /* @__PURE__ */ jsxs("tr", { children: [
32
32
  /* @__PURE__ */ jsx(Th, { children: "Merchant" }),
33
33
  /* @__PURE__ */ jsx(Th, { align: "right", children: "Orders" }),
34
- /* @__PURE__ */ jsx(Th, { align: "right", children: "AOV" }),
35
- /* @__PURE__ */ jsx(Th, { align: "right", children: "GMV" }),
36
- /* @__PURE__ */ jsx(Th, { align: "right", children: "% of GMV" })
34
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Average order" }),
35
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Sales" }),
36
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Share of sales" })
37
37
  ] }),
38
38
  children: tenants.map((t) => {
39
39
  const aov = t.orders > 0 ? t.gmv / t.orders : 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/top-tenants-panel.tsx"],"sourcesContent":["import Link from 'next/link';\nimport { useRouter } from 'next/navigation';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { PICK_CURRENCY, Panel, moneyIn } from '../_ui';\nimport { logoOf } from '../tenants/tenants-logo';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * Merchants ranked by GMV within one currency, with order count, AOV and\n * share of that currency's lifetime GMV.\n */\nexport function TopTenantsPanel({\n tenants,\n lifetime,\n focus,\n}: {\n tenants: PlatformRevenue['gmv']['topTenants'];\n /** Lifetime GMV in `focus`, the base for each share. */\n lifetime: number;\n /** The one currency ranked, or null when there are several and none is chosen. */\n focus: string | null;\n}) {\n const router = useRouter();\n const money = moneyIn(focus ?? '');\n\n return (\n <Panel\n title=\"Biggest merchants by sales\"\n count={tenants.length || undefined}\n caption={focus ?? undefined}\n action={<Link href=\"/admin/tenants\">View all</Link>}\n flush\n >\n {tenants.length === 0 ? (\n <EmptyNote className={styles.empty}>\n {focus ? `No ${focus} merchant revenue recorded yet.` : PICK_CURRENCY}\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={520}\n density=\"compact\"\n head={\n <tr>\n <Th>Merchant</Th>\n <Th align=\"right\">Orders</Th>\n <Th align=\"right\">AOV</Th>\n <Th align=\"right\">GMV</Th>\n <Th align=\"right\">% of GMV</Th>\n </tr>\n }\n >\n {tenants.map((t) => {\n const aov = t.orders > 0 ? t.gmv / t.orders : 0;\n const share = lifetime > 0 ? (t.gmv / lifetime) * 100 : 0;\n const logo = logoOf(t);\n return (\n <TableRow\n key={t.id}\n onClick={() => router.push(`/admin/tenants/${t.id}`)}\n >\n <td className={styles.name}>\n <span className={styles.tenant}>\n {logo && (\n <EntityAvatar\n name={t.businessName}\n src={logo}\n size={28}\n />\n )}\n <span className={styles.tenantText}>\n <Link\n href={`/admin/tenants/${t.id}`}\n title={t.businessName}\n onClick={(e) => e.stopPropagation()}\n >\n {t.businessName}\n </Link>\n {t.slug && (\n <span className={styles.slug} title={t.slug}>\n {t.slug}\n </span>\n )}\n </span>\n </span>\n </td>\n <td className={styles.num}>{integer.format(t.orders)}</td>\n <td className={styles.num}>{money(aov)}</td>\n <td className={styles.numStrong}>{money(t.gmv)}</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":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/top-tenants-panel.tsx"],"sourcesContent":["import Link from 'next/link';\nimport { useRouter } from 'next/navigation';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { PICK_CURRENCY, Panel, moneyIn } from '../_ui';\nimport { logoOf } from '../tenants/tenants-logo';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * Merchants ranked by GMV within one currency, with order count, AOV and\n * share of that currency's lifetime GMV.\n */\nexport function TopTenantsPanel({\n tenants,\n lifetime,\n focus,\n}: {\n tenants: PlatformRevenue['gmv']['topTenants'];\n /** Lifetime GMV in `focus`, the base for each share. */\n lifetime: number;\n /** The one currency ranked, or null when there are several and none is chosen. */\n focus: string | null;\n}) {\n const router = useRouter();\n const money = moneyIn(focus ?? '');\n\n return (\n <Panel\n title=\"Biggest merchants by sales\"\n count={tenants.length || undefined}\n caption={focus ?? undefined}\n action={<Link href=\"/admin/tenants\">View all</Link>}\n flush\n >\n {tenants.length === 0 ? (\n <EmptyNote className={styles.empty}>\n {focus ? `No ${focus} merchant revenue recorded yet.` : PICK_CURRENCY}\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={520}\n density=\"compact\"\n head={\n <tr>\n <Th>Merchant</Th>\n <Th align=\"right\">Orders</Th>\n <Th align=\"right\">Average order</Th>\n <Th align=\"right\">Sales</Th>\n <Th align=\"right\">Share of sales</Th>\n </tr>\n }\n >\n {tenants.map((t) => {\n const aov = t.orders > 0 ? t.gmv / t.orders : 0;\n const share = lifetime > 0 ? (t.gmv / lifetime) * 100 : 0;\n const logo = logoOf(t);\n return (\n <TableRow\n key={t.id}\n onClick={() => router.push(`/admin/tenants/${t.id}`)}\n >\n <td className={styles.name}>\n <span className={styles.tenant}>\n {logo && (\n <EntityAvatar\n name={t.businessName}\n src={logo}\n size={28}\n />\n )}\n <span className={styles.tenantText}>\n <Link\n href={`/admin/tenants/${t.id}`}\n title={t.businessName}\n onClick={(e) => e.stopPropagation()}\n >\n {t.businessName}\n </Link>\n {t.slug && (\n <span className={styles.slug} title={t.slug}>\n {t.slug}\n </span>\n )}\n </span>\n </span>\n </td>\n <td className={styles.num}>{integer.format(t.orders)}</td>\n <td className={styles.num}>{money(aov)}</td>\n <td className={styles.numStrong}>{money(t.gmv)}</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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/pages-admin",
3
- "version": "0.13.0-next.104",
3
+ "version": "0.13.0-next.105",
4
4
  "description": "Box console platform-admin routes: overview, revenue, accounts, tenants, admins, roles, audit.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": false,
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@radix-ui/react-popover": "^1.1.23",
33
33
  "@tanstack/react-query": "^5.90.20",
34
- "@withpotter/lib": "^0.13.0-next.104",
35
- "@withpotter/platform-admin": "^0.13.0-next.104",
36
- "@withpotter/ui-kit": "^0.13.0-next.104",
34
+ "@withpotter/lib": "^0.13.0-next.105",
35
+ "@withpotter/platform-admin": "^0.13.0-next.105",
36
+ "@withpotter/ui-kit": "^0.13.0-next.105",
37
37
  "lucide-react": "^0.562.0",
38
38
  "motion": "^12.42.2"
39
39
  },