@withpotter/pages-admin 0.13.0-next.122 → 0.13.0-next.124

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.
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { useChartTheme } from "@withpotter/components/charts";
3
3
  const ORDER = [
4
+ "online",
5
+ "pos",
4
6
  "payments",
5
7
  "payouts",
6
8
  "plans",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-colors.ts"],"sourcesContent":["'use client';\n\nimport { useChartTheme } from '@withpotter/components/charts';\nimport type { EarningsSource } from '@withpotter/lib/admin-earnings-api';\n\n/**\n * One colour per source, the same on its card, its pie slice and its legend row. They come from the chart system's\n * validated palette (fixed order, checked in light and dark), never picked here; the palette has eight slots, so\n * \"other\" takes a neutral rather than a ninth hue. The most important sources take the first slots.\n */\nconst ORDER: readonly EarningsSource[] = [\n 'payments',\n 'payouts',\n 'plans',\n 'settlements',\n 'cash',\n 'transfers',\n 'fx',\n 'lending',\n];\n\nexport function useSourceColors(): Record<EarningsSource, string> & {\n /** Colours for things that are not sources (payment methods, kept / cost), in palette order. */\n series: readonly string[];\n} {\n const { series } = useChartTheme();\n const colors = Object.fromEntries(\n ORDER.map((source, i) => [source, series[i % series.length]]),\n ) as Record<EarningsSource, string>;\n\n return { ...colors, other: 'var(--text-tertiary)', series };\n}\n"],"mappings":";AAEA,SAAS,qBAAqB;AAQ9B,MAAM,QAAmC;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,kBAGd;AACA,QAAM,EAAE,OAAO,IAAI,cAAc;AACjC,QAAM,SAAS,OAAO;AAAA,IACpB,MAAM,IAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,OAAO,IAAI,OAAO,MAAM,CAAC,CAAC;AAAA,EAC9D;AAEA,SAAO,EAAE,GAAG,QAAQ,OAAO,wBAAwB,OAAO;AAC5D;","names":[]}
1
+ {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-colors.ts"],"sourcesContent":["'use client';\n\nimport { useChartTheme } from '@withpotter/components/charts';\nimport type { EarningsSource } from '@withpotter/lib/admin-earnings-api';\n\n/**\n * One colour per source, the same on its card, its pie slice and its legend row. They come from the chart system's\n * validated palette (fixed order, checked in light and dark), never picked here; the palette has eight slots, so\n * \"other\" takes a neutral rather than a ninth hue. The most important sources take the first slots.\n */\nconst ORDER: readonly EarningsSource[] = [\n 'online',\n 'pos',\n 'payments',\n 'payouts',\n 'plans',\n 'settlements',\n 'cash',\n 'transfers',\n 'fx',\n 'lending',\n];\n\nexport function useSourceColors(): Record<EarningsSource, string> & {\n /** Colours for things that are not sources (payment methods, kept / cost), in palette order. */\n series: readonly string[];\n} {\n const { series } = useChartTheme();\n const colors = Object.fromEntries(\n ORDER.map((source, i) => [source, series[i % series.length]]),\n ) as Record<EarningsSource, string>;\n\n return { ...colors, other: 'var(--text-tertiary)', series };\n}\n"],"mappings":";AAEA,SAAS,qBAAqB;AAQ9B,MAAM,QAAmC;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,kBAGd;AACA,QAAM,EAAE,OAAO,IAAI,cAAc;AACjC,QAAM,SAAS,OAAO;AAAA,IACpB,MAAM,IAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,OAAO,IAAI,OAAO,MAAM,CAAC,CAAC;AAAA,EAC9D;AAEA,SAAO,EAAE,GAAG,QAAQ,OAAO,wBAAwB,OAAO;AAC5D;","names":[]}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { DonutChart } from "@withpotter/components/charts";
4
+ import { pctChange } from "@withpotter/lib/admin-earnings-model";
4
5
  import styles from "./earnings.module.css";
5
6
  function PieWithDetails({
6
7
  slices,
@@ -32,7 +33,43 @@ function PieWithDetails({
32
33
  ] }, s.key)) })
33
34
  ] });
34
35
  }
36
+ function CompareBars({
37
+ rows,
38
+ money
39
+ }) {
40
+ const widest = Math.max(1, ...rows.flatMap((r) => [r.value, r.previous]));
41
+ return /* @__PURE__ */ jsx("ul", { className: styles.cmp, children: rows.map((r) => {
42
+ const change = pctChange(r.value, r.previous);
43
+ const tone = change === null || change === 0 ? "" : change > 0 ? styles.up : styles.down;
44
+ return /* @__PURE__ */ jsxs("li", { className: styles.cmpRow, children: [
45
+ /* @__PURE__ */ jsxs("span", { className: styles.cmpHead, children: [
46
+ /* @__PURE__ */ jsx("span", { className: styles.swatch, style: { background: r.color }, "aria-hidden": true }),
47
+ /* @__PURE__ */ jsx("span", { className: styles.legendLabel, children: r.label }),
48
+ /* @__PURE__ */ jsx("span", { className: `${styles.cmpDelta} ${tone}`, children: change === null ? r.value > 0 ? "New" : "\u2013" : `${change > 0 ? "+" : change < 0 ? "\u2212" : ""}${Math.abs(change)}%` })
49
+ ] }),
50
+ /* @__PURE__ */ jsx("span", { className: styles.cmpTrack, "aria-hidden": true, children: /* @__PURE__ */ jsx(
51
+ "span",
52
+ {
53
+ className: styles.cmpPrev,
54
+ style: { width: `${r.previous / widest * 100}%`, background: r.color }
55
+ }
56
+ ) }),
57
+ /* @__PURE__ */ jsx("span", { className: styles.cmpTrack, "aria-hidden": true, children: /* @__PURE__ */ jsx(
58
+ "span",
59
+ {
60
+ className: styles.cmpNow,
61
+ style: { width: `${r.value / widest * 100}%`, background: r.color }
62
+ }
63
+ ) }),
64
+ /* @__PURE__ */ jsxs("span", { className: styles.cmpNums, children: [
65
+ /* @__PURE__ */ jsx("strong", { children: money(r.value) }),
66
+ /* @__PURE__ */ jsx("span", { children: `before ${money(r.previous)}` })
67
+ ] })
68
+ ] }, r.key);
69
+ }) });
70
+ }
35
71
  export {
72
+ CompareBars,
36
73
  PieWithDetails
37
74
  };
