@withpotter/pages-admin 0.13.0-next.82 → 0.13.0-next.85

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 (46) hide show
  1. package/dist/admin/_money/finder-format.js +1 -0
  2. package/dist/admin/_money/finder-format.js.map +1 -1
  3. package/dist/admin/_money/format.js +1 -0
  4. package/dist/admin/_money/format.js.map +1 -1
  5. package/dist/admin/_overview/attention.module.css +0 -3
  6. package/dist/admin/_overview/overdue-outstanding.js +2 -1
  7. package/dist/admin/_overview/overdue-outstanding.js.map +1 -1
  8. package/dist/admin/_ui/list-card.module.css +2 -16
  9. package/dist/admin/_ui/money-view.js +1 -0
  10. package/dist/admin/_ui/money-view.js.map +1 -1
  11. package/dist/admin/_ui/page-header.js +31 -4
  12. package/dist/admin/_ui/page-header.js.map +1 -1
  13. package/dist/admin/_ui/page-header.module.css +45 -0
  14. package/dist/admin/_ui/panel.module.css +0 -10
  15. package/dist/admin/commerce/commerce-overview.js +2 -2
  16. package/dist/admin/commerce/commerce-overview.js.map +1 -1
  17. package/dist/admin/commerce/merchant-meta.js +1 -0
  18. package/dist/admin/commerce/merchant-meta.js.map +1 -1
  19. package/dist/admin/commerce/merchant-view.js +2 -2
  20. package/dist/admin/commerce/merchant-view.js.map +1 -1
  21. package/dist/admin/feedback/prompt-operations.module.css +0 -2
  22. package/dist/admin/feedback/prompt-preview.module.css +0 -2
  23. package/dist/admin/revenue/platform-fees.js +9 -0
  24. package/dist/admin/revenue/platform-fees.js.map +1 -0
  25. package/dist/admin/revenue/revenue-kpis.js +1 -1
  26. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  27. package/dist/admin/revenue/revenue-overview.js +20 -1
  28. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  29. package/dist/admin/revenue/revenue-workspace.js +1 -1
  30. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  31. package/dist/admin/support/queue-card.module.css +1 -4
  32. package/dist/admin/tenants/[id]/commerce/format.js +2 -1
  33. package/dist/admin/tenants/[id]/commerce/format.js.map +1 -1
  34. package/dist/admin/tenants/[id]/page.js +31 -9
  35. package/dist/admin/tenants/[id]/page.js.map +1 -1
  36. package/dist/admin/tenants/[id]/tenant-layout.module.css +1 -4
  37. package/dist/admin/tenants/[id]/tenant-overview.js +3 -2
  38. package/dist/admin/tenants/[id]/tenant-overview.js.map +1 -1
  39. package/dist/admin/tenants/[id]/tenant-overview.module.css +1 -1
  40. package/dist/admin/tenants/[id]/tenant-wallet-card.js +3 -2
  41. package/dist/admin/tenants/[id]/tenant-wallet-card.js.map +1 -1
  42. package/dist/admin/tenants/[id]/use-tenant-wallet.js +23 -0
  43. package/dist/admin/tenants/[id]/use-tenant-wallet.js.map +1 -0
  44. package/dist/admin/tenants/tenants-table.js +1 -1
  45. package/dist/admin/tenants/tenants-table.js.map +1 -1
  46. package/package.json +4 -4
