@withpotter/pages-admin 0.13.0-next.122 → 0.13.0-next.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/revenue/earnings/earnings-colors.js +2 -0
- package/dist/admin/revenue/earnings/earnings-colors.js.map +1 -1
- package/dist/admin/revenue/earnings/earnings-legend.js +37 -0
- package/dist/admin/revenue/earnings/earnings-legend.js.map +1 -1
- package/dist/admin/revenue/earnings/earnings-summary.js +185 -83
- package/dist/admin/revenue/earnings/earnings-summary.js.map +1 -1
- package/dist/admin/revenue/earnings/earnings-table.js +3 -1
- package/dist/admin/revenue/earnings/earnings-table.js.map +1 -1
- package/dist/admin/revenue/earnings/earnings.module.css +83 -0
- package/dist/admin/revenue/revenue-overview.js +16 -131
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +2 -10
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/package.json +4 -4
|
@@ -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":";
|
|
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":";AAiCM,cAUM,YAVN;AA/BN,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,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;AAcO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AACF,GAGG;AACD,QAAM,SAAS,KAAK,IAAI,GAAG,GAAG,KAAK,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAExE,SACE,oBAAC,QAAG,WAAW,OAAO,KACnB,eAAK,IAAI,CAAC,MAAM;AACf,UAAM,SAAS,UAAU,EAAE,OAAO,EAAE,QAAQ;AAC5C,UAAM,OACJ,WAAW,QAAQ,WAAW,IAAI,KAAK,SAAS,IAAI,OAAO,KAAK,OAAO;AAEzE,WACE,qBAAC,QAAe,WAAW,OAAO,QAChC;AAAA,2BAAC,UAAK,WAAW,OAAO,SACtB;AAAA,4BAAC,UAAK,WAAW,OAAO,QAAQ,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,eAAW,MAAC;AAAA,QAC5E,oBAAC,UAAK,WAAW,OAAO,aAAc,YAAE,OAAM;AAAA,QAC9C,oBAAC,UAAK,WAAW,GAAG,OAAO,QAAQ,IAAI,IAAI,IACxC,qBAAW,OACR,EAAE,QAAQ,IACR,QACA,WACF,GAAG,SAAS,IAAI,MAAM,SAAS,IAAI,WAAW,EAAE,GAAG,KAAK,IAAI,MAAM,CAAC,KACzE;AAAA,SACF;AAAA,MACA,oBAAC,UAAK,WAAW,OAAO,UAAU,eAAW,MAC3C;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,OAAO;AAAA,UAClB,OAAO,EAAE,OAAO,GAAI,EAAE,WAAW,SAAU,GAAG,KAAK,YAAY,EAAE,MAAM;AAAA;AAAA,MACzE,GACF;AAAA,MACA,oBAAC,UAAK,WAAW,OAAO,UAAU,eAAW,MAC3C;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,OAAO;AAAA,UAClB,OAAO,EAAE,OAAO,GAAI,EAAE,QAAQ,SAAU,GAAG,KAAK,YAAY,EAAE,MAAM;AAAA;AAAA,MACtE,GACF;AAAA,MACA,qBAAC,UAAK,WAAW,OAAO,SACtB;AAAA,4BAAC,YAAQ,gBAAM,EAAE,KAAK,GAAE;AAAA,QACxB,oBAAC,UAAM,oBAAU,MAAM,EAAE,QAAQ,CAAC,IAAG;AAAA,SACvC;AAAA,SA3BO,EAAE,GA4BX;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":[]}
|
|
@@ -1,34 +1,81 @@
|
|
|
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
5
|
import { EmptyNote } from "@withpotter/ui-kit";
|
|
5
6
|
import { TimeSeriesChart } from "@withpotter/components/charts";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
earningsFigures,
|
|
9
|
+
pctChange,
|
|
10
|
+
toMajor
|
|
11
|
+
} from "@withpotter/lib/admin-earnings-model";
|
|
7
12
|
import { LoadFailed } from "../../audit/load-failed";
|
|
8
13
|
import { AdmLoading, Panel, StatCard, moneyIn, toTrend } from "../../_ui";
|
|
9
14
|
import { useSourceColors } from "./earnings-colors";
|
|
10
|
-
import { PieWithDetails } from "./earnings-legend";
|
|
15
|
+
import { CompareBars, PieWithDetails } from "./earnings-legend";
|
|
11
16
|
import { useEarnings } from "./use-earnings";
|
|
12
17
|
import styles from "../revenue.module.css";
|
|
13
18
|
import ledger from "../ledger.module.css";
|
|
14
19
|
import earn from "./earnings.module.css";
|
|
15
20
|
const integer = new Intl.NumberFormat("en-US");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const ORDER = ["online", "pos", "payouts", "settlements", "cash", "plans", "payments", "other"];
|
|
22
|
+
const ALWAYS = /* @__PURE__ */ new Set(["online", "pos", "payouts", "settlements", "cash", "plans"]);
|
|
23
|
+
const TITLES = {
|
|
24
|
+
online: "Online card payments",
|
|
25
|
+
pos: "POS card payments and transfers",
|
|
20
26
|
payouts: "Payouts",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
settlements: "Settlements to merchants' banks",
|
|
28
|
+
cash: "Cash received",
|
|
29
|
+
plans: "Plan subscriptions",
|
|
30
|
+
payments: "Other card payments",
|
|
24
31
|
other: "Other fees"
|
|
25
32
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const WHAT = {
|
|
34
|
+
online: "What Olive keeps when a customer pays a merchant by card online, including money that arrives through the payment gateway.",
|
|
35
|
+
pos: "What Olive keeps when a customer pays at a merchant's POS terminal, by card or by transfer.",
|
|
36
|
+
payouts: "What Olive charges when a merchant sends money out of their wallet.",
|
|
37
|
+
settlements: "What Olive charges when a merchant's money is settled to their bank account.",
|
|
38
|
+
cash: "What Olive charges when cash is received for a merchant.",
|
|
39
|
+
plans: "What merchants pay Olive for their plan, however they paid.",
|
|
40
|
+
payments: "Card payment fees that are not tagged as online or POS.",
|
|
41
|
+
other: "Fees from anything not listed here."
|
|
31
42
|
};
|
|
43
|
+
const MINOR = /* @__PURE__ */ new Set(["transfers", "fx", "lending", "other"]);
|
|
44
|
+
function named(parts) {
|
|
45
|
+
const rest = parts.filter((p) => MINOR.has(p.source));
|
|
46
|
+
const main = parts.filter((p) => !MINOR.has(p.source));
|
|
47
|
+
const sum = (k) => rest.reduce((a, p) => a + p[k], 0);
|
|
48
|
+
if (sum("value") <= 0 && sum("previous") <= 0) return main;
|
|
49
|
+
return [
|
|
50
|
+
...main,
|
|
51
|
+
{ source: "other", label: "Other fees", value: sum("value"), previous: sum("previous"), count: sum("count") }
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function MetricCard({
|
|
55
|
+
on,
|
|
56
|
+
onPick,
|
|
57
|
+
span,
|
|
58
|
+
children
|
|
59
|
+
}) {
|
|
60
|
+
return /* @__PURE__ */ jsx(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
className: `${span} ${earn.metric} ${on ? earn.metricOn : ""}`,
|
|
64
|
+
role: "button",
|
|
65
|
+
tabIndex: 0,
|
|
66
|
+
"aria-pressed": on,
|
|
67
|
+
onClick: onPick,
|
|
68
|
+
onKeyDown: (e) => {
|
|
69
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
onPick();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
children
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const count = (n) => `${integer.format(n)} ${n === 1 ? "transaction" : "transactions"}`;
|
|
32
79
|
function EarningsSummary({
|
|
33
80
|
startDate,
|
|
34
81
|
endDate,
|
|
@@ -38,6 +85,7 @@ function EarningsSummary({
|
|
|
38
85
|
}) {
|
|
39
86
|
const { data, isLoading, isError, refetch } = useEarnings({ startDate, endDate });
|
|
40
87
|
const colors = useSourceColors();
|
|
88
|
+
const [metric, setMetric] = useState("earned");
|
|
41
89
|
if (isError)
|
|
42
90
|
return /* @__PURE__ */ jsx(
|
|
43
91
|
LoadFailed,
|
|
@@ -53,77 +101,127 @@ function EarningsSummary({
|
|
|
53
101
|
return /* @__PURE__ */ jsx(EmptyNote, { className: styles.empty, children: "No fees have been earned in this period yet." });
|
|
54
102
|
const plan = recurring.currency === cur.currency ? recurring.collected ?? null : null;
|
|
55
103
|
const f = earningsFigures(cur, plan);
|
|
104
|
+
const have = named(f.parts);
|
|
105
|
+
const parts = ORDER.filter((k) => ALWAYS.has(k) || have.some((p) => p.source === k && (p.value !== 0 || p.previous !== 0))).map(
|
|
106
|
+
(k) => have.find((p) => p.source === k) ?? { source: k, label: TITLES[k], value: 0, previous: 0, count: 0 }
|
|
107
|
+
).sort((a, b) => ORDER.indexOf(a.source) - ORDER.indexOf(b.source));
|
|
56
108
|
const money = moneyIn(cur.currency, 2);
|
|
57
109
|
const caption = `${span[0].toUpperCase()}${span.slice(1)} \xB7 ${cur.currency}`;
|
|
110
|
+
const margin = f.gross > 0 ? Math.round(f.net / f.gross * 1e3) / 10 : null;
|
|
111
|
+
const green = colors.series[2];
|
|
112
|
+
const red = colors.series[7];
|
|
58
113
|
const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
114
|
+
const day = (p) => {
|
|
115
|
+
const fee = toMajor(p.feeMinor, cur.exponent);
|
|
116
|
+
const cost = toMajor(p.providerMinor, cur.exponent);
|
|
117
|
+
return metric === "earned" ? fee : metric === "costs" ? cost : fee - cost;
|
|
118
|
+
};
|
|
119
|
+
const trend = toTrend(cur.series.map((p) => ({ date: p.date, value: day(p) })), "value");
|
|
120
|
+
const before = toTrend(cur.previousSeries.map((p) => ({ date: p.date, value: day(p) })), "value");
|
|
121
|
+
const feeSpark = cur.series.map((p) => toMajor(p.feeMinor, cur.exponent));
|
|
122
|
+
const chartTitle = { earned: "Fees earned each day", costs: "Processing costs each day", profit: "Profit each day" }[metric];
|
|
63
123
|
const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);
|
|
124
|
+
const label = (p) => TITLES[p.source] ?? p.label;
|
|
64
125
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
65
126
|
/* @__PURE__ */ jsxs("p", { className: ledger.sectionNote, children: [
|
|
66
|
-
"
|
|
127
|
+
"How money flows through Olive: what it earned, what it cost, what is left, and where it came from. Showing",
|
|
128
|
+
" ",
|
|
67
129
|
cur.currency,
|
|
68
130
|
data.currencies.length > 1 ? "; use the currency filter for the others." : "."
|
|
69
131
|
] }),
|
|
70
|
-
/* @__PURE__ */ jsxs("section", { "aria-label": "
|
|
71
|
-
/* @__PURE__ */ jsx("
|
|
132
|
+
/* @__PURE__ */ jsxs("section", { "aria-label": "Earned, costs and profit", className: "adm-grid", children: [
|
|
133
|
+
/* @__PURE__ */ jsx(MetricCard, { on: metric === "earned", onPick: () => setMetric("earned"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
72
134
|
StatCard,
|
|
73
135
|
{
|
|
74
136
|
label: "Total earned",
|
|
75
137
|
accent: colors.series[0],
|
|
76
|
-
spark:
|
|
138
|
+
spark: feeSpark,
|
|
77
139
|
sparkColor: colors.series[0],
|
|
78
|
-
value: money(f.
|
|
79
|
-
sub:
|
|
80
|
-
|
|
140
|
+
value: money(f.gross),
|
|
141
|
+
sub: "Fees plus plan income",
|
|
142
|
+
delta: { pct: pctChange(f.gross, f.feesBefore + f.plan), caption: "vs the period before" },
|
|
143
|
+
hint: "Everything Olive earned in this period: fees on card payments, payouts, settlements and cash, plus what merchants paid for their plans."
|
|
81
144
|
}
|
|
82
145
|
) }),
|
|
83
|
-
/* @__PURE__ */ jsx("
|
|
146
|
+
/* @__PURE__ */ jsx(MetricCard, { on: metric === "costs", onPick: () => setMetric("costs"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
84
147
|
StatCard,
|
|
85
148
|
{
|
|
86
|
-
label: "
|
|
87
|
-
accent:
|
|
88
|
-
value: cur.providerCostsRecorded ?
|
|
89
|
-
sub: cur.providerCostsRecorded ? "Charged by the
|
|
90
|
-
hint: "What the payment
|
|
149
|
+
label: "Processing costs",
|
|
150
|
+
accent: red,
|
|
151
|
+
value: money(cur.providerCostsRecorded ? f.provider : 0),
|
|
152
|
+
sub: cur.providerCostsRecorded ? "Charged by the processors" : "None recorded yet",
|
|
153
|
+
hint: "What the payment processors kept from the same payments, as far as the system records it. Until costs are recorded this stays at zero and profit equals what was earned."
|
|
91
154
|
}
|
|
92
155
|
) }),
|
|
93
|
-
/* @__PURE__ */ jsx("
|
|
156
|
+
/* @__PURE__ */ jsx(MetricCard, { on: metric === "profit", onPick: () => setMetric("profit"), span: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
94
157
|
StatCard,
|
|
95
158
|
{
|
|
96
|
-
label: "
|
|
97
|
-
accent:
|
|
98
|
-
value: money(f.
|
|
99
|
-
sub:
|
|
100
|
-
hint: "
|
|
159
|
+
label: "Profit",
|
|
160
|
+
accent: green,
|
|
161
|
+
value: money(f.net),
|
|
162
|
+
sub: cur.providerCostsRecorded ? "After processing costs" : "Same as earned until costs are recorded",
|
|
163
|
+
hint: "Total earned less the processing costs the system records. Running costs such as hosting and staff are not included."
|
|
101
164
|
}
|
|
102
165
|
) }),
|
|
103
|
-
|
|
166
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
167
|
+
StatCard,
|
|
168
|
+
{
|
|
169
|
+
label: "Margin",
|
|
170
|
+
accent: colors.series[3],
|
|
171
|
+
value: margin === null ? "\u2013" : `${margin}%`,
|
|
172
|
+
sub: "Profit as a share of earned",
|
|
173
|
+
hint: "Profit divided by total earned."
|
|
174
|
+
}
|
|
175
|
+
) })
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(Panel, { title: chartTitle, caption: `${caption} \xB7 dotted line is the period before`, height: "calc(var(--adm-panel-h) + 32px)", children: /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(
|
|
178
|
+
TimeSeriesChart,
|
|
179
|
+
{
|
|
180
|
+
data: trend,
|
|
181
|
+
color: metric === "costs" ? red : metric === "profit" ? green : colors.series[0],
|
|
182
|
+
formatValue: money,
|
|
183
|
+
compare: before,
|
|
184
|
+
compareLabel: "Period before",
|
|
185
|
+
emptyMessage: "Nothing was recorded on any day in this period",
|
|
186
|
+
height: 240
|
|
187
|
+
}
|
|
188
|
+
) }) }) }) }),
|
|
189
|
+
/* @__PURE__ */ jsx("h2", { className: ledger.sectionTitle, children: "Where the money comes from" }),
|
|
190
|
+
/* @__PURE__ */ jsxs("section", { "aria-label": "Earnings by stream", className: "adm-grid", children: [
|
|
191
|
+
parts.map((p) => /* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
|
|
104
192
|
StatCard,
|
|
105
193
|
{
|
|
106
|
-
label:
|
|
194
|
+
label: label(p),
|
|
107
195
|
accent: colors[p.source],
|
|
108
196
|
value: money(p.value),
|
|
109
|
-
sub:
|
|
110
|
-
hint:
|
|
111
|
-
delta: { pct: pctChange(p.value, p.previous), caption: "vs the period before" }
|
|
197
|
+
sub: p.source === "plans" ? "Collected in this period" : p.count === 0 ? "Nothing in this period" : count(p.count),
|
|
198
|
+
hint: WHAT[p.source],
|
|
199
|
+
delta: p.source === "plans" ? void 0 : { pct: pctChange(p.value, p.previous), caption: "vs the period before" }
|
|
200
|
+
}
|
|
201
|
+
) }, p.source)),
|
|
202
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(
|
|
203
|
+
StatCard,
|
|
204
|
+
{
|
|
205
|
+
label: "Olive account and outlets",
|
|
206
|
+
accent: "var(--text-tertiary)",
|
|
207
|
+
value: "Not counted yet",
|
|
208
|
+
sub: "No earnings recorded",
|
|
209
|
+
hint: "Money received through the Olive account and outlets. The bank system does not report a fee for it yet, so nothing can be added here."
|
|
112
210
|
}
|
|
113
|
-
) }
|
|
211
|
+
) })
|
|
114
212
|
] }),
|
|
115
213
|
/* @__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:
|
|
214
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "Where it comes from", caption, height: "auto", children: parts.some((p) => p.value > 0) ? /* @__PURE__ */ jsx(
|
|
117
215
|
PieWithDetails,
|
|
118
216
|
{
|
|
119
217
|
money,
|
|
120
218
|
centerLabel: "Earned",
|
|
121
|
-
slices:
|
|
219
|
+
slices: parts.map((p) => ({
|
|
122
220
|
key: p.source,
|
|
123
|
-
label:
|
|
221
|
+
label: label(p),
|
|
124
222
|
value: p.value,
|
|
125
223
|
color: colors[p.source],
|
|
126
|
-
detail: p.
|
|
224
|
+
detail: p.source === "plans" ? "Collected" : count(p.count)
|
|
127
225
|
}))
|
|
128
226
|
}
|
|
129
227
|
) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "Nothing earned in this period." }) }) }),
|
|
@@ -131,13 +229,13 @@ function EarningsSummary({
|
|
|
131
229
|
PieWithDetails,
|
|
132
230
|
{
|
|
133
231
|
money,
|
|
134
|
-
centerLabel: "
|
|
232
|
+
centerLabel: "Earned",
|
|
135
233
|
slices: [
|
|
136
|
-
{ key: "
|
|
137
|
-
{ key: "cost", label: "
|
|
234
|
+
{ key: "profit", label: "Profit", value: Math.max(0, f.net), color: green, detail: "After processing costs" },
|
|
235
|
+
{ key: "cost", label: "Processing costs", value: Math.max(0, f.provider), color: red, detail: "Charged by the processors" }
|
|
138
236
|
]
|
|
139
237
|
}
|
|
140
|
-
) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No
|
|
238
|
+
) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No processing cost is recorded for these fees yet, so profit equals what was earned." }) }) }),
|
|
141
239
|
/* @__PURE__ */ jsx("div", { className: "adm-span-4", children: /* @__PURE__ */ jsx(Panel, { title: "How plans were paid", caption, height: "auto", children: byMethod.length > 0 && recurring.currency === cur.currency ? /* @__PURE__ */ jsx(
|
|
142
240
|
PieWithDetails,
|
|
143
241
|
{
|
|
@@ -153,39 +251,43 @@ function EarningsSummary({
|
|
|
153
251
|
}
|
|
154
252
|
) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No plan payments in this period." }) }) })
|
|
155
253
|
] }),
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
/* @__PURE__ */ jsxs("
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
254
|
+
/* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
255
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-6", children: /* @__PURE__ */ jsx(Panel, { title: "Stream by stream, against the period before", caption, height: "auto", children: /* @__PURE__ */ jsx(
|
|
256
|
+
CompareBars,
|
|
257
|
+
{
|
|
258
|
+
money,
|
|
259
|
+
rows: parts.filter((p) => p.source !== "plans").map((p) => ({
|
|
260
|
+
key: p.source,
|
|
261
|
+
label: label(p),
|
|
262
|
+
color: colors[p.source],
|
|
263
|
+
value: p.value,
|
|
264
|
+
previous: p.previous
|
|
265
|
+
}))
|
|
266
|
+
}
|
|
267
|
+
) }) }),
|
|
268
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-6", children: /* @__PURE__ */ jsx(Panel, { title: "The merchants it comes from", caption: `Fees, ${caption}`, height: "auto", children: cur.topMerchants.length === 0 ? /* @__PURE__ */ jsx("p", { className: styles.empty, children: "No fees to show in this period." }) : /* @__PURE__ */ jsx("ol", { className: earn.rankList, children: cur.topMerchants.map((m, i) => {
|
|
269
|
+
const value = toMajor(m.feeMinor, cur.exponent);
|
|
270
|
+
const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);
|
|
271
|
+
return /* @__PURE__ */ jsxs("li", { className: earn.rankRow, children: [
|
|
272
|
+
/* @__PURE__ */ jsx("span", { className: earn.rankName, children: m.tenantId && m.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${m.tenantId}`, children: m.name }) : "No merchant recorded" }),
|
|
273
|
+
/* @__PURE__ */ jsx("strong", { className: earn.rankAmount, children: money(value) }),
|
|
274
|
+
/* @__PURE__ */ jsxs("span", { className: earn.rankSub, children: [
|
|
275
|
+
count(m.count),
|
|
276
|
+
topTotal > 0 ? ` \xB7 ${Math.round(value / topTotal * 100)}% of these` : ""
|
|
277
|
+
] }),
|
|
278
|
+
/* @__PURE__ */ jsx("span", { className: earn.rankBar, "aria-hidden": true, children: /* @__PURE__ */ jsx(
|
|
279
|
+
"span",
|
|
280
|
+
{
|
|
281
|
+
className: earn.rankFill,
|
|
282
|
+
style: {
|
|
283
|
+
width: `${widest > 0 ? Math.max(2, value / widest * 100) : 0}%`,
|
|
284
|
+
background: colors.series[i % colors.series.length]
|
|
285
|
+
}
|
|
184
286
|
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
287
|
+
) })
|
|
288
|
+
] }, `${m.tenantId ?? "none"}-${i}`);
|
|
289
|
+
}) }) }) })
|
|
290
|
+
] })
|
|
189
291
|
] });
|
|
190
292
|
}
|
|
191
293
|
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 { EmptyNote } from '@withpotter/ui-kit';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport {\n earningsFigures,\n pctChange,\n toMajor,\n type EarningsPart,\n} from '@withpotter/lib/admin-earnings-model';\nimport { LoadFailed } from '../../audit/load-failed';\nimport { AdmLoading, Panel, StatCard, moneyIn, toTrend } from '../../_ui';\nimport { useSourceColors } from './earnings-colors';\nimport { CompareBars, PieWithDetails } from './earnings-legend';\nimport { useEarnings } from './use-earnings';\nimport styles from '../revenue.module.css';\nimport ledger from '../ledger.module.css';\nimport earn from './earnings.module.css';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The streams as the owner names them, in the order they are read. */\nconst ORDER = ['online', 'pos', 'payouts', 'settlements', 'cash', 'plans', 'payments', 'other'] as const;\n\n/** Streams that always have a card, even at zero; the rest appear when they earned something. */\nconst ALWAYS = new Set(['online', 'pos', 'payouts', 'settlements', 'cash', 'plans']);\n\nconst TITLES: Record<string, string> = {\n online: 'Online card payments',\n pos: 'POS card payments and transfers',\n payouts: 'Payouts',\n settlements: \"Settlements to merchants' banks\",\n cash: 'Cash received',\n plans: 'Plan subscriptions',\n payments: 'Other card payments',\n other: 'Other fees',\n};\n\nconst WHAT: Record<string, string> = {\n online: 'What Olive keeps when a customer pays a merchant by card online, including money that arrives through the payment gateway.',\n pos: 'What Olive keeps when a customer pays at a merchant\\'s POS terminal, by card or by transfer.',\n payouts: 'What Olive charges when a merchant sends money out of their wallet.',\n settlements: 'What Olive charges when a merchant\\'s money is settled to their bank account.',\n cash: 'What Olive charges when cash is received for a merchant.',\n plans: 'What merchants pay Olive for their plan, however they paid.',\n payments: 'Card payment fees that are not tagged as online or POS.',\n other: 'Fees from anything not listed here.',\n};\n\ntype Metric = 'earned' | 'costs' | 'profit';\n\nconst MINOR = new Set(['transfers', 'fx', 'lending', 'other']);\n\n/** Anything outside the named streams is one \"Other fees\" part, so the parts still add up to the total. */\nfunction named(parts: EarningsPart[]): EarningsPart[] {\n const rest = parts.filter((p) => MINOR.has(p.source));\n const main = parts.filter((p) => !MINOR.has(p.source));\n const sum = (k: 'value' | 'previous' | 'count') => rest.reduce((a, p) => a + p[k], 0);\n\n if (sum('value') <= 0 && sum('previous') <= 0) return main;\n\n return [\n ...main,\n { source: 'other', label: 'Other fees', value: sum('value'), previous: sum('previous'), count: sum('count') },\n ];\n}\n\n/** A card that picks which measure the chart shows, like the metric tabs of an analytics page. */\nfunction MetricCard({\n on,\n onPick,\n span,\n children,\n}: {\n on: boolean;\n onPick: () => void;\n span: string;\n children: React.ReactNode;\n}) {\n return (\n <div\n className={`${span} ${earn.metric} ${on ? earn.metricOn : ''}`}\n role=\"button\"\n tabIndex={0}\n aria-pressed={on}\n onClick={onPick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onPick();\n }\n }}\n >\n {children}\n </div>\n );\n}\n\nconst count = (n: number) => `${integer.format(n)} ${n === 1 ? 'transaction' : 'transactions'}`;\n\n/**\n * What Olive earns, as the owner reads it: the total, what it cost, what is left, each stream on its own card, where it\n * comes from, how it moves and which merchants it comes from. One currency at a time, never a mix.\n */\nexport function EarningsSummary({\n startDate,\n endDate,\n currency,\n recurring,\n span,\n}: {\n startDate?: string;\n endDate?: string;\n /** The currency filter's choice; empty follows the billing currency. */\n currency: string;\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const { data, isLoading, isError, refetch } = useEarnings({ startDate, endDate });\n const colors = useSourceColors();\n const [metric, setMetric] = useState<Metric>('earned');\n\n if (isError)\n return (\n <LoadFailed\n message=\"Earnings could not be loaded. This box may not have them yet.\"\n onRetry={() => void refetch()}\n />\n );\n\n if (isLoading || !data) return <AdmLoading label=\"Loading what Olive earns…\" />;\n\n const wanted = currency || recurring.currency || 'NGN';\n const cur = data.currencies.find((c) => c.currency === wanted) ?? data.currencies[0];\n\n if (!cur)\n return (\n <EmptyNote className={styles.empty}>No fees have been earned in this period yet.</EmptyNote>\n );\n\n const plan = recurring.currency === cur.currency ? (recurring.collected ?? null) : null;\n const f = earningsFigures(cur, plan);\n const have = named(f.parts);\n const parts = ORDER.filter((k) => ALWAYS.has(k) || have.some((p) => p.source === k && (p.value !== 0 || p.previous !== 0)))\n .map(\n (k): EarningsPart =>\n have.find((p) => p.source === k) ?? { source: k, label: TITLES[k], value: 0, previous: 0, count: 0 },\n )\n .sort((a, b) => ORDER.indexOf(a.source as never) - ORDER.indexOf(b.source as never));\n const money = moneyIn(cur.currency, 2);\n const caption = `${span[0].toUpperCase()}${span.slice(1)} · ${cur.currency}`;\n const margin = f.gross > 0 ? Math.round((f.net / f.gross) * 1000) / 10 : null;\n const green = colors.series[2];\n const red = colors.series[7];\n const byMethod = (recurring.collectedByMethod ?? []).filter((m) => m.amount > 0);\n const day = (p: { feeMinor: string; providerMinor: string }) => {\n const fee = toMajor(p.feeMinor, cur.exponent);\n const cost = toMajor(p.providerMinor, cur.exponent);\n\n return metric === 'earned' ? fee : metric === 'costs' ? cost : fee - cost;\n };\n const trend = toTrend(cur.series.map((p) => ({ date: p.date, value: day(p) })), 'value');\n const before = toTrend(cur.previousSeries.map((p) => ({ date: p.date, value: day(p) })), 'value');\n const feeSpark = cur.series.map((p) => toMajor(p.feeMinor, cur.exponent));\n const chartTitle = { earned: 'Fees earned each day', costs: 'Processing costs each day', profit: 'Profit each day' }[metric];\n const topTotal = cur.topMerchants.reduce((a, m) => a + toMajor(m.feeMinor, cur.exponent), 0);\n const label = (p: EarningsPart) => TITLES[p.source] ?? p.label;\n\n return (\n <>\n <p className={ledger.sectionNote}>\n How money flows through Olive: what it earned, what it cost, what is left, and where it came from. Showing{' '}\n {cur.currency}\n {data.currencies.length > 1 ? '; use the currency filter for the others.' : '.'}\n </p>\n\n <section aria-label=\"Earned, costs and profit\" className=\"adm-grid\">\n <MetricCard on={metric === 'earned'} onPick={() => setMetric('earned')} span=\"adm-span-3\">\n <StatCard\n label=\"Total earned\"\n accent={colors.series[0]}\n spark={feeSpark}\n sparkColor={colors.series[0]}\n value={money(f.gross)}\n sub=\"Fees plus plan income\"\n delta={{ pct: pctChange(f.gross, f.feesBefore + f.plan), caption: 'vs the period before' }}\n hint=\"Everything Olive earned in this period: fees on card payments, payouts, settlements and cash, plus what merchants paid for their plans.\"\n />\n </MetricCard>\n <MetricCard on={metric === 'costs'} onPick={() => setMetric('costs')} span=\"adm-span-3\">\n <StatCard\n label=\"Processing costs\"\n accent={red}\n value={money(cur.providerCostsRecorded ? f.provider : 0)}\n sub={cur.providerCostsRecorded ? 'Charged by the processors' : 'None recorded yet'}\n hint=\"What the payment processors kept from the same payments, as far as the system records it. Until costs are recorded this stays at zero and profit equals what was earned.\"\n />\n </MetricCard>\n <MetricCard on={metric === 'profit'} onPick={() => setMetric('profit')} span=\"adm-span-3\">\n <StatCard\n label=\"Profit\"\n accent={green}\n value={money(f.net)}\n sub={cur.providerCostsRecorded ? 'After processing costs' : 'Same as earned until costs are recorded'}\n hint=\"Total earned less the processing costs the system records. Running costs such as hosting and staff are not included.\"\n />\n </MetricCard>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Margin\"\n accent={colors.series[3]}\n value={margin === null ? '–' : `${margin}%`}\n sub=\"Profit as a share of earned\"\n hint=\"Profit divided by total earned.\"\n />\n </div>\n </section>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel title={chartTitle} caption={`${caption} · dotted line is the period before`} height=\"calc(var(--adm-panel-h) + 32px)\">\n <div className={styles.flatChart}>\n <TimeSeriesChart\n data={trend}\n color={metric === 'costs' ? red : metric === 'profit' ? green : colors.series[0]}\n formatValue={money}\n compare={before}\n compareLabel=\"Period before\"\n emptyMessage=\"Nothing was recorded on any day in this period\"\n height={240}\n />\n </div>\n </Panel>\n </div>\n </div>\n\n <h2 className={ledger.sectionTitle}>Where the money comes from</h2>\n <section aria-label=\"Earnings by stream\" className=\"adm-grid\">\n {parts.map((p) => (\n <div key={p.source} className=\"adm-span-4\">\n <StatCard\n label={label(p)}\n accent={colors[p.source]}\n value={money(p.value)}\n sub={p.source === 'plans' ? 'Collected in this period' : p.count === 0 ? 'Nothing in this period' : count(p.count)}\n hint={WHAT[p.source]}\n delta={\n p.source === 'plans'\n ? undefined\n : { pct: pctChange(p.value, p.previous), caption: 'vs the period before' }\n }\n />\n </div>\n ))}\n <div className=\"adm-span-4\">\n <StatCard\n label=\"Olive account and outlets\"\n accent=\"var(--text-tertiary)\"\n value=\"Not counted yet\"\n sub=\"No earnings recorded\"\n hint=\"Money received through the Olive account and outlets. The bank system does not report a fee for it yet, so nothing can be added here.\"\n />\n </div>\n </section>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-4\">\n <Panel title=\"Where it comes from\" caption={caption} height=\"auto\">\n {parts.some((p) => p.value > 0) ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Earned\"\n slices={parts.map((p) => ({\n key: p.source,\n label: label(p),\n value: p.value,\n color: colors[p.source],\n detail: p.source === 'plans' ? 'Collected' : count(p.count),\n }))}\n />\n ) : (\n <p className={styles.empty}>Nothing earned in this period.</p>\n )}\n </Panel>\n </div>\n <div className=\"adm-span-4\">\n <Panel title=\"What Olive keeps\" caption={caption} height=\"auto\">\n {cur.providerCostsRecorded && f.gross > 0 ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Earned\"\n slices={[\n { key: 'profit', label: 'Profit', value: Math.max(0, f.net), color: green, detail: 'After processing costs' },\n { key: 'cost', label: 'Processing costs', value: Math.max(0, f.provider), color: red, detail: 'Charged by the processors' },\n ]}\n />\n ) : (\n <p className={styles.empty}>\n No processing cost is recorded for these fees yet, so profit equals what was earned.\n </p>\n )}\n </Panel>\n </div>\n <div className=\"adm-span-4\">\n <Panel title=\"How plans were paid\" caption={caption} height=\"auto\">\n {byMethod.length > 0 && recurring.currency === cur.currency ? (\n <PieWithDetails\n money={money}\n centerLabel=\"Plans\"\n slices={byMethod.map((m, i) => ({\n key: m.method,\n label: m.method,\n value: m.amount,\n color: colors.series[i % colors.series.length],\n detail: `${integer.format(m.payments)} ${m.payments === 1 ? 'payment' : 'payments'}`,\n }))}\n />\n ) : (\n <p className={styles.empty}>No plan payments in this period.</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-6\">\n <Panel title=\"Stream by stream, against the period before\" caption={caption} height=\"auto\">\n <CompareBars\n money={money}\n rows={parts\n .filter((p) => p.source !== 'plans')\n .map((p) => ({\n key: p.source,\n label: label(p),\n color: colors[p.source],\n value: p.value,\n previous: p.previous,\n }))}\n />\n </Panel>\n </div>\n <div className=\"adm-span-6\">\n <Panel title=\"The merchants it comes from\" caption={`Fees, ${caption}`} height=\"auto\">\n {cur.topMerchants.length === 0 ? (\n <p className={styles.empty}>No fees to show in this period.</p>\n ) : (\n <ol className={earn.rankList}>\n {cur.topMerchants.map((m, i) => {\n const value = toMajor(m.feeMinor, cur.exponent);\n const widest = toMajor(cur.topMerchants[0].feeMinor, cur.exponent);\n\n return (\n <li key={`${m.tenantId ?? 'none'}-${i}`} className={earn.rankRow}>\n <span className={earn.rankName}>\n {m.tenantId && m.name ? (\n <Link href={`/admin/tenants/${m.tenantId}`}>{m.name}</Link>\n ) : (\n 'No merchant recorded'\n )}\n </span>\n <strong className={earn.rankAmount}>{money(value)}</strong>\n <span className={earn.rankSub}>\n {count(m.count)}\n {topTotal > 0 ? ` · ${Math.round((value / topTotal) * 100)}% of these` : ''}\n </span>\n <span className={earn.rankBar} aria-hidden>\n <span\n className={earn.rankFill}\n style={{\n width: `${widest > 0 ? Math.max(2, (value / widest) * 100) : 0}%`,\n background: colors.series[i % colors.series.length],\n }}\n />\n </span>\n </li>\n );\n })}\n </ol>\n )}\n </Panel>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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: "
|
|
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":"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","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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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: `
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.13.0-next.123",
|
|
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.
|
|
35
|
-
"@withpotter/platform-admin": "^0.13.0-next.
|
|
36
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
34
|
+
"@withpotter/lib": "^0.13.0-next.123",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.123",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.123",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|