38
75
  //# sourceMappingURL=earnings-legend.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-legend.tsx"],"sourcesContent":["'use client';\n\nimport { DonutChart } from '@withpotter/components/charts';\nimport styles from './earnings.module.css';\n\nexport interface LegendSlice {\n key: string;\n label: string;\n value: number;\n color: string;\n /** A second line: \"12 transactions\". */\n detail?: string;\n}\n\n/**\n * A pie with its key beside it: every slice named in its own colour with its amount and share, so the chart can be\n * read without hovering. Slices of nothing are left out.\n */\nexport function PieWithDetails({\n slices,\n money,\n centerLabel,\n}: {\n slices: LegendSlice[];\n money: (value: number) => string;\n centerLabel: string;\n}) {\n const shown = slices.filter((s) => s.value > 0);\n const total = shown.reduce((a, s) => a + s.value, 0);\n\n return (\n <div className={styles.pie}>\n <DonutChart\n data={shown.map((s) => ({ label: s.label, value: s.value, color: s.color }))}\n format={money}\n centerLabel={centerLabel}\n size={170}\n />\n <ul className={styles.legend}>\n {shown.map((s) => (\n <li key={s.key} className={styles.legendRow}>\n <span className={styles.swatch} style={{ background: s.color }} aria-hidden />\n <span className={styles.legendText}>\n <span className={styles.legendLabel}>{s.label}</span>\n {s.detail ? <span className={styles.legendDetail}>{s.detail}</span> : null}\n </span>\n <span className={styles.legendNumbers}>\n <strong>{money(s.value)}</strong>\n <span>{total > 0 ? `${Math.round((s.value / total) * 100)}%` : '–'}</span>\n </span>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n"],"mappings":";AAgCM,cAUM,YAVN;AA9BN,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAeZ,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,OAAO,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC9C,QAAM,QAAQ,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;AAEnD,SACE,qBAAC,SAAI,WAAW,OAAO,KACrB;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,EAAE;AAAA,QAC3E,QAAQ;AAAA,QACR;AAAA,QACA,MAAM;AAAA;AAAA,IACR;AAAA,IACA,oBAAC,QAAG,WAAW,OAAO,QACnB,gBAAM,IAAI,CAAC,MACV,qBAAC,QAAe,WAAW,OAAO,WAChC;AAAA,0BAAC,UAAK,WAAW,OAAO,QAAQ,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,eAAW,MAAC;AAAA,MAC5E,qBAAC,UAAK,WAAW,OAAO,YACtB;AAAA,4BAAC,UAAK,WAAW,OAAO,aAAc,YAAE,OAAM;AAAA,QAC7C,EAAE,SAAS,oBAAC,UAAK,WAAW,OAAO,cAAe,YAAE,QAAO,IAAU;AAAA,SACxE;AAAA,MACA,qBAAC,UAAK,WAAW,OAAO,eACtB;AAAA,4BAAC,YAAQ,gBAAM,EAAE,KAAK,GAAE;AAAA,QACxB,oBAAC,UAAM,kBAAQ,IAAI,GAAG,KAAK,MAAO,EAAE,QAAQ,QAAS,GAAG,CAAC,MAAM,UAAI;AAAA,SACrE;AAAA,SATO,EAAE,GAUX,CACD,GACH;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-legend.tsx"],"sourcesContent":["'use client';\n\nimport { DonutChart } from '@withpotter/components/charts';\nimport { pctChange } from '@withpotter/lib/admin-earnings-model';\nimport styles from './earnings.module.css';\n\nexport interface LegendSlice {\n key: string;\n label: string;\n value: number;\n color: string;\n /** A second line: \"12 transactions\". */\n detail?: string;\n}\n\n/**\n * A pie with its key beside it: every slice named in its own colour with its amount and share, so the chart can be\n * read without hovering. Slices of nothing are left out.\n */\nexport function PieWithDetails({\n slices,\n money,\n centerLabel,\n}: {\n slices: LegendSlice[];\n money: (value: number) => string;\n centerLabel: string;\n}) {\n const shown = slices.filter((s) => s.value > 0);\n const total = shown.reduce((a, s) => a + s.value, 0);\n\n return (\n <div className={styles.pie}>\n <DonutChart\n data={shown.map((s) => ({ label: s.label, value: s.value, color: s.color }))}\n format={money}\n centerLabel={centerLabel}\n size={170}\n />\n <ul className={styles.legend}>\n {shown.map((s) => (\n <li key={s.key} className={styles.legendRow}>\n <span className={styles.swatch} style={{ background: s.color }} aria-hidden />\n <span className={styles.legendText}>\n <span className={styles.legendLabel}>{s.label}</span>\n {s.detail ? <span className={styles.legendDetail}>{s.detail}</span> : null}\n </span>\n <span className={styles.legendNumbers}>\n <strong>{money(s.value)}</strong>\n <span>{total > 0 ? `${Math.round((s.value / total) * 100)}%` : '–'}</span>\n </span>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport interface CompareRow {\n key: string;\n label: string;\n color: string;\n value: number;\n previous: number;\n}\n\n/**\n * Each stream this period against the period before, as paired bars in the stream's own colour: the faded bar is\n * before, the solid bar is now, and the change is said in words beside them.\n */\nexport function CompareBars({\n rows,\n money,\n}: {\n rows: CompareRow[];\n money: (value: number) => string;\n}) {\n const widest = Math.max(1, ...rows.flatMap((r) => [r.value, r.previous]));\n\n return (\n <ul className={styles.cmp}>\n {rows.map((r) => {\n const change = pctChange(r.value, r.previous);\n const tone =\n change === null || change === 0 ? '' : change > 0 ? styles.up : styles.down;\n\n return (\n <li key={r.key} className={styles.cmpRow}>\n <span className={styles.cmpHead}>\n <span className={styles.swatch} style={{ background: r.color }} aria-hidden />\n <span className={styles.legendLabel}>{r.label}</span>\n <span className={`${styles.cmpDelta} ${tone}`}>\n {change === null\n ? r.value > 0\n ? 'New'\n : '–'\n : `${change > 0 ? '+' : change < 0 ? '\\u2212' : ''}${Math.abs(change)}%`}\n </span>\n </span>\n <span className={styles.cmpTrack} aria-hidden>\n <span\n className={styles.cmpPrev}\n style={{ width: `${(r.previous / widest) * 100}%`, background: r.color }}\n />\n </span>\n <span className={styles.cmpTrack} aria-hidden>\n <span\n className={styles.cmpNow}\n style={{ width: `${(r.value / widest) * 100}%`, background: r.color }}\n />\n </span>\n <span className={styles.cmpNums}>\n <strong>{money(r.value)}</strong>\n <span>{`before ${money(r.previous)}`}</span>\n </span>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,34 +1,95 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
3
4
  import Link from "next/link";
4
- import { EmptyNote } from "@withpotter/ui-kit";
5
5
  import { TimeSeriesChart } from "@withpotter/components/charts";
6
- import { toMajor, earningsFigures, pctChange } from "@withpotter/lib/admin-earnings-model";
6
+ import {
7
+ earningsFigures,
8
+ pctChange,
9
+ toMajor
10
+ } from "@withpotter/lib/admin-earnings-model";
7
11
  import { LoadFailed } from "../../audit/load-failed";
8
12
  import { AdmLoading, Panel, StatCard, moneyIn, toTrend } from "../../_ui";
9
13
  import { useSourceColors } from "./earnings-colors";
10
- import { PieWithDetails } from "./earnings-legend";
14
+ import { CompareBars, PieWithDetails } from "./earnings-legend";
11
15
  import { useEarnings } from "./use-earnings";
12
16
  import styles from "../revenue.module.css";
13
17
  import ledger from "../ledger.module.css";
14
18
  import earn from "./earnings.module.css";
15
19
  const integer = new Intl.NumberFormat("en-US");