@@ -9,6 +9,7 @@ function compactAmount(amount, currency) {
9
9
  return new Intl.NumberFormat("en-US", {
10
10
  ...base,
11
11
  style: "currency",
12
+ currencyDisplay: "narrowSymbol",
12
13
  currency
13
14
  }).format(amount);
14
15
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_money/finder-format.ts"],"sourcesContent":["/** Formatting for the merchant finder and its quick view. */\n\n/**\n * The merchant's currency when the row carries one, flat or on the profile.\n * The tenants list does not send it today, so callers fall back to plain.\n */\nexport function currencyOf(row: object): string | null {\n const r = row as {\n currency?: string | null;\n profile?: { currency?: string | null } | null;\n };\n\n return r.currency ?? r.profile?.currency ?? null;\n}\n\n/** A short figure: \"₦1.2M\" in a currency, \"1.2M\" without one. */\nexport function compactAmount(amount: number, currency: string | null): string {\n const base = { notation: 'compact', maximumFractionDigits: 1 } as const;\n\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n ...base,\n style: 'currency',\n currency,\n }).format(amount);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return new Intl.NumberFormat('en-US', base).format(amount ?? 0);\n}\n\n/** An account number cut to its last four: \"•••• 4821\". */\nexport function maskAccount(accountNumber: string): string {\n const last = accountNumber.replace(/\\s/g, '').slice(-4);\n\n return last ? `•••• ${last}` : '–';\n}\n\n/** True when a minor-unit string holds something other than zero. */\nexport function nonZero(minor: string): boolean {\n try {\n return BigInt(minor) !== BigInt(0);\n } catch {\n return false;\n }\n}\n\n/** The wallet ID when the tenant detail sends one (it is not typed yet). */\nexport function walletIdOf(wallet: object | null | undefined): string | null {\n const id = (wallet as { id?: unknown } | null | undefined)?.id;\n\n return typeof id === 'string' && id ? id : null;\n}\n"],"mappings":"AAMO,SAAS,WAAW,KAA4B;AACrD,QAAM,IAAI;AAKV,SAAO,EAAE,YAAY,EAAE,SAAS,YAAY;AAC9C;AAGO,SAAS,cAAc,QAAgB,UAAiC;AAC7E,QAAM,OAAO,EAAE,UAAU,WAAW,uBAAuB,EAAE;AAE7D,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,GAAG;AAAA,QACH,OAAO;AAAA,QACP;AAAA,MACF,CAAC,EAAE,OAAO,MAAM;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,aAAa,SAAS,IAAI,EAAE,OAAO,UAAU,CAAC;AAChE;AAGO,SAAS,YAAY,eAA+B;AACzD,QAAM,OAAO,cAAc,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE;AAEtD,SAAO,OAAO,4BAAQ,IAAI,KAAK;AACjC;AAGO,SAAS,QAAQ,OAAwB;AAC9C,MAAI;AACF,WAAO,OAAO,KAAK,MAAM,OAAO,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,QAAkD;AAC3E,QAAM,KAAM,QAAgD;AAE5D,SAAO,OAAO,OAAO,YAAY,KAAK,KAAK;AAC7C;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_money/finder-format.ts"],"sourcesContent":["/** Formatting for the merchant finder and its quick view. */\n\n/**\n * The merchant's currency when the row carries one, flat or on the profile.\n * The tenants list does not send it today, so callers fall back to plain.\n */\nexport function currencyOf(row: object): string | null {\n const r = row as {\n currency?: string | null;\n profile?: { currency?: string | null } | null;\n };\n\n return r.currency ?? r.profile?.currency ?? null;\n}\n\n/** A short figure: \"₦1.2M\" in a currency, \"1.2M\" without one. */\nexport function compactAmount(amount: number, currency: string | null): string {\n const base = { notation: 'compact', maximumFractionDigits: 1 } as const;\n\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n ...base,\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n }).format(amount);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return new Intl.NumberFormat('en-US', base).format(amount ?? 0);\n}\n\n/** An account number cut to its last four: \"•••• 4821\". */\nexport function maskAccount(accountNumber: string): string {\n const last = accountNumber.replace(/\\s/g, '').slice(-4);\n\n return last ? `•••• ${last}` : '–';\n}\n\n/** True when a minor-unit string holds something other than zero. */\nexport function nonZero(minor: string): boolean {\n try {\n return BigInt(minor) !== BigInt(0);\n } catch {\n return false;\n }\n}\n\n/** The wallet ID when the tenant detail sends one (it is not typed yet). */\nexport function walletIdOf(wallet: object | null | undefined): string | null {\n const id = (wallet as { id?: unknown } | null | undefined)?.id;\n\n return typeof id === 'string' && id ? id : null;\n}\n"],"mappings":"AAMO,SAAS,WAAW,KAA4B;AACrD,QAAM,IAAI;AAKV,SAAO,EAAE,YAAY,EAAE,SAAS,YAAY;AAC9C;AAGO,SAAS,cAAc,QAAgB,UAAiC;AAC7E,QAAM,OAAO,EAAE,UAAU,WAAW,uBAAuB,EAAE;AAE7D,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,GAAG;AAAA,QACH,OAAO;AAAA,QACP,iBAAiB;AAAA,QACjB;AAAA,MACF,CAAC,EAAE,OAAO,MAAM;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,aAAa,SAAS,IAAI,EAAE,OAAO,UAAU,CAAC;AAChE;AAGO,SAAS,YAAY,eAA+B;AACzD,QAAM,OAAO,cAAc,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE;AAEtD,SAAO,OAAO,4BAAQ,IAAI,KAAK;AACjC;AAGO,SAAS,QAAQ,OAAwB;AAC9C,MAAI;AACF,WAAO,OAAO,KAAK,MAAM,OAAO,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,QAAkD;AAC3E,QAAM,KAAM,QAAgD;AAE5D,SAAO,OAAO,OAAO,YAAY,KAAK,KAAK;AAC7C;","names":[]}
@@ -3,6 +3,7 @@ function money(amount, currency) {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
+ currencyDisplay: "narrowSymbol",
6
7
  currency: currency || "NGN",
7
8
  minimumFractionDigits: 2,
8
9
  maximumFractionDigits: 2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_money/format.ts"],"sourcesContent":["import { formatMinor } from '@withpotter/lib/money-minor';\n\n/** Money and time formatting for the money screens. Amounts are major units. */\n\nexport function money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency || 'NGN',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount ?? 0);\n } catch {\n return `${currency} ${(amount ?? 0).toFixed(2)}`;\n }\n}\n\n/**\n * Minor units held as a string, shown in the currency (\"150000\" NGN → ₦1,500.00,\n * \"1500\" JPY → ¥1,500). Split at the currency's own exponent, never ÷ 100.\n */\nexport function moneyMinor(minor: string, currency: string): string {\n return /^-?\\d+$/.test(minor)\n ? formatMinor(minor, currency || 'NGN')\n : `${minor} ${currency}`;\n}\n\nexport function when(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n return d.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function day(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n // \"15 Oct 2026\" everywhere, whatever the browser's locale.\n return d.toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"3h\", \"2d\": how long a record has been open. */\nexport function age(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n const h = Math.floor(ms / 3_600_000);\n\n if (h < 1) return `${Math.max(1, Math.floor(ms / 60_000))}m`;\n\n if (h < 48) return `${h}h`;\n\n return `${Math.floor(h / 24)}d`;\n}\n\n/** Keep the start and end of a long ID: \"8f2c1a…9b0d\". */\nexport function middle(id: string, keep = 6): string {\n return id.length <= keep * 2 + 1\n ? id\n : `${id.slice(0, keep)}…${id.slice(-4)}`;\n}\n\n/** An API error's own words when it has some, a plain line when not. */\nexport function errorText(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n\n return fallback;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAIrB,SAAS,MAAM,QAAgB,UAA0B;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,UAAU,CAAC;AAAA,EACvB,QAAQ;AACN,WAAO,GAAG,QAAQ,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC;AAAA,EAChD;AACF;AAMO,SAAS,WAAW,OAAe,UAA0B;AAClE,SAAO,UAAU,KAAK,KAAK,IACvB,YAAY,OAAO,YAAY,KAAK,IACpC,GAAG,KAAK,IAAI,QAAQ;AAC1B;AAEO,SAAS,KAAK,KAAwC;AAC3D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAEtC,SAAO,EAAE,eAAe,QAAW;AAAA,IACjC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;AAEO,SAAS,IAAI,KAAwC;AAC1D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAGtC,SAAO,EAAE,mBAAmB,SAAS;AAAA,IACnC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC;AACH;AAGO,SAAS,IAAI,KAAa,MAAM,KAAK,IAAI,GAAW;AACzD,QAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,CAAC;AACpD,QAAM,IAAI,KAAK,MAAM,KAAK,IAAS;AAEnC,MAAI,IAAI,EAAG,QAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC,CAAC;AAEzD,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AAEvB,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B;AAGO,SAAS,OAAO,IAAY,OAAO,GAAW;AACnD,SAAO,GAAG,UAAU,OAAO,IAAI,IAC3B,KACA,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,SAAI,GAAG,MAAM,EAAE,CAAC;AAC1C;AAGO,SAAS,UAAU,OAAgB,UAA0B;AAClE,MAAI,iBAAiB,SAAS,MAAM,QAAS,QAAO,MAAM;AAE1D,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_money/format.ts"],"sourcesContent":["import { formatMinor } from '@withpotter/lib/money-minor';\n\n/** Money and time formatting for the money screens. Amounts are major units. */\n\nexport function money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency: currency || 'NGN',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount ?? 0);\n } catch {\n return `${currency} ${(amount ?? 0).toFixed(2)}`;\n }\n}\n\n/**\n * Minor units held as a string, shown in the currency (\"150000\" NGN → ₦1,500.00,\n * \"1500\" JPY → ¥1,500). Split at the currency's own exponent, never ÷ 100.\n */\nexport function moneyMinor(minor: string, currency: string): string {\n return /^-?\\d+$/.test(minor)\n ? formatMinor(minor, currency || 'NGN')\n : `${minor} ${currency}`;\n}\n\nexport function when(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n return d.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function day(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n // \"15 Oct 2026\" everywhere, whatever the browser's locale.\n return d.toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"3h\", \"2d\": how long a record has been open. */\nexport function age(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n const h = Math.floor(ms / 3_600_000);\n\n if (h < 1) return `${Math.max(1, Math.floor(ms / 60_000))}m`;\n\n if (h < 48) return `${h}h`;\n\n return `${Math.floor(h / 24)}d`;\n}\n\n/** Keep the start and end of a long ID: \"8f2c1a…9b0d\". */\nexport function middle(id: string, keep = 6): string {\n return id.length <= keep * 2 + 1\n ? id\n : `${id.slice(0, keep)}…${id.slice(-4)}`;\n}\n\n/** An API error's own words when it has some, a plain line when not. */\nexport function errorText(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n\n return fallback;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAIrB,SAAS,MAAM,QAAgB,UAA0B;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,UAAU,CAAC;AAAA,EACvB,QAAQ;AACN,WAAO,GAAG,QAAQ,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC;AAAA,EAChD;AACF;AAMO,SAAS,WAAW,OAAe,UAA0B;AAClE,SAAO,UAAU,KAAK,KAAK,IACvB,YAAY,OAAO,YAAY,KAAK,IACpC,GAAG,KAAK,IAAI,QAAQ;AAC1B;AAEO,SAAS,KAAK,KAAwC;AAC3D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAEtC,SAAO,EAAE,eAAe,QAAW;AAAA,IACjC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;AAEO,SAAS,IAAI,KAAwC;AAC1D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAGtC,SAAO,EAAE,mBAAmB,SAAS;AAAA,IACnC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC;AACH;AAGO,SAAS,IAAI,KAAa,MAAM,KAAK,IAAI,GAAW;AACzD,QAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,CAAC;AACpD,QAAM,IAAI,KAAK,MAAM,KAAK,IAAS;AAEnC,MAAI,IAAI,EAAG,QAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC,CAAC;AAEzD,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AAEvB,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B;AAGO,SAAS,OAAO,IAAY,OAAO,GAAW;AACnD,SAAO,GAAG,UAAU,OAAO,IAAI,IAC3B,KACA,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,SAAI,GAAG,MAAM,EAAE,CAAC;AAC1C;AAGO,SAAS,UAAU,OAAgB,UAA0B;AAClE,MAAI,iBAAiB,SAAS,MAAM,QAAS,QAAO,MAAM;AAE1D,SAAO;AACT;","names":[]}
@@ -1,9 +1,6 @@
1
1
  /* Attention panel: filter per kind, rows scroll inside the card. */
2
2
 
3
3
  .filters {
4
- position: sticky;
5
- top: 0;
6
- z-index: 1;
7
4
  display: flex;
8
5
  gap: 4px;
9
6
  padding: 0 20px 12px;
@@ -3,7 +3,8 @@ function inCurrency(value, code) {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
- currency: code || "USD",
6
+ currencyDisplay: "narrowSymbol",
7
+ currency: code || "NGN",
7
8
  maximumFractionDigits: 0
8
9
  }).format(value);
9
10
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_overview/overdue-outstanding.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { totalsByCurrency } from '../_ui/currency-filter-model';\n\ntype Overdue = PlatformCockpit['attention']['overdueInvoices'];\n\n/** A main-unit amount in its own currency: 120000 NGN → \"NGN 120,000\". */\nexport function inCurrency(value: number, code: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code || 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n } catch {\n return `${code} ${Math.round(value)}`;\n }\n}\n\n/**\n * What the overdue invoices still owe, one amount per currency, or null when\n * it cannot be said honestly. Amounts are never added across currencies.\n *\n * The engine sends `totalByCurrency` (every overdue invoice, per currency);\n * `selected` narrows it to the currency filter's choice. An engine without it\n * only has the sample: when the sample IS every overdue invoice it is added\n * up per currency; when there are more, nothing is shown rather than a\n * partial figure. The mixed `total` is never shown.\n */\nexport function overdueOutstanding(\n overdue: Overdue,\n selected = '',\n): string | null {\n if (!overdue.count) return null;\n\n let totals: Array<{ currency: string; amount: number }>;\n if (overdue.totalByCurrency) {\n totals = overdue.totalByCurrency.filter(\n (t) => !selected || t.currency === selected,\n );\n if (!totals.length) return selected ? `No ${selected} outstanding` : null;\n } else {\n if (overdue.sample.length < overdue.count) return null;\n totals = totalsByCurrency(\n overdue.sample,\n (i) => i.currency,\n (i) => i.amountDue,\n ).filter((t) => !selected || t.currency === selected);\n }\n\n const lines = totals.map((t) => inCurrency(t.amount, t.currency));\n\n return lines.length ? `${lines.join(' · ')} outstanding` : null;\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AAK1B,SAAS,WAAW,OAAe,MAAsB;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,UAAU,QAAQ;AAAA,MAClB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO,GAAG,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AACF;AAYO,SAAS,mBACd,SACA,WAAW,IACI;AACf,MAAI,CAAC,QAAQ,MAAO,QAAO;AAE3B,MAAI;AACJ,MAAI,QAAQ,iBAAiB;AAC3B,aAAS,QAAQ,gBAAgB;AAAA,MAC/B,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa;AAAA,IACrC;AACA,QAAI,CAAC,OAAO,OAAQ,QAAO,WAAW,MAAM,QAAQ,iBAAiB;AAAA,EACvE,OAAO;AACL,QAAI,QAAQ,OAAO,SAAS,QAAQ,MAAO,QAAO;AAClD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,CAAC,MAAM,EAAE;AAAA,MACT,CAAC,MAAM,EAAE;AAAA,IACX,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AAAA,EACtD;AAEA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAEhE,SAAO,MAAM,SAAS,GAAG,MAAM,KAAK,QAAK,CAAC,iBAAiB;AAC7D;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_overview/overdue-outstanding.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { totalsByCurrency } from '../_ui/currency-filter-model';\n\ntype Overdue = PlatformCockpit['attention']['overdueInvoices'];\n\n/** A main-unit amount in its own currency: 120000 NGN → \"₦120,000\". */\nexport function inCurrency(value: number, code: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency: code || 'NGN',\n maximumFractionDigits: 0,\n }).format(value);\n } catch {\n return `${code} ${Math.round(value)}`;\n }\n}\n\n/**\n * What the overdue invoices still owe, one amount per currency, or null when\n * it cannot be said honestly. Amounts are never added across currencies.\n *\n * The engine sends `totalByCurrency` (every overdue invoice, per currency);\n * `selected` narrows it to the currency filter's choice. An engine without it\n * only has the sample: when the sample IS every overdue invoice it is added\n * up per currency; when there are more, nothing is shown rather than a\n * partial figure. The mixed `total` is never shown.\n */\nexport function overdueOutstanding(\n overdue: Overdue,\n selected = '',\n): string | null {\n if (!overdue.count) return null;\n\n let totals: Array<{ currency: string; amount: number }>;\n if (overdue.totalByCurrency) {\n totals = overdue.totalByCurrency.filter(\n (t) => !selected || t.currency === selected,\n );\n if (!totals.length) return selected ? `No ${selected} outstanding` : null;\n } else {\n if (overdue.sample.length < overdue.count) return null;\n totals = totalsByCurrency(\n overdue.sample,\n (i) => i.currency,\n (i) => i.amountDue,\n ).filter((t) => !selected || t.currency === selected);\n }\n\n const lines = totals.map((t) => inCurrency(t.amount, t.currency));\n\n return lines.length ? `${lines.join(' · ')} outstanding` : null;\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AAK1B,SAAS,WAAW,OAAe,MAAsB;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,UAAU,QAAQ;AAAA,MAClB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO,GAAG,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AACF;AAYO,SAAS,mBACd,SACA,WAAW,IACI;AACf,MAAI,CAAC,QAAQ,MAAO,QAAO;AAE3B,MAAI;AACJ,MAAI,QAAQ,iBAAiB;AAC3B,aAAS,QAAQ,gBAAgB;AAAA,MAC/B,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa;AAAA,IACrC;AACA,QAAI,CAAC,OAAO,OAAQ,QAAO,WAAW,MAAM,QAAQ,iBAAiB;AAAA,EACvE,OAAO;AACL,QAAI,QAAQ,OAAO,SAAS,QAAQ,MAAO,QAAO;AAClD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,CAAC,MAAM,EAAE;AAAA,MACT,CAAC,MAAM,EAAE;AAAA,IACX,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AAAA,EACtD;AAEA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAEhE,SAAO,MAAM,SAAS,GAAG,MAAM,KAAK,QAAK,CAAC,iBAAiB;AAC7D;","names":[]}
@@ -31,23 +31,13 @@
31
31
  color: var(--text-secondary);
32
32
  }
