@withpotter/pages-admin 0.13.0-next.101 → 0.13.0-next.103

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 (60) hide show
  1. package/dist/admin/_pos/tenant-operators.js +73 -0
  2. package/dist/admin/_pos/tenant-operators.js.map +1 -0
  3. package/dist/admin/_pos/tenant-pos-figures.js +82 -0
  4. package/dist/admin/_pos/tenant-pos-figures.js.map +1 -0
  5. package/dist/admin/_pos/use-pos.js +12 -0
  6. package/dist/admin/_pos/use-pos.js.map +1 -1
  7. package/dist/admin/hooks.js +19 -0
  8. package/dist/admin/hooks.js.map +1 -1
  9. package/dist/admin/pos/shifts-tab.js +15 -8
  10. package/dist/admin/pos/shifts-tab.js.map +1 -1
  11. package/dist/admin/revenue/cohort-panel.js +5 -5
  12. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  13. package/dist/admin/revenue/health-panel.js +17 -17
  14. package/dist/admin/revenue/health-panel.js.map +1 -1
  15. package/dist/admin/revenue/ledger-model.js +7 -0
  16. package/dist/admin/revenue/ledger-model.js.map +1 -1
  17. package/dist/admin/revenue/ledger.module.css +43 -0
  18. package/dist/admin/revenue/merchant-headline.js +37 -0
  19. package/dist/admin/revenue/merchant-headline.js.map +1 -0
  20. package/dist/admin/revenue/merchant-payments-view.js +46 -22
  21. package/dist/admin/revenue/merchant-payments-view.js.map +1 -1
  22. package/dist/admin/revenue/merchants-view.js +99 -0
  23. package/dist/admin/revenue/merchants-view.js.map +1 -0
  24. package/dist/admin/revenue/payment-drawer.js +53 -0
  25. package/dist/admin/revenue/payment-drawer.js.map +1 -0
  26. package/dist/admin/revenue/plan-mix-panel.js +1 -1
  27. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  28. package/dist/admin/revenue/plan-payment-drawer.js +42 -0
  29. package/dist/admin/revenue/plan-payment-drawer.js.map +1 -0
  30. package/dist/admin/revenue/print-summary-button.js +34 -0
  31. package/dist/admin/revenue/print-summary-button.js.map +1 -0
  32. package/dist/admin/revenue/quality-panel.js +19 -19
  33. package/dist/admin/revenue/quality-panel.js.map +1 -1
  34. package/dist/admin/revenue/revenue-kpis.js +64 -50
  35. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  36. package/dist/admin/revenue/revenue-overview.js +68 -50
  37. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  38. package/dist/admin/revenue/revenue-retention.js +17 -13
  39. package/dist/admin/revenue/revenue-retention.js.map +1 -1
  40. package/dist/admin/revenue/revenue-workspace.js +16 -0
  41. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  42. package/dist/admin/revenue/subscriptions-panel.js +14 -14
  43. package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
  44. package/dist/admin/revenue/subscriptions-view.js +20 -20
  45. package/dist/admin/revenue/subscriptions-view.js.map +1 -1
  46. package/dist/admin/revenue/summary-download.js +34 -0
  47. package/dist/admin/revenue/summary-download.js.map +1 -0
  48. package/dist/admin/revenue/summary-print.js +82 -0
  49. package/dist/admin/revenue/summary-print.js.map +1 -0
  50. package/dist/admin/revenue/summary-rows.js +44 -0
  51. package/dist/admin/revenue/summary-rows.js.map +1 -0
  52. package/dist/admin/revenue/top-tenants-panel.js +1 -1
  53. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  54. package/dist/admin/revenue/url-filters.js +36 -0
  55. package/dist/admin/revenue/url-filters.js.map +1 -0
  56. package/dist/admin/revenue/use-url-filters.js +24 -0
  57. package/dist/admin/revenue/use-url-filters.js.map +1 -0
  58. package/dist/admin/tenants/[id]/tenant-pos.js +53 -16
  59. package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
  60. package/package.json +4 -4
@@ -69,6 +69,12 @@ function downloadName(prefix, startDate, endDate) {
69
69
  const day = (iso) => iso ? iso.slice(0, 10) : "all";
70
70
  return `${prefix}_${day(startDate)}_${day(endDate)}.csv`;
71
71
  }
72
+ function plainAmount(minor) {
73
+ const text = String(minor);
74
+ const negative = text.startsWith("-");
75
+ const digits = text.replace("-", "").padStart(3, "0");
76
+ return `${negative ? "-" : ""}${digits.slice(0, -2)}.${digits.slice(-2)}`;
77
+ }
72
78
  export {
73
79
  METHOD_ORDER,
74
80
  PURPOSE_WORDS,
@@ -79,6 +85,7 @@ export {
79
85
  methodCards,
80
86
  minorToMoney,
81
87
  pctChange,
88
+ plainAmount,
82
89
  word
83
90
  };
84
91
  //# sourceMappingURL=ledger-model.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/ledger-model.ts"],"sourcesContent":["import type { GatewayChannelTotal } from '@withpotter/lib/admin-api';\nimport { formatMoney } from '../_ui/money-view';\n\n/** A kobo amount (a string, so no float is involved on the server) shown as naira. */\nexport function minorToMoney(minor: string | number, currency: string): string {\n return formatMoney(Number(minor) / 100, currency, 2);\n}\n\n/** Percent change from before to now; null when there was nothing before to compare with. */\nexport function pctChange(\n now: string | number | null | undefined,\n before: string | number | null | undefined,\n): number | null {\n const a = Number(now ?? 0);\n const b = Number(before ?? 0);\n if (b === 0) return null;\n\n return Number((((a - b) / Math.abs(b)) * 100).toFixed(1));\n}\n\n/** The four ways a customer can pay at the checkout, in the order the checkout shows them. */\nexport const METHOD_ORDER = ['card', 'ussd', 'transfer', 'opay'] as const;\n\nexport interface MethodCardRow {\n key: string;\n label: string;\n payments: number;\n amountMinor: string;\n /** Share of all money received in this currency, in percent. */\n share: number;\n /** Change in amount since the period before; null when there is no comparison. */\n change: number | null;\n}\n\n/**\n * One card per way of paying, for one currency. The four checkout methods are always there, with zero when nobody\n * used one, so a quiet method is visible rather than missing. Any other way found in the data follows.\n */\nexport function methodCards(\n now: readonly GatewayChannelTotal[],\n before: readonly GatewayChannelTotal[] | null,\n currency: string,\n): MethodCardRow[] {\n const pick = (rows: readonly GatewayChannelTotal[] | null) => (rows ?? []).filter((r) => r.currency === currency);\n const current = pick(now);\n const total = current.reduce((sum, r) => sum + BigInt(r.amountMinor), BigInt(0));\n const labels: Record<string, string> = { card: 'Card', ussd: 'USSD', transfer: 'Bank transfer', opay: 'Opay' };\n\n const keys = [\n ...METHOD_ORDER,\n ...current.map((r) => r.channel.key).filter((k) => !(METHOD_ORDER as readonly string[]).includes(k)),\n ];\n\n return [...new Set(keys)].map((key) => {\n const mine = current.find((r) => r.channel.key === key);\n const prior = pick(before).find((r) => r.channel.key === key);\n const amount = BigInt(mine?.amountMinor ?? '0');\n\n return {\n key,\n label: mine?.channel.label ?? labels[key] ?? key,\n payments: mine?.count ?? 0,\n amountMinor: amount.toString(),\n share: total > BigInt(0) ? Number(((Number(amount) / Number(total)) * 100).toFixed(1)) : 0,\n change: before ? pctChange(amount.toString(), prior?.amountMinor ?? '0') : null,\n };\n });\n}\n\n/** Words an accountant would use, not the database's. */\nexport const STATUS_WORDS: Record<string, string> = {\n success: 'Paid',\n pending: 'Waiting',\n failed: 'Failed',\n refunded: 'Refunded',\n cancelled: 'Cancelled',\n abandoned: 'Abandoned',\n succeeded: 'Paid',\n};\n\nexport const TYPE_WORDS: Record<string, string> = {\n order: 'Order',\n invoice: 'Invoice',\n booking: 'Booking',\n membership: 'Membership',\n subscription: 'Subscription',\n deposit: 'Deposit',\n tip: 'Tip',\n refund: 'Refund',\n payout: 'Payout',\n};\n\nexport const PURPOSE_WORDS: Record<string, string> = {\n select_plan: 'First payment',\n upgrade: 'Upgrade',\n renewal: 'Renewal',\n reactivation: 'Came back',\n mandate_setup: 'Card saved',\n};\n\nexport const SUBSCRIPTION_METHOD_WORDS: Record<string, string> = {\n gateway: 'Card or bank',\n wallet: 'Wallet',\n manual: 'Recorded by hand',\n};\n\nexport const word = (words: Record<string, string>, key: string): string => words[key] ?? key.replace(/_/g, ' ');\n\n/** The dates in a download's file name, so a file says what it holds. */\nexport function downloadName(prefix: string, startDate?: string, endDate?: string): string {\n const day = (iso?: string) => (iso ? iso.slice(0, 10) : 'all');\n\n return `${prefix}_${day(startDate)}_${day(endDate)}.csv`;\n}\n"],"mappings":"AACA,SAAS,mBAAmB;AAGrB,SAAS,aAAa,OAAwB,UAA0B;AAC7E,SAAO,YAAY,OAAO,KAAK,IAAI,KAAK,UAAU,CAAC;AACrD;AAGO,SAAS,UACd,KACA,QACe;AACf,QAAM,IAAI,OAAO,OAAO,CAAC;AACzB,QAAM,IAAI,OAAO,UAAU,CAAC;AAC5B,MAAI,MAAM,EAAG,QAAO;AAEpB,SAAO,SAAU,IAAI,KAAK,KAAK,IAAI,CAAC,IAAK,KAAK,QAAQ,CAAC,CAAC;AAC1D;AAGO,MAAM,eAAe,CAAC,QAAQ,QAAQ,YAAY,MAAM;AAiBxD,SAAS,YACd,KACA,QACA,UACiB;AACjB,QAAM,OAAO,CAAC,UAAiD,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,QAAQ;AAChH,QAAM,UAAU,KAAK,GAAG;AACxB,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC;AAC/E,QAAM,SAAiC,EAAE,MAAM,QAAQ,MAAM,QAAQ,UAAU,iBAAiB,MAAM,OAAO;AAE7G,QAAM,OAAO;AAAA,IACX,GAAG;AAAA,IACH,GAAG,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,EAAE,OAAO,CAAC,MAAM,CAAE,aAAmC,SAAS,CAAC,CAAC;AAAA,EACrG;AAEA,SAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ;AACrC,UAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,QAAQ,GAAG;AACtD,UAAM,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,QAAQ,GAAG;AAC5D,UAAM,SAAS,OAAO,MAAM,eAAe,GAAG;AAE9C,WAAO;AAAA,MACL;AAAA,MACA,OAAO,MAAM,QAAQ,SAAS,OAAO,GAAG,KAAK;AAAA,MAC7C,UAAU,MAAM,SAAS;AAAA,MACzB,aAAa,OAAO,SAAS;AAAA,MAC7B,OAAO,QAAQ,OAAO,CAAC,IAAI,QAAS,OAAO,MAAM,IAAI,OAAO,KAAK,IAAK,KAAK,QAAQ,CAAC,CAAC,IAAI;AAAA,MACzF,QAAQ,SAAS,UAAU,OAAO,SAAS,GAAG,OAAO,eAAe,GAAG,IAAI;AAAA,IAC7E;AAAA,EACF,CAAC;AACH;AAGO,MAAM,eAAuC;AAAA,EAClD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AACb;AAEO,MAAM,aAAqC;AAAA,EAChD,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,SAAS;AAAA,EACT,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,QAAQ;AACV;AAEO,MAAM,gBAAwC;AAAA,EACnD,aAAa;AAAA,EACb,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAc;AAAA,EACd,eAAe;AACjB;AAEO,MAAM,4BAAoD;AAAA,EAC/D,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AACV;AAEO,MAAM,OAAO,CAAC,OAA+B,QAAwB,MAAM,GAAG,KAAK,IAAI,QAAQ,MAAM,GAAG;AAGxG,SAAS,aAAa,QAAgB,WAAoB,SAA0B;AACzF,QAAM,MAAM,CAAC,QAAkB,MAAM,IAAI,MAAM,GAAG,EAAE,IAAI;AAExD,SAAO,GAAG,MAAM,IAAI,IAAI,SAAS,CAAC,IAAI,IAAI,OAAO,CAAC;AACpD;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/ledger-model.ts"],"sourcesContent":["import type { GatewayChannelTotal } from '@withpotter/lib/admin-api';\nimport { formatMoney } from '../_ui/money-view';\n\n/** A kobo amount (a string, so no float is involved on the server) shown as naira. */\nexport function minorToMoney(minor: string | number, currency: string): string {\n return formatMoney(Number(minor) / 100, currency, 2);\n}\n\n/** Percent change from before to now; null when there was nothing before to compare with. */\nexport function pctChange(\n now: string | number | null | undefined,\n before: string | number | null | undefined,\n): number | null {\n const a = Number(now ?? 0);\n const b = Number(before ?? 0);\n if (b === 0) return null;\n\n return Number((((a - b) / Math.abs(b)) * 100).toFixed(1));\n}\n\n/** The four ways a customer can pay at the checkout, in the order the checkout shows them. */\nexport const METHOD_ORDER = ['card', 'ussd', 'transfer', 'opay'] as const;\n\nexport interface MethodCardRow {\n key: string;\n label: string;\n payments: number;\n amountMinor: string;\n /** Share of all money received in this currency, in percent. */\n share: number;\n /** Change in amount since the period before; null when there is no comparison. */\n change: number | null;\n}\n\n/**\n * One card per way of paying, for one currency. The four checkout methods are always there, with zero when nobody\n * used one, so a quiet method is visible rather than missing. Any other way found in the data follows.\n */\nexport function methodCards(\n now: readonly GatewayChannelTotal[],\n before: readonly GatewayChannelTotal[] | null,\n currency: string,\n): MethodCardRow[] {\n const pick = (rows: readonly GatewayChannelTotal[] | null) => (rows ?? []).filter((r) => r.currency === currency);\n const current = pick(now);\n const total = current.reduce((sum, r) => sum + BigInt(r.amountMinor), BigInt(0));\n const labels: Record<string, string> = { card: 'Card', ussd: 'USSD', transfer: 'Bank transfer', opay: 'Opay' };\n\n const keys = [\n ...METHOD_ORDER,\n ...current.map((r) => r.channel.key).filter((k) => !(METHOD_ORDER as readonly string[]).includes(k)),\n ];\n\n return [...new Set(keys)].map((key) => {\n const mine = current.find((r) => r.channel.key === key);\n const prior = pick(before).find((r) => r.channel.key === key);\n const amount = BigInt(mine?.amountMinor ?? '0');\n\n return {\n key,\n label: mine?.channel.label ?? labels[key] ?? key,\n payments: mine?.count ?? 0,\n amountMinor: amount.toString(),\n share: total > BigInt(0) ? Number(((Number(amount) / Number(total)) * 100).toFixed(1)) : 0,\n change: before ? pctChange(amount.toString(), prior?.amountMinor ?? '0') : null,\n };\n });\n}\n\n/** Words an accountant would use, not the database's. */\nexport const STATUS_WORDS: Record<string, string> = {\n success: 'Paid',\n pending: 'Waiting',\n failed: 'Failed',\n refunded: 'Refunded',\n cancelled: 'Cancelled',\n abandoned: 'Abandoned',\n succeeded: 'Paid',\n};\n\nexport const TYPE_WORDS: Record<string, string> = {\n order: 'Order',\n invoice: 'Invoice',\n booking: 'Booking',\n membership: 'Membership',\n subscription: 'Subscription',\n deposit: 'Deposit',\n tip: 'Tip',\n refund: 'Refund',\n payout: 'Payout',\n};\n\nexport const PURPOSE_WORDS: Record<string, string> = {\n select_plan: 'First payment',\n upgrade: 'Upgrade',\n renewal: 'Renewal',\n reactivation: 'Came back',\n mandate_setup: 'Card saved',\n};\n\nexport const SUBSCRIPTION_METHOD_WORDS: Record<string, string> = {\n gateway: 'Card or bank',\n wallet: 'Wallet',\n manual: 'Recorded by hand',\n};\n\nexport const word = (words: Record<string, string>, key: string): string => words[key] ?? key.replace(/_/g, ' ');\n\n/** The dates in a download's file name, so a file says what it holds. */\nexport function downloadName(prefix: string, startDate?: string, endDate?: string): string {\n const day = (iso?: string) => (iso ? iso.slice(0, 10) : 'all');\n\n return `${prefix}_${day(startDate)}_${day(endDate)}.csv`;\n}\n\n/** Kobo as plain text a spreadsheet reads (\"1234.50\"), with no currency sign or commas. */\nexport function plainAmount(minor: string | number): string {\n const text = String(minor);\n const negative = text.startsWith('-');\n const digits = text.replace('-', '').padStart(3, '0');\n\n return `${negative ? '-' : ''}${digits.slice(0, -2)}.${digits.slice(-2)}`;\n}\n"],"mappings":"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","names":[]}
@@ -99,3 +99,46 @@
99
99
  line-height: 1.5;
100
100
  color: var(--text-secondary);
101
101
  }