16
- const CARD_TITLES = {
17
- payments: "Customer payments",
18
- cash: "Cash received",
19
- settlements: "Settlements to bank",
20
+ const ORDER = ["online", "pos", "payouts", "settlements", "cash", "plans", "payments", "other"];
21
+ const ALWAYS = /* @__PURE__ */ new Set(["online", "pos", "payouts", "settlements", "cash", "plans"]);
22
+ const TITLES = {
23
+ online: "Online card payments",
24
+ pos: "POS card payments and transfers",
20
25
  payouts: "Payouts",
21
- transfers: "Wallet transfers",
22
- fx: "Currency conversion",
23
- lending: "Lending",
26
+ settlements: "Settlements to merchants' banks",
27
+ cash: "Cash received",
28
+ plans: "Plan subscriptions",
29
+ payments: "Other card payments",
24
30
  other: "Other fees"
25
31
  };
26
- const CARD_NOTES = {
27
- payments: "Fees on what customers paid merchants",
28
- cash: "Fees on cash received for merchants",
29
- settlements: "Charges when merchants move money to their bank",
30
- payouts: "Fees when merchants send money out of their wallet"
32
+ const WHAT = {
33
+ online: "What Olive keeps when a customer pays a merchant by card online, including money that arrives through the payment gateway.",
34
+ pos: "What Olive keeps when a customer pays at a merchant's POS terminal, by card or by transfer.",
35
+ payouts: "What Olive charges when a merchant sends money out of their wallet.",
36
+ settlements: "What Olive charges when a merchant's money is settled to their bank account.",
37
+ cash: "What Olive charges when cash is received for a merchant.",
38
+ plans: "What merchants pay Olive for their plan, however they paid.",
39
+ payments: "Card payment fees that are not tagged as online or POS.",
40
+ other: "Fees from anything not listed here."
31
41
  };
42
+ const NOTHING_YET = (currency) => ({
43
+ currency,
44
+ exponent: 2,
45
+ feeMinor: "0",
46
+ providerMinor: "0",
47
+ netMinor: "0",
48
+ previousFeeMinor: "0",
49
+ previousProviderMinor: "0",
50
+ count: 0,
51
+ providerCostsRecorded: false,
52
+ bySource: [],
53
+ series: [],
54
+ previousSeries: [],
55
+ topMerchants: []
56
+ });
57
+ const MINOR = /* @__PURE__ */ new Set(["transfers", "fx", "lending", "other"]);
58
+ function named(parts) {
59
+ const rest = parts.filter((p) => MINOR.has(p.source));
60
+ const main = parts.filter((p) => !MINOR.has(p.source));
61
+ const sum = (k) => rest.reduce((a, p) => a + p[k], 0);
62
+ if (sum("value") <= 0 && sum("previous") <= 0) return main;
63
+ return [
64
+ ...main,
65
+ { source: "other", label: "Other fees", value: sum("value"), previous: sum("previous"), count: sum("count") }
66
+ ];
67
+ }
68
+ function MetricCard({
69
+ on,
70
+ onPick,
71
+ span,
72
+ children
73
+ }) {
74
+ return /* @__PURE__ */ jsx(
75
+ "div",
76
+ {
77
+ className: `${span} ${earn.metric} ${on ? earn.metricOn : ""}`,
78
+ role: "button",
79
+ tabIndex: 0,
80
+ "aria-pressed": on,
81
+ onClick: onPick,
82
+ onKeyDown: (e) => {
83
+ if (e.key === "Enter" || e.key === " ") {
84
+ e.preventDefault();
85
+ onPick();
86
+ }
87
+ },
88
+ children
89
+ }
90
+ );
91
+ }
92
+ const count = (n) => `${integer.format(n)} ${n === 1 ? "transaction" : "transactions"}`;
32
93
  function EarningsSummary({
33
94
  startDate,
34
95
  endDate,
@@ -38,6 +99,7 @@ function EarningsSummary({
38
99
  }) {
39
100
  const { data, isLoading, isError, refetch } = useEarnings({ startDate, endDate });
40
101
  const colors = useSourceColors();
102
+ const [metric, setMetric] = useState("earned");
41
103
  if (isError)
42
104
  return /* @__PURE__ */ jsx(
43
105
  LoadFailed,
@@ -48,82 +110,130 @@ function EarningsSummary({
48
110
  );
49
111
  if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading what Olive earns\u2026" });
50
112
  const wanted = currency || recurring.currency || "NGN";
51
- const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0];
52
- if (!cur)
53
- return /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "No fees have been earned in this period yet." });
113
+ const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0] ?? NOTHING_YET(wanted);
54
114
  const plan = recurring.currency === cur.currency ? recurring.collected ?? null : null;
55
115
  const f = earningsFigures(cur, plan);
116
+ const have = named(f.parts);
117
+ const parts = ORDER.filter((k) => ALWAYS.has(k) || have.some((p) => p.source === k && (p.value !== 0 || p.previous !== 0))).map(
118
+ (k) => have.find((p) => p.source === k) ?? { source: k, label: TITLES[k], value: 0, previous: 0, count: 0 }
119
+ ).sort((a, b) => ORDER.indexOf(a.source) - ORDER.indexOf(b.source));
56
120
  const money = moneyIn(cur.currency, 2);
57
121
  const caption = `${span[0].toUpperCase()}${span.slice(1)} \xB7 ${cur.currency}`;
122
+ const margin = f.gross > 0 ? Math.round(f.net / f.gross * 1e3) / 10 : null;
123
+ const green = colors.series[2];
124
+ const red = colors.series[7];
58
125
  const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);
59
- const trend = toTrend(
60
- cur.series.map((p) => ({ date: p.date, value: toMajor(p.feeMinor, cur.exponent) })),
61
- "value"
62
- );
126
+ const day = (p) => {
127
+ const fee = toMajor(p.feeMinor, cur.exponent);
128
+ const cost = toMajor(p.providerMinor, cur.exponent);
129
+ return metric === "earned" ? fee : metric === "costs" ? cost : fee - cost;
130
+ };
131
+ const trend = toTrend(cur.series.map((p) => ({ date: p.date, value: day(p) })), "value");
132
+ const before = toTrend(cur.previousSeries.map((p) => ({ date: p.date, value: day(p) })), "value");
133
+ const feeSpark = cur.series.map((p) => toMajor(p.feeMinor, cur.exponent));
134
+ const chartTitle = { earned: "Fees earned each day", costs: "Processing costs each day", profit: "Profit each day" }[metric];
63
135
  const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);
136
+ const label = (p) => TITLES[p.source] ?? p.label;
64
137
  return /* @__PURE__ */ jsxs(Fragment, { children: [
65
138
  /* @__PURE__ */ jsxs("p", { className: ledger.sectionNote, children: [
66
- "Everything Olive makes in this period: fees on what customers pay, on money merchants send out and move to their bank, and what merchants pay for their plan. Showing ",
139
+ "How money flows through Olive: what it earned, what it cost, what is left, and where it came from. Showing",
140
+ " ",
67
141
  cur.currency,
68
142
  data.currencies.length > 1 ? "; use the currency filter for the others." : "."
69
143
  ] }),
70
- /* @__PURE__ */ jsxs("section", { "aria-label": "What Olive earns", className: "adm-grid", children: [
71
- /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
144
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Earned, costs and profit", className: "adm-grid", children: [
145
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "earned", onPick: () => setMetric("earned"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
72
146
  StatCard,
73
147
  {
74
148
  label: "Total earned",
75
149
  accent: colors.series[0],
76
- spark: trend.map((p) => p.value),
150
+ spark: feeSpark,
77
151
  sparkColor: colors.series[0],
78
- value: money(f.net),
79
- sub: `${money(f.gross)} before processor costs`,
80
- hint: "Fees Olive earned plus plan income, less what the card processor charged."
152
+ value: money(f.gross),
153
+ sub: "Fees plus plan income",
154
+ delta: { pct: pctChange(f.gross, f.feesBefore + f.plan), caption: "vs the period before" },
155
+ hint: "Everything Olive earned in this period: fees on card payments, payouts, settlements and cash, plus what merchants paid for their plans."
81
156
  }
82
157
  ) }),
83
- /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
158
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "costs", onPick: () => setMetric("costs"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
84
159
  StatCard,
85
160
  {
86
- label: "Processor costs",
87
- accent: colors.series[7],
88
- value: cur.providerCostsRecorded ? money(f.provider) : "\u2013",
89
- sub: cur.providerCostsRecorded ? "Charged by the card processor" : "None recorded in the ledger",
90
- hint: "What the payment processor kept from the same payments. When none is recorded, the total earned is before any such cost."
161
+ label: "Processing costs",
162
+ accent: red,
163
+ value: money(cur.providerCostsRecorded ? f.provider : 0),
164
+ sub: cur.providerCostsRecorded ? "Charged by the processors" : "None recorded yet",
165
+ 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."
91
166
  }
92
167
  ) }),
93
- /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
168
+ /* @__PURE__ */ jsx(MetricCard, { on: metric === "profit", onPick: () => setMetric("profit"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
94
169
  StatCard,
95
170
  {
96
- label: "Plan subscriptions",
97
- accent: colors.plans,
98
- value: money(f.plan),
99
- sub: recurring.payingMerchants ? `${integer.format(recurring.payingMerchants)} ${recurring.payingMerchants === 1 ? "merchant" : "merchants"} paying` : "Plan payments collected",
100
- hint: "What merchants paid Olive for their plans in this period, however they paid."
171
+ label: "Profit",
172
+ accent: green,
173
+ value: money(f.net),
174
+ sub: cur.providerCostsRecorded ? "After processing costs" : "Same as earned until costs are recorded",
175
+ hint: "Total earned less the processing costs the system records. Running costs such as hosting and staff are not included."
101
176
  }
102
177
  ) }),
103
- f.parts.filter((p) => p.source !== "plans").map((p) => /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
178
+ /* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
104
179
  StatCard,
105
180
  {
106
- label: CARD_TITLES[p.source] ?? p.label,
181
+ label: "Margin",
182
+ accent: colors.series[3],
183
+ value: margin === null ? "\u2013" : `${margin}%`,
184
+ sub: "Profit as a share of earned",
185
+ hint: "Profit divided by total earned."
186
+ }
187
+ ) })
188
+ ] }),
189
+ /* @__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(
190
+ TimeSeriesChart,
191
+ {
192
+ data: trend,
193
+ color: metric === "costs" ? red : metric === "profit" ? green : colors.series[0],
194
+ formatValue: money,
195
+ compare: before,
196
+ compareLabel: "Period before",
197
+ emptyMessage: "Nothing was recorded on any day in this period",
198
+ height: 240
199
+ }
200
+ ) }) }) }) }),
201
+ /* @__PURE__ */ jsx("h2", { className: ledger.sectionTitle, children: "Where the money comes from" }),
202
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Earnings by stream", className: "adm-grid", children: [
203
+ parts.map((p) => /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
204
+ StatCard,
205
+ {
206
+ label: label(p),
107
207
  accent: colors[p.source],
108
208
  value: money(p.value),
109
- sub: `${integer.format(p.count)} ${p.count === 1 ? "transaction" : "transactions"}`,
110
- hint: CARD_NOTES[p.source],
111
- delta: { pct: pctChange(p.value, p.previous), caption: "vs the period before" }
209
+ sub: p.source === "plans" ? "Collected in this period" : p.count === 0 ? "Nothing in this period" : count(p.count),
210
+ hint: WHAT[p.source],
211
+ delta: p.source === "plans" ? void 0 : { pct: pctChange(p.value, p.previous), caption: "vs the period before" }
212
+ }
213
+ ) }, p.source)),
214
+ /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
215
+ StatCard,
216
+ {
217
+ label: "Olive account and outlets",
218
+ accent: "var(--text-tertiary)",
219
+ value: "Not counted yet",
220
+ sub: "No earnings recorded",
221
+ 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."
112
222
  }
113
- ) }, p.source))
223
+ ) })
114
224
  ] }),
