@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport { usePlatformCockpit, usePlatformRevenue } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/** Revenue: headline money on Overview, revenue quality and cohorts on Retention. */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'USD';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`Every revenue on ${ADMIN_NAME} is reported here: its subscription income each month and year, and the merchant sales (GMV) it is based on.`}\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport { usePlatformCockpit, usePlatformRevenue } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/** Revenue: headline money on Overview, revenue quality and cohorts on Retention. */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`Every revenue on ${ADMIN_NAME} is reported here: its subscription income each month and year, and the merchant sales (GMV) it is based on.`}\n actions={\n <div className=\"adm-toolbar\">\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -75,11 +75,8 @@
75
75
  outline-offset: -2px;
76
76
  }
77
77
 
78
- /* Sticky column header with a hairline, not a shadow. */
78
+ /* Column header with a hairline, not a shadow. */
79
79
  .scroll thead th {
80
- position: sticky;
81
- top: 0;
82
- z-index: 1;
83
80
  background: var(--bg-base);
84
81
  box-shadow: inset 0 -1px 0 var(--border-subtle);
85
82
  white-space: nowrap;
@@ -3,7 +3,8 @@ function moneyIn(currency, decimals = 0) {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
- currency: currency || "USD",
6
+ currencyDisplay: "narrowSymbol",
7
+ currency: currency || "NGN",
7
8
  maximumFractionDigits: decimals
8
9
  }).format(n);