33
33
 
34
- /* The table's own scroller holds both axes, so the header can stick to it. */
34
+ /* The list grows with its rows; only a wide table scrolls, sideways. */
35
35
  .card div:has(> table),
36
36
  .fill div:has(> table) {
37
- max-height: calc(var(--adm-panel-h) * 1.4);
38
- overflow: auto;
39
- overscroll-behavior: contain;
37
+ overflow-x: auto;
40
38
  scrollbar-width: thin;
41
39
  }
42
40
 
43
- /* Inside a Panel: the table fills the body and scrolls on its own. */
44
- .fill,
45
- .fill > div,
46
- .fill div:has(> table) {
47
- height: 100%;
48
- max-height: 100%;
49
- }
50
-
51
41
  .note {
52
42
  margin: 0;
53
43
  padding: 0 20px;
@@ -55,12 +45,8 @@
55
45
 
56
46
  .card thead th,
57
47
  .fill thead th {
58
- position: sticky;
59
- top: 0;
60
- z-index: 1;
61
48
  white-space: nowrap;
62
49
  background: var(--bg-base);
63
- box-shadow: inset 0 -1px 0 var(--border-subtle);
64
50
  }
65
51
 
66
52
  .footer {
@@ -4,6 +4,7 @@ function formatMoney(amount, currency, decimals = 0) {
4
4
  try {
5
5
  return new Intl.NumberFormat("en-US", {
6
6
  style: "currency",
7
+ currencyDisplay: "narrowSymbol",
7
8
  currency,
8
9
  maximumFractionDigits: decimals
9
10
  }).format(amount);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"NGN 120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"₦120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}
@@ -1,6 +1,33 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as Popover from "@radix-ui/react-popover";
4
+ import { Info } from "lucide-react";
5
+ import styles from "./page-header.module.css";
3
6
  import { useInWorkspaceTab } from "./workspace-tab";
7
+ function PageInfo({ title, text }) {
8
+ return /* @__PURE__ */ jsx("span", { className: `adm-page-desc ${styles.info}`, children: /* @__PURE__ */ jsxs(Popover.Root, { children: [
9
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
10
+ "button",
11
+ {
12
+ type: "button",
13
+ className: styles.trigger,
14
+ "aria-label": `About ${title}`,
15
+ children: /* @__PURE__ */ jsx(Info, { size: 16, "aria-hidden": true })
16
+ }
17
+ ) }),
18
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
19
+ Popover.Content,
20
+ {
21
+ side: "bottom",
22
+ align: "start",
23
+ sideOffset: 6,
24
+ collisionPadding: 16,
25
+ className: styles.popover,
26
+ children: text
27
+ }
28
+ ) })
29
+ ] }) });
30
+ }
4
31
  function PageHeader({
5
32
  title,
6
33
  description,
@@ -9,15 +36,15 @@ function PageHeader({
9
36
  if (useInWorkspaceTab()) {
10
37
  if (!description && !actions) return null;
11
38
  return /* @__PURE__ */ jsxs("div", { className: "adm-page-header adm-tab-lead", children: [
12
- description && /* @__PURE__ */ jsx("p", { className: "adm-page-desc", children: description }),
39
+ description && /* @__PURE__ */ jsx(PageInfo, { title, text: description }),
13
40
  actions && /* @__PURE__ */ jsx("div", { className: "adm-page-actions", children: actions })
14
41
  ] });
15
42
  }
16
43
  return /* @__PURE__ */ jsxs("div", { className: "adm-page-header", children: [
17
- /* @__PURE__ */ jsx("div", { className: "adm-page-lead", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
44
+ /* @__PURE__ */ jsxs("div", { className: "adm-page-lead", children: [
18
45
  /* @__PURE__ */ jsx("h1", { className: "adm-page-title", children: title }),
19
- description && /* @__PURE__ */ jsx("p", { className: "adm-page-desc", children: description })
20
- ] }) }),
46
+ description && /* @__PURE__ */ jsx(PageInfo, { title, text: description })
47
+ ] }),
21
48
  actions && /* @__PURE__ */ jsx("div", { className: "adm-page-actions", children: actions })
22
49
  ] });
23
50
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/page-header.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport type { Tone } from '../_fx';\nimport { useInWorkspaceTab } from './workspace-tab';\n\n/**\n * Standard page header: title, one line of context, actions on the right.\n * No icon: the nav already says where you are. `icon`/`tone` are accepted\n * and ignored so existing callers compile.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string;\n description?: string;\n actions?: ReactNode;\n icon?: LucideIcon;\n tone?: Tone;\n}) {\n // Inside a workspace tab the tab is the title: keep the context and actions.\n if (useInWorkspaceTab()) {\n if (!description && !actions) return null;\n return (\n <div className=\"adm-page-header adm-tab-lead\">\n {description && <p className=\"adm-page-desc\">{description}</p>}\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"adm-page-header\">\n <div className=\"adm-page-lead\">\n <div style={{ minWidth: 0 }}>\n <h1 className=\"adm-page-title\">{title}</h1>\n {description && <p className=\"adm-page-desc\">{description}</p>}\n </div>\n </div>\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n}\n"],"mappings":";AA2BM,SACkB,KADlB;AAtBN,SAAS,yBAAyB;AAO3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAMG;AAED,MAAI,kBAAkB,GAAG;AACvB,QAAI,CAAC,eAAe,CAAC,QAAS,QAAO;AACrC,WACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,qBAAe,oBAAC,OAAE,WAAU,iBAAiB,uBAAY;AAAA,MACzD,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,OACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,wBAAC,SAAI,WAAU,iBACb,+BAAC,SAAI,OAAO,EAAE,UAAU,EAAE,GACxB;AAAA,0BAAC,QAAG,WAAU,kBAAkB,iBAAM;AAAA,MACrC,eAAe,oBAAC,OAAE,WAAU,iBAAiB,uBAAY;AAAA,OAC5D,GACF;AAAA,IACC,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,KACzD;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/page-header.tsx"],"sourcesContent":["'use client';\n\nimport * as Popover from '@radix-ui/react-popover';\nimport { Info } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport type { Tone } from '../_fx';\nimport styles from './page-header.module.css';\nimport { useInWorkspaceTab } from './workspace-tab';\n\n/**\n * The page's explanation: an info icon with the text in a popover, never a\n * line under the title. It keeps the `adm-page-desc` class as a global hook so\n * an app that draws its own info icon can hide this one with CSS.\n */\nfunction PageInfo({ title, text }: { title: string; text: string }) {\n return (\n <span className={`adm-page-desc ${styles.info}`}>\n <Popover.Root>\n <Popover.Trigger asChild>\n <button\n type=\"button\"\n className={styles.trigger}\n aria-label={`About ${title}`}\n >\n <Info size={16} aria-hidden />\n </button>\n </Popover.Trigger>\n <Popover.Portal>\n <Popover.Content\n side=\"bottom\"\n align=\"start\"\n sideOffset={6}\n collisionPadding={16}\n className={styles.popover}\n >\n {text}\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n </span>\n );\n}\n\n/**\n * Standard page header: title, an info icon for context, actions on the right.\n * No icon: the nav already says where you are. `icon`/`tone` are accepted\n * and ignored so existing callers compile.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string;\n description?: string;\n actions?: ReactNode;\n icon?: LucideIcon;\n tone?: Tone;\n}) {\n // Inside a workspace tab the tab is the title: keep the context and actions.\n if (useInWorkspaceTab()) {\n if (!description && !actions) return null;\n return (\n <div className=\"adm-page-header adm-tab-lead\">\n {description && <PageInfo title={title} text={description} />}\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"adm-page-header\">\n <div className=\"adm-page-lead\">\n <h1 className=\"adm-page-title\">{title}</h1>\n {description && <PageInfo title={title} text={description} />}\n </div>\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n}\n"],"mappings":";AAkBM,SAOM,KAPN;AAhBN,YAAY,aAAa;AACzB,SAAS,YAAY;AAIrB,OAAO,YAAY;AACnB,SAAS,yBAAyB;AAOlC,SAAS,SAAS,EAAE,OAAO,KAAK,GAAoC;AAClE,SACE,oBAAC,UAAK,WAAW,iBAAiB,OAAO,IAAI,IAC3C,+BAAC,QAAQ,MAAR,EACC;AAAA,wBAAC,QAAQ,SAAR,EAAgB,SAAO,MACtB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,cAAY,SAAS,KAAK;AAAA,QAE1B,8BAAC,QAAK,MAAM,IAAI,eAAW,MAAC;AAAA;AAAA,IAC9B,GACF;AAAA,IACA,oBAAC,QAAQ,QAAR,EACC;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,YAAY;AAAA,QACZ,kBAAkB;AAAA,QAClB,WAAW,OAAO;AAAA,QAEjB;AAAA;AAAA,IACH,GACF;AAAA,KACF,GACF;AAEJ;AAOO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAMG;AAED,MAAI,kBAAkB,GAAG;AACvB,QAAI,CAAC,eAAe,CAAC,QAAS,QAAO;AACrC,WACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,qBAAe,oBAAC,YAAS,OAAc,MAAM,aAAa;AAAA,MAC1D,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,OACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,SAAI,WAAU,iBACb;AAAA,0BAAC,QAAG,WAAU,kBAAkB,iBAAM;AAAA,MACrC,eAAe,oBAAC,YAAS,OAAc,MAAM,aAAa;AAAA,OAC7D;AAAA,IACC,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,KACzD;AAEJ;","names":[]}
@@ -0,0 +1,45 @@
1
+ /* The page description as an info icon with a popover. */
2
+ .info {
3
+ display: inline-flex;
4
+ flex-shrink: 0;
5
+ margin: 0;
6
+ }
7
+
8
+ .trigger {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ width: 24px;
13
+ height: 24px;
14
+ padding: 0;
15
+ border: 0;
16
+ border-radius: 6px;
17
+ background: transparent;
18
+ color: var(--text-tertiary);
19
+ cursor: pointer;
20
+ transition: background var(--adm-fast);
21
+ }
22
+
23
+ .trigger:hover,
24
+ .trigger[data-state='open'] {
25
+ background: var(--adm-hover);
26
+ color: var(--text-primary);
27
+ }
28
+
29
+ .trigger:focus-visible {
30
+ outline: 2px solid var(--accent-primary);
31
+ outline-offset: 2px;
32
+ }
33
+
34
+ .popover {
35
+ z-index: 70;
36
+ width: min(320px, calc(100vw - 32px));
37
+ padding: 12px;
38
+ border: 1px solid var(--border-subtle);
39
+ border-radius: var(--adm-card-radius);
40
+ background: var(--bg-base);
41
+ box-shadow: var(--adm-shadow-pop);
42
+ font-size: 13px;
43
+ line-height: 20px;
44
+ color: var(--text-secondary);
45
+ }
@@ -75,17 +75,7 @@
75
75
  padding: 0 0 24px;
76
76
  }