115
225
  /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
116
- /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "Where it comes from", caption, height: "auto", children: f.parts.some((p) => p.value > 0) ? /* @__PURE__ */ jsx(
226
+ /* @__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(
117
227
  PieWithDetails,
118
228
  {
119
229
  money,
120
230
  centerLabel: "Earned",
121
- slices: f.parts.map((p) => ({
231
+ slices: parts.map((p) => ({
122
232
  key: p.source,
123
- label: CARD_TITLES[p.source] ?? p.label,
233
+ label: label(p),
124
234
  value: p.value,
125
235
  color: colors[p.source],
126
- detail: p.count > 0 ? `${integer.format(p.count)} ${p.count === 1 ? "transaction" : "transactions"}` : void 0
236
+ detail: p.source === "plans" ? "Collected" : count(p.count)
127
237
  }))
128
238
  }
129
239
  ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "Nothing earned in this period." }) }) }),
@@ -131,13 +241,13 @@ function EarningsSummary({
131
241
  PieWithDetails,
132
242
  {
133
243
  money,
134
- centerLabel: "Before costs",
244
+ centerLabel: "Earned",
135
245
  slices: [
136
- { key: "kept", label: "Kept by Olive", value: Math.max(0, f.net), color: colors.series[2], detail: "After processor costs" },
137
- { key: "cost", label: "Processor costs", value: Math.max(0, f.provider), color: colors.series[7], detail: "Charged by the card processor" }
246
+ { key: "profit", label: "Profit", value: Math.max(0, f.net), color: green, detail: "After processing costs" },
247
+ { key: "cost", label: "Processing costs", value: Math.max(0, f.provider), color: red, detail: "Charged by the processors" }
138
248
  ]
139
249
  }
140
- ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No processor cost is recorded for these fees, so everything shown is before costs." }) }) }),
250
+ ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No processing cost is recorded for these fees yet, so profit equals what was earned." }) }) }),
141
251
  /* @__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(
142
252
  PieWithDetails,
143
253
  {
@@ -153,39 +263,43 @@ function EarningsSummary({
153
263
  }
154
264
  ) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No plan payments in this period." }) }) })
155
265
  ] }),
156
- /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(Panel, { title: "Fees earned each day", caption, height: "calc(var(--adm-panel-h) + 32px)", children: /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(
157
- TimeSeriesChart,
158
- {
159
- data: trend,
160
- color: colors.series[0],
161
- formatValue: money,
162
- compare: null,
163
- emptyMessage: "No fees were earned on any day in this period",
164
- height: 240
165
- }
166
- ) }) }) }) }),
167
- /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(Panel, { title: "Where the fees come from", caption: `Merchants, ${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) => {
168
- const value = toMajor(m.feeMinor, cur.exponent);
169
- const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);
170
- return /* @__PURE__ */ jsxs("li", { className: earn.rankRow, children: [
171
- /* @__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" }),
172
- /* @__PURE__ */ jsx("strong", { className: earn.rankAmount, children: money(value) }),
173
- /* @__PURE__ */ jsxs("span", { className: earn.rankSub, children: [
174
- `${integer.format(m.count)} ${m.count === 1 ? "transaction" : "transactions"}`,
175
- topTotal > 0 ? ` \xB7 ${Math.round(value / topTotal * 100)}% of these` : ""
176
- ] }),
177
- /* @__PURE__ */ jsx("span", { className: earn.rankBar, "aria-hidden": true, children: /* @__PURE__ */ jsx(
178
- "span",
179
- {
180
- className: earn.rankFill,
181
- style: {
182
- width: `${widest > 0 ? Math.max(2, value / widest * 100) : 0}%`,
183
- background: colors.series[0]
266
+ /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
267
+ /* @__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(
268
+ CompareBars,
269
+ {
270
+ money,
271
+ rows: parts.filter((p) => p.source !== "plans").map((p) => ({
272
+ key: p.source,
273
+ label: label(p),
274
+ color: colors[p.source],
275
+ value: p.value,
276
+ previous: p.previous
277
+ }))
278
+ }
279
+ ) }) }),
280
+ /* @__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) => {
281
+ const value = toMajor(m.feeMinor, cur.exponent);
282
+ const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);
283
+ return /* @__PURE__ */ jsxs("li", { className: earn.rankRow, children: [
284
+ /* @__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" }),
285
+ /* @__PURE__ */ jsx("strong", { className: earn.rankAmount, children: money(value) }),
286
+ /* @__PURE__ */ jsxs("span", { className: earn.rankSub, children: [
287
+ count(m.count),
288
+ topTotal > 0 ? ` \xB7 ${Math.round(value / topTotal * 100)}% of these` : ""
289
+ ] }),
290
+ /* @__PURE__ */ jsx("span", { className: earn.rankBar, "aria-hidden": true, children: /* @__PURE__ */ jsx(
291
+ "span",
292
+ {
293
+ className: earn.rankFill,
294
+ style: {
295
+ width: `${widest > 0 ? Math.max(2, value / widest * 100) : 0}%`,
296
+ background: colors.series[i % colors.series.length]
297
+ }
184
298
  }
185
- }
186
- ) })
187
- ] }, `${m.tenantId ?? "none"}-${i}`);
188
- }) }) }) }) })
299
+ ) })
300
+ ] }, `${m.tenantId ?? "none"}-${i}`);
301
+ }) }) }) })
302
+ ] })
189
303
  ] });