9
10
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/admin/tenants/%5Bid%5D/commerce/format.ts"],"sourcesContent":["import type { TenantProduct } from '@withpotter/lib/admin-api';\n\n/** Amounts in the merchant's own currency, whole units for headlines. */\nexport function moneyIn(currency: string, decimals = 0) {\n return (n: number) => {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency || 'USD',\n maximumFractionDigits: decimals,\n }).format(n);\n } catch {\n return `${currency} ${Math.round(n).toLocaleString()}`;\n }\n };\n}\n\nexport type Money = (n: number) => string;\n\n/** 1.2k / 3.4m, for figures in tight tiles. */\nexport const compact = (n: number) =>\n new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n);\n\nexport const shortDate = (iso: string) =>\n new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric' }).format(\n new Date(iso),\n );\n\n/** Sentence-case a machine word: \"checked_in\" → \"Checked in\". */\nexport const humanize = (s: string) => {\n const t = s.replace(/[_-]+/g, ' ').trim();\n return t ? t[0].toUpperCase() + t.slice(1) : '–';\n};\n\nexport type PillTone =\n 'success' | 'pending' | 'failed' | 'processing' | 'neutral';\n\nconst TONE_OF: Record<string, PillTone> = {\n completed: 'success',\n paid: 'success',\n success: 'success',\n settled: 'success',\n fulfilled: 'success',\n delivered: 'success',\n collected: 'success',\n pending: 'pending',\n unpaid: 'pending',\n awaiting_confirmation: 'pending',\n authorized: 'pending',\n unfulfilled: 'pending',\n partial: 'pending',\n confirmed: 'processing',\n processing: 'processing',\n checked_in: 'processing',\n ready: 'processing',\n failed: 'failed',\n reversed: 'failed',\n expired: 'failed',\n cancelled: 'neutral',\n refunded: 'neutral',\n partially_refunded: 'neutral',\n};\n\nexport const toneOf = (status: string): PillTone =>\n TONE_OF[status?.toLowerCase()] ?? 'neutral';\n\nexport type StockState = {\n label: string;\n tone: PillTone;\n available: number | null;\n};\n\n/** What a shelf looks like for one item, from the counts the API sends. */\nexport function stockOf(p: TenantProduct): StockState {\n if (!p.trackInventory || p.quantity == null)\n return { label: 'Not tracked', tone: 'neutral', available: null };\n\n const available = p.quantity - (p.reserved ?? 0);\n\n if (available <= 0)\n return { label: 'Out of stock', tone: 'failed', available };\n\n if (p.lowStockThreshold != null && available <= p.lowStockThreshold)\n return { label: `Low · ${available}`, tone: 'pending', available };\n\n return {\n label: `In stock · ${compact(available)}`,\n tone: 'success',\n available,\n };\n}\n"],"mappings":"AAGO,SAAS,QAAQ,UAAkB,WAAW,GAAG;AACtD,SAAO,CAAC,MAAc;AACpB,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,OAAO;AAAA,QACP,UAAU,YAAY;AAAA,QACtB,uBAAuB;AAAA,MACzB,CAAC,EAAE,OAAO,CAAC;AAAA,IACb,QAAQ;AACN,aAAO,GAAG,QAAQ,IAAI,KAAK,MAAM,CAAC,EAAE,eAAe,CAAC;AAAA,IACtD;AAAA,EACF;AACF;AAKO,MAAM,UAAU,CAAC,MACtB,IAAI,KAAK,aAAa,SAAS;AAAA,EAC7B,UAAU;AAAA,EACV,uBAAuB;AACzB,CAAC,EAAE,OAAO,CAAC;AAEN,MAAM,YAAY,CAAC,QACxB,IAAI,KAAK,eAAe,MAAM,EAAE,OAAO,SAAS,KAAK,UAAU,CAAC,EAAE;AAAA,EAChE,IAAI,KAAK,GAAG;AACd;AAGK,MAAM,WAAW,CAAC,MAAc;AACrC,QAAM,IAAI,EAAE,QAAQ,UAAU,GAAG,EAAE,KAAK;AACxC,SAAO,IAAI,EAAE,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,IAAI;AAC/C;AAKA,MAAM,UAAoC;AAAA,EACxC,WAAW;AAAA,EACX,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,uBAAuB;AAAA,EACvB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,oBAAoB;AACtB;AAEO,MAAM,SAAS,CAAC,WACrB,QAAQ,QAAQ,YAAY,CAAC,KAAK;AAS7B,SAAS,QAAQ,GAA8B;AACpD,MAAI,CAAC,EAAE,kBAAkB,EAAE,YAAY;AACrC,WAAO,EAAE,OAAO,eAAe,MAAM,WAAW,WAAW,KAAK;AAElE,QAAM,YAAY,EAAE,YAAY,EAAE,YAAY;AAE9C,MAAI,aAAa;AACf,WAAO,EAAE,OAAO,gBAAgB,MAAM,UAAU,UAAU;AAE5D,MAAI,EAAE,qBAAqB,QAAQ,aAAa,EAAE;AAChD,WAAO,EAAE,OAAO,YAAS,SAAS,IAAI,MAAM,WAAW,UAAU;AAEnE,SAAO;AAAA,IACL,OAAO,iBAAc,QAAQ,SAAS,CAAC;AAAA,IACvC,MAAM;AAAA,IACN;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../../src/admin/tenants/%5Bid%5D/commerce/format.ts"],"sourcesContent":["import type { TenantProduct } from '@withpotter/lib/admin-api';\n\n/** Amounts in the merchant's own currency, whole units for headlines. */\nexport function moneyIn(currency: string, decimals = 0) {\n return (n: number) => {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency: currency || 'NGN',\n maximumFractionDigits: decimals,\n }).format(n);\n } catch {\n return `${currency} ${Math.round(n).toLocaleString()}`;\n }\n };\n}\n\nexport type Money = (n: number) => string;\n\n/** 1.2k / 3.4m, for figures in tight tiles. */\nexport const compact = (n: number) =>\n new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n);\n\nexport const shortDate = (iso: string) =>\n new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric' }).format(\n new Date(iso),\n );\n\n/** Sentence-case a machine word: \"checked_in\" → \"Checked in\". */\nexport const humanize = (s: string) => {\n const t = s.replace(/[_-]+/g, ' ').trim();\n return t ? t[0].toUpperCase() + t.slice(1) : '–';\n};\n\nexport type PillTone =\n 'success' | 'pending' | 'failed' | 'processing' | 'neutral';\n\nconst TONE_OF: Record<string, PillTone> = {\n completed: 'success',\n paid: 'success',\n success: 'success',\n settled: 'success',\n fulfilled: 'success',\n delivered: 'success',\n collected: 'success',\n pending: 'pending',\n unpaid: 'pending',\n awaiting_confirmation: 'pending',\n authorized: 'pending',\n unfulfilled: 'pending',\n partial: 'pending',\n confirmed: 'processing',\n processing: 'processing',\n checked_in: 'processing',\n ready: 'processing',\n failed: 'failed',\n reversed: 'failed',\n expired: 'failed',\n cancelled: 'neutral',\n refunded: 'neutral',\n partially_refunded: 'neutral',\n};\n\nexport const toneOf = (status: string): PillTone =>\n TONE_OF[status?.toLowerCase()] ?? 'neutral';\n\nexport type StockState = {\n label: string;\n tone: PillTone;\n available: number | null;\n};\n\n/** What a shelf looks like for one item, from the counts the API sends. */\nexport function stockOf(p: TenantProduct): StockState {\n if (!p.trackInventory || p.quantity == null)\n return { label: 'Not tracked', tone: 'neutral', available: null };\n\n const available = p.quantity - (p.reserved ?? 0);\n\n if (available <= 0)\n return { label: 'Out of stock', tone: 'failed', available };\n\n if (p.lowStockThreshold != null && available <= p.lowStockThreshold)\n return { label: `Low · ${available}`, tone: 'pending', available };\n\n return {\n label: `In stock · ${compact(available)}`,\n tone: 'success',\n available,\n };\n}\n"],"mappings":"AAGO,SAAS,QAAQ,UAAkB,WAAW,GAAG;AACtD,SAAO,CAAC,MAAc;AACpB,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,OAAO;AAAA,QACP,iBAAiB;AAAA,QACjB,UAAU,YAAY;AAAA,QACtB,uBAAuB;AAAA,MACzB,CAAC,EAAE,OAAO,CAAC;AAAA,IACb,QAAQ;AACN,aAAO,GAAG,QAAQ,IAAI,KAAK,MAAM,CAAC,EAAE,eAAe,CAAC;AAAA,IACtD;AAAA,EACF;AACF;AAKO,MAAM,UAAU,CAAC,MACtB,IAAI,KAAK,aAAa,SAAS;AAAA,EAC7B,UAAU;AAAA,EACV,uBAAuB;AACzB,CAAC,EAAE,OAAO,CAAC;AAEN,MAAM,YAAY,CAAC,QACxB,IAAI,KAAK,eAAe,MAAM,EAAE,OAAO,SAAS,KAAK,UAAU,CAAC,EAAE;AAAA,EAChE,IAAI,KAAK,GAAG;AACd;AAGK,MAAM,WAAW,CAAC,MAAc;AACrC,QAAM,IAAI,EAAE,QAAQ,UAAU,GAAG,EAAE,KAAK;AACxC,SAAO,IAAI,EAAE,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,IAAI;AAC/C;AAKA,MAAM,UAAoC;AAAA,EACxC,WAAW;AAAA,EACX,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,uBAAuB;AAAA,EACvB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,oBAAoB;AACtB;AAEO,MAAM,SAAS,CAAC,WACrB,QAAQ,QAAQ,YAAY,CAAC,KAAK;AAS7B,SAAS,QAAQ,GAA8B;AACpD,MAAI,CAAC,EAAE,kBAAkB,EAAE,YAAY;AACrC,WAAO,EAAE,OAAO,eAAe,MAAM,WAAW,WAAW,KAAK;AAElE,QAAM,YAAY,EAAE,YAAY,EAAE,YAAY;AAE9C,MAAI,aAAa;AACf,WAAO,EAAE,OAAO,gBAAgB,MAAM,UAAU,UAAU;AAE5D,MAAI,EAAE,qBAAqB,QAAQ,aAAa,EAAE;AAChD,WAAO,EAAE,OAAO,YAAS,SAAS,IAAI,MAAM,WAAW,UAAU;AAEnE,SAAO;AAAA,IACL,OAAO,iBAAc,QAAQ,SAAS,CAAC;AAAA,IACvC,MAAM;AAAA,IACN;AAAA,EACF;AACF;","names":[]}
@@ -14,6 +14,7 @@ import { moduleOn } from "../../_shell/nav-modules";
14
14
  import { TenantOverview } from "./tenant-overview";