102
+
103
+ .drawerBody {
104
+ display: grid;
105
+ gap: 16px;
106
+ padding: 4px 0 24px;
107
+ }
108
+
109
+ .drawerHeading {
110
+ margin: 8px 0 0;
111
+ font-size: 13px;
112
+ font-weight: 600;
113
+ color: var(--text-primary);
114
+ }
115
+
116
+ .methodBar {
117
+ display: flex;
118
+ height: 6px;
119
+ min-width: 120px;
120
+ overflow: hidden;
121
+ border-radius: 999px;
122
+ background: var(--bg-subtle, rgba(0, 0, 0, 0.06));
123
+ }
124
+
125
+ .methodSeg {
126
+ height: 100%;
127
+ }
128
+
129
+ .methodText {
130
+ margin-top: 4px;
131
+ font-size: 12px;
132
+ color: var(--text-tertiary);
133
+ }
134
+
135
+ .clickRow {
136
+ cursor: pointer;
137
+ }
138
+
139
+ .subTitle {
140
+ margin: 20px 0 8px;
141
+ font-size: 14px;
142
+ font-weight: 600;
143
+ color: var(--text-primary);
144
+ }
@@ -0,0 +1,37 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { CountUp, Reveal } from "../_fx";
3
+ import { StatCard } from "../_ui";
4
+ import { minorToMoney, pctChange } from "./ledger-model";
5
+ function MerchantHeadline({ ledger, focus }) {
6
+ const now = ledger.cards.current.find((card) => card.currency === focus);
7
+ const before = ledger.cards.previous?.find((card) => card.currency === focus);
8
+ if (!now) return null;
9
+ const money = (minor) => minorToMoney(minor, focus);
10
+ const delta = (a, b) => before ? { pct: pctChange(a, b), caption: "vs the period before" } : void 0;
11
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
12
+ /* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
13
+ StatCard,
14
+ {
15
+ label: "Money received",
16
+ value: /* @__PURE__ */ jsx(CountUp, { value: Number(now.grossMinor) / 100, format: (v) => money(String(Math.round(v * 100))) }),
17
+ sub: `${now.payments} payments that went through`,
18
+ hint: "Everything customers paid to merchants that went through, before any fees are taken.",
19
+ delta: delta(now.grossMinor, before?.grossMinor)
20
+ }
21
+ ) }),
22
+ /* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
23
+ StatCard,
24
+ {
25
+ label: "Merchants keep",
26
+ value: money(now.netMinor),
27
+ sub: `After ${money(now.feeMinor)} in fees`,
28
+ hint: "What is left for merchants once the fees are taken: money received minus fees.",
29
+ delta: delta(now.netMinor, before?.netMinor)
30
+ }
31
+ ) })
32
+ ] });
33
+ }
34
+ export {
35
+ MerchantHeadline
36
+ };
37
+ //# sourceMappingURL=merchant-headline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/merchant-headline.tsx"],"sourcesContent":["import type { MerchantPaymentsPage } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal } from '../_fx';\nimport { StatCard } from '../_ui';\nimport { minorToMoney, pctChange } from './ledger-model';\n\n/**\n * Two more cards for the merchant group on the Overview: the money customers paid that went through, and what\n * merchants are left with after fees, each against the period before. Nothing shows when the box has no payments view.\n */\nexport function MerchantHeadline({ ledger, focus }: { ledger: MerchantPaymentsPage; focus: string }) {\n const now = ledger.cards.current.find((card) => card.currency === focus);\n const before = ledger.cards.previous?.find((card) => card.currency === focus);\n\n if (!now) return null;\n\n const money = (minor: string) => minorToMoney(minor, focus);\n const delta = (a: string, b?: string) =>\n before ? { pct: pctChange(a, b), caption: 'vs the period before' } : undefined;\n\n return (\n <>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Money received\"\n value={<CountUp value={Number(now.grossMinor) / 100} format={(v) => money(String(Math.round(v * 100)))} />}\n sub={`${now.payments} payments that went through`}\n hint=\"Everything customers paid to merchants that went through, before any fees are taken.\"\n delta={delta(now.grossMinor, before?.grossMinor)}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants keep\"\n value={money(now.netMinor)}\n sub={`After ${money(now.feeMinor)} in fees`}\n hint=\"What is left for merchants once the fees are taken: money received minus fees.\"\n delta={delta(now.netMinor, before?.netMinor)}\n />\n </Reveal>\n </>\n );\n}\n"],"mappings":"AAoBI,mBAIa,KAJb;AAnBJ,SAAS,SAAS,cAAc;AAChC,SAAS,gBAAgB;AACzB,SAAS,cAAc,iBAAiB;AAMjC,SAAS,iBAAiB,EAAE,QAAQ,MAAM,GAAoD;AACnG,QAAM,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,KAAK,aAAa,KAAK;AACvE,QAAM,SAAS,OAAO,MAAM,UAAU,KAAK,CAAC,SAAS,KAAK,aAAa,KAAK;AAE5E,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,QAAQ,CAAC,UAAkB,aAAa,OAAO,KAAK;AAC1D,QAAM,QAAQ,CAAC,GAAW,MACxB,SAAS,EAAE,KAAK,UAAU,GAAG,CAAC,GAAG,SAAS,uBAAuB,IAAI;AAEvE,SACE,iCACE;AAAA,wBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,oBAAC,WAAQ,OAAO,OAAO,IAAI,UAAU,IAAI,KAAK,QAAQ,CAAC,MAAM,MAAM,OAAO,KAAK,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG;AAAA,QACxG,KAAK,GAAG,IAAI,QAAQ;AAAA,QACpB,MAAK;AAAA,QACL,OAAO,MAAM,IAAI,YAAY,QAAQ,UAAU;AAAA;AAAA,IACjD,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,MAAM,IAAI,QAAQ;AAAA,QACzB,KAAK,SAAS,MAAM,IAAI,QAAQ,CAAC;AAAA,QACjC,MAAK;AAAA,QACL,OAAO,MAAM,IAAI,UAAU,QAAQ,QAAQ;AAAA;AAAA,IAC7C,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
6
6
  import { platformAdminApi } from "@withpotter/lib/admin-api";
7
- import { useMerchantPayments } from "../hooks";
7
+ import { useMerchantOptions, useMerchantPayments } from "../hooks";
8
8
  import { LoadFailed } from "../audit/load-failed";
9
9
  import { CountUp, Reveal } from "../_fx";
10
10
  import {
@@ -26,9 +26,12 @@ import {
26
26
  word
27
27
  } from "./ledger-model";
28
28
  import { MethodCards } from "./method-cards";
29
+ import { PaymentDrawer } from "./payment-drawer";
30
+ import { useUrlFilters } from "./use-url-filters";
29
31
  import styles from "./ledger.module.css";
30
32
  import table from "./revenue.module.css";
31
33
  const PAGE_SIZE = 25;
34
+ const FILTERS = { q: "", method: "", status: "", type: "", merchant: "" };
32
35
  const integer = new Intl.NumberFormat("en-US");
33
36
  const METHODS = [
34
37
  { value: "", label: "Every way of paying" },
@@ -56,24 +59,34 @@ function MerchantPaymentsView({
56
59
  endDate,
57
60
  currency
58
61
  }) {
59
- const [search, setSearch] = useState("");
60
- const [q, setQ] = useState("");
61
- const [method, setMethod] = useState("");
62
- const [status, setStatus] = useState("");
63
- const [type, setType] = useState("");
64
- const [page, setPage] = useState(1);
62
+ const { values, page, set } = useUrlFilters("mp", FILTERS);
63
+ const [search, setSearch] = useState(values.q);
64
+ const [open, setOpen] = useState(null);
65
65
  useEffect(() => {
66
- const timer = setTimeout(() => setQ(search.trim()), 300);
66
+ const timer = setTimeout(() => {
67
+ if (search.trim() !== values.q) set({ q: search.trim() });
68
+ }, 300);
67
69
  return () => clearTimeout(timer);
68
70
  }, [search]);
69
71
  const filters = useMemo(
70
- () => ({ startDate, endDate, method, status, type, q, page, limit: PAGE_SIZE }),
71
- [startDate, endDate, method, status, type, q, page]
72
+ () => ({
73
+ startDate,
74
+ endDate,
75
+ method: values.method,
76
+ status: values.status,
77
+ type: values.type,
78
+ q: values.q,
79
+ merchantId: values.merchant,
80
+ page,
81
+ limit: PAGE_SIZE
82
+ }),
83
+ [startDate, endDate, values, page]
72
84
  );
73
- const change = (set) => (value) => {
74
- set(value);
75
- setPage(1);
76
- };
85
+ const merchants = useMerchantOptions({ startDate, endDate });
86
+ const merchantChoices = [
87
+ { value: "", label: "Every merchant" },
88
+ ...(merchants.data?.merchants ?? []).map((one) => ({ value: one.id, label: one.name }))
89
+ ];
77
90
  const { data, isLoading, isError, refetch } = useMerchantPayments(filters);
78
91
  if (isError) {
79
92
  return /* @__PURE__ */ jsx(
@@ -144,14 +157,24 @@ function MerchantPaymentsView({
144
157
  FilterSearch,
145
158
  {
146
159
  value: search,
147
- onChange: change(setSearch),
160
+ onChange: setSearch,
148
161
  placeholder: "Search a name, email, phone, reference or merchant",
149
162
  label: "Search payments"
150
163
  }
151
164
  ),
152
- /* @__PURE__ */ jsx(FilterSelect, { value: method, onChange: change(setMethod), options: METHODS, label: "Way of paying" }),
153
- /* @__PURE__ */ jsx(FilterSelect, { value: status, onChange: change(setStatus), options: STATUSES, label: "Payment result" }),
154
- /* @__PURE__ */ jsx(FilterSelect, { value: type, onChange: change(setType), options: TYPES, label: "What it was for" })
165
+ /* @__PURE__ */ jsx(
166
+ FilterSelect,
167
+ {
168
+ value: values.merchant,
169
+ onChange: (merchant) => set({ merchant }),
170
+ options: merchantChoices,
171
+ label: "Merchant",
172
+ searchable: true
173
+ }
174
+ ),
175
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.method, onChange: (method) => set({ method }), options: METHODS, label: "Way of paying" }),
176
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.status, onChange: (status) => set({ status }), options: STATUSES, label: "Payment result" }),
177
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.type, onChange: (type) => set({ type }), options: TYPES, label: "What it was for" })
155
178
  ] }),
