@withpotter/pages-admin 0.13.0-next.120 → 0.13.0-next.123

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 (35) hide show
  1. package/dist/admin/_ui/admin.css +7 -0
  2. package/dist/admin/_ui/stat-card.js +11 -2
  3. package/dist/admin/_ui/stat-card.js.map +1 -1
  4. package/dist/admin/admins/access-dialog.js +2 -7
  5. package/dist/admin/admins/access-dialog.js.map +1 -1
  6. package/dist/admin/admins/invite-dialog.js +0 -6
  7. package/dist/admin/admins/invite-dialog.js.map +1 -1
  8. package/dist/admin/hooks.js +0 -7
  9. package/dist/admin/hooks.js.map +1 -1
  10. package/dist/admin/revenue/earnings/earnings-colors.js +25 -0
  11. package/dist/admin/revenue/earnings/earnings-colors.js.map +1 -0
  12. package/dist/admin/revenue/earnings/earnings-legend.js +75 -0
  13. package/dist/admin/revenue/earnings/earnings-legend.js.map +1 -0
  14. package/dist/admin/revenue/earnings/earnings-summary.js +296 -0
  15. package/dist/admin/revenue/earnings/earnings-summary.js.map +1 -0
  16. package/dist/admin/revenue/earnings/earnings-table.js +143 -0
  17. package/dist/admin/revenue/earnings/earnings-table.js.map +1 -0
  18. package/dist/admin/revenue/earnings/earnings.module.css +201 -0
  19. package/dist/admin/revenue/earnings/use-earnings.js +27 -0
  20. package/dist/admin/revenue/earnings/use-earnings.js.map +1 -0
  21. package/dist/admin/revenue/health-panel.js +5 -4
  22. package/dist/admin/revenue/health-panel.js.map +1 -1
  23. package/dist/admin/revenue/revenue-kpis.js +1 -1
  24. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  25. package/dist/admin/revenue/revenue-overview.js +20 -118
  26. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  27. package/dist/admin/revenue/revenue-workspace.js +4 -10
  28. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  29. package/dist/admin/roles/page.js +4 -14
  30. package/dist/admin/roles/page.js.map +1 -1
  31. package/dist/admin/roles/role-dialog.js +1 -2
  32. package/dist/admin/roles/role-dialog.js.map +1 -1
  33. package/package.json +4 -4
  34. package/dist/admin/roles/role-requests.js +0 -80
  35. package/dist/admin/roles/role-requests.js.map +0 -1