15
15
  import { TenantPeoplePanel } from "./tenant-people-panel";
16
16
  import { TenantWalletCard } from "./tenant-wallet-card";
17
+ import { useTenantWallet } from "./use-tenant-wallet";
17
18
  import { LedgerCurrencyFilter, MerchantLedger } from "./merchant-ledger";
18
19
  import { TenantSupport } from "./tenant-support";
19
20
  import { TenantVerification } from "./verification/tenant-verification";
@@ -61,6 +62,7 @@ function moneyCur(n, currency) {
61
62
  try {
62
63
  return new Intl.NumberFormat("en-US", {
63
64
  style: "currency",
65
+ currencyDisplay: "narrowSymbol",
64
66
  currency,
65
67
  maximumFractionDigits: 0
66
68
  }).format(n);
@@ -68,6 +70,10 @@ function moneyCur(n, currency) {
68
70
  return `${currency} ${n.toFixed(0)}`;
69
71
  }
70
72
  }
73
+ function sharedCurrency(rows) {
74
+ const codes = new Set(rows.map((r) => r.currency));
75
+ return codes.size === 1 ? [...codes][0] || null : null;
76
+ }
71
77
  function num(n) {
72
78
  return new Intl.NumberFormat("en-US").format(n ?? 0);
73
79
  }
@@ -153,6 +159,11 @@ function TenantDetailPage({
153
159
  startDate: range.startDate,
154
160
  endDate: range.endDate
155
161
  });
162
+ const { wallet, walletNote } = useTenantWallet(
163
+ id,
164
+ walletLedger,
165
+ data?.wallet
166
+ );
156
167
  const queryClient = useQueryClient();
157
168
  const statusMutation = useMutation({
158
169
  mutationFn: (status) => platformAdminApi.setTenantStatus(id, status),
@@ -169,7 +180,8 @@ function TenantDetailPage({
169
180
  return /* @__PURE__ */ jsx("p", { style: { color: "var(--error)" }, children: "Failed to load tenant." });
170
181
  }
171
182
  if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading tenant\u2026" });
172
- const currency = data.currency ?? "USD";
183
+ const view = { ...data, wallet };
184
+ const currency = data.currency ?? "NGN";
173
185
  const currencyControl = /* @__PURE__ */ jsx(
174
186
  CurrencyFilter,
175
187
  {
@@ -177,7 +189,7 @@ function TenantDetailPage({
177
189
  onChange: setShown,
178
190
  currencies: currenciesIn(
179
191
  data.currencies,
180
- data.wallet?.balances.map((b) => b.currency),
192
+ view.wallet?.balances.map((b) => b.currency),
181
193
  data.recentOrders.map((o) => o.currency)
182
194
  )
183
195
  }
@@ -208,7 +220,8 @@ function TenantDetailPage({
208
220
  tab === "overview" && /* @__PURE__ */ jsx(
209
221
  TenantOverview,
210
222
  {
211
- data,
223
+ data: view,
224
+ walletNote,
212
225
  periodLabel: periodPhrase(range),
213
226
  periodControl,
214
227
  recentOrders: /* @__PURE__ */ jsx(
@@ -265,7 +278,14 @@ function TenantDetailPage({
265
278
  ]
266
279
  }
267
280
  ),
268
- group === "wallet" && current !== "money" && /* @__PURE__ */ jsx(TenantWalletCard, { data, currency: shown }),
281
+ group === "wallet" && current !== "money" && /* @__PURE__ */ jsx(
282
+ TenantWalletCard,
283
+ {
284
+ data: view,
285
+ currency: shown,
286
+ walletNote
287
+ }
288
+ ),
269
289
  current === "money" && /* @__PURE__ */ jsx(
270
290
  MerchantLedger,
271
291
  {
@@ -1550,15 +1570,17 @@ function InvoicesTab({ id }) {
1550
1570
  if (isError) return /* @__PURE__ */ jsx(EmptyPanel, { children: "Failed to load invoices." });
1551
1571
  const rows = data?.invoices ?? [];
1552
1572
  const s = data?.stats;
1573
+ const invoiceCurrency = sharedCurrency(rows) ?? "NGN";
1574
+ const invoiceMoney = (n) => moneyCur(n, invoiceCurrency);
1553
1575
  const statItems = [
1554
1576
  {
1555
1577
  label: "Revenue",
1556
- value: moneyCur(s?.totalRevenue ?? 0, "USD"),
1578
+ value: invoiceMoney(s?.totalRevenue ?? 0),
1557
1579
  hint: "Total amount collected from invoices that have been paid."
1558
1580
  },
1559
1581
  {
1560
1582
  label: "Outstanding",
1561
- value: moneyCur(s?.totalOutstanding ?? 0, "USD"),
1583
+ value: invoiceMoney(s?.totalOutstanding ?? 0),
1562
1584
  hint: "Amount still owed across sent and overdue invoices that have not been paid."
1563
1585
  },
1564
1586
  {
@@ -1754,7 +1776,7 @@ function DiscountsTab({ id }) {
1754
1776
  /* @__PURE__ */ jsx("tbody", { children: rows.map((d) => /* @__PURE__ */ jsxs("tr", { children: [
1755
1777
  /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: d.code }),
1756
1778
  /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: d.name }),
1757
- /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: d.discountType === "percentage" ? `${d.value}%` : moneyCur(d.value, "USD") }),
1779
+ /* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: d.discountType === "percentage" ? `${d.value}%` : moneyCur(d.value, "NGN") }),
1758
1780
  /* @__PURE__ */ jsxs("td", { style: tableStyles.cellMuted, children: [
1759
1781
  num(d.usageCount),
1760
1782
  d.usageLimit ? ` / ${num(d.usageLimit)}` : ""
@@ -1779,7 +1801,7 @@ function SettlementsTab({ id, range }) {
1779
1801
  if (isError) return /* @__PURE__ */ jsx(EmptyPanel, { children: "Failed to load settlements." });
1780
1802
  if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading settlements\u2026" });
1781
1803
  const sum = data.summary;
1782
- const cur = sum.currency || "USD";
1804
+ const cur = sum.currency || "NGN";
1783
1805
  const banks = data.bankAccounts ?? [];
1784
1806
  const rows = data.settlements ?? [];
1785
1807
  const statItems = [
@@ -1869,7 +1891,7 @@ function SubscriptionsTab({ id }) {
1869
1891
  if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading subscriptions\u2026" });
1870
1892
  const stats = data.stats;
1871
1893
  const rows = data.subscriptions ?? [];
1872
- const mrrCurrency = rows[0]?.currency || "USD";
1894
+ const mrrCurrency = rows[0]?.currency || "NGN";
1873
1895
  const statItems = [
1874
1896
  {
1875
1897
  label: "Active",