77
77
 
78
- /* A table's own x-scroll wrapper would capture the sticky header; let the
79
- panel body be the one scroller so column headers stay pinned. */
80
- .body div:has(> table) {
81
- overflow: visible;
82
- }
83
-
84
- /* Sticky column headers when a table scrolls inside the card. */
85
78
  .body thead th {
86
- position: sticky;
87
- top: 0;
88
- z-index: 1;
89
79
  background: var(--bg-base);
90
80
  }
91
81
 
@@ -70,7 +70,7 @@ function CommerceOverview({
70
70
  previousOrdersTrend: [],
71
71
  compare: false,
72
72
  span,
73
- money: moneyIn(focus ?? "USD")
73
+ money: moneyIn(focus ?? "NGN")
74
74
  }
75
75
  ) : pick }) }) }),
76
76
  /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
@@ -79,7 +79,7 @@ function CommerceOverview({
79
79
  {
80
80
  rows: rowFor(data?.topTenantsByCurrency, focus)?.tenants ?? [],
81
81
  loading,
82
- money: moneyIn(focus ?? "USD"),
82
+ money: moneyIn(focus ?? "NGN"),
83
83
  caption,
84
84
  onOpen: onOpenMerchant
85
85
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/commerce/commerce-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { Button, ErrorBanner } from '@withpotter/ui-kit';\nimport { usePlatformCockpit } from '../hooks';\nimport {\n GmvOrdersChart,\n PICK_CURRENCY,\n Panel,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rowFor,\n toTrend,\n type DateRange,\n} from '../_ui';\nimport { OverviewKpis } from './overview-kpis';\nimport { SalesByService } from './overview-services';\nimport { TopMerchants } from './overview-top-merchants';\nimport styles from './overview.module.css';\n\n/**\n * Commerce across every merchant, from the one cockpit call: the headline\n * figures, who sells most, where the base sits and who needs a look.\n * Nothing here is fetched per merchant. Money comes back once per currency\n * and is never added across currencies or converted.\n */\nexport function CommerceOverview({\n range,\n currency,\n compare,\n onOpenMerchant,\n}: {\n range: DateRange;\n /** The currency filter's choice; '' shows every currency. */\n currency: string;\n /** False for \"all time\": there is no earlier period to compare with. */\n compare: boolean;\n onOpenMerchant: (id: string) => void;\n}) {\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n const data = cockpit.data;\n const currencies = data?.currencies ?? [];\n // A chart or a ranking draws one currency: the filter's, or the only one.\n const focus = focusCurrency(currencies, currency);\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n const caption = focus ? `${period} · ${focus}` : period;\n\n const series = useMemo(\n () => rowFor(data?.trend.performanceSeriesByCurrency, focus)?.series,\n [data, focus],\n );\n const gmvTrend = useMemo(() => toTrend(series, 'gmv'), [series]);\n const ordersTrend = useMemo(() => toTrend(series, 'orders'), [series]);\n // Sales by service is one mixed figure: only honest with one currency.\n const services =\n data?.salesByService?.length && currencies.length === 1\n ? data.salesByService\n : null;\n\n if (cockpit.isError) {\n return (\n <ErrorBanner>\n The commerce overview could not be loaded.{' '}\n <Button size=\"sm\" variant=\"secondary\" onClick={() => cockpit.refetch()}>\n Try again\n </Button>\n </ErrorBanner>\n );\n }\n\n const loading = cockpit.isLoading || !data;\n const pick = (\n <p className=\"text-sm text-[var(--text-secondary)]\">{PICK_CURRENCY}</p>\n );\n\n return (\n <div className={styles.stack}>\n <OverviewKpis\n data={data}\n loading={loading}\n currency={currency}\n focus={focus}\n compare={compare}\n caption={caption}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel title=\"GMV & orders\" caption={caption}>\n {focus || loading ? (\n <GmvOrdersChart\n gmvTrend={gmvTrend}\n ordersTrend={ordersTrend}\n previousGmvTrend={[]}\n previousOrdersTrend={[]}\n compare={false}\n span={span}\n money={moneyIn(focus ?? 'USD')}\n />\n ) : (\n pick\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className={services ? 'adm-span-7' : 'adm-span-12'}>\n {focus || loading ? (\n <TopMerchants\n rows={rowFor(data?.topTenantsByCurrency, focus)?.tenants ?? []}\n loading={loading}\n money={moneyIn(focus ?? 'USD')}\n caption={caption}\n onOpen={onOpenMerchant}\n />\n ) : (\n <Panel title=\"Top merchants by sales\" caption={caption}>\n {pick}\n </Panel>\n )}\n </div>\n {services && (\n <div className=\"adm-span-5\">\n <SalesByService\n rows={services}\n loading={loading}\n money={moneyIn(currencies[0])}\n caption={caption}\n />\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkEM,SAEE,KAFF;AAhEN,SAAS,eAAe;AACxB,SAAS,QAAQ,mBAAmB;AACpC,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,OAAO,YAAY;AAQZ,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,UAAU,mBAAmB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AACD,QAAM,OAAO,QAAQ;AACrB,QAAM,aAAa,MAAM,cAAc,CAAC;AAExC,QAAM,QAAQ,cAAc,YAAY,QAAQ;AAChD,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,SAAS,GAAG,KAAK,CAAC,EAAE,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AACvD,QAAM,UAAU,QAAQ,GAAG,MAAM,SAAM,KAAK,KAAK;AAEjD,QAAM,SAAS;AAAA,IACb,MAAM,OAAO,MAAM,MAAM,6BAA6B,KAAK,GAAG;AAAA,IAC9D,CAAC,MAAM,KAAK;AAAA,EACd;AACA,QAAM,WAAW,QAAQ,MAAM,QAAQ,QAAQ,KAAK,GAAG,CAAC,MAAM,CAAC;AAC/D,QAAM,cAAc,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG,CAAC,MAAM,CAAC;AAErE,QAAM,WACJ,MAAM,gBAAgB,UAAU,WAAW,WAAW,IAClD,KAAK,iBACL;AAEN,MAAI,QAAQ,SAAS;AACnB,WACE,qBAAC,eAAY;AAAA;AAAA,MACgC;AAAA,MAC3C,oBAAC,UAAO,MAAK,MAAK,SAAQ,aAAY,SAAS,MAAM,QAAQ,QAAQ,GAAG,uBAExE;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,UAAU,QAAQ,aAAa,CAAC;AACtC,QAAM,OACJ,oBAAC,OAAE,WAAU,wCAAwC,yBAAc;AAGrE,SACE,qBAAC,SAAI,WAAW,OAAO,OACrB;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEA,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb,8BAAC,SAAM,OAAM,gBAAe,SACzB,mBAAS,UACR;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,kBAAkB,CAAC;AAAA,QACnB,qBAAqB,CAAC;AAAA,QACtB,SAAS;AAAA,QACT;AAAA,QACA,OAAO,QAAQ,SAAS,KAAK;AAAA;AAAA,IAC/B,IAEA,MAEJ,GACF,GACF;AAAA,IAEA,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,WAAW,eAAe,eACvC,mBAAS,UACR;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,OAAO,MAAM,sBAAsB,KAAK,GAAG,WAAW,CAAC;AAAA,UAC7D;AAAA,UACA,OAAO,QAAQ,SAAS,KAAK;AAAA,UAC7B;AAAA,UACA,QAAQ;AAAA;AAAA,MACV,IAEA,oBAAC,SAAM,OAAM,0BAAyB,SACnC,gBACH,GAEJ;AAAA,MACC,YACC,oBAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN;AAAA,UACA,OAAO,QAAQ,WAAW,CAAC,CAAC;AAAA,UAC5B;AAAA;AAAA,MACF,GACF;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/commerce/commerce-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { Button, ErrorBanner } from '@withpotter/ui-kit';\nimport { usePlatformCockpit } from '../hooks';\nimport {\n GmvOrdersChart,\n PICK_CURRENCY,\n Panel,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rowFor,\n toTrend,\n type DateRange,\n} from '../_ui';\nimport { OverviewKpis } from './overview-kpis';\nimport { SalesByService } from './overview-services';\nimport { TopMerchants } from './overview-top-merchants';\nimport styles from './overview.module.css';\n\n/**\n * Commerce across every merchant, from the one cockpit call: the headline\n * figures, who sells most, where the base sits and who needs a look.\n * Nothing here is fetched per merchant. Money comes back once per currency\n * and is never added across currencies or converted.\n */\nexport function CommerceOverview({\n range,\n currency,\n compare,\n onOpenMerchant,\n}: {\n range: DateRange;\n /** The currency filter's choice; '' shows every currency. */\n currency: string;\n /** False for \"all time\": there is no earlier period to compare with. */\n compare: boolean;\n onOpenMerchant: (id: string) => void;\n}) {\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n const data = cockpit.data;\n const currencies = data?.currencies ?? [];\n // A chart or a ranking draws one currency: the filter's, or the only one.\n const focus = focusCurrency(currencies, currency);\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n const caption = focus ? `${period} · ${focus}` : period;\n\n const series = useMemo(\n () => rowFor(data?.trend.performanceSeriesByCurrency, focus)?.series,\n [data, focus],\n );\n const gmvTrend = useMemo(() => toTrend(series, 'gmv'), [series]);\n const ordersTrend = useMemo(() => toTrend(series, 'orders'), [series]);\n // Sales by service is one mixed figure: only honest with one currency.\n const services =\n data?.salesByService?.length && currencies.length === 1\n ? data.salesByService\n : null;\n\n if (cockpit.isError) {\n return (\n <ErrorBanner>\n The commerce overview could not be loaded.{' '}\n <Button size=\"sm\" variant=\"secondary\" onClick={() => cockpit.refetch()}>\n Try again\n </Button>\n </ErrorBanner>\n );\n }\n\n const loading = cockpit.isLoading || !data;\n const pick = (\n <p className=\"text-sm text-[var(--text-secondary)]\">{PICK_CURRENCY}</p>\n );\n\n return (\n <div className={styles.stack}>\n <OverviewKpis\n data={data}\n loading={loading}\n currency={currency}\n focus={focus}\n compare={compare}\n caption={caption}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel title=\"GMV & orders\" caption={caption}>\n {focus || loading ? (\n <GmvOrdersChart\n gmvTrend={gmvTrend}\n ordersTrend={ordersTrend}\n previousGmvTrend={[]}\n previousOrdersTrend={[]}\n compare={false}\n span={span}\n money={moneyIn(focus ?? 'NGN')}\n />\n ) : (\n pick\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className={services ? 'adm-span-7' : 'adm-span-12'}>\n {focus || loading ? (\n <TopMerchants\n rows={rowFor(data?.topTenantsByCurrency, focus)?.tenants ?? []}\n loading={loading}\n money={moneyIn(focus ?? 'NGN')}\n caption={caption}\n onOpen={onOpenMerchant}\n />\n ) : (\n <Panel title=\"Top merchants by sales\" caption={caption}>\n {pick}\n </Panel>\n )}\n </div>\n {services && (\n <div className=\"adm-span-5\">\n <SalesByService\n rows={services}\n loading={loading}\n money={moneyIn(currencies[0])}\n caption={caption}\n />\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkEM,SAEE,KAFF;AAhEN,SAAS,eAAe;AACxB,SAAS,QAAQ,mBAAmB;AACpC,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,OAAO,YAAY;AAQZ,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,UAAU,mBAAmB;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,EACjB,CAAC;AACD,QAAM,OAAO,QAAQ;AACrB,QAAM,aAAa,MAAM,cAAc,CAAC;AAExC,QAAM,QAAQ,cAAc,YAAY,QAAQ;AAChD,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,SAAS,GAAG,KAAK,CAAC,EAAE,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AACvD,QAAM,UAAU,QAAQ,GAAG,MAAM,SAAM,KAAK,KAAK;AAEjD,QAAM,SAAS;AAAA,IACb,MAAM,OAAO,MAAM,MAAM,6BAA6B,KAAK,GAAG;AAAA,IAC9D,CAAC,MAAM,KAAK;AAAA,EACd;AACA,QAAM,WAAW,QAAQ,MAAM,QAAQ,QAAQ,KAAK,GAAG,CAAC,MAAM,CAAC;AAC/D,QAAM,cAAc,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG,CAAC,MAAM,CAAC;AAErE,QAAM,WACJ,MAAM,gBAAgB,UAAU,WAAW,WAAW,IAClD,KAAK,iBACL;AAEN,MAAI,QAAQ,SAAS;AACnB,WACE,qBAAC,eAAY;AAAA;AAAA,MACgC;AAAA,MAC3C,oBAAC,UAAO,MAAK,MAAK,SAAQ,aAAY,SAAS,MAAM,QAAQ,QAAQ,GAAG,uBAExE;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,UAAU,QAAQ,aAAa,CAAC;AACtC,QAAM,OACJ,oBAAC,OAAE,WAAU,wCAAwC,yBAAc;AAGrE,SACE,qBAAC,SAAI,WAAW,OAAO,OACrB;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEA,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb,8BAAC,SAAM,OAAM,gBAAe,SACzB,mBAAS,UACR;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,kBAAkB,CAAC;AAAA,QACnB,qBAAqB,CAAC;AAAA,QACtB,SAAS;AAAA,QACT;AAAA,QACA,OAAO,QAAQ,SAAS,KAAK;AAAA;AAAA,IAC/B,IAEA,MAEJ,GACF,GACF;AAAA,IAEA,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,WAAW,eAAe,eACvC,mBAAS,UACR;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,OAAO,MAAM,sBAAsB,KAAK,GAAG,WAAW,CAAC;AAAA,UAC7D;AAAA,UACA,OAAO,QAAQ,SAAS,KAAK;AAAA,UAC7B;AAAA,UACA,QAAQ;AAAA;AAAA,MACV,IAEA,oBAAC,SAAM,OAAM,0BAAyB,SACnC,gBACH,GAEJ;AAAA,MACC,YACC,oBAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN;AAAA,UACA,OAAO,QAAQ,WAAW,CAAC,CAAC;AAAA,UAC5B;AAAA;AAAA,MACF,GACF;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
@@ -3,6 +3,7 @@ const compactMoney = (n, currency) => {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
+ currencyDisplay: "narrowSymbol",
6
7
  currency,
7
8
  notation: "compact",
8
9
  maximumFractionDigits: 1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/commerce/merchant-meta.ts"],"sourcesContent":["import type { TenantRow, TenantStatus } from '@withpotter/lib/admin-api';\nimport type { PillTone } from '../tenants/[id]/commerce/format';\n\n/**\n * GMV is in the merchant's own currency. With the currency known (tenant\n * detail), show it; list rows carry none, so they get a plain number.\n */\nexport const compactMoney = (n: number, currency?: string | null) => {\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n ?? 0);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return compactNumber(n);\n};\n\n/**\n * Sales per currency, compact and never added together: \"₦1.2M · $3.4K\".\n * Falls back to the plain number when the API sends no per-currency rows.\n */\nexport const compactSales = (\n rows: ReadonlyArray<{ currency: string; amount: number }> | undefined,\n fallback: number,\n) =>\n rows?.length\n ? rows.map((r) => compactMoney(r.amount, r.currency)).join(' · ')\n : compactNumber(fallback);\n\n/** \"12.4K\": sales without a currency, for list rows. */\nexport const compactNumber = (n: number) =>\n new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n ?? 0);\n\nexport const STATUS_TONE: Record<TenantStatus, PillTone> = {\n active: 'success',\n pending: 'pending',\n suspended: 'failed',\n deleted: 'neutral',\n};\n\n/**\n * A merchant's logo when the API sends one, flat (`logoUrl`) or on the\n * profile (`profile.logoUrl`). Without it EntityAvatar falls back to initials,\n * so every list picks logos up the day the API starts sending them.\n */\nexport const logoOf = (row: object): string | null => {\n const r = row as {\n logoUrl?: string | null;\n profile?: { logoUrl?: string | null } | null;\n };\n\n return r.logoUrl ?? r.profile?.logoUrl ?? null;\n};\n\n/**\n * Ranked by sales within the rows shown only. Each figure is in its own\n * merchant's currency, so this orders a page; it is not a platform league.\n */\nexport const bySales = (rows: TenantRow[]) =>\n [...rows].sort((a, b) => (b.gmv ?? 0) - (a.gmv ?? 0));\n"],"mappings":"AAOO,MAAM,eAAe,CAAC,GAAW,aAA6B;AACnE,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,OAAO;AAAA,QACP;AAAA,QACA,UAAU;AAAA,QACV,uBAAuB;AAAA,MACzB,CAAC,EAAE,OAAO,KAAK,CAAC;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,cAAc,CAAC;AACxB;AAMO,MAAM,eAAe,CAC1B,MACA,aAEA,MAAM,SACF,KAAK,IAAI,CAAC,MAAM,aAAa,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,KAAK,QAAK,IAC9D,cAAc,QAAQ;AAGrB,MAAM,gBAAgB,CAAC,MAC5B,IAAI,KAAK,aAAa,SAAS;AAAA,EAC7B,UAAU;AAAA,EACV,uBAAuB;AACzB,CAAC,EAAE,OAAO,KAAK,CAAC;AAEX,MAAM,cAA8C;AAAA,EACzD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AACX;AAOO,MAAM,SAAS,CAAC,QAA+B;AACpD,QAAM,IAAI;AAKV,SAAO,EAAE,WAAW,EAAE,SAAS,WAAW;AAC5C;AAMO,MAAM,UAAU,CAAC,SACtB,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,OAAO,EAAE;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/commerce/merchant-meta.ts"],"sourcesContent":["import type { TenantRow, TenantStatus } from '@withpotter/lib/admin-api';\nimport type { PillTone } from '../tenants/[id]/commerce/format';\n\n/**\n * GMV is in the merchant's own currency. With the currency known (tenant\n * detail), show it; list rows carry none, so they get a plain number.\n */\nexport const compactMoney = (n: number, currency?: string | null) => {\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n ?? 0);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return compactNumber(n);\n};\n\n/**\n * Sales per currency, compact and never added together: \"₦1.2M · $3.4K\".\n * Falls back to the plain number when the API sends no per-currency rows.\n */\nexport const compactSales = (\n rows: ReadonlyArray<{ currency: string; amount: number }> | undefined,\n fallback: number,\n) =>\n rows?.length\n ? rows.map((r) => compactMoney(r.amount, r.currency)).join(' · ')\n : compactNumber(fallback);\n\n/** \"12.4K\": sales without a currency, for list rows. */\nexport const compactNumber = (n: number) =>\n new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n ?? 0);\n\nexport const STATUS_TONE: Record<TenantStatus, PillTone> = {\n active: 'success',\n pending: 'pending',\n suspended: 'failed',\n deleted: 'neutral',\n};\n\n/**\n * A merchant's logo when the API sends one, flat (`logoUrl`) or on the\n * profile (`profile.logoUrl`). Without it EntityAvatar falls back to initials,\n * so every list picks logos up the day the API starts sending them.\n */\nexport const logoOf = (row: object): string | null => {\n const r = row as {\n logoUrl?: string | null;\n profile?: { logoUrl?: string | null } | null;\n };\n\n return r.logoUrl ?? r.profile?.logoUrl ?? null;\n};\n\n/**\n * Ranked by sales within the rows shown only. Each figure is in its own\n * merchant's currency, so this orders a page; it is not a platform league.\n */\nexport const bySales = (rows: TenantRow[]) =>\n [...rows].sort((a, b) => (b.gmv ?? 0) - (a.gmv ?? 0));\n"],"mappings":"AAOO,MAAM,eAAe,CAAC,GAAW,aAA6B;AACnE,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,OAAO;AAAA,QACP,iBAAiB;AAAA,QACjB;AAAA,QACA,UAAU;AAAA,QACV,uBAAuB;AAAA,MACzB,CAAC,EAAE,OAAO,KAAK,CAAC;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,cAAc,CAAC;AACxB;AAMO,MAAM,eAAe,CAC1B,MACA,aAEA,MAAM,SACF,KAAK,IAAI,CAAC,MAAM,aAAa,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,KAAK,QAAK,IAC9D,cAAc,QAAQ;AAGrB,MAAM,gBAAgB,CAAC,MAC5B,IAAI,KAAK,aAAa,SAAS;AAAA,EAC7B,UAAU;AAAA,EACV,uBAAuB;AACzB,CAAC,EAAE,OAAO,KAAK,CAAC;AAEX,MAAM,cAA8C;AAAA,EACzD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AACX;AAOO,MAAM,SAAS,CAAC,QAA+B;AACpD,QAAM,IAAI;AAKV,SAAO,EAAE,WAAW,EAAE,SAAS,WAAW;AAC5C;AAMO,MAAM,UAAU,CAAC,SACtB,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,OAAO,EAAE;","names":[]}
@@ -78,7 +78,7 @@ function MerchantView({ range }) {
78
78
  ] }),