156
179
  /* @__PURE__ */ jsx(
157
180
  DownloadButton,
@@ -197,17 +220,18 @@ function MerchantPaymentsView({
197
220
  /* @__PURE__ */ jsx(Th, { align: "right", children: "Fee" }),
198
221
  /* @__PURE__ */ jsx(Th, { align: "right", children: "Merchant gets" })
199
222
  ] }),
200
- children: data.rows.map((row) => /* @__PURE__ */ jsx(PaymentRow, { row }, row.id))
223
+ children: data.rows.map((row) => /* @__PURE__ */ jsx(PaymentRow, { row, onOpen: setOpen }, row.id))
201
224
  }
202
225
  ),
203
- /* @__PURE__ */ jsx(Pagination, { page, totalPages, total: data.total, noun: "payments", onPage: setPage })
226
+ /* @__PURE__ */ jsx(Pagination, { page, totalPages, total: data.total, noun: "payments", onPage: (next) => set({ page: next }) }),
227
+ /* @__PURE__ */ jsx(PaymentDrawer, { row: open, onClose: () => setOpen(null) })
204
228
  ] });
205
229
  }
206
- function PaymentRow({ row }) {
230
+ function PaymentRow({ row, onOpen }) {
207
231
  const { payer } = row;
208
232
  const contact = [payer.email, payer.phone].filter(Boolean).join(" \xB7 ");
209
233
  const money = (minor) => minorToMoney(minor, row.currency);
210
- return /* @__PURE__ */ jsxs(TableRow, { children: [
234
+ return /* @__PURE__ */ jsxs(TableRow, { onClick: () => onOpen(row), children: [
211
235
  /* @__PURE__ */ jsx("td", { className: table.num, children: new Date(row.paidAt).toLocaleString(void 0, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) }),
212
236
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
213
237
  /* @__PURE__ */ jsx("span", { className: styles.whoName, children: payer.name || "Name not recorded" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/merchant-payments-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { platformAdminApi, type MerchantPaymentRow } from '@withpotter/lib/admin-api';\nimport { useMerchantPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport {\n AdmLoading,\n FilterSearch,\n FilterSelect,\n Filters,\n Pagination,\n StatCard,\n focusCurrency,\n} from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n STATUS_WORDS,\n TYPE_WORDS,\n downloadName,\n minorToMoney,\n pctChange,\n word,\n} from './ledger-model';\nimport { MethodCards } from './method-cards';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst integer = new Intl.NumberFormat('en-US');\n\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'card', label: 'Card' },\n { value: 'ussd', label: 'USSD' },\n { value: 'transfer', label: 'Bank transfer' },\n { value: 'opay', label: 'Opay' },\n];\nconst STATUSES = [\n { value: '', label: 'Paid' },\n { value: 'pending', label: 'Waiting' },\n { value: 'failed', label: 'Failed' },\n { value: 'refunded', label: 'Refunded' },\n];\nconst TYPES = [\n { value: '', label: 'Every kind' },\n ...['order', 'invoice', 'booking', 'membership', 'subscription', 'deposit', 'tip', 'refund'].map((value) => ({\n value,\n label: word(TYPE_WORDS, value),\n })),\n];\n\nconst tone = (status: string) =>\n status === 'success' ? styles.statusGood : status === 'failed' ? styles.statusBad : status === 'pending' ? styles.statusWait : styles.statusMuted;\n\n/** The merchant payments view: who paid, how, and which merchant received it. */\nexport function MerchantPaymentsView({\n startDate,\n endDate,\n currency,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the only one there is. */\n currency: string;\n}) {\n const [search, setSearch] = useState('');\n const [q, setQ] = useState('');\n const [method, setMethod] = useState('');\n const [status, setStatus] = useState('');\n const [type, setType] = useState('');\n const [page, setPage] = useState(1);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => setQ(search.trim()), 300);\n\n return () => clearTimeout(timer);\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, method, status, type, q, page, limit: PAGE_SIZE }),\n [startDate, endDate, method, status, type, q, page],\n );\n // Any change of what is shown starts again from the first page.\n const change = <T,>(set: (v: T) => void) => (value: T) => {\n set(value);\n setPage(1);\n };\n\n const { data, isLoading, isError, refetch } = useMerchantPayments(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"Merchant payments could not be loaded. This box may not have them yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchant payments…\" />;\n\n const currencies = [...new Set(data.cards.current.map((c) => c.currency))];\n const focus = focusCurrency(currencies, currency) ?? 'NGN';\n const now = data.cards.current.find((c) => c.currency === focus);\n const before = data.cards.previous?.find((c) => c.currency === focus) ?? null;\n const strip = data.strip.find((s) => s.currency === focus);\n const money = (minor: string | number) => minorToMoney(minor, focus);\n const delta = (a?: string, b?: string, goodWhenUp = true) =>\n before ? { pct: pctChange(a, b), positiveIsGood: goodWhenUp, caption: 'vs the period before' } : undefined;\n const totalPages = Math.max(Math.ceil(data.total / PAGE_SIZE), 1);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that customers paid to merchants. Use the filters to narrow it down, and download exactly what you\n see.\n </p>\n\n <section aria-label=\"Merchant payments at a glance\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Money received\"\n value={<CountUp value={Number(now?.grossMinor ?? 0) / 100} format={(v) => money(Math.round(v * 100))} />}\n sub={now ? `${integer.format(now.payments)} payments that went through` : 'No payments in this period'}\n hint=\"Everything customers paid to merchants that went through, before any fees are taken.\"\n delta={delta(now?.grossMinor, before?.grossMinor)}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Fees taken\"\n value={money(now?.feeMinor ?? 0)}\n hint=\"What the payment gateway and Olive kept from those payments.\"\n delta={delta(now?.feeMinor, before?.feeMinor, false)}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants keep\"\n value={money(now?.netMinor ?? 0)}\n hint=\"What is left for merchants after the fees: money received minus fees taken.\"\n delta={delta(now?.netMinor, before?.netMinor)}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Payments that worked\"\n value={now?.successRate == null ? '–' : `${now.successRate}%`}\n sub={now?.successRate == null ? 'Nothing was tried in this period' : `Refunded: ${money(now.refundsMinor)}`}\n hint=\"Out of every payment that was tried, the share that went through. A low number can mean a way of paying is broken.\"\n />\n </Reveal>\n </section>\n\n <h2 className={styles.sectionTitle}>How customers paid</h2>\n <p className={styles.sectionNote}>The ways of paying on the checkout, and how much came in through each.</p>\n <MethodCards now={data.byChannel.current} before={data.byChannel.previous} currency={focus} />\n\n <h2 className={styles.sectionTitle}>Every payment</h2>\n <div className={styles.toolbar}>\n <Filters label=\"Filter payments\">\n <FilterSearch\n value={search}\n onChange={change(setSearch)}\n placeholder=\"Search a name, email, phone, reference or merchant\"\n label=\"Search payments\"\n />\n <FilterSelect value={method} onChange={change(setMethod)} options={METHODS} label=\"Way of paying\" />\n <FilterSelect value={status} onChange={change(setStatus)} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={type} onChange={change(setType)} options={TYPES} label=\"What it was for\" />\n </Filters>\n <DownloadButton\n filename={downloadName('merchant-payments', startDate, endDate)}\n load={() => platformAdminApi.merchantPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {strip ? (\n <p className={styles.strip} aria-live=\"polite\">\n <span>\n Showing <strong>{integer.format(data.total)}</strong> {data.total === 1 ? 'payment' : 'payments'}\n </span>\n <span>\n <strong>{money(strip.amountMinor)}</strong> received\n </span>\n <span>\n <strong>{money(strip.feeMinor)}</strong> in fees\n </span>\n <span>\n <strong>{money(strip.netMinor)}</strong> to merchants\n </span>\n </p>\n ) : null}\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={980}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Paid by</Th>\n <Th>How</Th>\n <Th>Merchant</Th>\n <Th>For</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n <Th align=\"right\">Fee</Th>\n <Th align=\"right\">Merchant gets</Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <PaymentRow key={row.id} row={row} />\n ))}\n </TableShell>\n )}\n\n <Pagination page={page} totalPages={totalPages} total={data.total} noun=\"payments\" onPage={setPage} />\n </div>\n );\n}\n\nfunction PaymentRow({ row }: { row: MerchantPaymentRow }) {\n const { payer } = row;\n const contact = [payer.email, payer.phone].filter(Boolean).join(' · ');\n const money = (minor: string) => minorToMoney(minor, row.currency);\n\n return (\n <TableRow>\n <td className={table.num}>\n {new Date(row.paidAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{payer.name || 'Name not recorded'}</span>\n {contact ? <span className={styles.whoSub}>{contact}</span> : null}\n </span>\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.channel.label}</span>\n {row.methodDetail ? <span className={styles.whoSub}>{row.methodDetail}</span> : null}\n </span>\n </td>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td>{word(TYPE_WORDS, row.type)}</td>\n <td>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n </td>\n <td className={table.numStrong}>{money(row.amountMinor)}</td>\n <td className={table.num}>{money(row.feeMinor)}</td>\n <td className={table.num}>{money(row.netMinor)}</td>\n </TableRow>\n );\n}\n"],"mappings":";AAiGM,cAyBA,YAzBA;AA/FN,SAAS,WAAW,SAAS,gBAAgB;AAC7C,OAAO,UAAU;AACjB,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,wBAAiD;AAC1D,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,SAAS,cAAc;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAmB;AAC5B,OAAO,YAAY;AACnB,OAAO,WAAW;AAElB,MAAM,YAAY;AAClB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAE7C,MAAM,UAAU;AAAA,EACd,EAAE,OAAO,IAAI,OAAO,sBAAsB;AAAA,EAC1C,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B,EAAE,OAAO,YAAY,OAAO,gBAAgB;AAAA,EAC5C,EAAE,OAAO,QAAQ,OAAO,OAAO;AACjC;AACA,MAAM,WAAW;AAAA,EACf,EAAE,OAAO,IAAI,OAAO,OAAO;AAAA,EAC3B,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,YAAY,OAAO,WAAW;AACzC;AACA,MAAM,QAAQ;AAAA,EACZ,EAAE,OAAO,IAAI,OAAO,aAAa;AAAA,EACjC,GAAG,CAAC,SAAS,WAAW,WAAW,cAAc,gBAAgB,WAAW,OAAO,QAAQ,EAAE,IAAI,CAAC,WAAW;AAAA,IAC3G;AAAA,IACA,OAAO,KAAK,YAAY,KAAK;AAAA,EAC/B,EAAE;AACJ;AAEA,MAAM,OAAO,CAAC,WACZ,WAAW,YAAY,OAAO,aAAa,WAAW,WAAW,OAAO,YAAY,WAAW,YAAY,OAAO,aAAa,OAAO;AAGjI,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE;AAC7B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAGlC,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW,MAAM,KAAK,OAAO,KAAK,CAAC,GAAG,GAAG;AAEvD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU;AAAA,IACd,OAAO,EAAE,WAAW,SAAS,QAAQ,QAAQ,MAAM,GAAG,MAAM,OAAO,UAAU;AAAA,IAC7E,CAAC,WAAW,SAAS,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAAA,EACpD;AAEA,QAAM,SAAS,CAAK,QAAwB,CAAC,UAAa;AACxD,QAAI,KAAK;AACT,YAAQ,CAAC;AAAA,EACX;AAEA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,oBAAoB,OAAO;AAEzE,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAAA,EAEJ;AACA,MAAI,aAAa,CAAC,KAAM,QAAO,oBAAC,cAAW,OAAM,mCAA6B;AAE9E,QAAM,aAAa,CAAC,GAAG,IAAI,IAAI,KAAK,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACzE,QAAM,QAAQ,cAAc,YAAY,QAAQ,KAAK;AACrD,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK;AAC/D,QAAM,SAAS,KAAK,MAAM,UAAU,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK,KAAK;AACzE,QAAM,QAAQ,KAAK,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK;AACzD,QAAM,QAAQ,CAAC,UAA2B,aAAa,OAAO,KAAK;AACnE,QAAM,QAAQ,CAAC,GAAY,GAAY,aAAa,SAClD,SAAS,EAAE,KAAK,UAAU,GAAG,CAAC,GAAG,gBAAgB,YAAY,SAAS,uBAAuB,IAAI;AACnG,QAAM,aAAa,KAAK,IAAI,KAAK,KAAK,KAAK,QAAQ,SAAS,GAAG,CAAC;AAEhE,SACE,qBAAC,SACC;AAAA,wBAAC,OAAE,WAAW,OAAO,MAAM,mIAG3B;AAAA,IAEA,qBAAC,aAAQ,cAAW,iCAAgC,WAAU,YAC5D;AAAA,0BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,oBAAC,WAAQ,OAAO,OAAO,KAAK,cAAc,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG;AAAA,UACtG,KAAK,MAAM,GAAG,QAAQ,OAAO,IAAI,QAAQ,CAAC,gCAAgC;AAAA,UAC1E,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,YAAY,QAAQ,UAAU;AAAA;AAAA,MAClD,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,MAAM,KAAK,YAAY,CAAC;AAAA,UAC/B,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,UAAU,QAAQ,UAAU,KAAK;AAAA;AAAA,MACrD,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,MAAM,KAAK,YAAY,CAAC;AAAA,UAC/B,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,UAAU,QAAQ,QAAQ;AAAA;AAAA,MAC9C,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK,eAAe,OAAO,WAAM,GAAG,IAAI,WAAW;AAAA,UAC1D,KAAK,KAAK,eAAe,OAAO,qCAAqC,aAAa,MAAM,IAAI,YAAY,CAAC;AAAA,UACzG,MAAK;AAAA;AAAA,MACP,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,QAAG,WAAW,OAAO,cAAc,gCAAkB;AAAA,IACtD,oBAAC,OAAE,WAAW,OAAO,aAAa,oFAAsE;AAAA,IACxG,oBAAC,eAAY,KAAK,KAAK,UAAU,SAAS,QAAQ,KAAK,UAAU,UAAU,UAAU,OAAO;AAAA,IAE5F,oBAAC,QAAG,WAAW,OAAO,cAAc,2BAAa;AAAA,IACjD,qBAAC,SAAI,WAAW,OAAO,SACrB;AAAA,2BAAC,WAAQ,OAAM,mBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU,OAAO,SAAS;AAAA,YAC1B,aAAY;AAAA,YACZ,OAAM;AAAA;AAAA,QACR;AAAA,QACA,oBAAC,gBAAa,OAAO,QAAQ,UAAU,OAAO,SAAS,GAAG,SAAS,SAAS,OAAM,iBAAgB;AAAA,QAClG,oBAAC,gBAAa,OAAO,QAAQ,UAAU,OAAO,SAAS,GAAG,SAAS,UAAU,OAAM,kBAAiB;AAAA,QACpG,oBAAC,gBAAa,OAAO,MAAM,UAAU,OAAO,OAAO,GAAG,SAAS,OAAO,OAAM,mBAAkB;AAAA,SAChG;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,UAAU,aAAa,qBAAqB,WAAW,OAAO;AAAA,UAC9D,MAAM,MAAM,iBAAiB,oBAAoB,EAAE,GAAG,SAAS,MAAM,QAAW,OAAO,OAAU,CAAC;AAAA;AAAA,MACpG;AAAA,OACF;AAAA,IAEC,QACC,qBAAC,OAAE,WAAW,OAAO,OAAO,aAAU,UACpC;AAAA,2BAAC,UAAK;AAAA;AAAA,QACI,oBAAC,YAAQ,kBAAQ,OAAO,KAAK,KAAK,GAAE;AAAA,QAAS;AAAA,QAAE,KAAK,UAAU,IAAI,YAAY;AAAA,SACxF;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,WAAW,GAAE;AAAA,QAAS;AAAA,SAC7C;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,QAAQ,GAAE;AAAA,QAAS;AAAA,SAC1C;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,QAAQ,GAAE;AAAA,QAAS;AAAA,SAC1C;AAAA,OACF,IACE;AAAA,IAEH,KAAK,KAAK,WAAW,IACpB,oBAAC,aAAU,WAAW,MAAM,OAAO,0EAA4D,IAE/F;AAAA,MAAC;AAAA;AAAA,QACC,UAAU;AAAA,QACV,SAAQ;AAAA,QACR,MACE,qBAAC,QACC;AAAA,8BAAC,MAAG,kBAAI;AAAA,UACR,oBAAC,MAAG,qBAAO;AAAA,UACX,oBAAC,MAAG,iBAAG;AAAA,UACP,oBAAC,MAAG,sBAAQ;AAAA,UACZ,oBAAC,MAAG,iBAAG;AAAA,UACP,oBAAC,MAAG,oBAAM;AAAA,UACV,oBAAC,MAAG,OAAM,SAAQ,oBAAM;AAAA,UACxB,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,UACrB,oBAAC,MAAG,OAAM,SAAQ,2BAAa;AAAA,WACjC;AAAA,QAGD,eAAK,KAAK,IAAI,CAAC,QACd,oBAAC,cAAwB,OAAR,IAAI,EAAc,CACpC;AAAA;AAAA,IACH;AAAA,IAGF,oBAAC,cAAW,MAAY,YAAwB,OAAO,KAAK,OAAO,MAAK,YAAW,QAAQ,SAAS;AAAA,KACtG;AAEJ;AAEA,SAAS,WAAW,EAAE,IAAI,GAAgC;AACxD,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,UAAU,CAAC,MAAM,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,QAAK;AACrE,QAAM,QAAQ,CAAC,UAAkB,aAAa,OAAO,IAAI,QAAQ;AAEjE,SACE,qBAAC,YACC;AAAA,wBAAC,QAAG,WAAW,MAAM,KAClB,cAAI,KAAK,IAAI,MAAM,EAAE,eAAe,QAAW,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,WAAW,MAAM,WAAW,QAAQ,UAAU,CAAC,GACzI;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,SAAU,gBAAM,QAAQ,qBAAoB;AAAA,MACnE,UAAU,oBAAC,UAAK,WAAW,OAAO,QAAS,mBAAQ,IAAU;AAAA,OAChE,GACF;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,SAAU,cAAI,QAAQ,OAAM;AAAA,MACnD,IAAI,eAAe,oBAAC,UAAK,WAAW,OAAO,QAAS,cAAI,cAAa,IAAU;AAAA,OAClF,GACF;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,MAClB,cAAI,SAAS,OAAO,oBAAC,QAAK,MAAM,kBAAkB,IAAI,SAAS,EAAE,IAAK,cAAI,SAAS,MAAK,IAAU,oBACrG;AAAA,IACA,oBAAC,QAAI,eAAK,YAAY,IAAI,IAAI,GAAE;AAAA,IAChC,oBAAC,QACC,8BAAC,UAAK,WAAW,GAAG,OAAO,MAAM,IAAI,KAAK,IAAI,MAAM,CAAC,IAAK,eAAK,cAAc,IAAI,MAAM,GAAE,GAC3F;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,WAAY,gBAAM,IAAI,WAAW,GAAE;AAAA,IACxD,oBAAC,QAAG,WAAW,MAAM,KAAM,gBAAM,IAAI,QAAQ,GAAE;AAAA,IAC/C,oBAAC,QAAG,WAAW,MAAM,KAAM,gBAAM,IAAI,QAAQ,GAAE;AAAA,KACjD;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/merchant-payments-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { platformAdminApi, type MerchantPaymentRow } from '@withpotter/lib/admin-api';\nimport { useMerchantOptions, useMerchantPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport {\n AdmLoading,\n FilterSearch,\n FilterSelect,\n Filters,\n Pagination,\n StatCard,\n focusCurrency,\n} from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n STATUS_WORDS,\n TYPE_WORDS,\n downloadName,\n minorToMoney,\n pctChange,\n word,\n} from './ledger-model';\nimport { MethodCards } from './method-cards';\nimport { PaymentDrawer } from './payment-drawer';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', method: '', status: '', type: '', merchant: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'card', label: 'Card' },\n { value: 'ussd', label: 'USSD' },\n { value: 'transfer', label: 'Bank transfer' },\n { value: 'opay', label: 'Opay' },\n];\nconst STATUSES = [\n { value: '', label: 'Paid' },\n { value: 'pending', label: 'Waiting' },\n { value: 'failed', label: 'Failed' },\n { value: 'refunded', label: 'Refunded' },\n];\nconst TYPES = [\n { value: '', label: 'Every kind' },\n ...['order', 'invoice', 'booking', 'membership', 'subscription', 'deposit', 'tip', 'refund'].map((value) => ({\n value,\n label: word(TYPE_WORDS, value),\n })),\n];\n\nconst tone = (status: string) =>\n status === 'success' ? styles.statusGood : status === 'failed' ? styles.statusBad : status === 'pending' ? styles.statusWait : styles.statusMuted;\n\n/** The merchant payments view: who paid, how, and which merchant received it. */\nexport function MerchantPaymentsView({\n startDate,\n endDate,\n currency,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the only one there is. */\n currency: string;\n}) {\n // The filters live in the page address, so this view can be bookmarked or sent to someone.\n const { values, page, set } = useUrlFilters('mp', FILTERS);\n const [search, setSearch] = useState(values.q);\n const [open, setOpen] = useState<MerchantPaymentRow | null>(null);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({\n startDate,\n endDate,\n method: values.method,\n status: values.status,\n type: values.type,\n q: values.q,\n merchantId: values.merchant,\n page,\n limit: PAGE_SIZE,\n }),\n [startDate, endDate, values, page],\n );\n const merchants = useMerchantOptions({ startDate, endDate });\n const merchantChoices = [\n { value: '', label: 'Every merchant' },\n ...(merchants.data?.merchants ?? []).map((one) => ({ value: one.id, label: one.name })),\n ];\n\n const { data, isLoading, isError, refetch } = useMerchantPayments(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"Merchant payments could not be loaded. This box may not have them yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchant payments…\" />;\n\n const currencies = [...new Set(data.cards.current.map((c) => c.currency))];\n const focus = focusCurrency(currencies, currency) ?? 'NGN';\n const now = data.cards.current.find((c) => c.currency === focus);\n const before = data.cards.previous?.find((c) => c.currency === focus) ?? null;\n const strip = data.strip.find((s) => s.currency === focus);\n const money = (minor: string | number) => minorToMoney(minor, focus);\n const delta = (a?: string, b?: string, goodWhenUp = true) =>\n before ? { pct: pctChange(a, b), positiveIsGood: goodWhenUp, caption: 'vs the period before' } : undefined;\n const totalPages = Math.max(Math.ceil(data.total / PAGE_SIZE), 1);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that customers paid to merchants. Use the filters to narrow it down, and download exactly what you\n see.\n </p>\n\n <section aria-label=\"Merchant payments at a glance\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Money received\"\n value={<CountUp value={Number(now?.grossMinor ?? 0) / 100} format={(v) => money(Math.round(v * 100))} />}\n sub={now ? `${integer.format(now.payments)} payments that went through` : 'No payments in this period'}\n hint=\"Everything customers paid to merchants that went through, before any fees are taken.\"\n delta={delta(now?.grossMinor, before?.grossMinor)}\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Fees taken\"\n value={money(now?.feeMinor ?? 0)}\n hint=\"What the payment gateway and Olive kept from those payments.\"\n delta={delta(now?.feeMinor, before?.feeMinor, false)}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Merchants keep\"\n value={money(now?.netMinor ?? 0)}\n hint=\"What is left for merchants after the fees: money received minus fees taken.\"\n delta={delta(now?.netMinor, before?.netMinor)}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Payments that worked\"\n value={now?.successRate == null ? '–' : `${now.successRate}%`}\n sub={now?.successRate == null ? 'Nothing was tried in this period' : `Refunded: ${money(now.refundsMinor)}`}\n hint=\"Out of every payment that was tried, the share that went through. A low number can mean a way of paying is broken.\"\n />\n </Reveal>\n </section>\n\n <h2 className={styles.sectionTitle}>How customers paid</h2>\n <p className={styles.sectionNote}>The ways of paying on the checkout, and how much came in through each.</p>\n <MethodCards now={data.byChannel.current} before={data.byChannel.previous} currency={focus} />\n\n <h2 className={styles.sectionTitle}>Every payment</h2>\n <div className={styles.toolbar}>\n <Filters label=\"Filter payments\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a name, email, phone, reference or merchant\"\n label=\"Search payments\"\n />\n <FilterSelect\n value={values.merchant}\n onChange={(merchant) => set({ merchant })}\n options={merchantChoices}\n label=\"Merchant\"\n searchable\n />\n <FilterSelect value={values.method} onChange={(method) => set({ method })} options={METHODS} label=\"Way of paying\" />\n <FilterSelect value={values.status} onChange={(status) => set({ status })} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={values.type} onChange={(type) => set({ type })} options={TYPES} label=\"What it was for\" />\n </Filters>\n <DownloadButton\n filename={downloadName('merchant-payments', startDate, endDate)}\n load={() => platformAdminApi.merchantPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {strip ? (\n <p className={styles.strip} aria-live=\"polite\">\n <span>\n Showing <strong>{integer.format(data.total)}</strong> {data.total === 1 ? 'payment' : 'payments'}\n </span>\n <span>\n <strong>{money(strip.amountMinor)}</strong> received\n </span>\n <span>\n <strong>{money(strip.feeMinor)}</strong> in fees\n </span>\n <span>\n <strong>{money(strip.netMinor)}</strong> to merchants\n </span>\n </p>\n ) : null}\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={980}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Paid by</Th>\n <Th>How</Th>\n <Th>Merchant</Th>\n <Th>For</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n <Th align=\"right\">Fee</Th>\n <Th align=\"right\">Merchant gets</Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <PaymentRow key={row.id} row={row} onOpen={setOpen} />\n ))}\n </TableShell>\n )}\n\n <Pagination page={page} totalPages={totalPages} total={data.total} noun=\"payments\" onPage={(next) => set({ page: next })} />\n <PaymentDrawer row={open} onClose={() => setOpen(null)} />\n </div>\n );\n}\n\nfunction PaymentRow({ row, onOpen }: { row: MerchantPaymentRow; onOpen: (row: MerchantPaymentRow) => void }) {\n const { payer } = row;\n const contact = [payer.email, payer.phone].filter(Boolean).join(' · ');\n const money = (minor: string) => minorToMoney(minor, row.currency);\n\n return (\n <TableRow onClick={() => onOpen(row)}>\n <td className={table.num}>\n {new Date(row.paidAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{payer.name || 'Name not recorded'}</span>\n {contact ? <span className={styles.whoSub}>{contact}</span> : null}\n </span>\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.channel.label}</span>\n {row.methodDetail ? <span className={styles.whoSub}>{row.methodDetail}</span> : null}\n </span>\n </td>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td>{word(TYPE_WORDS, row.type)}</td>\n <td>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n </td>\n <td className={table.numStrong}>{money(row.amountMinor)}</td>\n <td className={table.num}>{money(row.feeMinor)}</td>\n <td className={table.num}>{money(row.netMinor)}</td>\n </TableRow>\n );\n}\n"],"mappings":";AAgHM,cAyBA,YAzBA;AA9GN,SAAS,WAAW,SAAS,gBAAgB;AAC7C,OAAO,UAAU;AACjB,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,wBAAiD;AAC1D,SAAS,oBAAoB,2BAA2B;AACxD,SAAS,kBAAkB;AAC3B,SAAS,SAAS,cAAc;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,OAAO,YAAY;AACnB,OAAO,WAAW;AAElB,MAAM,YAAY;AAClB,MAAM,UAAU,EAAE,GAAG,IAAI,QAAQ,IAAI,QAAQ,IAAI,MAAM,IAAI,UAAU,GAAG;AACxE,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAE7C,MAAM,UAAU;AAAA,EACd,EAAE,OAAO,IAAI,OAAO,sBAAsB;AAAA,EAC1C,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B,EAAE,OAAO,YAAY,OAAO,gBAAgB;AAAA,EAC5C,EAAE,OAAO,QAAQ,OAAO,OAAO;AACjC;AACA,MAAM,WAAW;AAAA,EACf,EAAE,OAAO,IAAI,OAAO,OAAO;AAAA,EAC3B,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,YAAY,OAAO,WAAW;AACzC;AACA,MAAM,QAAQ;AAAA,EACZ,EAAE,OAAO,IAAI,OAAO,aAAa;AAAA,EACjC,GAAG,CAAC,SAAS,WAAW,WAAW,cAAc,gBAAgB,WAAW,OAAO,QAAQ,EAAE,IAAI,CAAC,WAAW;AAAA,IAC3G;AAAA,IACA,OAAO,KAAK,YAAY,KAAK;AAAA,EAC/B,EAAE;AACJ;AAEA,MAAM,OAAO,CAAC,WACZ,WAAW,YAAY,OAAO,aAAa,WAAW,WAAW,OAAO,YAAY,WAAW,YAAY,OAAO,aAAa,OAAO;AAGjI,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAKG;AAED,QAAM,EAAE,QAAQ,MAAM,IAAI,IAAI,cAAc,MAAM,OAAO;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,OAAO,CAAC;AAC7C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAoC,IAAI;AAGhE,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW,MAAM;AAC7B,UAAI,OAAO,KAAK,MAAM,OAAO,EAAG,KAAI,EAAE,GAAG,OAAO,KAAK,EAAE,CAAC;AAAA,IAC1D,GAAG,GAAG;AAEN,WAAO,MAAM,aAAa,KAAK;AAAA,EAGjC,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU;AAAA,IACd,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO;AAAA,MACf,MAAM,OAAO;AAAA,MACb,GAAG,OAAO;AAAA,MACV,YAAY,OAAO;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,IACT;AAAA,IACA,CAAC,WAAW,SAAS,QAAQ,IAAI;AAAA,EACnC;AACA,QAAM,YAAY,mBAAmB,EAAE,WAAW,QAAQ,CAAC;AAC3D,QAAM,kBAAkB;AAAA,IACtB,EAAE,OAAO,IAAI,OAAO,iBAAiB;AAAA,IACrC,IAAI,UAAU,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,EAAE;AAAA,EACxF;AAEA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,oBAAoB,OAAO;AAEzE,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAAA,EAEJ;AACA,MAAI,aAAa,CAAC,KAAM,QAAO,oBAAC,cAAW,OAAM,mCAA6B;AAE9E,QAAM,aAAa,CAAC,GAAG,IAAI,IAAI,KAAK,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACzE,QAAM,QAAQ,cAAc,YAAY,QAAQ,KAAK;AACrD,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK;AAC/D,QAAM,SAAS,KAAK,MAAM,UAAU,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK,KAAK;AACzE,QAAM,QAAQ,KAAK,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK;AACzD,QAAM,QAAQ,CAAC,UAA2B,aAAa,OAAO,KAAK;AACnE,QAAM,QAAQ,CAAC,GAAY,GAAY,aAAa,SAClD,SAAS,EAAE,KAAK,UAAU,GAAG,CAAC,GAAG,gBAAgB,YAAY,SAAS,uBAAuB,IAAI;AACnG,QAAM,aAAa,KAAK,IAAI,KAAK,KAAK,KAAK,QAAQ,SAAS,GAAG,CAAC;AAEhE,SACE,qBAAC,SACC;AAAA,wBAAC,OAAE,WAAW,OAAO,MAAM,mIAG3B;AAAA,IAEA,qBAAC,aAAQ,cAAW,iCAAgC,WAAU,YAC5D;AAAA,0BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,oBAAC,WAAQ,OAAO,OAAO,KAAK,cAAc,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG;AAAA,UACtG,KAAK,MAAM,GAAG,QAAQ,OAAO,IAAI,QAAQ,CAAC,gCAAgC;AAAA,UAC1E,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,YAAY,QAAQ,UAAU;AAAA;AAAA,MAClD,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,MAAM,KAAK,YAAY,CAAC;AAAA,UAC/B,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,UAAU,QAAQ,UAAU,KAAK;AAAA;AAAA,MACrD,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,MAAM,KAAK,YAAY,CAAC;AAAA,UAC/B,MAAK;AAAA,UACL,OAAO,MAAM,KAAK,UAAU,QAAQ,QAAQ;AAAA;AAAA,MAC9C,GACF;AAAA,MACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK,eAAe,OAAO,WAAM,GAAG,IAAI,WAAW;AAAA,UAC1D,KAAK,KAAK,eAAe,OAAO,qCAAqC,aAAa,MAAM,IAAI,YAAY,CAAC;AAAA,UACzG,MAAK;AAAA;AAAA,MACP,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,QAAG,WAAW,OAAO,cAAc,gCAAkB;AAAA,IACtD,oBAAC,OAAE,WAAW,OAAO,aAAa,oFAAsE;AAAA,IACxG,oBAAC,eAAY,KAAK,KAAK,UAAU,SAAS,QAAQ,KAAK,UAAU,UAAU,UAAU,OAAO;AAAA,IAE5F,oBAAC,QAAG,WAAW,OAAO,cAAc,2BAAa;AAAA,IACjD,qBAAC,SAAI,WAAW,OAAO,SACrB;AAAA,2BAAC,WAAQ,OAAM,mBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAM;AAAA;AAAA,QACR;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,UAAU,CAAC,aAAa,IAAI,EAAE,SAAS,CAAC;AAAA,YACxC,SAAS;AAAA,YACT,OAAM;AAAA,YACN,YAAU;AAAA;AAAA,QACZ;AAAA,QACA,oBAAC,gBAAa,OAAO,OAAO,QAAQ,UAAU,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,SAAS,OAAM,iBAAgB;AAAA,QACnH,oBAAC,gBAAa,OAAO,OAAO,QAAQ,UAAU,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,OAAM,kBAAiB;AAAA,QACrH,oBAAC,gBAAa,OAAO,OAAO,MAAM,UAAU,CAAC,SAAS,IAAI,EAAE,KAAK,CAAC,GAAG,SAAS,OAAO,OAAM,mBAAkB;AAAA,SAC/G;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,UAAU,aAAa,qBAAqB,WAAW,OAAO;AAAA,UAC9D,MAAM,MAAM,iBAAiB,oBAAoB,EAAE,GAAG,SAAS,MAAM,QAAW,OAAO,OAAU,CAAC;AAAA;AAAA,MACpG;AAAA,OACF;AAAA,IAEC,QACC,qBAAC,OAAE,WAAW,OAAO,OAAO,aAAU,UACpC;AAAA,2BAAC,UAAK;AAAA;AAAA,QACI,oBAAC,YAAQ,kBAAQ,OAAO,KAAK,KAAK,GAAE;AAAA,QAAS;AAAA,QAAE,KAAK,UAAU,IAAI,YAAY;AAAA,SACxF;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,WAAW,GAAE;AAAA,QAAS;AAAA,SAC7C;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,QAAQ,GAAE;AAAA,QAAS;AAAA,SAC1C;AAAA,MACA,qBAAC,UACC;AAAA,4BAAC,YAAQ,gBAAM,MAAM,QAAQ,GAAE;AAAA,QAAS;AAAA,SAC1C;AAAA,OACF,IACE;AAAA,IAEH,KAAK,KAAK,WAAW,IACpB,oBAAC,aAAU,WAAW,MAAM,OAAO,0EAA4D,IAE/F;AAAA,MAAC;AAAA;AAAA,QACC,UAAU;AAAA,QACV,SAAQ;AAAA,QACR,MACE,qBAAC,QACC;AAAA,8BAAC,MAAG,kBAAI;AAAA,UACR,oBAAC,MAAG,qBAAO;AAAA,UACX,oBAAC,MAAG,iBAAG;AAAA,UACP,oBAAC,MAAG,sBAAQ;AAAA,UACZ,oBAAC,MAAG,iBAAG;AAAA,UACP,oBAAC,MAAG,oBAAM;AAAA,UACV,oBAAC,MAAG,OAAM,SAAQ,oBAAM;AAAA,UACxB,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,UACrB,oBAAC,MAAG,OAAM,SAAQ,2BAAa;AAAA,WACjC;AAAA,QAGD,eAAK,KAAK,IAAI,CAAC,QACd,oBAAC,cAAwB,KAAU,QAAQ,WAA1B,IAAI,EAA+B,CACrD;AAAA;AAAA,IACH;AAAA,IAGF,oBAAC,cAAW,MAAY,YAAwB,OAAO,KAAK,OAAO,MAAK,YAAW,QAAQ,CAAC,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC,GAAG;AAAA,IAC1H,oBAAC,iBAAc,KAAK,MAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AAAA,KAC1D;AAEJ;AAEA,SAAS,WAAW,EAAE,KAAK,OAAO,GAA2E;AAC3G,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,UAAU,CAAC,MAAM,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,QAAK;AACrE,QAAM,QAAQ,CAAC,UAAkB,aAAa,OAAO,IAAI,QAAQ;AAEjE,SACE,qBAAC,YAAS,SAAS,MAAM,OAAO,GAAG,GACjC;AAAA,wBAAC,QAAG,WAAW,MAAM,KAClB,cAAI,KAAK,IAAI,MAAM,EAAE,eAAe,QAAW,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,WAAW,MAAM,WAAW,QAAQ,UAAU,CAAC,GACzI;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,SAAU,gBAAM,QAAQ,qBAAoB;AAAA,MACnE,UAAU,oBAAC,UAAK,WAAW,OAAO,QAAS,mBAAQ,IAAU;AAAA,OAChE,GACF;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,SAAU,cAAI,QAAQ,OAAM;AAAA,MACnD,IAAI,eAAe,oBAAC,UAAK,WAAW,OAAO,QAAS,cAAI,cAAa,IAAU;AAAA,OAClF,GACF;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,MAClB,cAAI,SAAS,OAAO,oBAAC,QAAK,MAAM,kBAAkB,IAAI,SAAS,EAAE,IAAK,cAAI,SAAS,MAAK,IAAU,oBACrG;AAAA,IACA,oBAAC,QAAI,eAAK,YAAY,IAAI,IAAI,GAAE;AAAA,IAChC,oBAAC,QACC,8BAAC,UAAK,WAAW,GAAG,OAAO,MAAM,IAAI,KAAK,IAAI,MAAM,CAAC,IAAK,eAAK,cAAc,IAAI,MAAM,GAAE,GAC3F;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,WAAY,gBAAM,IAAI,WAAW,GAAE;AAAA,IACxD,oBAAC,QAAG,WAAW,MAAM,KAAM,gBAAM,IAAI,QAAQ,GAAE;AAAA,IAC/C,oBAAC,QAAG,WAAW,MAAM,KAAM,gBAAM,IAAI,QAAQ,GAAE;AAAA,KACjD;AAEJ;","names":[]}
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import Link from "next/link";
5
+ import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
6
+ import { useMerchantRevenue } from "../hooks";
7
+ import { LoadFailed } from "../audit/load-failed";
8
+ import { AdmLoading, FilterSearch, Filters, Pagination } from "../_ui";
9
+ import { minorToMoney } from "./ledger-model";
10
+ import { useUrlFilters } from "./use-url-filters";
11
+ import styles from "./ledger.module.css";
12
+ import table from "./revenue.module.css";
13
+ const PAGE_SIZE = 25;
14
+ const FILTERS = { q: "" };
15
+ const integer = new Intl.NumberFormat("en-US");
16
+ const METHOD_COLOUR = {
17
+ card: "#4f7a28",
18
+ ussd: "#c68a1b",
19
+ transfer: "#2f6f9f",
20
+ opay: "#7a4fa0"
21
+ };
22
+ function MerchantsView({ startDate, endDate }) {
23
+ const { values, page, set } = useUrlFilters("mr", FILTERS);
24
+ const [search, setSearch] = useState(values.q);
25
+ useEffect(() => {
26
+ const timer = setTimeout(() => {
27
+ if (search.trim() !== values.q) set({ q: search.trim() });
28
+ }, 300);
29
+ return () => clearTimeout(timer);
30
+ }, [search]);
31
+ const filters = useMemo(
32
+ () => ({ startDate, endDate, q: values.q, page, limit: PAGE_SIZE }),
33
+ [startDate, endDate, values.q, page]
34
+ );
35
+ const { data, isLoading, isError, refetch } = useMerchantRevenue(filters);
36
+ if (isError) {
37
+ return /* @__PURE__ */ jsx(
38
+ LoadFailed,
39
+ {
40
+ message: "The merchants could not be loaded. This box may not have this view yet.",
41
+ onRetry: () => void refetch()
42
+ }
43
+ );
44
+ }
45
+ if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading merchants\u2026" });
46
+ const hasMore = data.rows.length === PAGE_SIZE;
47
+ return /* @__PURE__ */ jsxs("div", { children: [
48
+ /* @__PURE__ */ jsx("p", { className: styles.lede, children: "This shows which merchants received the most money in this period, and how their customers paid them. Open a merchant to see each payment." }),
49
+ /* @__PURE__ */ jsx("div", { className: styles.toolbar, children: /* @__PURE__ */ jsx(Filters, { label: "Filter merchants", children: /* @__PURE__ */ jsx(FilterSearch, { value: search, onChange: setSearch, placeholder: "Search a merchant by name", label: "Search merchants" }) }) }),
50
+ data.rows.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No merchant took a payment in this period. Try a wider date range." }) : /* @__PURE__ */ jsx(
51
+ TableShell,
52
+ {
53
+ minWidth: 760,
54
+ density: "compact",
55
+ head: /* @__PURE__ */ jsxs("tr", { children: [
56
+ /* @__PURE__ */ jsx(Th, { children: "Merchant" }),
57
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Money received" }),
58
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Payments" }),
59
+ /* @__PURE__ */ jsx(Th, { children: "How customers paid" }),
60
+ /* @__PURE__ */ jsx(Th, { children: " " })
61
+ ] }),
62
+ children: data.rows.map((row) => /* @__PURE__ */ jsx(MerchantRow, { row }, `${row.merchant.id}-${row.currency}`))
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsx(
66
+ Pagination,
67
+ {
68
+ page,
69
+ totalPages: hasMore ? page + 1 : page,
70
+ noun: "merchants",
71
+ onPage: (next) => set({ page: next })
72
+ }
73
+ )
74
+ ] });
75
+ }
76
+ function MerchantRow({ row }) {
77
+ const total = Number(row.grossMinor) || 1;
78
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
79
+ /* @__PURE__ */ jsx("td", { className: table.name, children: row.merchant.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.merchant.id}`, children: row.merchant.name }) : "Unknown merchant" }),
80
+ /* @__PURE__ */ jsx("td", { className: table.numStrong, children: minorToMoney(row.grossMinor, row.currency) }),
81
+ /* @__PURE__ */ jsx("td", { className: table.num, children: integer.format(row.payments) }),
82
+ /* @__PURE__ */ jsxs("td", { children: [
83
+ /* @__PURE__ */ jsx("div", { className: styles.methodBar, role: "img", "aria-label": row.byMethod.map((m) => `${m.label} ${minorToMoney(m.amountMinor, row.currency)}`).join(", "), children: row.byMethod.map((m) => /* @__PURE__ */ jsx(
84
+ "span",
85
+ {
86
+ className: styles.methodSeg,
87
+ style: { width: `${Number(m.amountMinor) / total * 100}%`, background: METHOD_COLOUR[m.key] ?? "#8a8f85" }
88
+ },
89
+ m.key
90
+ )) }),
91
+ /* @__PURE__ */ jsx("div", { className: styles.methodText, children: row.byMethod.map((m) => `${m.label} ${Math.round(Number(m.amountMinor) / total * 100)}%`).join(" \xB7 ") })
92
+ ] }),
93
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Link, { href: `?tab=merchants&mp_merchant=${row.merchant.id}`, children: "See payments" }) })
94
+ ] });
95
+ }
96
+ export {
97
+ MerchantsView
98
+ };
99
+ //# sourceMappingURL=merchants-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/merchants-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport type { MerchantRevenueRow } from '@withpotter/lib/admin-api';\nimport { useMerchantRevenue } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { AdmLoading, FilterSearch, Filters, Pagination } from '../_ui';\nimport { minorToMoney } from './ledger-model';\nimport { useUrlFilters } from './use-url-filters';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '' };\nconst integer = new Intl.NumberFormat('en-US');\n\n/** One colour for each way of paying, so a merchant's mix reads at a glance. The words sit under the bar too. */\nconst METHOD_COLOUR: Record<string, string> = {\n card: '#4f7a28',\n ussd: '#c68a1b',\n transfer: '#2f6f9f',\n opay: '#7a4fa0',\n};\n\n/** Which merchant received how much, and how their customers paid them. */\nexport function MerchantsView({ startDate, endDate }: { startDate?: string; endDate?: string }) {\n const { values, page, set } = useUrlFilters('mr', FILTERS);\n const [search, setSearch] = useState(values.q);\n\n // Typing waits a moment, so a search is one request and not one per key.\n useEffect(() => {\n const timer = setTimeout(() => {\n if (search.trim() !== values.q) set({ q: search.trim() });\n }, 300);\n\n return () => clearTimeout(timer);\n // Only a change the person typed should write to the address.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, q: values.q, page, limit: PAGE_SIZE }),\n [startDate, endDate, values.q, page],\n );\n const { data, isLoading, isError, refetch } = useMerchantRevenue(filters);\n\n if (isError) {\n return (\n <LoadFailed\n message=\"The merchants could not be loaded. This box may not have this view yet.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (isLoading || !data) return <AdmLoading label=\"Loading merchants…\" />;\n\n const hasMore = data.rows.length === PAGE_SIZE;\n\n return (\n <div>\n <p className={styles.lede}>\n This shows which merchants received the most money in this period, and how their customers paid them. Open a\n merchant to see each payment.\n </p>\n <div className={styles.toolbar}>\n <Filters label=\"Filter merchants\">\n <FilterSearch value={search} onChange={setSearch} placeholder=\"Search a merchant by name\" label=\"Search merchants\" />\n </Filters>\n </div>\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No merchant took a payment in this period. Try a wider date range.</EmptyNote>\n ) : (\n <TableShell\n minWidth={760}\n density=\"compact\"\n head={\n <tr>\n <Th>Merchant</Th>\n <Th align=\"right\">Money received</Th>\n <Th align=\"right\">Payments</Th>\n <Th>How customers paid</Th>\n <Th> </Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <MerchantRow key={`${row.merchant.id}-${row.currency}`} row={row} />\n ))}\n </TableShell>\n )}\n\n <Pagination\n page={page}\n totalPages={hasMore ? page + 1 : page}\n noun=\"merchants\"\n onPage={(next) => set({ page: next })}\n />\n </div>\n );\n}\n\nfunction MerchantRow({ row }: { row: MerchantRevenueRow }) {\n const total = Number(row.grossMinor) || 1;\n\n return (\n <TableRow>\n <td className={table.name}>\n {row.merchant.name ? <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link> : 'Unknown merchant'}\n </td>\n <td className={table.numStrong}>{minorToMoney(row.grossMinor, row.currency)}</td>\n <td className={table.num}>{integer.format(row.payments)}</td>\n <td>\n <div className={styles.methodBar} role=\"img\" aria-label={row.byMethod.map((m) => `${m.label} ${minorToMoney(m.amountMinor, row.currency)}`).join(', ')}>\n {row.byMethod.map((m) => (\n <span\n key={m.key}\n className={styles.methodSeg}\n style={{ width: `${(Number(m.amountMinor) / total) * 100}%`, background: METHOD_COLOUR[m.key] ?? '#8a8f85' }}\n />\n ))}\n </div>\n <div className={styles.methodText}>\n {row.byMethod.map((m) => `${m.label} ${Math.round((Number(m.amountMinor) / total) * 100)}%`).join(' · ')}\n </div>\n </td>\n <td>\n <Link href={`?tab=merchants&mp_merchant=${row.merchant.id}`}>See payments</Link>\n </td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import { Fact, FactList } from "@withpotter/ui-kit";
5
+ import { StatusPill } from "../audit/status-pill";
6
+ import { Drawer } from "../_ui";
7
+ import { STATUS_WORDS, TYPE_WORDS, minorToMoney, word } from "./ledger-model";
8
+ import styles from "./ledger.module.css";
9
+ const when = (iso) => new Date(iso).toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
10
+ const TONE = { success: "success", pending: "pending", failed: "failed" };
11
+ function PaymentDrawer({ row, onClose }) {
12
+ const money = (minor) => row ? minorToMoney(minor, row.currency) : "";
13
+ return /* @__PURE__ */ jsx(
14
+ Drawer,
15
+ {
16
+ open: Boolean(row),
17
+ onClose,
18
+ title: row ? money(row.amountMinor) : "Payment",
19
+ subtitle: row ? row.payer.name ?? row.payer.email ?? "Customer name not recorded" : void 0,
20
+ meta: row ? /* @__PURE__ */ jsx(StatusPill, { status: TONE[row.status] ?? "neutral", label: word(STATUS_WORDS, row.status) }) : void 0,
21
+ children: row ? /* @__PURE__ */ jsxs("div", { className: styles.drawerBody, children: [
22
+ /* @__PURE__ */ jsxs(FactList, { columns: 2, children: [
23
+ /* @__PURE__ */ jsx(Fact, { label: "Paid on", value: when(row.paidAt) }),
24
+ /* @__PURE__ */ jsx(Fact, { label: "What it was for", value: word(TYPE_WORDS, row.type) }),
25
+ /* @__PURE__ */ jsx(Fact, { label: "Paid by", value: row.payer.name ?? "Name not recorded" }),
26
+ /* @__PURE__ */ jsx(Fact, { label: "Email", value: row.payer.email ?? "\u2013" }),
27
+ /* @__PURE__ */ jsx(Fact, { label: "Phone", value: row.payer.phone ?? "\u2013" }),
28
+ /* @__PURE__ */ jsx(Fact, { label: "How they paid", value: row.channel.label }),
29
+ /* @__PURE__ */ jsx(Fact, { label: "Card or bank", value: row.methodDetail ?? "\u2013" }),
30
+ /* @__PURE__ */ jsx(
31
+ Fact,
32
+ {
33
+ label: "Merchant",
34
+ value: row.merchant.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.merchant.id}`, children: row.merchant.name }) : "Unknown merchant"
35
+ }
36
+ )
37
+ ] }),
38
+ /* @__PURE__ */ jsx("h3", { className: styles.drawerHeading, children: "Where the money went" }),
39
+ /* @__PURE__ */ jsxs(FactList, { columns: 2, children: [
40
+ /* @__PURE__ */ jsx(Fact, { label: "Customer paid", value: money(row.amountMinor) }),
41
+ /* @__PURE__ */ jsx(Fact, { label: "Fee taken", value: money(row.feeMinor) }),
42
+ /* @__PURE__ */ jsx(Fact, { label: "Merchant gets", value: money(row.netMinor) }),
43
+ /* @__PURE__ */ jsx(Fact, { label: "Reference", value: row.reference })
44
+ ] }),
45
+ row.orderId ? /* @__PURE__ */ jsx("p", { className: styles.sectionNote, children: "This payment is for an order. Find it on the merchant's page, under Orders." }) : null
46
+ ] }) : null
47
+ }
48
+ );
49
+ }
50
+ export {
51
+ PaymentDrawer
52
+ };
53
+ //# sourceMappingURL=payment-drawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/revenue/payment-drawer.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { Fact, FactList } from '@withpotter/ui-kit';\nimport type { MerchantPaymentRow } from '@withpotter/lib/admin-api';\nimport { StatusPill } from '../audit/status-pill';\nimport { Drawer } from '../_ui';\nimport { STATUS_WORDS, TYPE_WORDS, minorToMoney, word } from './ledger-model';\nimport styles from './ledger.module.css';\n\nconst when = (iso: string): string =>\n new Date(iso).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' });\n\nconst TONE: Record<string, string> = { success: 'success', pending: 'pending', failed: 'failed' };\n\n/** One payment in full: who paid, how, to which merchant, and where the money went. */\nexport function PaymentDrawer({ row, onClose }: { row: MerchantPaymentRow | null; onClose: () => void }) {\n const money = (minor: string) => (row ? minorToMoney(minor, row.currency) : '');\n\n return (\n <Drawer\n open={Boolean(row)}\n onClose={onClose}\n title={row ? money(row.amountMinor) : 'Payment'}\n subtitle={row ? (row.payer.name ?? row.payer.email ?? 'Customer name not recorded') : undefined}\n meta={\n row ? (\n <StatusPill status={TONE[row.status] ?? 'neutral'} label={word(STATUS_WORDS, row.status)} />\n ) : undefined\n }\n >\n {row ? (\n <div className={styles.drawerBody}>\n <FactList columns={2}>\n <Fact label=\"Paid on\" value={when(row.paidAt)} />\n <Fact label=\"What it was for\" value={word(TYPE_WORDS, row.type)} />\n <Fact label=\"Paid by\" value={row.payer.name ?? 'Name not recorded'} />\n <Fact label=\"Email\" value={row.payer.email ?? '–'} />\n <Fact label=\"Phone\" value={row.payer.phone ?? '–'} />\n <Fact label=\"How they paid\" value={row.channel.label} />\n <Fact label=\"Card or bank\" value={row.methodDetail ?? '–'} />\n <Fact\n label=\"Merchant\"\n value={\n row.merchant.name ? (\n <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link>\n ) : (\n 'Unknown merchant'\n )\n }\n />\n </FactList>\n <h3 className={styles.drawerHeading}>Where the money went</h3>\n <FactList columns={2}>\n <Fact label=\"Customer paid\" value={money(row.amountMinor)} />\n <Fact label=\"Fee taken\" value={money(row.feeMinor)} />\n <Fact label=\"Merchant gets\" value={money(row.netMinor)} />\n <Fact label=\"Reference\" value={row.reference} />\n </FactList>\n {row.orderId ? (\n <p className={styles.sectionNote}>\n This payment is for an order. Find it on the merchant's page, under Orders.\n </p>\n ) : null}\n </div>\n ) : null}\n </Drawer>\n );\n}\n"],"mappings":";AA2BU,cAMA,YANA;AAzBV,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAE/B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,cAAc,YAAY,cAAc,YAAY;AAC7D,OAAO,YAAY;AAEnB,MAAM,OAAO,CAAC,QACZ,IAAI,KAAK,GAAG,EAAE,eAAe,SAAS,EAAE,WAAW,UAAU,WAAW,QAAQ,CAAC;AAEnF,MAAM,OAA+B,EAAE,SAAS,WAAW,SAAS,WAAW,QAAQ,SAAS;AAGzF,SAAS,cAAc,EAAE,KAAK,QAAQ,GAA4D;AACvG,QAAM,QAAQ,CAAC,UAAmB,MAAM,aAAa,OAAO,IAAI,QAAQ,IAAI;AAE5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,QAAQ,GAAG;AAAA,MACjB;AAAA,MACA,OAAO,MAAM,MAAM,IAAI,WAAW,IAAI;AAAA,MACtC,UAAU,MAAO,IAAI,MAAM,QAAQ,IAAI,MAAM,SAAS,+BAAgC;AAAA,MACtF,MACE,MACE,oBAAC,cAAW,QAAQ,KAAK,IAAI,MAAM,KAAK,WAAW,OAAO,KAAK,cAAc,IAAI,MAAM,GAAG,IACxF;AAAA,MAGL,gBACC,qBAAC,SAAI,WAAW,OAAO,YACrB;AAAA,6BAAC,YAAS,SAAS,GACjB;AAAA,8BAAC,QAAK,OAAM,WAAU,OAAO,KAAK,IAAI,MAAM,GAAG;AAAA,UAC/C,oBAAC,QAAK,OAAM,mBAAkB,OAAO,KAAK,YAAY,IAAI,IAAI,GAAG;AAAA,UACjE,oBAAC,QAAK,OAAM,WAAU,OAAO,IAAI,MAAM,QAAQ,qBAAqB;AAAA,UACpE,oBAAC,QAAK,OAAM,SAAQ,OAAO,IAAI,MAAM,SAAS,UAAK;AAAA,UACnD,oBAAC,QAAK,OAAM,SAAQ,OAAO,IAAI,MAAM,SAAS,UAAK;AAAA,UACnD,oBAAC,QAAK,OAAM,iBAAgB,OAAO,IAAI,QAAQ,OAAO;AAAA,UACtD,oBAAC,QAAK,OAAM,gBAAe,OAAO,IAAI,gBAAgB,UAAK;AAAA,UAC3D;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,IAAI,SAAS,OACX,oBAAC,QAAK,MAAM,kBAAkB,IAAI,SAAS,EAAE,IAAK,cAAI,SAAS,MAAK,IAEpE;AAAA;AAAA,UAGN;AAAA,WACF;AAAA,QACA,oBAAC,QAAG,WAAW,OAAO,eAAe,kCAAoB;AAAA,QACzD,qBAAC,YAAS,SAAS,GACjB;AAAA,8BAAC,QAAK,OAAM,iBAAgB,OAAO,MAAM,IAAI,WAAW,GAAG;AAAA,UAC3D,oBAAC,QAAK,OAAM,aAAY,OAAO,MAAM,IAAI,QAAQ,GAAG;AAAA,UACpD,oBAAC,QAAK,OAAM,iBAAgB,OAAO,MAAM,IAAI,QAAQ,GAAG;AAAA,UACxD,oBAAC,QAAK,OAAM,aAAY,OAAO,IAAI,WAAW;AAAA,WAChD;AAAA,QACC,IAAI,UACH,oBAAC,OAAE,WAAW,OAAO,aAAa,yFAElC,IACE;AAAA,SACN,IACE;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -13,7 +13,7 @@ function PlanMixPanel({
13
13
  return /* @__PURE__ */ jsx(
14
14
  Panel,
15
15
  {
16
- title: "Plan mix",
16
+ title: "Who pays what",
17
17
  count: plans.length || void 0,
18
18
  caption,
19
19
  flush: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/plan-mix-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Subscriber count and MRR per plan, ranked by MRR. */\nexport function PlanMixPanel({\n planBreakdown,\n money,\n caption,\n}: {\n planBreakdown: PlatformRevenue['recurring']['planBreakdown'];\n money: (value: number) => string;\n caption: string;\n}) {\n const plans = [...planBreakdown].sort((a, b) => b.mrr - a.mrr);\n const totalMrr = plans.reduce((sum, p) => sum + p.mrr, 0);\n\n return (\n <Panel\n title=\"Plan mix\"\n count={plans.length || undefined}\n caption={caption}\n flush\n >\n {plans.length === 0 ? (\n <EmptyNote className={styles.empty}>\n No active plans in this period.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={420}\n density=\"compact\"\n head={\n <tr>\n <Th>Plan</Th>\n <Th align=\"right\">Subscribers</Th>\n <Th align=\"right\">MRR</Th>\n <Th align=\"right\">% of MRR</Th>\n </tr>\n }\n >\n {plans.map((p) => {\n const share = totalMrr > 0 ? (p.mrr / totalMrr) * 100 : 0;\n return (\n <TableRow key={p.planId}>\n <td className={styles.name}>\n <span className={styles.tenantText}>\n {p.name}\n <span className={styles.slug}>{p.interval}</span>\n </span>\n </td>\n <td className={styles.num}>\n {integer.format(p.subscribers)}\n </td>\n <td className={styles.numStrong}>{money(p.mrr)}</td>\n <td className={styles.num}>\n <span className={styles.share}>\n <span className={styles.shareTrack} aria-hidden>\n <span\n className={styles.shareFill}\n style={{ width: `${Math.min(share, 100)}%` }}\n />\n </span>\n {share.toFixed(1)}%\n </span>\n </td>\n </TableRow>\n );\n })}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":"AA4BQ,cASI,YATJ;AA3BR,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC7D,QAAM,WAAW,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM,UAAU;AAAA,MACvB;AAAA,MACA,OAAK;AAAA,MAEJ,gBAAM,WAAW,IAChB,oBAAC,aAAU,WAAW,OAAO,OAAO,6CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,OAAM,SAAQ,yBAAW;AAAA,YAC7B,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,YACrB,oBAAC,MAAG,OAAM,SAAQ,sBAAQ;AAAA,aAC5B;AAAA,UAGD,gBAAM,IAAI,CAAC,MAAM;AAChB,kBAAM,QAAQ,WAAW,IAAK,EAAE,MAAM,WAAY,MAAM;AACxD,mBACE,qBAAC,YACC;AAAA,kCAAC,QAAG,WAAW,OAAO,MACpB,+BAAC,UAAK,WAAW,OAAO,YACrB;AAAA,kBAAE;AAAA,gBACH,oBAAC,UAAK,WAAW,OAAO,MAAO,YAAE,UAAS;AAAA,iBAC5C,GACF;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,KACnB,kBAAQ,OAAO,EAAE,WAAW,GAC/B;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,cAC/C,oBAAC,QAAG,WAAW,OAAO,KACpB,+BAAC,UAAK,WAAW,OAAO,OACtB;AAAA,oCAAC,UAAK,WAAW,OAAO,YAAY,eAAW,MAC7C;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,OAAO;AAAA,oBAClB,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,GAAG,CAAC,IAAI;AAAA;AAAA,gBAC7C,GACF;AAAA,gBACC,MAAM,QAAQ,CAAC;AAAA,gBAAE;AAAA,iBACpB,GACF;AAAA,iBArBa,EAAE,MAsBjB;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/plan-mix-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport { Panel } from '../_ui';\nimport styles from './revenue.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Subscriber count and MRR per plan, ranked by MRR. */\nexport function PlanMixPanel({\n planBreakdown,\n money,\n caption,\n}: {\n planBreakdown: PlatformRevenue['recurring']['planBreakdown'];\n money: (value: number) => string;\n caption: string;\n}) {\n const plans = [...planBreakdown].sort((a, b) => b.mrr - a.mrr);\n const totalMrr = plans.reduce((sum, p) => sum + p.mrr, 0);\n\n return (\n <Panel\n title=\"Who pays what\"\n count={plans.length || undefined}\n caption={caption}\n flush\n >\n {plans.length === 0 ? (\n <EmptyNote className={styles.empty}>\n No active plans in this period.\n </EmptyNote>\n ) : (\n <TableShell\n bare\n minWidth={420}\n density=\"compact\"\n head={\n <tr>\n <Th>Plan</Th>\n <Th align=\"right\">Subscribers</Th>\n <Th align=\"right\">MRR</Th>\n <Th align=\"right\">% of MRR</Th>\n </tr>\n }\n >\n {plans.map((p) => {\n const share = totalMrr > 0 ? (p.mrr / totalMrr) * 100 : 0;\n return (\n <TableRow key={p.planId}>\n <td className={styles.name}>\n <span className={styles.tenantText}>\n {p.name}\n <span className={styles.slug}>{p.interval}</span>\n </span>\n </td>\n <td className={styles.num}>\n {integer.format(p.subscribers)}\n </td>\n <td className={styles.numStrong}>{money(p.mrr)}</td>\n <td className={styles.num}>\n <span className={styles.share}>\n <span className={styles.shareTrack} aria-hidden>\n <span\n className={styles.shareFill}\n style={{ width: `${Math.min(share, 100)}%` }}\n />\n </span>\n {share.toFixed(1)}%\n </span>\n </td>\n </TableRow>\n );\n })}\n </TableShell>\n )}\n </Panel>\n );\n}\n"],"mappings":"AA4BQ,cASI,YATJ;AA3BR,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,aAAa;AACtB,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC7D,QAAM,WAAW,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,MAAM,UAAU;AAAA,MACvB;AAAA,MACA,OAAK;AAAA,MAEJ,gBAAM,WAAW,IAChB,oBAAC,aAAU,WAAW,OAAO,OAAO,6CAEpC,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,kBAAI;AAAA,YACR,oBAAC,MAAG,OAAM,SAAQ,yBAAW;AAAA,YAC7B,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,YACrB,oBAAC,MAAG,OAAM,SAAQ,sBAAQ;AAAA,aAC5B;AAAA,UAGD,gBAAM,IAAI,CAAC,MAAM;AAChB,kBAAM,QAAQ,WAAW,IAAK,EAAE,MAAM,WAAY,MAAM;AACxD,mBACE,qBAAC,YACC;AAAA,kCAAC,QAAG,WAAW,OAAO,MACpB,+BAAC,UAAK,WAAW,OAAO,YACrB;AAAA,kBAAE;AAAA,gBACH,oBAAC,UAAK,WAAW,OAAO,MAAO,YAAE,UAAS;AAAA,iBAC5C,GACF;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,KACnB,kBAAQ,OAAO,EAAE,WAAW,GAC/B;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,cAC/C,oBAAC,QAAG,WAAW,OAAO,KACpB,+BAAC,UAAK,WAAW,OAAO,OACtB;AAAA,oCAAC,UAAK,WAAW,OAAO,YAAY,eAAW,MAC7C;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,OAAO;AAAA,oBAClB,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,GAAG,CAAC,IAAI;AAAA;AAAA,gBAC7C,GACF;AAAA,gBACC,MAAM,QAAQ,CAAC;AAAA,gBAAE;AAAA,iBACpB,GACF;AAAA,iBArBa,EAAE,MAsBjB;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}