190
304
  }
191
305
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/admin/revenue/earnings/earnings-summary.tsx"],"sourcesContent":["'use client';\n\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 { toMajor, earningsFigures, pctChange } 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 { 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 ledger's words for what a merchant was doing, as the card says them. */\nconst CARD_TITLES: Record<string, string> = {\n payments: 'Customer payments',\n cash: 'Cash received',\n settlements: 'Settlements to bank',\n payouts: 'Payouts',\n transfers: 'Wallet transfers',\n fx: 'Currency conversion',\n lending: 'Lending',\n other: 'Other fees',\n};\n\nconst CARD_NOTES: Record<string, string> = {\n payments: 'Fees on what customers paid merchants',\n cash: 'Fees on cash received for merchants',\n settlements: 'Charges when merchants move money to their bank',\n payouts: 'Fees when merchants send money out of their wallet',\n};\n\n/**\n * What Olive earns, in one view: the total, where it comes from, what is left after the processor, how it moves day to\n * day 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\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}>\n No fees have been earned in this period yet.\n </EmptyNote>\n );\n\n const plan = recurring.currency === cur.currency ? (recurring.collected ?? null) : null;\n const f = earningsFigures(cur, plan);\n const money = moneyIn(cur.currency, 2);\n const caption = `${span[0].toUpperCase()}${span.slice(1)} · ${cur.currency}`;\n const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);\n const trend = toTrend(\n cur.series.map((p) => ({ date: p.date, value: toMajor(p.feeMinor, cur.exponent) })),\n 'value',\n );\n const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);\n\n return (\n <>\n <p className={ledger.sectionNote}>\n Everything Olive makes in this period: fees on what customers pay, on money merchants send out and move to\n their bank, and what merchants pay for their plan. Showing {cur.currency}\n {data.currencies.length > 1 ? '; use the currency filter for the others.' : '.'}\n </p>\n\n <section aria-label=\"What Olive earns\" className=\"adm-grid\">\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Total earned\"\n accent={colors.series[0]}\n spark={trend.map((p) => p.value)}\n sparkColor={colors.series[0]}\n value={money(f.net)}\n sub={`${money(f.gross)} before processor costs`}\n hint=\"Fees Olive earned plus plan income, less what the card processor charged.\"\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Processor costs\"\n accent={colors.series[7]}\n value={cur.providerCostsRecorded ? money(f.provider) : '–'}\n sub={cur.providerCostsRecorded ? 'Charged by the card processor' : 'None recorded in the ledger'}\n hint=\"What the payment processor kept from the same payments. When none is recorded, the total earned is before any such cost.\"\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Plan subscriptions\"\n accent={colors.plans}\n value={money(f.plan)}\n sub={recurring.payingMerchants ? `${integer.format(recurring.payingMerchants)} ${recurring.payingMerchants === 1 ? 'merchant' : 'merchants'} paying` : 'Plan payments collected'}\n hint=\"What merchants paid Olive for their plans in this period, however they paid.\"\n />\n </div>\n {f.parts\n .filter((p) => p.source !== 'plans')\n .map((p) => (\n <div key={p.source} className=\"adm-span-3\">\n <StatCard\n label={CARD_TITLES[p.source] ?? p.label}\n accent={colors[p.source]}\n value={money(p.value)}\n sub={`${integer.format(p.count)} ${p.count === 1 ? 'transaction' : 'transactions'}`}\n hint={CARD_NOTES[p.source]}\n delta={{ pct: pctChange(p.value, p.previous), caption: 'vs the period before' }}\n />\n </div>\n ))}\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 {f.parts.some((p) => p.value > 0) ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Earned\"\n slices={f.parts.map((p) => ({\n key: p.source,\n label: CARD_TITLES[p.source] ?? p.label,\n value: p.value,\n color: colors[p.source],\n detail:\n p.count > 0\n ? `${integer.format(p.count)} ${p.count === 1 ? 'transaction' : 'transactions'}`\n : undefined,\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=\"Before costs\"\n slices={[\n { key: 'kept', label: 'Kept by Olive', value: Math.max(0, f.net), color: colors.series[2], detail: 'After processor costs' },\n { key: 'cost', label: 'Processor costs', value: Math.max(0, f.provider), color: colors.series[7], detail: 'Charged by the card processor' },\n ]}\n />\n ) : (\n <p className={styles.empty}>No processor cost is recorded for these fees, so everything shown is before costs.</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-12\">\n <Panel title=\"Fees earned each day\" caption={caption} height=\"calc(var(--adm-panel-h) + 32px)\">\n <div className={styles.flatChart}>\n <TimeSeriesChart\n data={trend}\n color={colors.series[0]}\n formatValue={money}\n compare={null}\n emptyMessage=\"No fees were earned on any day in this period\"\n height={240}\n />\n </div>\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel title=\"Where the fees come from\" caption={`Merchants, ${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 {`${integer.format(m.count)} ${m.count === 1 ? 'transaction' : 'transactions'}`}\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[0],\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":[]}
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 { TimeSeriesChart } from '@withpotter/components/charts';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { EarningsCurrency } from '@withpotter/lib/admin-earnings-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 NOTHING_YET = (currency: string): EarningsCurrency => ({\n currency,\n exponent: 2,\n feeMinor: '0',\n providerMinor: '0',\n netMinor: '0',\n previousFeeMinor: '0',\n previousProviderMinor: '0',\n count: 0,\n providerCostsRecorded: false,\n bySource: [],\n series: [],\n previousSeries: [],\n topMerchants: [],\n});\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 // Nothing earned yet is a real answer: every card stays on the page, at zero, until the first fee comes in.\n const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0] ?? NOTHING_YET(wanted);\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":[]}
@@ -19,7 +19,9 @@ const PAGE_SIZE = 25;
19
19
  const FILTERS = { q: "", source: "", merchant: "" };
20
20
  const SOURCES = [
21
21
  { value: "", label: "Every source" },
22
- { value: "payments", label: "Customer payments" },
22
+ { value: "online", label: "Online card payments" },
23
+ { value: "pos", label: "POS card payments and transfers" },
24
+ { value: "payments", label: "Other card payments" },
23
25
  { value: "payouts", label: "Payouts (money sent out)" },
24
26
  { value: "settlements", label: "Settlements to bank" },
25
27
  { value: "cash", label: "Cash received" },
@@ -1 +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: 'payments', label: 'Customer 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":";AAoEM,cAeE,YAfF;AAlEN,SAAS,WAAW,SAAS,gBAAgB;AAC7C,OAAO,UAAU;AACjB,SAAS,WAAW,UAAU,YAAY,UAAU;AACpD,SAAS,wBAA0C;AACnD,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAS,YAAY,cAAc,cAAc,SAAS,kBAAkB;AAC5E,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AACxC,OAAO,YAAY;AACnB,OAAO,WAAW;AAElB,MAAM,YAAY;AAClB,MAAM,UAAU,EAAE,GAAG,IAAI,QAAQ,IAAI,UAAU,GAAG;AAElD,MAAM,UAAU;AAAA,EACd,EAAE,OAAO,IAAI,OAAO,eAAe;AAAA,EACnC,EAAE,OAAO,YAAY,OAAO,oBAAoB;AAAA,EAChD,EAAE,OAAO,WAAW,OAAO,2BAA2B;AAAA,EACtD,EAAE,OAAO,eAAe,OAAO,sBAAsB;AAAA,EACrD,EAAE,OAAO,QAAQ,OAAO,gBAAgB;AAAA,EACxC,EAAE,OAAO,SAAS,OAAO,mCAAmC;AAAA,EAC5D,EAAE,OAAO,aAAa,OAAO,mBAAmB;AAAA,EAChD,EAAE,OAAO,MAAM,OAAO,sBAAsB;AAAA,EAC5C,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,SAAS,OAAO,aAAa;AACxC;AAOO,SAAS,cAAc,EAAE,WAAW,QAAQ,GAA6C;AAC9F,QAAM,SAAS,gBAAgB;AAC/B,QAAM,EAAE,QAAQ,MAAM,IAAI,IAAI,cAAc,MAAM,OAAO;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,OAAO,CAAC;AAG7C,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,EAAE,WAAW,SAAS,QAAQ,OAAO,QAAQ,YAAY,OAAO,UAAU,GAAG,OAAO,GAAG,MAAM,OAAO,UAAU;AAAA,IACrH,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;AACA,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,wBAAwB,OAAO;AAE7E,MAAI;AACF,WACE;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,SAAS,MAAM,KAAK,QAAQ;AAAA;AAAA,IAC9B;AAGJ,MAAI,aAAa,CAAC,KAAM,QAAO,oBAAC,cAAW,OAAM,mCAA6B;AAE9E,SACE,qBAAC,SACC;AAAA,wBAAC,OAAE,WAAW,OAAO,aAAa,6IAGlC;AAAA,IACA,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,oBAAC,gBAAa,OAAO,OAAO,QAAQ,UAAU,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,SAAS,OAAM,UAAS;AAAA,QAC5G;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,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,UAAU,aAAa,kBAAkB,WAAW,OAAO;AAAA,UAC3D,MAAM,MAAM,iBAAiB,gBAAgB,EAAE,GAAG,SAAS,MAAM,QAAW,OAAO,OAAU,CAAC;AAAA;AAAA,MAChG;AAAA,OACF;AAAA,IAEC,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,oBAAM;AAAA,UACV,oBAAC,MAAG,sBAAQ;AAAA,UACZ,oBAAC,MAAG,6BAAe;AAAA,UACnB,oBAAC,MAAG,OAAM,SAAQ,0BAAY;AAAA,UAC9B,oBAAC,MAAG,OAAM,SAAQ,4BAAc;AAAA,UAChC,oBAAC,MAAG,OAAM,SAAQ,kBAAI;AAAA,WACxB;AAAA,QAGD,eAAK,KAAK,IAAI,CAAC,QACd,oBAAC,cAA6C,KAAU,OAAO,OAAO,IAAI,MAAM,KAA/D,GAAG,IAAI,EAAE,IAAI,IAAI,QAAQ,EAAyC,CACpF;AAAA;AAAA,IACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,YAAY,KAAK;AAAA,QACjB,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK,UAAU,IAAI,YAAY;AAAA,QACrC,QAAQ,CAAC,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC;AAAA;AAAA,IACtC;AAAA,KACF;AAEJ;AAEA,SAAS,WAAW,EAAE,KAAK,MAAM,GAAwC;AACvE,QAAM,QAAQ,CAAC,UAAkB,YAAY,OAAO,IAAI,QAAQ;AAEhE,SACE,qBAAC,YACC;AAAA,wBAAC,QAAG,WAAW,MAAM,KAClB,cAAI,KAAK,IAAI,EAAE,EAAE,eAAe,QAAW,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,WAAW,MAAM,WAAW,QAAQ,UAAU,CAAC,GACrI;AAAA,IACA,oBAAC,QACC,+BAAC,UAAK,OAAO,EAAE,SAAS,eAAe,YAAY,UAAU,KAAK,EAAE,GAClE;AAAA,0BAAC,UAAK,eAAW,MAAC,OAAO,EAAE,OAAO,IAAI,QAAQ,IAAI,cAAc,GAAG,YAAY,OAAO,YAAY,EAAE,GAAG;AAAA,MACtG,IAAI;AAAA,OACP,GACF;AAAA,IACA,oBAAC,QAAG,WAAW,MAAM,MAClB,cAAI,SAAS,MAAM,IAAI,SAAS,OAC/B,oBAAC,QAAK,MAAM,kBAAkB,IAAI,SAAS,EAAE,IAAK,cAAI,SAAS,MAAK,IAEpE,wBAEJ;AAAA,IACA,oBAAC,QAAI,cAAI,aAAY;AAAA,IACrB,oBAAC,QAAG,WAAW,MAAM,WAAY,gBAAM,IAAI,QAAQ,GAAE;AAAA,IACrD,oBAAC,QAAG,WAAW,MAAM,KAAM,cAAI,kBAAkB,MAAM,WAAM,MAAM,IAAI,aAAa,GAAE;AAAA,IACtF,oBAAC,QAAG,WAAW,MAAM,KAAM,gBAAM,IAAI,QAAQ,GAAE;AAAA,KACjD;AAEJ;","names":[]}
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":[]}
@@ -116,3 +116,86 @@
116
116
  height: 100%;
117
117
  border-radius: 999px;
118
118
  }
119
+
120
+ /* Stream against the period before: a faded bar for then, a solid one for now. */
121
+ .cmp {
122
+ display: grid;
123
+ gap: 16px;
124
+ margin: 0;
125
+ padding: 0;
126
+ list-style: none;
127
+ }
128
+
129
+ .cmpRow {
130
+ display: grid;
131
+ gap: 4px;
132
+ }
133
+
134
+ .cmpHead {
135
+ display: grid;
136
+ grid-template-columns: 12px minmax(0, 1fr) auto;
137
+ align-items: center;
138
+ gap: 10px;
139
+ }
140
+
141
+ .cmpDelta {
142
+ font-size: 12px;
143
+ font-weight: 500;
144
+ color: var(--text-tertiary);
145
+ font-variant-numeric: tabular-nums;
146
+ }
147
+
148
+ .up {
149
+ color: var(--success);
150
+ }
151
+
152
+ .down {
153
+ color: var(--error);
154
+ }
155
+
156
+ .cmpTrack {
157
+ display: block;
158
+ height: 8px;
159
+ border-radius: 999px;
160
+ background: var(--bg-subtle);
161
+ overflow: hidden;
162
+ }
163
+
164
+ .cmpPrev,
165
+ .cmpNow {
166
+ display: block;
167
+ height: 100%;
168
+ border-radius: 999px;
169
+ }
170
+
171
+ .cmpPrev {
172
+ opacity: 0.35;
173
+ }
174
+
175
+ .cmpNums {
176
+ display: flex;
177
+ justify-content: space-between;
178
+ gap: 12px;
179
+ font-size: 12px;
180
+ color: var(--text-tertiary);
181
+ font-variant-numeric: tabular-nums;
182
+ }
183
+
184
+ .cmpNums strong {
185
+ font-size: 13px;
186
+ font-weight: 500;
187
+ color: var(--text-primary);
188
+ }
189
+
190
+ /* A metric card that drives the chart: the chosen one is outlined. */
191
+ .metric {
192
+ cursor: pointer;
193
+ border-radius: var(--adm-card-radius);
194
+ outline: 2px solid transparent;
195
+ outline-offset: 2px;
196
+ }
197
+
198
+ .metric:focus-visible,
199
+ .metricOn {
200
+ outline-color: var(--text-tertiary);
201
+ }
@@ -1,44 +1,13 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useMemo } from "react";
4
- import { TimeSeriesChart } from "@withpotter/components/charts";
5
- import {
6
- MoneyLines,
7
- PICK_CURRENCY,
8
- PageTabs,
9
- Panel,
10
- StatCard,
11
- moneyIn,
12
- rowFor,
13
- toTrend,
14
- useTabParam
15
- } from "../_ui";
16
- import { Reveal } from "../_fx";
17
- import { Tinted } from "../audit/tinted";
18
- import { ChannelPanel } from "./channel-panel";
19
3
  import { EarningsSummary } from "./earnings/earnings-summary";