79
79
  /* @__PURE__ */ jsxs("span", { className: styles.sub, children: [
80
80
  "Shown in ",
81
- t.currency || "USD",
81
+ t.currency || "NGN",
82
82
  ", the merchant\u2019s currency"
83
83
  ] })
84
84
  ] })
@@ -90,7 +90,7 @@ function MerchantView({ range }) {
90
90
  MerchantCommerce,
91
91
  {
92
92
  tenantId: id,
93
- currency: t?.currency ?? "USD",
93
+ currency: t?.currency ?? "NGN",
94
94
  range
95
95
  },
96
96
  id
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/commerce/merchant-view.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation';\nimport { Button, Card, EmptyNote, ErrorBanner } from '@withpotter/ui-kit';\nimport { usePlatformTenant, usePlatformTenants } from '../hooks';\nimport { EntityAvatar, Reveal } from '../_fx';\nimport { Skeleton, type DateRange } from '../_ui';\nimport { MerchantCommerce } from '../tenants/[id]/commerce';\nimport { StatusPill } from '../tenants/[id]/commerce/status-pill';\nimport { bySales, compactSales, STATUS_TONE } from './merchant-meta';\nimport { MerchantPicker } from './merchant-picker';\nimport styles from './console.module.css';\n\n/** \"By merchant\": pick a merchant, then read their commerce. */\nexport function MerchantView({ range }: { range: DateRange }) {\n const router = useRouter();\n const pathname = usePathname();\n const params = useSearchParams();\n const [picking, setPicking] = useState(false);\n\n const top = usePlatformTenants({ pageSize: 50 });\n const fallback = bySales(top.data?.rows ?? [])[0]?.id ?? null;\n const id = params.get('merchant') || fallback;\n const tenant = usePlatformTenant(id ?? '', {\n startDate: range.startDate,\n endDate: range.endDate,\n });\n const t = tenant.data;\n\n const pick = (next: string) => {\n const qs = new URLSearchParams(params.toString());\n qs.set('merchant', next);\n router.replace(`${pathname}?${qs.toString()}`, { scroll: false });\n setPicking(false);\n };\n\n const noMerchants = !top.isLoading && !top.isError && !id;\n\n return (\n <div className={styles.body}>\n {top.isError && !id ? (\n <ErrorBanner>\n Merchants could not be loaded.{' '}\n <Button size=\"sm\" variant=\"secondary\" onClick={() => top.refetch()}>\n Try again\n </Button>\n </ErrorBanner>\n ) : noMerchants ? (\n <EmptyNote\n well\n action={\n <Button variant=\"secondary\" asChild>\n <Link href=\"/admin/tenants\">Go to merchants</Link>\n </Button>\n }\n >\n Once a business signs up, its items, orders and stock appear here.\n </EmptyNote>\n ) : (\n <>\n <Card>\n <Reveal className={styles.current}>\n {t?.profile?.logoUrl && (\n <EntityAvatar\n name={t.businessName}\n src={t.profile.logoUrl}\n size={40}\n />\n )}\n <div className={styles.currentMain}>\n <span className={styles.eyebrow}>Merchant</span>\n <span className={styles.currentName} title={t?.businessName}>\n {t ? t.businessName : <Skeleton width={160} height={20} />}\n </span>\n {t && (\n <span className={styles.currentMeta}>\n <StatusPill\n status={t.status}\n tone={STATUS_TONE[t.status]}\n />\n <span className={styles.sub}>\n {compactSales(\n t.metrics.byCurrency?.map((r) => ({\n currency: r.currency,\n amount: r.gmv,\n })),\n t.metrics.gmv,\n )}{' '}\n lifetime sales\n </span>\n <span className={styles.sub}>\n Shown in {t.currency || 'USD'}, the merchant’s currency\n </span>\n </span>\n )}\n </div>\n {id && (\n <Button variant=\"ghost\" asChild>\n <Link href={`/admin/tenants/${id}`}>Merchant profile</Link>\n </Button>\n )}\n <Button variant=\"secondary\" onClick={() => setPicking(true)}>\n Change merchant\n </Button>\n </Reveal>\n </Card>\n\n {id && (\n <MerchantCommerce\n key={id}\n tenantId={id}\n currency={t?.currency ?? 'USD'}\n range={range}\n />\n )}\n </>\n )}\n\n <MerchantPicker\n open={picking}\n onOpenChange={setPicking}\n selectedId={id}\n onPick={(row) => pick(row.id)}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/commerce/merchant-view.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation';\nimport { Button, Card, EmptyNote, ErrorBanner } from '@withpotter/ui-kit';\nimport { usePlatformTenant, usePlatformTenants } from '../hooks';\nimport { EntityAvatar, Reveal } from '../_fx';\nimport { Skeleton, type DateRange } from '../_ui';\nimport { MerchantCommerce } from '../tenants/[id]/commerce';\nimport { StatusPill } from '../tenants/[id]/commerce/status-pill';\nimport { bySales, compactSales, STATUS_TONE } from './merchant-meta';\nimport { MerchantPicker } from './merchant-picker';\nimport styles from './console.module.css';\n\n/** \"By merchant\": pick a merchant, then read their commerce. */\nexport function MerchantView({ range }: { range: DateRange }) {\n const router = useRouter();\n const pathname = usePathname();\n const params = useSearchParams();\n const [picking, setPicking] = useState(false);\n\n const top = usePlatformTenants({ pageSize: 50 });\n const fallback = bySales(top.data?.rows ?? [])[0]?.id ?? null;\n const id = params.get('merchant') || fallback;\n const tenant = usePlatformTenant(id ?? '', {\n startDate: range.startDate,\n endDate: range.endDate,\n });\n const t = tenant.data;\n\n const pick = (next: string) => {\n const qs = new URLSearchParams(params.toString());\n qs.set('merchant', next);\n router.replace(`${pathname}?${qs.toString()}`, { scroll: false });\n setPicking(false);\n };\n\n const noMerchants = !top.isLoading && !top.isError && !id;\n\n return (\n <div className={styles.body}>\n {top.isError && !id ? (\n <ErrorBanner>\n Merchants could not be loaded.{' '}\n <Button size=\"sm\" variant=\"secondary\" onClick={() => top.refetch()}>\n Try again\n </Button>\n </ErrorBanner>\n ) : noMerchants ? (\n <EmptyNote\n well\n action={\n <Button variant=\"secondary\" asChild>\n <Link href=\"/admin/tenants\">Go to merchants</Link>\n </Button>\n }\n >\n Once a business signs up, its items, orders and stock appear here.\n </EmptyNote>\n ) : (\n <>\n <Card>\n <Reveal className={styles.current}>\n {t?.profile?.logoUrl && (\n <EntityAvatar\n name={t.businessName}\n src={t.profile.logoUrl}\n size={40}\n />\n )}\n <div className={styles.currentMain}>\n <span className={styles.eyebrow}>Merchant</span>\n <span className={styles.currentName} title={t?.businessName}>\n {t ? t.businessName : <Skeleton width={160} height={20} />}\n </span>\n {t && (\n <span className={styles.currentMeta}>\n <StatusPill\n status={t.status}\n tone={STATUS_TONE[t.status]}\n />\n <span className={styles.sub}>\n {compactSales(\n t.metrics.byCurrency?.map((r) => ({\n currency: r.currency,\n amount: r.gmv,\n })),\n t.metrics.gmv,\n )}{' '}\n lifetime sales\n </span>\n <span className={styles.sub}>\n Shown in {t.currency || 'NGN'}, the merchant’s currency\n </span>\n </span>\n )}\n </div>\n {id && (\n <Button variant=\"ghost\" asChild>\n <Link href={`/admin/tenants/${id}`}>Merchant profile</Link>\n </Button>\n )}\n <Button variant=\"secondary\" onClick={() => setPicking(true)}>\n Change merchant\n </Button>\n </Reveal>\n </Card>\n\n {id && (\n <MerchantCommerce\n key={id}\n tenantId={id}\n currency={t?.currency ?? 'NGN'}\n range={range}\n />\n )}\n </>\n )}\n\n <MerchantPicker\n open={picking}\n onOpenChange={setPicking}\n selectedId={id}\n onPick={(row) => pick(row.id)}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -109,8 +109,6 @@
109
109
  }
110
110
 
111
111
  .trend thead th {
112
- position: sticky;
113
- top: 0;
114
112
  background: var(--bg-base);
115
113
  }
116
114
 
@@ -1,8 +1,6 @@
1
1
  /* The merchant preview: their card on a brand-tinted stage. */
2
2
 
3
3
  .frame {
4
- position: sticky;
5
- top: 0;
6
4
  display: grid;
7
5
  gap: 12px;
8
6
  padding: 16px;
@@ -0,0 +1,9 @@
1
+ import { shownRows } from "../_ui/money-view";
2
+ function platformFeeRows(rows, selected) {
3
+ const shown = shownRows(rows, selected);
4
+ return shown.length ? shown : null;
5
+ }
6
+ export {
7
+ platformFeeRows
8
+ };
9
+ //# sourceMappingURL=platform-fees.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/platform-fees.ts"],"sourcesContent":["import { shownRows } from '../_ui/money-view';\n\n/**\n * The platform fees rows to show, or null when the figure should be hidden.\n * The api field is optional: absent or empty means nothing to show, never a\n * zero. Rows follow the currency filter and are never added together.\n */\nexport function platformFeeRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] | null {\n const shown = shownRows(rows, selected);\n return shown.length ? shown : null;\n}\n"],"mappings":"AAAA,SAAS,iBAAiB;AAOnB,SAAS,gBACd,MACA,UACY;AACZ,QAAM,QAAQ,UAAU,MAAM,QAAQ;AACtC,SAAO,MAAM,SAAS,QAAQ;AAChC;","names":[]}
@@ -23,7 +23,7 @@ function RevenueKpis({
23
23
  {
24
24
  label: "Monthly recurring revenue (MRR)",
25
25
  value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
26
- hint: `Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || "USD"}. Yearly plans are divided by 12.`,
26
+ hint: `Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || "NGN"}. Yearly plans are divided by 12.`,
27
27
  sub: tracked ? `${integer.format(recurring.activeSubscriptions)} paying` : noSource
28
28
  }
29
29
  ) }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The four headline figures: recurring revenue first, then merchant GMV. */\nexport function RevenueKpis({\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without a billing source the API sends 0s; show that nothing is measured.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not tracked';\n const noSource = 'No billing source connected';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section aria-label=\"Revenue indicators\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Monthly recurring revenue (MRR)\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || 'USD'}. Yearly plans are divided by 12.`}\n sub={\n tracked\n ? `${integer.format(recurring.activeSubscriptions)} paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Annual recurring revenue (ARR)\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR × 12).\"\n sub={tracked ? 'MRR × 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average revenue per account (ARPA)\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.\"\n sub={tracked ? 'Per paying account' : noSource}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales (GMV)\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`The merchant sales ${ADMIN_NAME}'s recurring revenue sits on, not ${ADMIN_NAME}'s own income (that's MRR). One line per currency; currencies are never added together or converted.`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n </section>\n );\n}\n"],"mappings":"AAsCI,SAMU,KANV;AAtCJ,SAAS,kBAAkB;AAE3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE,qBAAC,aAAQ,cAAW,sBAAqB,WAAU,YACjD;AAAA,wBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAM,4EAA4E,KAAK,YAAY,KAAK;AAAA,QACxG,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,CAAC,YAChD;AAAA;AAAA,IAER,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,gBAAa;AAAA;AAAA,IAC9B,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,uBAAuB;AAAA;AAAA,IACxC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX;AAAA,YACA,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,OAAM;AAAA;AAAA,QACR;AAAA,QAEF,MAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM,sBAAsB,UAAU,qCAAqC,UAAU;AAAA,QACrF,OAAO,QAAQ,WAAW;AAAA,QAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,IACzB,GACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The four headline figures: recurring revenue first, then merchant GMV. */\nexport function RevenueKpis({\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n data: PlatformRevenue;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without a billing source the API sends 0s; show that nothing is measured.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not tracked';\n const noSource = 'No billing source connected';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section aria-label=\"Revenue indicators\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Monthly recurring revenue (MRR)\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`Monthly recurring revenue from active merchant billing subscriptions, in ${data.currency || 'NGN'}. Yearly plans are divided by 12.`}\n sub={\n tracked\n ? `${integer.format(recurring.activeSubscriptions)} paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Annual recurring revenue (ARR)\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"Annual recurring revenue: what the monthly recurring revenue adds up to over a year if nothing changes (MRR × 12).\"\n sub={tracked ? 'MRR × 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average revenue per account (ARPA)\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What the average paying merchant pays each month: monthly recurring revenue divided by the number of paying merchants.\"\n sub={tracked ? 'Per paying account' : noSource}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales (GMV)\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`The merchant sales ${ADMIN_NAME}'s recurring revenue sits on, not ${ADMIN_NAME}'s own income (that's MRR). One line per currency; currencies are never added together or converted.`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n </section>\n );\n}\n"],"mappings":"AAsCI,SAMU,KANV;AAtCJ,SAAS,kBAAkB;AAE3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE,qBAAC,aAAQ,cAAW,sBAAqB,WAAU,YACjD;AAAA,wBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAM,4EAA4E,KAAK,YAAY,KAAK;AAAA,QACxG,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,CAAC,YAChD;AAAA;AAAA,IAER,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,gBAAa;AAAA;AAAA,IAC9B,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,uBAAuB;AAAA;AAAA,IACxC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX;AAAA,YACA,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,OAAM;AAAA;AAAA,QACR;AAAA,QAEF,MAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM,sBAAsB,UAAU,qCAAqC,UAAU;AAAA,QACrF,OAAO,QAAQ,WAAW;AAAA,QAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,IACzB,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -2,10 +2,12 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { TimeSeriesChart } from "@withpotter/components/charts";
5
- import { PICK_CURRENCY, Panel, moneyIn, rowFor, toTrend } from "../_ui";
5
+ import { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from "../_ui";
6
+ import { Reveal } from "../_fx";
6
7
  import { Tinted } from "../audit/tinted";
7
8
  import { HealthPanel } from "./health-panel";
8
9
  import { PlanMixPanel } from "./plan-mix-panel";
10
+ import { platformFeeRows } from "./platform-fees";
9
11
  import { RevenueKpis } from "./revenue-kpis";
10
12
  import { SubscriptionsPanel } from "./subscriptions-panel";
11
13
  import { TopTenantsPanel } from "./top-tenants-panel";
@@ -25,6 +27,7 @@ function RevenueOverview({
25
27
  const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
26
28
  const gmvCaption = focus ? `${period} \xB7 ${focus}` : period;
27
29
  const recurringTracked = data.recurring.tracked !== false;
30
+ const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
28
31
  return /* @__PURE__ */ jsxs(Fragment, { children: [
29
32
  /* @__PURE__ */ jsx(
30
33
  RevenueKpis,
@@ -37,6 +40,22 @@ function RevenueOverview({
37
40
  gmvSpark: gmvSeries.map((p) => p.value)
38
41
  }
39
42
  ),
43
+ platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
44
+ StatCard,
45
+ {
46
+ label: "Platform fees earned",
47
+ value: /* @__PURE__ */ jsx(
48
+ MoneyLines,
49
+ {
50
+ rows: platformFees,
51
+ currency: "",
52
+ amount: (r) => r.amount,
53
+ decimals: 2
54
+ }
55
+ ),
56
+ hint: "What Olive keeps from payouts, settlements and subscriptions."
57
+ }
58
+ ) }) }),
40
59
  /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
41
60
  Panel,
42
61
  {
@@ -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 { PlatformCockpit, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { PICK_CURRENCY, Panel, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Tinted } from '../audit/tinted';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n\n return (\n <>\n <RevenueKpis\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"GMV trend\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n )}\n\n <div className=\"adm-grid\">\n <div className={recurringTracked ? 'adm-span-8' : 'adm-span-12'}>\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n {recurringTracked && (\n <div className=\"adm-span-4\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n )}\n </div>\n </>\n );\n}\n"],"mappings":";AAkDI,mBACE,KAqCE,YAtCJ;AAhDJ,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAEhC,SAAS,eAAe,OAAO,SAAS,QAAQ,eAAe;AAC/D,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,OAAO,YAAY;AAQZ,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,YAAY,QAAQ,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG,CAAC,GAAG,CAAC;AAClE,QAAM,aAAa,QAAQ,GAAG,MAAM,SAAM,KAAK,KAAK;AACpD,QAAM,mBAAmB,KAAK,UAAU,YAAY;AAEpD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK;AAAA;AAAA,IACxC;AAAA,IAEA,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SAAS;AAAA,QACT,QAAO;AAAA,QAEN,kBACC,oBAAC,SAAI,WAAW,OAAO,WACrB,8BAAC,UAAO,MAAK,SACX;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,SAAS;AAAA,YACT,cAAc,YAAY,IAAI;AAAA,YAC9B,aAAa,QAAQ,KAAK;AAAA,YAC1B,cAAc,WAAW,KAAK;AAAA,YAC9B,QAAQ;AAAA;AAAA,QACV,GACF,GACF,IAEA,oBAAC,OAAE,WAAW,OAAO,OAAQ,yBAAc;AAAA;AAAA,IAE/C,GACF,GACF;AAAA,IAEC,oBACC,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,MACA,oBAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,KAAK,UAAU;AAAA,UAC9B;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,mBAAmB,eAAe,eAChD;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,cAAc,CAAC;AAAA,UAC7B,UAAU,KAAK,YAAY;AAAA,UAC3B;AAAA;AAAA,MACF,GACF;AAAA,MACC,oBACC,oBAAC,SAAI,WAAU,cACb,8BAAC,sBAAmB,WAAW,KAAK,WAAW,MAAY,GAC7D;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type { PlatformCockpit, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\n\n/**\n * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n}) {\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n return (\n <>\n <RevenueKpis\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Platform fees earned\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts, settlements and subscriptions.\"\n />\n </Reveal>\n </section>\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"GMV trend\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"adm-span-5\">\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n )}\n\n <div className=\"adm-grid\">\n <div className={recurringTracked ? 'adm-span-8' : 'adm-span-12'}>\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n {recurringTracked && (\n <div className=\"adm-span-4\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n )}\n </div>\n </>\n );\n}\n"],"mappings":";AAsDI,mBACE,KAwDE,YAzDJ;AApDJ,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAEhC,SAAS,YAAY,eAAe,OAAO,UAAU,SAAS,QAAQ,eAAe;AACrF,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,OAAO,YAAY;AAQZ,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;AACD,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,YAAY,QAAQ,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG,CAAC,GAAG,CAAC;AAClE,QAAM,aAAa,QAAQ,GAAG,MAAM,SAAM,KAAK,KAAK;AACpD,QAAM,mBAAmB,KAAK,UAAU,YAAY;AAEpD,QAAM,eAAe,gBAAgB,KAAK,8BAA8B,QAAQ;AAEhF,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK;AAAA;AAAA,IACxC;AAAA,IAEC,gBACC,oBAAC,aAAQ,cAAW,iBAAgB,WAAU,YAC5C,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,UAAS;AAAA,YACT,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,UAAU;AAAA;AAAA,QACZ;AAAA,QAEF,MAAK;AAAA;AAAA,IACP,GACF,GACF;AAAA,IAGF,oBAAC,SAAI,WAAU,YACb,8BAAC,SAAI,WAAU,eACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,SAAS;AAAA,QACT,QAAO;AAAA,QAEN,kBACC,oBAAC,SAAI,WAAW,OAAO,WACrB,8BAAC,UAAO,MAAK,SACX;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,SAAS;AAAA,YACT,cAAc,YAAY,IAAI;AAAA,YAC9B,aAAa,QAAQ,KAAK;AAAA,YAC1B,cAAc,WAAW,KAAK;AAAA,YAC9B,QAAQ;AAAA;AAAA,QACV,GACF,GACF,IAEA,oBAAC,OAAE,WAAW,OAAO,OAAQ,yBAAc;AAAA;AAAA,IAE/C,GACF,GACF;AAAA,IAEC,oBACC,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,MACA,oBAAC,SAAI,WAAU,cACb;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,KAAK,UAAU;AAAA,UAC9B;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GACF;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,mBAAmB,eAAe,eAChD;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,cAAc,CAAC;AAAA,UAC7B,UAAU,KAAK,YAAY;AAAA,UAC3B;AAAA;AAAA,MACF,GACF;AAAA,MACC,oBACC,oBAAC,SAAI,WAAU,cACb,8BAAC,sBAAmB,WAAW,KAAK,WAAW,MAAY,GAC7D;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
@@ -50,7 +50,7 @@ function RevenueWorkspace() {
50
50
  });
51
51
  const currencies = useMemo(() => data?.currencies ?? [], [data]);
52
52
  const focus = focusCurrency(currencies, currency);
53
- const recurringCurrency = data?.currency || "USD";
53
+ const recurringCurrency = data?.currency || "NGN";
54
54
  const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);
55
55
  const money2 = useMemo(
56
56
  () => moneyIn(recurringCurrency, 2),