@@ -0,0 +1,296 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import { EmptyNote } from "@withpotter/ui-kit";
6
+ import { TimeSeriesChart } from "@withpotter/components/charts";
7
+ import {
8
+ earningsFigures,
9
+ pctChange,
10
+ toMajor
11
+ } from "@withpotter/lib/admin-earnings-model";
12
+ import { LoadFailed } from "../../audit/load-failed";
13
+ import { AdmLoading, Panel, StatCard, moneyIn, toTrend } from "../../_ui";
14
+ import { useSourceColors } from "./earnings-colors";
15
+ import { CompareBars, PieWithDetails } from "./earnings-legend";
16
+ import { useEarnings } from "./use-earnings";
17
+ import styles from "../revenue.module.css";
18
+ import ledger from "../ledger.module.css";
19
+ import earn from "./earnings.module.css";
20
+ const integer = new Intl.NumberFormat("en-US");
21
+ const ORDER = ["online", "pos", "payouts", "settlements", "cash", "plans", "payments", "other"];
22
+ const ALWAYS = /* @__PURE__ */ new Set(["online", "pos", "payouts", "settlements", "cash", "plans"]);
23
+ const TITLES = {
24
+ online: "Online card payments",
25
+ pos: "POS card payments and transfers",
26
+ payouts: "Payouts",
27
+ settlements: "Settlements to merchants' banks",
28
+ cash: "Cash received",
29
+ plans: "Plan subscriptions",
30
+ payments: "Other card payments",
31
+ other: "Other fees"
32
+ };
33
+ const WHAT = {
34
+ online: "What Olive keeps when a customer pays a merchant by card online, including money that arrives through the payment gateway.",
35
+ pos: "What Olive keeps when a customer pays at a merchant's POS terminal, by card or by transfer.",
36
+ payouts: "What Olive charges when a merchant sends money out of their wallet.",
37
+ settlements: "What Olive charges when a merchant's money is settled to their bank account.",
38
+ cash: "What Olive charges when cash is received for a merchant.",
39
+ plans: "What merchants pay Olive for their plan, however they paid.",
40
+ payments: "Card payment fees that are not tagged as online or POS.",
41
+ other: "Fees from anything not listed here."
42
+ };
43
+ const MINOR = /* @__PURE__ */ new Set(["transfers", "fx", "lending", "other"]);
44
+ function named(parts) {
45
+ const rest = parts.filter((p) => MINOR.has(p.source));
46
+ const main = parts.filter((p) => !MINOR.has(p.source));
47
+ const sum = (k) => rest.reduce((a, p) => a + p[k], 0);
48
+ if (sum("value") <= 0 && sum("previous") <= 0) return main;
49
+ return [
50
+ ...main,
51
+ { source: "other", label: "Other fees", value: sum("value"), previous: sum("previous"), count: sum("count") }
52
+ ];
53
+ }
54
+ function MetricCard({
55
+ on,
56
+ onPick,
57
+ span,
58
+ children
59
+ }) {
60
+ return /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ className: `${span} ${earn.metric} ${on ? earn.metricOn : ""}`,
64
+ role: "button",
65
+ tabIndex: 0,
66
+ "aria-pressed": on,
67
+ onClick: onPick,
68
+ onKeyDown: (e) => {
69
+ if (e.key === "Enter" || e.key === " ") {
70
+ e.preventDefault();
71
+ onPick();
72
+ }
73
+ },
74
+ children
75
+ }
76
+ );
77
+ }
78
+ const count = (n) => `${integer.format(n)} ${n === 1 ? "transaction" : "transactions"}`;
79
+ function EarningsSummary({
80
+ startDate,
81
+ endDate,
82
+ currency,
83
+ recurring,
84
+ span
85
+ }) {
86
+ const { data, isLoading, isError, refetch } = useEarnings({ startDate, endDate });
87
+ const colors = useSourceColors();
88
+ const [metric, setMetric] = useState("earned");
89
+ if (isError)
90
+ return /* @__PURE__ */ jsx(
91
+ LoadFailed,
92
+ {
93
+ message: "Earnings could not be loaded. This box may not have them yet.",
94
+ onRetry: () => void refetch()
95
+ }
96
+ );
97
+ if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading what Olive earns\u2026" });
98
+ const wanted = currency || recurring.currency || "NGN";
99
+ const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0];
100
+ if (!cur)
101
+ return /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "No fees have been earned in this period yet." });
102
+ const plan = recurring.currency === cur.currency ? recurring.collected ?? null : null;
103
+ const f = earningsFigures(cur, plan);
104
+ const have = named(f.parts);
105
+ const parts = ORDER.filter((k) => ALWAYS.has(k) || have.some((p) => p.source === k && (p.value !== 0 || p.previous !== 0))).map(
106
+ (k) => have.find((p) => p.source === k) ?? { source: k, label: TITLES[k], value: 0, previous: 0, count: 0 }
107
+ ).sort((a, b) => ORDER.indexOf(a.source) - ORDER.indexOf(b.source));
108
+ const money = moneyIn(cur.currency, 2);
109
+ const caption = `${span[0].toUpperCase()}${span.slice(1)} \xB7 ${cur.currency}`;
110
+ const margin = f.gross > 0 ? Math.round(f.net / f.gross * 1e3) / 10 : null;
111
+ const green = colors.series[2];
112
+ const red = colors.series[7];
113
+ const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);
114
+ const day = (p) => {
115
+ const fee = toMajor(p.feeMinor, cur.exponent);
116
+ const cost = toMajor(p.providerMinor, cur.exponent);
117
+ return metric === "earned" ? fee : metric === "costs" ? cost : fee - cost;
118
+ };
119
+ const trend = toTrend(cur.series.map((p) => ({ date: p.date, value: day(p) })), "value");
120
+ const before = toTrend(cur.previousSeries.map((p) => ({ date: p.date, value: day(p) })), "value");
121
+ const feeSpark = cur.series.map((p) => toMajor(p.feeMinor, cur.exponent));
122
+ const chartTitle = { earned: "Fees earned each day", costs: "Processing costs each day", profit: "Profit each day" }[metric];
123
+ const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);
124
+ const label = (p) => TITLES[p.source] ?? p.label;
125
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
126
+ /* @__PURE__ */ jsxs("p", { className: ledger.sectionNote, children: [
127
+ "How money flows through Olive: what it earned, what it cost, what is left, and where it came from. Showing",
128
+ " ",
129
+ cur.currency,
130
+ data.currencies.length > 1 ? "; use the currency filter for the others." : "."
131
+ ] }),
132
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Earned, costs and profit", className: "adm-grid", children: [
133
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "earned", onPick: () => setMetric("earned"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
134
+ StatCard,
135
+ {
136
+ label: "Total earned",
137
+ accent: colors.series[0],
138
+ spark: feeSpark,
139
+ sparkColor: colors.series[0],
140
+ value: money(f.gross),
141
+ sub: "Fees plus plan income",
142
+ delta: { pct: pctChange(f.gross, f.feesBefore + f.plan), caption: "vs the period before" },
143
+ hint: "Everything Olive earned in this period: fees on card payments, payouts, settlements and cash, plus what merchants paid for their plans."
144
+ }
145
+ ) }),
146
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "costs", onPick: () => setMetric("costs"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
147
+ StatCard,
148
+ {
149
+ label: "Processing costs",
150
+ accent: red,
151
+ value: money(cur.providerCostsRecorded ? f.provider : 0),
152
+ sub: cur.providerCostsRecorded ? "Charged by the processors" : "None recorded yet",
153
+ hint: "What the payment processors kept from the same payments, as far as the system records it. Until costs are recorded this stays at zero and profit equals what was earned."
154
+ }
155
+ ) }),
156
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "profit", onPick: () => setMetric("profit"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
157
+ StatCard,
158
+ {
159
+ label: "Profit",
160
+ accent: green,
161
+ value: money(f.net),
162
+ sub: cur.providerCostsRecorded ? "After processing costs" : "Same as earned until costs are recorded",
163
+ hint: "Total earned less the processing costs the system records. Running costs such as hosting and staff are not included."
164
+ }
165
+ ) }),
166
+ /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
167
+ StatCard,
168
+ {
169
+ label: "Margin",
170
+ accent: colors.series[3],
171
+ value: margin === null ? "\u2013" : `${margin}%`,
172
+ sub: "Profit as a share of earned",
173
+ hint: "Profit divided by total earned."
174
+ }
175
+ ) })
176
+ ] }),
177
+ /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(Panel, { title: chartTitle, caption: `${caption} \xB7 dotted line is the period before`, height: "calc(var(--adm-panel-h) + 32px)", children: /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(
178
+ TimeSeriesChart,
179
+ {
180
+ data: trend,
181
+ color: metric === "costs" ? red : metric === "profit" ? green : colors.series[0],
182
+ formatValue: money,
183
+ compare: before,
184
+ compareLabel: "Period before",
185
+ emptyMessage: "Nothing was recorded on any day in this period",
186
+ height: 240
187
+ }
188
+ ) }) }) }) }),
189
+ /* @__PURE__ */ jsx("h2", { className: ledger.sectionTitle, children: "Where the money comes from" }),
190
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Earnings by stream", className: "adm-grid", children: [
191
+ parts.map((p) => /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
192
+ StatCard,
193
+ {
194
+ label: label(p),
195
+ accent: colors[p.source],
196
+ value: money(p.value),
197
+ sub: p.source === "plans" ? "Collected in this period" : p.count === 0 ? "Nothing in this period" : count(p.count),
198
+ hint: WHAT[p.source],
199
+ delta: p.source === "plans" ? void 0 : { pct: pctChange(p.value, p.previous), caption: "vs the period before" }
200
+ }
201
+ ) }, p.source)),
202
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
203
+ StatCard,
204
+ {
205
+ label: "Olive account and outlets",
206
+ accent: "var(--text-tertiary)",
207
+ value: "Not counted yet",
208
+ sub: "No earnings recorded",
209
+ hint: "Money received through the Olive account and outlets. The bank system does not report a fee for it yet, so nothing can be added here."
210
+ }
211
+ ) })
212
+ ] }),
213
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
214
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "Where it comes from", caption, height: "auto", children: parts.some((p) => p.value > 0) ? /* @__PURE__ */ jsx(
215
+ PieWithDetails,
216
+ {
217
+ money,
218
+ centerLabel: "Earned",
219
+ slices: parts.map((p) => ({
220
+ key: p.source,
221
+ label: label(p),
222
+ value: p.value,
223
+ color: colors[p.source],
224
+ detail: p.source === "plans" ? "Collected" : count(p.count)
225
+ }))
226
+ }
227
+ ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "Nothing earned in this period." }) }) }),
228
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "What Olive keeps", caption, height: "auto", children: cur.providerCostsRecorded && f.gross > 0 ? /* @__PURE__ */ jsx(
229
+ PieWithDetails,
230
+ {
231
+ money,
232
+ centerLabel: "Earned",
233
+ slices: [
234
+ { key: "profit", label: "Profit", value: Math.max(0, f.net), color: green, detail: "After processing costs" },
235
+ { key: "cost", label: "Processing costs", value: Math.max(0, f.provider), color: red, detail: "Charged by the processors" }
236
+ ]
237
+ }
238
+ ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No processing cost is recorded for these fees yet, so profit equals what was earned." }) }) }),
239
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "How plans were paid", caption, height: "auto", children: byMethod.length > 0 && recurring.currency === cur.currency ? /* @__PURE__ */ jsx(
240
+ PieWithDetails,
241
+ {
242
+ money,
243
+ centerLabel: "Plans",
244
+ slices: byMethod.map((m, i) => ({
245
+ key: m.method,
246
+ label: m.method,
247
+ value: m.amount,
248
+ color: colors.series[i % colors.series.length],
249
+ detail: `${integer.format(m.payments)} ${m.payments === 1 ? "payment" : "payments"}`
250
+ }))
251
+ }
252
+ ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No plan payments in this period." }) }) })
253
+ ] }),
254
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
255
+ /* @__PURE__ */ jsx("div", { className: "adm-span-6", children: /* @__PURE__ */ jsx(Panel, { title: "Stream by stream, against the period before", caption, height: "auto", children: /* @__PURE__ */ jsx(
256
+ CompareBars,
257
+ {
258
+ money,
259
+ rows: parts.filter((p) => p.source !== "plans").map((p) => ({
260
+ key: p.source,
261
+ label: label(p),
262
+ color: colors[p.source],
263
+ value: p.value,
264
+ previous: p.previous
265
+ }))
266
+ }
267
+ ) }) }),
268
+ /* @__PURE__ */ jsx("div", { className: "adm-span-6", children: /* @__PURE__ */ jsx(Panel, { title: "The merchants it comes from", caption: `Fees, ${caption}`, height: "auto", children: cur.topMerchants.length === 0 ? /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No fees to show in this period." }) : /* @__PURE__ */ jsx("ol", { className: earn.rankList, children: cur.topMerchants.map((m, i) => {
269
+ const value = toMajor(m.feeMinor, cur.exponent);
270
+ const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);
271
+ return /* @__PURE__ */ jsxs("li", { className: earn.rankRow, children: [
272
+ /* @__PURE__ */ jsx("span", { className: earn.rankName, children: m.tenantId && m.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${m.tenantId}`, children: m.name }) : "No merchant recorded" }),
273
+ /* @__PURE__ */ jsx("strong", { className: earn.rankAmount, children: money(value) }),
274
+ /* @__PURE__ */ jsxs("span", { className: earn.rankSub, children: [
275
+ count(m.count),
276
+ topTotal > 0 ? ` \xB7 ${Math.round(value / topTotal * 100)}% of these` : ""
277
+ ] }),
278
+ /* @__PURE__ */ jsx("span", { className: earn.rankBar, "aria-hidden": true, children: /* @__PURE__ */ jsx(
279
+ "span",
280
+ {
281
+ className: earn.rankFill,
282
+ style: {
283
+ width: `${widest > 0 ? Math.max(2, value / widest * 100) : 0}%`,
284
+ background: colors.series[i % colors.series.length]
285
+ }
286
+ }
287
+ ) })
288
+ ] }, `${m.tenantId ?? "none"}-${i}`);
289
+ }) }) }) })
290
+ ] })
291
+ ] });
292
+ }
293
+ export {
294
+ EarningsSummary
295
+ };
296
+ //# sourceMappingURL=earnings-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-summary.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport {\n earningsFigures,\n pctChange,\n toMajor,\n type EarningsPart,\n} from '@withpotter/lib/admin-earnings-model';\nimport { LoadFailed } from '../../audit/load-failed';\nimport { AdmLoading, Panel, StatCard, moneyIn, toTrend } from '../../_ui';\nimport { useSourceColors } from './earnings-colors';\nimport { CompareBars, PieWithDetails } from './earnings-legend';\nimport { useEarnings } from './use-earnings';\nimport styles from '../revenue.module.css';\nimport ledger from '../ledger.module.css';\nimport earn from './earnings.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The streams as the owner names them, in the order they are read. */\nconst ORDER = ['online', 'pos', 'payouts', 'settlements', 'cash', 'plans', 'payments', 'other'] as const;\n\n/** Streams that always have a card, even at zero; the rest appear when they earned something. */\nconst ALWAYS = new Set(['online', 'pos', 'payouts', 'settlements', 'cash', 'plans']);\n\nconst TITLES: Record<string, string> = {\n online: 'Online card payments',\n pos: 'POS card payments and transfers',\n payouts: 'Payouts',\n settlements: \"Settlements to merchants' banks\",\n cash: 'Cash received',\n plans: 'Plan subscriptions',\n payments: 'Other card payments',\n other: 'Other fees',\n};\n\nconst WHAT: Record<string, string> = {\n online: 'What Olive keeps when a customer pays a merchant by card online, including money that arrives through the payment gateway.',\n pos: 'What Olive keeps when a customer pays at a merchant\\'s POS terminal, by card or by transfer.',\n payouts: 'What Olive charges when a merchant sends money out of their wallet.',\n settlements: 'What Olive charges when a merchant\\'s money is settled to their bank account.',\n cash: 'What Olive charges when cash is received for a merchant.',\n plans: 'What merchants pay Olive for their plan, however they paid.',\n payments: 'Card payment fees that are not tagged as online or POS.',\n other: 'Fees from anything not listed here.',\n};\n\ntype Metric = 'earned' | 'costs' | 'profit';\n\nconst MINOR = new Set(['transfers', 'fx', 'lending', 'other']);\n\n/** Anything outside the named streams is one \"Other fees\" part, so the parts still add up to the total. */\nfunction named(parts: EarningsPart[]): EarningsPart[] {\n const rest = parts.filter((p) => MINOR.has(p.source));\n const main = parts.filter((p) => !MINOR.has(p.source));\n const sum = (k: 'value' | 'previous' | 'count') => rest.reduce((a, p) => a + p[k], 0);\n\n if (sum('value') <= 0 && sum('previous') <= 0) return main;\n\n return [\n ...main,\n { source: 'other', label: 'Other fees', value: sum('value'), previous: sum('previous'), count: sum('count') },\n ];\n}\n\n/** A card that picks which measure the chart shows, like the metric tabs of an analytics page. */\nfunction MetricCard({\n on,\n onPick,\n span,\n children,\n}: {\n on: boolean;\n onPick: () => void;\n span: string;\n children: React.ReactNode;\n}) {\n return (\n <div\n className={`${span} ${earn.metric} ${on ? earn.metricOn : ''}`}\n role=\"button\"\n tabIndex={0}\n aria-pressed={on}\n onClick={onPick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onPick();\n }\n }}\n >\n {children}\n </div>\n );\n}\n\nconst count = (n: number) => `${integer.format(n)} ${n === 1 ? 'transaction' : 'transactions'}`;\n\n/**\n * What Olive earns, as the owner reads it: the total, what it cost, what is left, each stream on its own card, where it\n * comes from, how it moves and which merchants it comes from. One currency at a time, never a mix.\n */\nexport function EarningsSummary({\n startDate,\n endDate,\n currency,\n recurring,\n span,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the billing currency. */\n currency: string;\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const { data, isLoading, isError, refetch } = useEarnings({ startDate, endDate });\n const colors = useSourceColors();\n const [metric, setMetric] = useState<Metric>('earned');\n\n if (isError)\n return (\n <LoadFailed\n message=\"Earnings 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 what Olive earns…\" />;\n\n const wanted = currency || recurring.currency || 'NGN';\n const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0];\n\n if (!cur)\n return (\n <EmptyNote className={styles.empty}>No fees have been earned in this period yet.</EmptyNote>\n );\n\n const plan = recurring.currency === cur.currency ? (recurring.collected ?? null) : null;\n const f = earningsFigures(cur, plan);\n const have = named(f.parts);\n const parts = ORDER.filter((k) => ALWAYS.has(k) || have.some((p) => p.source === k && (p.value !== 0 || p.previous !== 0)))\n .map(\n (k): EarningsPart =>\n have.find((p) => p.source === k) ?? { source: k, label: TITLES[k], value: 0, previous: 0, count: 0 },\n )\n .sort((a, b) => ORDER.indexOf(a.source as never) - ORDER.indexOf(b.source as never));\n const money = moneyIn(cur.currency, 2);\n const caption = `${span[0].toUpperCase()}${span.slice(1)} · ${cur.currency}`;\n const margin = f.gross > 0 ? Math.round((f.net / f.gross) * 1000) / 10 : null;\n const green = colors.series[2];\n const red = colors.series[7];\n const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);\n const day = (p: { feeMinor: string; providerMinor: string }) => {\n const fee = toMajor(p.feeMinor, cur.exponent);\n const cost = toMajor(p.providerMinor, cur.exponent);\n\n return metric === 'earned' ? fee : metric === 'costs' ? cost : fee - cost;\n };\n const trend = toTrend(cur.series.map((p) => ({ date: p.date, value: day(p) })), 'value');\n const before = toTrend(cur.previousSeries.map((p) => ({ date: p.date, value: day(p) })), 'value');\n const feeSpark = cur.series.map((p) => toMajor(p.feeMinor, cur.exponent));\n const chartTitle = { earned: 'Fees earned each day', costs: 'Processing costs each day', profit: 'Profit each day' }[metric];\n const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);\n const label = (p: EarningsPart) => TITLES[p.source] ?? p.label;\n\n return (\n <>\n <p className={ledger.sectionNote}>\n How money flows through Olive: what it earned, what it cost, what is left, and where it came from. Showing{' '}\n {cur.currency}\n {data.currencies.length > 1 ? '; use the currency filter for the others.' : '.'}\n </p>\n\n <section aria-label=\"Earned, costs and profit\" className=\"adm-grid\">\n <MetricCard on={metric === 'earned'} onPick={() => setMetric('earned')} span=\"adm-span-3\">\n <StatCard\n label=\"Total earned\"\n accent={colors.series[0]}\n spark={feeSpark}\n sparkColor={colors.series[0]}\n value={money(f.gross)}\n sub=\"Fees plus plan income\"\n delta={{ pct: pctChange(f.gross, f.feesBefore + f.plan), caption: 'vs the period before' }}\n hint=\"Everything Olive earned in this period: fees on card payments, payouts, settlements and cash, plus what merchants paid for their plans.\"\n />\n </MetricCard>\n <MetricCard on={metric === 'costs'} onPick={() => setMetric('costs')} span=\"adm-span-3\">\n <StatCard\n label=\"Processing costs\"\n accent={red}\n value={money(cur.providerCostsRecorded ? f.provider : 0)}\n sub={cur.providerCostsRecorded ? 'Charged by the processors' : 'None recorded yet'}\n hint=\"What the payment processors kept from the same payments, as far as the system records it. Until costs are recorded this stays at zero and profit equals what was earned.\"\n />\n </MetricCard>\n <MetricCard on={metric === 'profit'} onPick={() => setMetric('profit')} span=\"adm-span-3\">\n <StatCard\n label=\"Profit\"\n accent={green}\n value={money(f.net)}\n sub={cur.providerCostsRecorded ? 'After processing costs' : 'Same as earned until costs are recorded'}\n hint=\"Total earned less the processing costs the system records. Running costs such as hosting and staff are not included.\"\n />\n </MetricCard>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Margin\"\n accent={colors.series[3]}\n value={margin === null ? '–' : `${margin}%`}\n sub=\"Profit as a share of earned\"\n hint=\"Profit divided by total earned.\"\n />\n </div>\n </section>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel title={chartTitle} caption={`${caption} · dotted line is the period before`} height=\"calc(var(--adm-panel-h) + 32px)\">\n <div className={styles.flatChart}>\n <TimeSeriesChart\n data={trend}\n color={metric === 'costs' ? red : metric === 'profit' ? green : colors.series[0]}\n formatValue={money}\n compare={before}\n compareLabel=\"Period before\"\n emptyMessage=\"Nothing was recorded on any day in this period\"\n height={240}\n />\n </div>\n </Panel>\n </div>\n </div>\n\n <h2 className={ledger.sectionTitle}>Where the money comes from</h2>\n <section aria-label=\"Earnings by stream\" className=\"adm-grid\">\n {parts.map((p) => (\n <div key={p.source} className=\"adm-span-4\">\n <StatCard\n label={label(p)}\n accent={colors[p.source]}\n value={money(p.value)}\n sub={p.source === 'plans' ? 'Collected in this period' : p.count === 0 ? 'Nothing in this period' : count(p.count)}\n hint={WHAT[p.source]}\n delta={\n p.source === 'plans'\n ? undefined\n : { pct: pctChange(p.value, p.previous), caption: 'vs the period before' }\n }\n />\n </div>\n ))}\n <div className=\"adm-span-4\">\n <StatCard\n label=\"Olive account and outlets\"\n accent=\"var(--text-tertiary)\"\n value=\"Not counted yet\"\n sub=\"No earnings recorded\"\n hint=\"Money received through the Olive account and outlets. The bank system does not report a fee for it yet, so nothing can be added here.\"\n />\n </div>\n </section>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-4\">\n <Panel title=\"Where it comes from\" caption={caption} height=\"auto\">\n {parts.some((p) => p.value > 0) ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Earned\"\n slices={parts.map((p) => ({\n key: p.source,\n label: label(p),\n value: p.value,\n color: colors[p.source],\n detail: p.source === 'plans' ? 'Collected' : count(p.count),\n }))}\n />\n ) : (\n <p className={styles.empty}>Nothing earned in this period.</p>\n )}\n </Panel>\n </div>\n <div className=\"adm-span-4\">\n <Panel title=\"What Olive keeps\" caption={caption} height=\"auto\">\n {cur.providerCostsRecorded && f.gross > 0 ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Earned\"\n slices={[\n { key: 'profit', label: 'Profit', value: Math.max(0, f.net), color: green, detail: 'After processing costs' },\n { key: 'cost', label: 'Processing costs', value: Math.max(0, f.provider), color: red, detail: 'Charged by the processors' },\n ]}\n />\n ) : (\n <p className={styles.empty}>\n No processing cost is recorded for these fees yet, so profit equals what was earned.\n </p>\n )}\n </Panel>\n </div>\n <div className=\"adm-span-4\">\n <Panel title=\"How plans were paid\" caption={caption} height=\"auto\">\n {byMethod.length > 0 && recurring.currency === cur.currency ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Plans\"\n slices={byMethod.map((m, i) => ({\n key: m.method,\n label: m.method,\n value: m.amount,\n color: colors.series[i % colors.series.length],\n detail: `${integer.format(m.payments)} ${m.payments === 1 ? 'payment' : 'payments'}`,\n }))}\n />\n ) : (\n <p className={styles.empty}>No plan payments in this period.</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-6\">\n <Panel title=\"Stream by stream, against the period before\" caption={caption} height=\"auto\">\n <CompareBars\n money={money}\n rows={parts\n .filter((p) => p.source !== 'plans')\n .map((p) => ({\n key: p.source,\n label: label(p),\n color: colors[p.source],\n value: p.value,\n previous: p.previous,\n }))}\n />\n </Panel>\n </div>\n <div className=\"adm-span-6\">\n <Panel title=\"The merchants it comes from\" caption={`Fees, ${caption}`} height=\"auto\">\n {cur.topMerchants.length === 0 ? (\n <p className={styles.empty}>No fees to show in this period.</p>\n ) : (\n <ol className={earn.rankList}>\n {cur.topMerchants.map((m, i) => {\n const value = toMajor(m.feeMinor, cur.exponent);\n const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);\n\n return (\n <li key={`${m.tenantId ?? 'none'}-${i}`} className={earn.rankRow}>\n <span className={earn.rankName}>\n {m.tenantId && m.name ? (\n <Link href={`/admin/tenants/${m.tenantId}`}>{m.name}</Link>\n ) : (\n 'No merchant recorded'\n )}\n </span>\n <strong className={earn.rankAmount}>{money(value)}</strong>\n <span className={earn.rankSub}>\n {count(m.count)}\n {topTotal > 0 ? ` · ${Math.round((value / topTotal) * 100)}% of these` : ''}\n </span>\n <span className={earn.rankBar} aria-hidden>\n <span\n className={earn.rankFill}\n style={{\n width: `${widest > 0 ? Math.max(2, (value / widest) * 100) : 0}%`,\n background: colors.series[i % colors.series.length],\n }}\n />\n </span>\n </li>\n );\n })}\n </ol>\n )}\n </Panel>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,143 @@
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 { adminEarningsApi } from "@withpotter/lib/admin-earnings-api";
7
+ import { formatMinor } from "@withpotter/lib/money-minor";
8
+ import { useMerchantOptions } from "../../hooks";
9
+ import { LoadFailed } from "../../audit/load-failed";
10
+ import { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination } from "../../_ui";
11
+ import { DownloadButton } from "../download-button";
12
+ import { downloadName } from "../ledger-model";
13
+ import { useUrlFilters } from "../use-url-filters";
14
+ import { useSourceColors } from "./earnings-colors";
15
+ import { useEarningsTransactions } from "./use-earnings";
16
+ import styles from "../ledger.module.css";
17
+ import table from "../revenue.module.css";
18
+ const PAGE_SIZE = 25;
19
+ const FILTERS = { q: "", source: "", merchant: "" };
20
+ const SOURCES = [
21
+ { value: "", label: "Every source" },
22
+ { value: "online", label: "Online card payments" },
23
+ { value: "pos", label: "POS card payments and transfers" },
24
+ { value: "payments", label: "Other card payments" },
25
+ { value: "payouts", label: "Payouts (money sent out)" },
26
+ { value: "settlements", label: "Settlements to bank" },
27
+ { value: "cash", label: "Cash received" },
28
+ { value: "plans", label: "Plan subscriptions (from wallet)" },
29
+ { value: "transfers", label: "Wallet transfers" },
30
+ { value: "fx", label: "Currency conversion" },
31
+ { value: "lending", label: "Lending" },
32
+ { value: "other", label: "Other fees" }
33
+ ];
34
+ function EarningsTable({ startDate, endDate }) {
35
+ const colors = useSourceColors();
36
+ const { values, page, set } = useUrlFilters("er", FILTERS);
37
+ const [search, setSearch] = useState(values.q);
38
+ useEffect(() => {
39
+ const timer = setTimeout(() => {
40
+ if (search.trim() !== values.q) set({ q: search.trim() });
41
+ }, 300);
42
+ return () => clearTimeout(timer);
43
+ }, [search]);
44
+ const filters = useMemo(
45
+ () => ({ startDate, endDate, source: values.source, merchantId: values.merchant, q: values.q, page, limit: PAGE_SIZE }),
46
+ [startDate, endDate, values, page]
47
+ );
48
+ const merchants = useMerchantOptions({ startDate, endDate });
49
+ const merchantChoices = [
50
+ { value: "", label: "Every merchant" },
51
+ ...(merchants.data?.merchants ?? []).map((one) => ({ value: one.id, label: one.name }))
52
+ ];
53
+ const { data, isLoading, isError, refetch } = useEarningsTransactions(filters);
54
+ if (isError)
55
+ return /* @__PURE__ */ jsx(
56
+ LoadFailed,
57
+ {
58
+ message: "The earnings could not be loaded. This box may not have them yet.",
59
+ onRetry: () => void refetch()
60
+ }
61
+ );
62
+ if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading what Olive earned\u2026" });
63
+ return /* @__PURE__ */ jsxs("div", { children: [
64
+ /* @__PURE__ */ jsx("p", { className: styles.sectionNote, children: "Every fee Olive earned in this period, and where it came from. Use the filters to narrow it, and download exactly what you see." }),
65
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
66
+ /* @__PURE__ */ jsxs(Filters, { label: "Filter earnings", children: [
67
+ /* @__PURE__ */ jsx(
68
+ FilterSearch,
69
+ {
70
+ value: search,
71
+ onChange: setSearch,
72
+ placeholder: "Search a merchant, what it was for, or a source",
73
+ label: "Search earnings"
74
+ }
75
+ ),
76
+ /* @__PURE__ */ jsx(FilterSelect, { value: values.source, onChange: (source) => set({ source }), options: SOURCES, label: "Source" }),
77
+ /* @__PURE__ */ jsx(
78
+ FilterSelect,
79
+ {
80
+ value: values.merchant,
81
+ onChange: (merchant) => set({ merchant }),
82
+ options: merchantChoices,
83
+ label: "Merchant",
84
+ searchable: true
85
+ }
86
+ )
87
+ ] }),
88
+ /* @__PURE__ */ jsx(
89
+ DownloadButton,
90
+ {
91
+ filename: downloadName("olive-earnings", startDate, endDate),
92
+ load: () => adminEarningsApi.transactionsCsv({ ...filters, page: void 0, limit: void 0 })
93
+ }
94
+ )
95
+ ] }),
96
+ data.rows.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No earnings match. Try a wider date range or clear a filter." }) : /* @__PURE__ */ jsx(
97
+ TableShell,
98
+ {
99
+ minWidth: 960,
100
+ density: "compact",
101
+ head: /* @__PURE__ */ jsxs("tr", { children: [
102
+ /* @__PURE__ */ jsx(Th, { children: "When" }),
103
+ /* @__PURE__ */ jsx(Th, { children: "Source" }),
104
+ /* @__PURE__ */ jsx(Th, { children: "Merchant" }),
105
+ /* @__PURE__ */ jsx(Th, { children: "What it was for" }),
106
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Olive earned" }),
107
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Processor cost" }),
108
+ /* @__PURE__ */ jsx(Th, { align: "right", children: "Kept" })
109
+ ] }),
110
+ children: data.rows.map((row) => /* @__PURE__ */ jsx(EarningRow, { row, color: colors[row.source] }, `${row.id}-${row.currency}`))
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx(
114
+ Pagination,
115
+ {
116
+ page,
117
+ totalPages: data.totalPages,
118
+ total: data.total,
119
+ noun: data.total === 1 ? "earning" : "earnings",
120
+ onPage: (next) => set({ page: next })
121
+ }
122
+ )
123
+ ] });
124
+ }
125
+ function EarningRow({ row, color }) {
126
+ const money = (minor) => formatMinor(minor, row.currency);
127
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
128
+ /* @__PURE__ */ jsx("td", { className: table.num, children: new Date(row.at).toLocaleString(void 0, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) }),
129
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
130
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: { width: 10, height: 10, borderRadius: 3, background: color, flexShrink: 0 } }),
131
+ row.sourceLabel
132
+ ] }) }),
133
+ /* @__PURE__ */ jsx("td", { className: table.name, children: row.merchant.id && row.merchant.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.merchant.id}`, children: row.merchant.name }) : "No merchant recorded" }),
134
+ /* @__PURE__ */ jsx("td", { children: row.description }),
135
+ /* @__PURE__ */ jsx("td", { className: table.numStrong, children: money(row.feeMinor) }),
136
+ /* @__PURE__ */ jsx("td", { className: table.num, children: row.providerMinor === "0" ? "\u2013" : money(row.providerMinor) }),
137
+ /* @__PURE__ */ jsx("td", { className: table.num, children: money(row.netMinor) })
138
+ ] });
139
+ }
140
+ export {
141
+ EarningsTable
142
+ };
143
+ //# sourceMappingURL=earnings-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-table.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 { adminEarningsApi, type EarningsRow } from '@withpotter/lib/admin-earnings-api';\nimport { formatMinor } from '@withpotter/lib/money-minor';\nimport { useMerchantOptions } from '../../hooks';\nimport { LoadFailed } from '../../audit/load-failed';\nimport { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination } from '../../_ui';\nimport { DownloadButton } from '../download-button';\nimport { downloadName } from '../ledger-model';\nimport { useUrlFilters } from '../use-url-filters';\nimport { useSourceColors } from './earnings-colors';\nimport { useEarningsTransactions } from './use-earnings';\nimport styles from '../ledger.module.css';\nimport table from '../revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst FILTERS = { q: '', source: '', merchant: '' };\n\nconst SOURCES = [\n { value: '', label: 'Every source' },\n { value: 'online', label: 'Online card payments' },\n { value: 'pos', label: 'POS card payments and transfers' },\n { value: 'payments', label: 'Other card payments' },\n { value: 'payouts', label: 'Payouts (money sent out)' },\n { value: 'settlements', label: 'Settlements to bank' },\n { value: 'cash', label: 'Cash received' },\n { value: 'plans', label: 'Plan subscriptions (from wallet)' },\n { value: 'transfers', label: 'Wallet transfers' },\n { value: 'fx', label: 'Currency conversion' },\n { value: 'lending', label: 'Lending' },\n { value: 'other', label: 'Other fees' },\n];\n\n/**\n * Every fee Olive earned, one row each: what it came from, which merchant, what Olive kept and what the processor\n * charged. The filters live in the page address so a view can be shared. Plans paid another way than from a wallet are\n * on the Subscriptions tab.\n */\nexport function EarningsTable({ startDate, endDate }: { startDate?: string; endDate?: string }) {\n const colors = useSourceColors();\n const { values, page, set } = useUrlFilters('er', 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, source: values.source, merchantId: values.merchant, q: values.q, page, limit: PAGE_SIZE }),\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 const { data, isLoading, isError, refetch } = useEarningsTransactions(filters);\n\n if (isError)\n return (\n <LoadFailed\n message=\"The earnings 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 what Olive earned…\" />;\n\n return (\n <div>\n <p className={styles.sectionNote}>\n Every fee Olive earned in this period, and where it came from. Use the filters to narrow it, and download exactly\n what you see.\n </p>\n <div className={styles.toolbar}>\n <Filters label=\"Filter earnings\">\n <FilterSearch\n value={search}\n onChange={setSearch}\n placeholder=\"Search a merchant, what it was for, or a source\"\n label=\"Search earnings\"\n />\n <FilterSelect value={values.source} onChange={(source) => set({ source })} options={SOURCES} label=\"Source\" />\n <FilterSelect\n value={values.merchant}\n onChange={(merchant) => set({ merchant })}\n options={merchantChoices}\n label=\"Merchant\"\n searchable\n />\n </Filters>\n <DownloadButton\n filename={downloadName('olive-earnings', startDate, endDate)}\n load={() => adminEarningsApi.transactionsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {data.rows.length === 0 ? (\n <EmptyNote className={table.empty}>No earnings match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={960}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Source</Th>\n <Th>Merchant</Th>\n <Th>What it was for</Th>\n <Th align=\"right\">Olive earned</Th>\n <Th align=\"right\">Processor cost</Th>\n <Th align=\"right\">Kept</Th>\n </tr>\n }\n >\n {data.rows.map((row) => (\n <EarningRow key={`${row.id}-${row.currency}`} row={row} color={colors[row.source]} />\n ))}\n </TableShell>\n )}\n\n <Pagination\n page={page}\n totalPages={data.totalPages}\n total={data.total}\n noun={data.total === 1 ? 'earning' : 'earnings'}\n onPage={(next) => set({ page: next })}\n />\n </div>\n );\n}\n\nfunction EarningRow({ row, color }: { row: EarningsRow; color: string }) {\n const money = (minor: string) => formatMinor(minor, row.currency);\n\n return (\n <TableRow>\n <td className={table.num}>\n {new Date(row.at).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>\n <span aria-hidden style={{ width: 10, height: 10, borderRadius: 3, background: color, flexShrink: 0 }} />\n {row.sourceLabel}\n </span>\n </td>\n <td className={table.name}>\n {row.merchant.id && row.merchant.name ? (\n <Link href={`/admin/tenants/${row.merchant.id}`}>{row.merchant.name}</Link>\n ) : (\n 'No merchant recorded'\n )}\n </td>\n <td>{row.description}</td>\n <td className={table.numStrong}>{money(row.feeMinor)}</td>\n <td className={table.num}>{row.providerMinor === '0' ? '–' : money(row.providerMinor)}</td>\n <td className={table.num}>{money(row.netMinor)}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}