20
4
  import { EarningsTable } from "./earnings/earnings-table";
21
- import { MerchantHeadline } from "./merchant-headline";
22
- import { MethodCards } from "./method-cards";
23
5
  import { HealthPanel } from "./health-panel";
24
6
  import { PlanMixPanel } from "./plan-mix-panel";
25
7
  import { planLines } from "./plan-lines";
26
8
  import { PlansPanel } from "./plans-panel";
27
- import { platformFeeRows } from "./platform-fees";
28
9
  import { RevenueKpis } from "./revenue-kpis";
29
- import { SubscriptionsPanel } from "./subscriptions-panel";
30
- import { TopTenantsPanel } from "./top-tenants-panel";
31
- import styles from "./revenue.module.css";
32
10
  import ledgerStyles from "./ledger.module.css";
33
- const VIEWS = [
34
- { id: "earnings", label: "Earnings" },
35
- { id: "transactions", label: "Transactions" },
36
- { id: "olive", label: "Money Olive earns" },
37
- { id: "customers", label: "How customers pay" },
38
- { id: "merchants", label: "Money merchants earn" },
39
- { id: "plans", label: "Plan subscriptions" }
40
- ];
41
- const VIEW_KEYS = VIEWS.map((v) => v.id);
42
11
  function RevenueOverview({
43
12
  startDate,
44
13
  endDate,
@@ -48,22 +17,25 @@ function RevenueOverview({
48
17
  money2,
49
18
  currency,
50
19
  focus,
51
- period,
52
20
  span,
53
21
  recurringCaption,
54
- channels,
55
- ledger,
56
22
  plans
57
23
  }) {
58
- const [view, setView] = useTabParam(VIEW_KEYS, "view");
59
- const gmv = rowFor(data.gmvByCurrency, focus);
60
- const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
61
- const gmvCaption = focus ? `${period} \xB7 ${focus}` : period;
62
24
  const recurringTracked = data.recurring.tracked !== false;
63
- const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
64
- const olive = /* @__PURE__ */ jsxs(Fragment, { children: [
25
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
26
+ /* @__PURE__ */ jsx(
27
+ EarningsSummary,
28
+ {
29
+ startDate,
30
+ endDate,
31
+ currency,
32
+ recurring: data.recurring,
33
+ span
34
+ }
35
+ ),
36
+ /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Plan income" }),
65
37
  /* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
66
- "What merchants pay Olive: their plan payments, and the fees Olive keeps.",
38
+ "What merchants pay Olive for their plans, each month and over a year.",
67
39
  recurringTracked ? "" : " Plan income is not set up on this box yet."
68
40
  ] }),
69
41
  /* @__PURE__ */ jsx(
@@ -78,22 +50,6 @@ function RevenueOverview({
78
50
  gmvSpark: []
79
51
  }
80
52
  ),
81
- platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
82
- StatCard,
83
- {
84
- label: "Fees Olive keeps",
85
- value: /* @__PURE__ */ jsx(
86
- MoneyLines,
87
- {
88
- rows: platformFees,
89
- currency: "",
90
- amount: (r) => r.amount,
91
- decimals: 2
92
- }
93
- ),
94
- hint: "What Olive keeps from payouts and plan payments, after costs. All time, not the period."
95
- }
96
- ) }) }),
97
53
  recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
98
54
  /* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
99
55
  /* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
@@ -111,80 +67,9 @@ function RevenueOverview({
111
67
  caption: recurringCaption
112
68
  }
113
69
  ) })
114
- ] })
115
- ] });
116
- const customers = /* @__PURE__ */ jsxs(Fragment, { children: [
117
- /* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "The ways of paying on the checkout, and how much came in through each." }),
118
- ledger && focus ? /* @__PURE__ */ jsx(
119
- MethodCards,
120
- {
121
- now: ledger.byChannel.current,
122
- before: ledger.byChannel.previous,
123
- currency: focus
124
- }
125
- ) : channels && channels.length > 0 ? /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(ChannelPanel, { totals: channels, focus, caption: gmvCaption }) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: focus ? "Nothing to show for this period." : PICK_CURRENCY })
126
- ] });
127
- const merchants = /* @__PURE__ */ jsxs(Fragment, { children: [
128
- /* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "What customers paid to merchants. This is the merchants' money, not Olive's own income." }),
129
- /* @__PURE__ */ jsx(
130
- RevenueKpis,
131
- {
132
- part: "merchant",
133
- data,
134
- money,
135
- money2,
136
- currency,
137
- focus,
138
- gmvSpark: gmvSeries.map((p) => p.value),
139
- extra: ledger && focus ? /* @__PURE__ */ jsx(MerchantHeadline, { ledger, focus }) : null
140
- }
141
- ),
142
- /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
143
- Panel,
144
- {
145
- title: "Merchant sales over time",
146
- caption: gmvCaption,
147
- height: "calc(var(--adm-panel-h) + 32px)",
148
- children: focus ? /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(Tinted, { tone: "money", children: /* @__PURE__ */ jsx(
149
- TimeSeriesChart,
150
- {
151
- data: gmvSeries,
152
- compare: null,
153
- compareLabel: `Previous ${span}`,
154
- formatValue: moneyIn(focus),
155
- emptyMessage: `No paid ${focus} orders in this window yet`,
156
- height: 240
157
- }
158
- ) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
159
- }
160
- ) }) }),
161
- /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
162
- TopTenantsPanel,
163
- {
164
- tenants: gmv?.topTenants ?? [],
165
- lifetime: gmv?.lifetime ?? 0,
166
- focus
167
- }
168
- ) }) })
169
- ] });
170
- const planSubscriptions = recurringTracked ? /* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "Plan income is not set up on this box yet." });
171
- return /* @__PURE__ */ jsxs(Fragment, { children: [
172
- /* @__PURE__ */ jsx(PageTabs, { tabs: VIEWS, active: view, onChange: setView }),
173
- view === "earnings" && /* @__PURE__ */ jsx(
174
- EarningsSummary,
175
- {
176
- startDate,
177
- endDate,
178
- currency,
179
- recurring: data.recurring,
180
- span
181
- }
182
- ),
183
- view === "transactions" && /* @__PURE__ */ jsx(EarningsTable, { startDate, endDate }),
184
- view === "olive" && olive,
185
- view === "customers" && customers,
186
- view === "merchants" && merchants,
187
- view === "plans" && planSubscriptions
70
+ ] }),
71
+ /* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Every earning" }),
72
+ /* @__PURE__ */ jsx(EarningsTable, { startDate, endDate })
188
73
  ] });
189
74
  }
190
75
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport {\n MoneyLines,\n PICK_CURRENCY,\n PageTabs,\n Panel,\n StatCard,\n moneyIn,\n rowFor,\n toTrend,\n useTabParam,\n} from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { EarningsSummary } from './earnings/earnings-summary';\nimport { EarningsTable } from './earnings/earnings-table';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * The Overview's own tabs. Earnings and Transactions are first: what Olive makes from everything, and the fees behind\n * it. The rest are as they were: Money Olive earns (plan figures and fees), how customers pay, the merchants' money and\n * the plan subscriptions.\n */\nconst VIEWS = [\n { id: 'earnings', label: 'Earnings' },\n { id: 'transactions', label: 'Transactions' },\n { id: 'olive', label: 'Money Olive earns' },\n { id: 'customers', label: 'How customers pay' },\n { id: 'merchants', label: 'Money merchants earn' },\n { id: 'plans', label: 'Plan subscriptions' },\n] as const;\n\nconst VIEW_KEYS = VIEWS.map((v) => v.id);\n\n/**\n * Overview tab, one view at a time. Earnings is what Olive makes, whatever it comes from, with the fee transactions\n * behind it beside it; the other views are as they were. Merchant GMV is per currency: the chart and the leaderboard\n * follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n startDate,\n endDate,\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n /** The window, for the earnings views, which read their own data. */\n startDate?: string;\n endDate?: string;\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const [view, setView] = useTabParam(VIEW_KEYS, 'view');\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n const olive = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs. All time, not the period.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className={plans ? 'adm-span-12' : 'adm-span-7'}>\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n {/* The Plans table has five columns, so it gets a full row; the older plan mix keeps its narrow slot. */}\n <div className={plans ? 'adm-span-12' : 'adm-span-5'}>\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n </>\n );\n\n const customers = (\n <>\n <p className={ledgerStyles.sectionNote}>\n The ways of paying on the checkout, and how much came in through each.\n </p>\n {ledger && focus ? (\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n ) : channels && channels.length > 0 ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>{focus ? 'Nothing to show for this period.' : PICK_CURRENCY}</p>\n )}\n </>\n );\n\n const merchants = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n </>\n );\n\n const planSubscriptions = recurringTracked ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>Plan income is not set up on this box yet.</p>\n );\n\n return (\n <>\n <PageTabs tabs={VIEWS} active={view} onChange={setView} />\n {view === 'earnings' && (\n <EarningsSummary\n startDate={startDate}\n endDate={endDate}\n currency={currency}\n recurring={data.recurring}\n span={span}\n />\n )}\n {view === 'transactions' && <EarningsTable startDate={startDate} endDate={endDate} />}\n {view === 'olive' && olive}\n {view === 'customers' && customers}\n {view === 'merchants' && merchants}\n {view === 'plans' && planSubscriptions}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformCockpit, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport { EarningsSummary } from './earnings/earnings-summary';\nimport { EarningsTable } from './earnings/earnings-table';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { RevenueKpis } from './revenue-kpis';\nimport ledgerStyles from './ledger.module.css';\n\n/**\n * Money Olive earns: one page, no sub-tabs, read top to bottom. What Olive made and kept (cards, pies, graphs, the\n * merchants it came from), then Olive's own plan income, then every earning in a table. It is Olive's money only: what\n * customers pay merchants lives on the Merchant payments tab, and the plan subscriptions on the Subscriptions tab.\n */\nexport function RevenueOverview({\n startDate,\n endDate,\n data,\n health,\n money,\n money2,\n currency,\n focus,\n span,\n recurringCaption,\n plans,\n}: {\n /** The window, for the earnings views, which read their own data. */\n startDate?: string;\n endDate?: string;\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty follows the billing currency. */\n currency: string;\n /** The one currency the plan figures show, or null. */\n focus: string | null;\n span: string;\n recurringCaption: string;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const recurringTracked = data.recurring.tracked !== false;\n\n return (\n <>\n <EarningsSummary\n startDate={startDate}\n endDate={endDate}\n currency={currency}\n recurring={data.recurring}\n span={span}\n />\n\n <h2 className={ledgerStyles.sectionTitle}>Plan income</h2>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive for their plans, each month and over a year.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className={plans ? 'adm-span-12' : 'adm-span-7'}>\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n {/* The Plans table has five columns, so it gets a full row; the older plan mix keeps its narrow slot. */}\n <div className={plans ? 'adm-span-12' : 'adm-span-5'}>\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n\n <h2 className={ledgerStyles.sectionTitle}>Every earning</h2>\n <EarningsTable startDate={startDate} endDate={endDate} />\n </>\n );\n}\n"],"mappings":";AAmDI,mBACE,KASA,YAVF;AA/CJ,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,OAAO,kBAAkB;AAOlB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAiBG;AACD,QAAM,mBAAmB,KAAK,UAAU,YAAY;AAEpD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,KAAK;AAAA,QAChB;AAAA;AAAA,IACF;AAAA,IAEA,oBAAC,QAAG,WAAW,aAAa,cAAc,yBAAW;AAAA,IACrD,qBAAC,OAAE,WAAW,aAAa,aAAa;AAAA;AAAA,MAErC,mBAAmB,KAAK;AAAA,OAC3B;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,CAAC;AAAA;AAAA,IACb;AAAA,IAEC,oBACC,qBAAC,SAAI,WAAU,YACb;AAAA,0BAAC,SAAI,WAAW,QAAQ,gBAAgB,cACtC,8BAAC,eAAY,QAAgB,OAAc,SAAS,kBAAkB,GACxE;AAAA,MAEA,oBAAC,SAAI,WAAW,QAAQ,gBAAgB,cACrC,kBACC;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,UAAU,MAAM,OAAO,MAAM,YAAY,KAAK,UAAU,aAAa;AAAA,UAC5E;AAAA,UACA,SAAS;AAAA;AAAA,MACX,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,eAAe,KAAK,UAAU;AAAA,UAC9B;AAAA,UACA,SAAS;AAAA;AAAA,MACX,GAEJ;AAAA,OACF;AAAA,IAGF,oBAAC,QAAG,WAAW,aAAa,cAAc,2BAAa;AAAA,IACvD,oBAAC,iBAAc,WAAsB,SAAkB;AAAA,KACzD;AAEJ;","names":[]}
@@ -3,7 +3,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { ADMIN_NAME } from "../_shell/admin-brand";
5
5
  import {
6
- useGatewayChannels,
7
6
  useMerchantPayments,
8
7
  usePlatformCockpit,
9
8
  usePlatformRevenue
@@ -31,7 +30,7 @@ import { RevenueRetention } from "./revenue-retention";
31
30
  import { SubscriptionsView } from "./subscriptions-view";
32
31
  import { SummaryDownload } from "./summary-download";
33
32
  const TABS = [
34
- { id: "overview", label: "Overview" },
33
+ { id: "overview", label: "Money Olive earns" },
35
34
  { id: "merchants", label: "Merchant payments" },
36
35
  { id: "subscriptions", label: "Subscriptions" },
37
36
  { id: "retention", label: "Retention" }
@@ -60,10 +59,6 @@ function RevenueWorkspace() {
60
59
  startDate: range.startDate,
61
60
  endDate: range.endDate
62
61
  });
63
- const channels = useGatewayChannels({
64
- startDate: range.startDate,
65
- endDate: range.endDate
66
- });
67
62
  const ledger = useMerchantPayments({
68
63
  startDate: range.startDate,
69
64
  endDate: range.endDate,
@@ -130,11 +125,8 @@ function RevenueWorkspace() {
130
125
  money2,
131
126
  currency,
132
127
  focus,
133
- period,
134
128
  span,
135
129
  recurringCaption: `${period} \xB7 ${recurringCurrency}`,
136
- channels: channels.data?.byChannel,
137
- ledger: ledger.data,
138
130
  plans: plans.data
139
131
  }
140
132
  );
@@ -144,7 +136,7 @@ function RevenueWorkspace() {
144
136
  PageHeader,
145
137
  {
146
138
  title: "Revenue",
147
- description: `All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`,
139
+ description: `How money flows through ${ADMIN_NAME}: what it earns, where it comes from, what it costs and what is left.`,
148
140
  actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
149
141
  tab === "overview" ? /* @__PURE__ */ jsx(
150
142
  SummaryDownload,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { usePlanList } from '../pricing/use-plans';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const plans = usePlanList();\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n startDate={range.startDate}\n endDate={range.endDate}\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n plans={plans.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { usePlanList } from '../pricing/use-plans';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Money Olive earns' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const plans = usePlanList();\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the summary download.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n startDate={range.startDate}\n endDate={range.endDate}\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n plans={plans.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`How money flows through ${ADMIN_NAME}: what it earns, where it comes from, what it costs and what is left.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/pages-admin",
3
- "version": "0.13.0-next.122",
3
+ "version": "0.13.0-next.124",
4
4
  "description": "Box console platform-admin routes: overview, revenue, accounts, tenants, admins, roles, audit.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": false,
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@radix-ui/react-popover": "^1.1.23",
33
33
  "@tanstack/react-query": "^5.90.20",
34
- "@withpotter/lib": "^0.13.0-next.122",
35
- "@withpotter/platform-admin": "^0.13.0-next.122",
36
- "@withpotter/ui-kit": "^0.13.0-next.122",
34
+ "@withpotter/lib": "^0.13.0-next.124",
35
+ "@withpotter/platform-admin": "^0.13.0-next.124",
36
+ "@withpotter/ui-kit": "^0.13.0-next.124",
37
37
  "lucide-react": "^0.562.0",
38
38
  "motion": "^12.42.2"
39
39
  },