@withpotter/pages-admin 0.13.0-next.113 → 0.13.0-next.115
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/_pos/payments-model.js +55 -0
- package/dist/admin/_pos/payments-model.js.map +1 -0
- package/dist/admin/_pos/use-pos.js +10 -4
- package/dist/admin/_pos/use-pos.js.map +1 -1
- package/dist/admin/pos/transactions-tab.js +168 -25
- package/dist/admin/pos/transactions-tab.js.map +1 -1
- package/dist/admin/revenue/merchant-payments-view.js +1 -22
- package/dist/admin/revenue/merchant-payments-view.js.map +1 -1
- package/dist/admin/revenue/plan-lines.js +4 -3
- package/dist/admin/revenue/plan-lines.js.map +1 -1
- package/dist/admin/revenue/print-summary-button.js +21 -28
- package/dist/admin/revenue/print-summary-button.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +92 -76
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +0 -4
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/dist/admin/revenue/subscriptions-panel.js +1 -1
- package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
- package/dist/admin/revenue/summary-download.js +18 -14
- package/dist/admin/revenue/summary-download.js.map +1 -1
- package/package.json +4 -4
- package/dist/admin/revenue/merchants-view.js +0 -99
- package/dist/admin/revenue/merchants-view.js.map +0 -1
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Button, useToast } from "@withpotter/ui-kit";
|
|
4
1
|
import { ADMIN_NAME } from "../_shell/admin-brand";
|
|
5
2
|
import { summaryHtml } from "./summary-print";
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setTimeout(() => frame.remove(), 1e3);
|
|
27
|
-
}, 150);
|
|
28
|
-
};
|
|
29
|
-
return /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: run, children: "Save as PDF" });
|
|
3
|
+
function printSummary(model, onFail) {
|
|
4
|
+
const frame = document.createElement("iframe");
|
|
5
|
+
frame.setAttribute("aria-hidden", "true");
|
|
6
|
+
frame.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
|
|
7
|
+
document.body.appendChild(frame);
|
|
8
|
+
const doc = frame.contentDocument;
|
|
9
|
+
const view = frame.contentWindow;
|
|
10
|
+
if (!doc || !view) {
|
|
11
|
+
frame.remove();
|
|
12
|
+
onFail();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
doc.open();
|
|
16
|
+
doc.write(summaryHtml(model, ADMIN_NAME, (/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { dateStyle: "long" })));
|
|
17
|
+
doc.close();
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
view.focus();
|
|
20
|
+
view.print();
|
|
21
|
+
setTimeout(() => frame.remove(), 1e3);
|
|
22
|
+
}, 150);
|
|
30
23
|
}
|
|
31
24
|
export {
|
|
32
|
-
|
|
25
|
+
printSummary
|
|
33
26
|
};
|
|
34
27
|
//# sourceMappingURL=print-summary-button.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport { summaryHtml, type SummaryModel } from './summary-print';\n\n/**\n * Opens the one-page summary in the browser's print window, where \"Save as PDF\" makes the file. The page is drawn in a\n * hidden frame, so the admin itself is never printed and no pop-up is needed. `onFail` runs when the frame cannot open.\n */\nexport function printSummary(model: SummaryModel, onFail: () => void): void {\n const frame = document.createElement('iframe');\n frame.setAttribute('aria-hidden', 'true');\n frame.style.cssText = 'position:fixed;width:0;height:0;border:0;visibility:hidden';\n document.body.appendChild(frame);\n\n const doc = frame.contentDocument;\n const view = frame.contentWindow;\n\n if (!doc || !view) {\n frame.remove();\n onFail();\n\n return;\n }\n\n doc.open();\n doc.write(summaryHtml(model, ADMIN_NAME, new Date().toLocaleDateString('en-GB', { dateStyle: 'long' })));\n doc.close();\n\n // Print once the page has laid out, and tidy the frame up when the print window closes.\n setTimeout(() => {\n view.focus();\n view.print();\n setTimeout(() => frame.remove(), 1000);\n }, 150);\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAC3B,SAAS,mBAAsC;AAMxC,SAAS,aAAa,OAAqB,QAA0B;AAC1E,QAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,QAAM,aAAa,eAAe,MAAM;AACxC,QAAM,MAAM,UAAU;AACtB,WAAS,KAAK,YAAY,KAAK;AAE/B,QAAM,MAAM,MAAM;AAClB,QAAM,OAAO,MAAM;AAEnB,MAAI,CAAC,OAAO,CAAC,MAAM;AACjB,UAAM,OAAO;AACb,WAAO;AAEP;AAAA,EACF;AAEA,MAAI,KAAK;AACT,MAAI,MAAM,YAAY,OAAO,aAAY,oBAAI,KAAK,GAAE,mBAAmB,SAAS,EAAE,WAAW,OAAO,CAAC,CAAC,CAAC;AACvG,MAAI,MAAM;AAGV,aAAW,MAAM;AACf,SAAK,MAAM;AACX,SAAK,MAAM;AACX,eAAW,MAAM,MAAM,OAAO,GAAG,GAAI;AAAA,EACvC,GAAG,GAAG;AACR;","names":[]}
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { TimeSeriesChart } from "@withpotter/components/charts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MoneyLines,
|
|
7
|
+
PICK_CURRENCY,
|
|
8
|
+
PageTabs,
|
|
9
|
+
Panel,
|
|
10
|
+
StatCard,
|
|
11
|
+
moneyIn,
|
|
12
|
+
rowFor,
|
|
13
|
+
toTrend,
|
|
14
|
+
useTabParam
|
|
15
|
+
} from "../_ui";
|
|
6
16
|
import { Reveal } from "../_fx";
|
|
7
17
|
import { Tinted } from "../audit/tinted";
|
|
8
18
|
import { ChannelPanel } from "./channel-panel";
|
|
@@ -18,6 +28,13 @@ import { SubscriptionsPanel } from "./subscriptions-panel";
|
|
|
18
28
|
import { TopTenantsPanel } from "./top-tenants-panel";
|
|
19
29
|
import styles from "./revenue.module.css";
|
|
20
30
|
import ledgerStyles from "./ledger.module.css";
|
|
31
|
+
const VIEWS = [
|
|
32
|
+
{ id: "olive", label: "Money Olive earns" },
|
|
33
|
+
{ id: "customers", label: "How customers pay" },
|
|
34
|
+
{ id: "merchants", label: "Money merchants earn" },
|
|
35
|
+
{ id: "plans", label: "Plan subscriptions" }
|
|
36
|
+
];
|
|
37
|
+
const VIEW_KEYS = VIEWS.map((v) => v.id);
|
|
21
38
|
function RevenueOverview({
|
|
22
39
|
data,
|
|
23
40
|
health,
|
|
@@ -32,13 +49,76 @@ function RevenueOverview({
|
|
|
32
49
|
ledger,
|
|
33
50
|
plans
|
|
34
51
|
}) {
|
|
52
|
+
const [view, setView] = useTabParam(VIEW_KEYS, "view");
|
|
35
53
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
36
54
|
const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
|
|
37
55
|
const gmvCaption = focus ? `${period} \xB7 ${focus}` : period;
|
|
38
56
|
const recurringTracked = data.recurring.tracked !== false;
|
|
39
57
|
const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
|
|
40
|
-
|
|
41
|
-
/* @__PURE__ */
|
|
58
|
+
const olive = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
|
|
60
|
+
"What merchants pay Olive: their plan payments, and the fees Olive keeps.",
|
|
61
|
+
recurringTracked ? "" : " Plan income is not set up on this box yet."
|
|
62
|
+
] }),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
RevenueKpis,
|
|
65
|
+
{
|
|
66
|
+
part: "olive",
|
|
67
|
+
data,
|
|
68
|
+
money,
|
|
69
|
+
money2,
|
|
70
|
+
currency,
|
|
71
|
+
focus,
|
|
72
|
+
gmvSpark: []
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
76
|
+
StatCard,
|
|
77
|
+
{
|
|
78
|
+
label: "Fees Olive keeps",
|
|
79
|
+
value: /* @__PURE__ */ jsx(
|
|
80
|
+
MoneyLines,
|
|
81
|
+
{
|
|
82
|
+
rows: platformFees,
|
|
83
|
+
currency: "",
|
|
84
|
+
amount: (r) => r.amount,
|
|
85
|
+
decimals: 2
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
hint: "What Olive keeps from payouts and plan payments, after costs."
|
|
89
|
+
}
|
|
90
|
+
) }) }),
|
|
91
|
+
recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
92
|
+
/* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
|
|
93
|
+
/* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
94
|
+
PlansPanel,
|
|
95
|
+
{
|
|
96
|
+
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
97
|
+
money,
|
|
98
|
+
caption: recurringCaption
|
|
99
|
+
}
|
|
100
|
+
) : /* @__PURE__ */ jsx(
|
|
101
|
+
PlanMixPanel,
|
|
102
|
+
{
|
|
103
|
+
planBreakdown: data.recurring.planBreakdown,
|
|
104
|
+
money,
|
|
105
|
+
caption: recurringCaption
|
|
106
|
+
}
|
|
107
|
+
) })
|
|
108
|
+
] })
|
|
109
|
+
] });
|
|
110
|
+
const customers = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "The ways of paying on the checkout, and how much came in through each." }),
|
|
112
|
+
ledger && focus ? /* @__PURE__ */ jsx(
|
|
113
|
+
MethodCards,
|
|
114
|
+
{
|
|
115
|
+
now: ledger.byChannel.current,
|
|
116
|
+
before: ledger.byChannel.previous,
|
|
117
|
+
currency: focus
|
|
118
|
+
}
|
|
119
|
+
) : 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 })
|
|
120
|
+
] });
|
|
121
|
+
const merchants = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
122
|
/* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "What customers paid to merchants. This is the merchants' money, not Olive's own income." }),
|
|
43
123
|
/* @__PURE__ */ jsx(
|
|
44
124
|
RevenueKpis,
|
|
@@ -72,17 +152,6 @@ function RevenueOverview({
|
|
|
72
152
|
) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
|
|
73
153
|
}
|
|
74
154
|
) }) }),
|
|
75
|
-
ledger && focus ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
-
/* @__PURE__ */ jsx("h3", { className: ledgerStyles.subTitle, children: "How customers paid" }),
|
|
77
|
-
/* @__PURE__ */ jsx(
|
|
78
|
-
MethodCards,
|
|
79
|
-
{
|
|
80
|
-
now: ledger.byChannel.current,
|
|
81
|
-
before: ledger.byChannel.previous,
|
|
82
|
-
currency: focus
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
] }) : 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 }) }) }),
|
|
86
155
|
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
|
|
87
156
|
TopTenantsPanel,
|
|
88
157
|
{
|
|
@@ -90,68 +159,15 @@ function RevenueOverview({
|
|
|
90
159
|
lifetime: gmv?.lifetime ?? 0,
|
|
91
160
|
focus
|
|
92
161
|
}
|
|
93
|
-
) }) })
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
part: "olive",
|
|
103
|
-
data,
|
|
104
|
-
money,
|
|
105
|
-
money2,
|
|
106
|
-
currency,
|
|
107
|
-
focus,
|
|
108
|
-
gmvSpark: []
|
|
109
|
-
}
|
|
110
|
-
),
|
|
111
|
-
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
112
|
-
StatCard,
|
|
113
|
-
{
|
|
114
|
-
label: "Fees Olive keeps",
|
|
115
|
-
value: /* @__PURE__ */ jsx(
|
|
116
|
-
MoneyLines,
|
|
117
|
-
{
|
|
118
|
-
rows: platformFees,
|
|
119
|
-
currency: "",
|
|
120
|
-
amount: (r) => r.amount,
|
|
121
|
-
decimals: 2
|
|
122
|
-
}
|
|
123
|
-
),
|
|
124
|
-
hint: "What Olive keeps from payouts and plan payments, after costs."
|
|
125
|
-
}
|
|
126
|
-
) }) }),
|
|
127
|
-
recurringTracked && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
128
|
-
/* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
129
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
|
|
130
|
-
HealthPanel,
|
|
131
|
-
{
|
|
132
|
-
health,
|
|
133
|
-
money,
|
|
134
|
-
caption: recurringCaption
|
|
135
|
-
}
|
|
136
|
-
) }),
|
|
137
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
138
|
-
PlansPanel,
|
|
139
|
-
{
|
|
140
|
-
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
141
|
-
money,
|
|
142
|
-
caption: recurringCaption
|
|
143
|
-
}
|
|
144
|
-
) : /* @__PURE__ */ jsx(
|
|
145
|
-
PlanMixPanel,
|
|
146
|
-
{
|
|
147
|
-
planBreakdown: data.recurring.planBreakdown,
|
|
148
|
-
money,
|
|
149
|
-
caption: recurringCaption
|
|
150
|
-
}
|
|
151
|
-
) })
|
|
152
|
-
] }),
|
|
153
|
-
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) })
|
|
154
|
-
] })
|
|
162
|
+
) }) })
|
|
163
|
+
] });
|
|
164
|
+
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." });
|
|
165
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(PageTabs, { tabs: VIEWS, active: view, onChange: setView }),
|
|
167
|
+
view === "olive" && olive,
|
|
168
|
+
view === "customers" && customers,
|
|
169
|
+
view === "merchants" && merchants,
|
|
170
|
+
view === "plans" && planSubscriptions
|
|
155
171
|
] });
|
|
156
172
|
}
|
|
157
173
|
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 { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend } from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\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 * Overview tab: the four headline figures, then the GMV trend as the one\n * full-width primary chart, then revenue health, plan mix, top merchants\n * and subscriptions beneath it. Merchant GMV is per currency: the chart and\n * the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\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 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 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 return (\n <>\n <h2 className={ledgerStyles.sectionTitle}>Money merchants earned</h2>\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 {ledger && focus ? (\n <>\n <h3 className={ledgerStyles.subTitle}>How customers paid</h3>\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n </>\n ) : (\n channels &&\n 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 )}\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 <h2 className={ledgerStyles.sectionTitle}>Money Olive earns</h2>\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.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <>\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel\n health={health}\n money={money}\n caption={recurringCaption}\n />\n </div>\n <div className=\"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 <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n </>\n )}\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 { 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 { 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/** The Overview's own tabs, in the order the money is read: Olive's, how customers pay, merchants', then plans. */\nconst VIEWS = [\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: what Olive earns (headline figures, fees, revenue health and plan mix), how\n * customers pay, what merchants earn (the GMV trend and the leaderboard), and the plan subscriptions. Merchant GMV is\n * per currency: the chart and the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\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 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.\"\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 === 'olive' && olive}\n {view === 'customers' && customers}\n {view === 'merchants' && merchants}\n {view === 'plans' && planSubscriptions}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
useTabParam
|
|
26
26
|
} from "../_ui";
|
|
27
27
|
import { MerchantPaymentsView } from "./merchant-payments-view";
|
|
28
|
-
import { MerchantsView } from "./merchants-view";
|
|
29
28
|
import { usePlanList } from "../pricing/use-plans";
|
|
30
29
|
import { RevenueOverview } from "./revenue-overview";
|
|
31
30
|
import { RevenueRetention } from "./revenue-retention";
|
|
@@ -34,7 +33,6 @@ import { SummaryDownload } from "./summary-download";
|
|
|
34
33
|
const TABS = [
|
|
35
34
|
{ id: "overview", label: "Overview" },
|
|
36
35
|
{ id: "merchants", label: "Merchant payments" },
|
|
37
|
-
{ id: "by-merchant", label: "Merchants" },
|
|
38
36
|
{ id: "subscriptions", label: "Subscriptions" },
|
|
39
37
|
{ id: "retention", label: "Retention" }
|
|
40
38
|
];
|
|
@@ -101,8 +99,6 @@ function RevenueWorkspace() {
|
|
|
101
99
|
currency
|
|
102
100
|
}
|
|
103
101
|
);
|
|
104
|
-
} else if (tab === "by-merchant") {
|
|
105
|
-
body = /* @__PURE__ */ jsx(MerchantsView, { startDate: range.startDate, endDate: range.endDate });
|
|
106
102
|
} else if (tab === "subscriptions") {
|
|
107
103
|
body = /* @__PURE__ */ jsx(
|
|
108
104
|
SubscriptionsView,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-workspace.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { ADMIN_NAME } from '../_shell/admin-brand';\nimport {\n useGatewayChannels,\n useMerchantPayments,\n usePlatformCockpit,\n usePlatformRevenue,\n} from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport {\n AdmLoading,\n CurrencyFilter,\n PageHeader,\n PageTabs,\n PeriodSelector,\n customRange,\n focusCurrency,\n moneyIn,\n periodPhrase,\n rangeFromKey,\n useCurrencyParam,\n usePersistedDateRange,\n useTabParam,\n} from '../_ui';\nimport { MerchantPaymentsView } from './merchant-payments-view';\nimport { usePlanList } from '../pricing/use-plans';\nimport { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\nimport { SummaryDownload } from './summary-download';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'merchants', label: 'Merchant payments' },\n { id: 'subscriptions', label: 'Subscriptions' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\n/**\n * Revenue: the whole picture on Overview, what customers paid merchants on\n * Merchant payments, what merchants pay Olive on Subscriptions, and revenue\n * quality and cohorts on Retention.\n */\nexport function RevenueWorkspace() {\n const [tab, setTab] = useTabParam(KEYS);\n const {\n rangeKey,\n setRangeKey,\n customStart,\n customEnd,\n onCustomChange,\n } = usePersistedDateRange();\n // Merchant GMV comes back once per currency; `?currency=` narrows it.\n const [currency, setCurrency] = useCurrencyParam();\n\n const range = useMemo(\n () =>\n rangeKey === 'custom'\n ? customRange(customStart, customEnd)\n : rangeFromKey(rangeKey),\n [rangeKey, customStart, customEnd],\n );\n\n const { data, isLoading, isError, refetch } = usePlatformRevenue({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n // Revenue health (MRR movement, logo churn, concentration) is computed as\n // part of the cockpit, not the revenue endpoint — fetched here too rather\n // than duplicating that aggregation.\n const plans = usePlanList();\n const cockpit = usePlatformCockpit({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n const channels = useGatewayChannels({\n startDate: range.startDate,\n endDate: range.endDate,\n });\n\n // The same totals the Merchant payments tab reads, one row only, for the method cards.\n const ledger = useMerchantPayments({\n startDate: range.startDate,\n endDate: range.endDate,\n limit: 1,\n });\n\n const currencies = useMemo(() => data?.currencies ?? [], [data]);\n const focus = focusCurrency(currencies, currency);\n // Recurring revenue is the platform's own billing, in the currency the API\n // names for it (`currency`), shown as it comes, never converted.\n const recurringCurrency = data?.currency || 'NGN';\n const money = useMemo(() => moneyIn(recurringCurrency), [recurringCurrency]);\n const money2 = useMemo(\n () => moneyIn(recurringCurrency, 2),\n [recurringCurrency],\n );\n\n const span = periodPhrase(range);\n const period = `${span[0].toUpperCase()}${span.slice(1)}`;\n\n let body;\n if (isError) {\n body = (\n <LoadFailed\n message=\"Revenue for this period could not be loaded.\"\n onRetry={() => void refetch()}\n />\n );\n } else if (isLoading || !data) {\n body = <AdmLoading label=\"Loading revenue metrics…\" />;\n } else if (tab === 'merchants') {\n body = (\n <MerchantPaymentsView\n startDate={range.startDate}\n endDate={range.endDate}\n currency={currency}\n />\n );\n } else if (tab === 'subscriptions') {\n body = (\n <SubscriptionsView\n startDate={range.startDate}\n endDate={range.endDate}\n revenue={data}\n />\n );\n } else if (tab === 'retention') {\n body = (\n <RevenueRetention\n quality={data.quality}\n money={money}\n currency={currency}\n caption={currency ? `${period} · ${currency}` : period}\n />\n );\n } else {\n body = (\n <RevenueOverview\n data={data}\n health={cockpit.data?.health}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n period={period}\n span={span}\n recurringCaption={`${period} · ${recurringCurrency}`}\n channels={channels.data?.byChannel}\n ledger={ledger.data}\n plans={plans.data}\n />\n );\n }\n\n return (\n <div>\n <PageHeader\n title=\"Revenue\"\n description={`All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`}\n actions={\n <div className=\"adm-toolbar\">\n {tab === 'overview' ? (\n <SummaryDownload\n ledger={ledger.data}\n revenue={data}\n currency={focus ?? 'NGN'}\n startDate={range.startDate}\n endDate={range.endDate}\n period={period}\n />\n ) : null}\n <CurrencyFilter\n value={currency}\n onChange={setCurrency}\n currencies={currencies}\n />\n <PeriodSelector\n value={rangeKey}\n onChange={setRangeKey}\n customStart={customStart}\n customEnd={customEnd}\n onCustomChange={onCustomChange}\n />\n </div>\n }\n />\n <PageTabs tabs={TABS} active={tab} onChange={setTab} />\n {body}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -50,7 +50,7 @@ function SubscriptionsPanel({
|
|
|
50
50
|
value: integer.format(recurring.cancelledSubscriptions)
|
|
51
51
|
}
|
|
52
52
|
];
|
|
53
|
-
return /* @__PURE__ */ jsx(Panel, { title: "Plan subscriptions
|
|
53
|
+
return /* @__PURE__ */ jsx(Panel, { title: "Plan subscriptions", caption: "Right now", height: "var(--adm-panel-h)", children: /* @__PURE__ */ jsx(FigureList, { rows }) });
|
|
54
54
|
}
|
|
55
55
|
export {
|
|
56
56
|
SubscriptionsPanel
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Payment missed',\n status: 'past_due',\n note: 'Their last payment failed, so they may leave',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Set to end',\n status: 'non_renewing',\n note: 'Will not renew when the paid period ends',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying for their plan',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'On a trial',\n status: 'trialing',\n note: 'Trying a plan, not yet paying',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'Joined this period',\n note: `Started paying in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Left this period',\n note: `Stopped paying in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled in total',\n note: 'Since the start',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Plan subscriptions
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/subscriptions-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** Where the subscription base stands, and what started in the window. */\nexport function SubscriptionsPanel({\n recurring,\n span,\n}: {\n recurring: PlatformRevenue['recurring'];\n span: string;\n}) {\n const rows: Figure[] = [\n {\n label: 'Payment missed',\n status: 'past_due',\n note: 'Their last payment failed, so they may leave',\n value: integer.format(recurring.pastDueSubscriptions),\n tone: recurring.pastDueSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Set to end',\n status: 'non_renewing',\n note: 'Will not renew when the paid period ends',\n value: integer.format(recurring.nonRenewingSubscriptions),\n tone: recurring.nonRenewingSubscriptions > 0 ? 'danger' : 'default',\n },\n {\n label: 'Active',\n status: 'active',\n note: 'Paying for their plan',\n value: integer.format(recurring.activeSubscriptions),\n },\n {\n label: 'On a trial',\n status: 'trialing',\n note: 'Trying a plan, not yet paying',\n value: integer.format(recurring.trialingSubscriptions),\n },\n {\n label: 'Joined this period',\n note: `Started paying in ${span}`,\n value: integer.format(recurring.newInRange),\n },\n {\n label: 'Left this period',\n note: `Stopped paying in ${span}`,\n value: integer.format(recurring.churnedInRange),\n tone: recurring.churnedInRange > 0 ? 'danger' : 'default',\n },\n {\n label: 'Cancelled in total',\n note: 'Since the start',\n value: integer.format(recurring.cancelledSubscriptions),\n },\n ];\n\n return (\n <Panel title=\"Plan subscriptions\" caption=\"Right now\" height=\"var(--adm-panel-h)\">\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6DM;AA5DN,SAAS,aAAa;AACtB,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGG;AACD,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,oBAAoB;AAAA,MACpD,MAAM,UAAU,uBAAuB,IAAI,WAAW;AAAA,IACxD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,wBAAwB;AAAA,MACxD,MAAM,UAAU,2BAA2B,IAAI,WAAW;AAAA,IAC5D;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,mBAAmB;AAAA,IACrD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,qBAAqB;AAAA,IACvD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,UAAU;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM,qBAAqB,IAAI;AAAA,MAC/B,OAAO,QAAQ,OAAO,UAAU,cAAc;AAAA,MAC9C,MAAM,UAAU,iBAAiB,IAAI,WAAW;AAAA,IAClD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,OAAO,UAAU,sBAAsB;AAAA,IACxD;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,sBAAqB,SAAQ,aAAY,QAAO,sBAC3D,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DropdownMenu, useToast } from "@withpotter/ui-kit";
|
|
3
4
|
import { toCsv } from "@withpotter/lib/csv";
|
|
4
|
-
import {
|
|
5
|
+
import { saveCsv } from "./download-button";
|
|
5
6
|
import { downloadName } from "./ledger-model";
|
|
6
|
-
import {
|
|
7
|
+
import { printSummary } from "./print-summary-button";
|
|
7
8
|
import { SUMMARY_HEAD, summaryRows } from "./summary-rows";
|
|
8
9
|
import { summaryModel } from "./summary-print";
|
|
10
|
+
const CSV = "Download CSV";
|
|
11
|
+
const PDF = "Save PDF";
|
|
9
12
|
function SummaryDownload({
|
|
10
13
|
ledger,
|
|
11
14
|
revenue,
|
|
@@ -14,19 +17,20 @@ function SummaryDownload({
|
|
|
14
17
|
endDate,
|
|
15
18
|
period
|
|
16
19
|
}) {
|
|
20
|
+
const { toast } = useToast();
|
|
17
21
|
const rows = summaryRows(ledger, revenue, currency);
|
|
18
22
|
if (rows.length === 0) return null;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
] });
|
|
23
|
+
const choose = (item) => {
|
|
24
|
+
if (item === CSV) {
|
|
25
|
+
saveCsv(downloadName("revenue-summary", startDate, endDate), toCsv([SUMMARY_HEAD, ...rows]));
|
|
26
|
+
} else {
|
|
27
|
+
printSummary(
|
|
28
|
+
summaryModel(ledger, revenue, currency, period),
|
|
29
|
+
() => toast({ title: "The summary could not be opened. Please try again." })
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx(DropdownMenu, { label: "Download", items: [CSV, PDF], onSelect: choose });
|
|
30
34
|
}
|
|
31
35
|
export {
|
|
32
36
|
SummaryDownload
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/summary-download.tsx"],"sourcesContent":["'use client';\n\nimport { toCsv } from '@withpotter/lib/csv';\nimport type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/summary-download.tsx"],"sourcesContent":["'use client';\n\nimport { DropdownMenu, useToast } from '@withpotter/ui-kit';\nimport { toCsv } from '@withpotter/lib/csv';\nimport type { MerchantPaymentsPage, PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { saveCsv } from './download-button';\nimport { downloadName } from './ledger-model';\nimport { printSummary } from './print-summary-button';\nimport { SUMMARY_HEAD, summaryRows } from './summary-rows';\nimport { summaryModel } from './summary-print';\n\nconst CSV = 'Download CSV';\nconst PDF = 'Save PDF';\n\n/** The figures on the Overview as a small spreadsheet or a one-page PDF (through the print window), from one menu. */\nexport function SummaryDownload({\n ledger,\n revenue,\n currency,\n startDate,\n endDate,\n period,\n}: {\n ledger?: MerchantPaymentsPage;\n revenue?: PlatformRevenue;\n currency: string;\n startDate?: string;\n endDate?: string;\n /** The period in words, such as \"last 30 days\". */\n period: string;\n}) {\n const { toast } = useToast();\n const rows = summaryRows(ledger, revenue, currency);\n\n if (rows.length === 0) return null;\n\n const choose = (item: string) => {\n if (item === CSV) {\n saveCsv(downloadName('revenue-summary', startDate, endDate), toCsv([SUMMARY_HEAD, ...rows]));\n } else {\n printSummary(summaryModel(ledger, revenue, currency, period), () =>\n toast({ title: 'The summary could not be opened. Please try again.' }),\n );\n }\n };\n\n return <DropdownMenu label=\"Download\" items={[CSV, PDF]} onSelect={choose} />;\n}\n"],"mappings":";AA8CS;AA5CT,SAAS,cAAc,gBAAgB;AACvC,SAAS,aAAa;AAEtB,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,cAAc,mBAAmB;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,MAAM;AACZ,MAAM,MAAM;AAGL,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,OAAO,YAAY,QAAQ,SAAS,QAAQ;AAElD,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,SAAS,CAAC,SAAiB;AAC/B,QAAI,SAAS,KAAK;AAChB,cAAQ,aAAa,mBAAmB,WAAW,OAAO,GAAG,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;AAAA,IAC7F,OAAO;AACL;AAAA,QAAa,aAAa,QAAQ,SAAS,UAAU,MAAM;AAAA,QAAG,MAC5D,MAAM,EAAE,OAAO,qDAAqD,CAAC;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAEA,SAAO,oBAAC,gBAAa,OAAM,YAAW,OAAO,CAAC,KAAK,GAAG,GAAG,UAAU,QAAQ;AAC7E;","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.115",
|
|
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.115",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.115",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.115",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
|
|
6
|
-
import { useMerchantRevenue } from "../hooks";
|
|
7
|
-
import { LoadFailed } from "../audit/load-failed";
|
|
8
|
-
import { AdmLoading, FilterSearch, Filters, Pagination } from "../_ui";
|
|
9
|
-
import { minorToMoney } from "./ledger-model";
|
|
10
|
-
import { useUrlFilters } from "./use-url-filters";
|
|
11
|
-
import styles from "./ledger.module.css";
|
|
12
|
-
import table from "./revenue.module.css";
|
|
13
|
-
const PAGE_SIZE = 25;
|
|
14
|
-
const FILTERS = { q: "" };
|
|
15
|
-
const integer = new Intl.NumberFormat("en-US");
|
|
16
|
-
const METHOD_COLOUR = {
|
|
17
|
-
card: "#4f7a28",
|
|
18
|
-
ussd: "#c68a1b",
|
|
19
|
-
transfer: "#2f6f9f",
|
|
20
|
-
opay: "#7a4fa0"
|
|
21
|
-
};
|
|
22
|
-
function MerchantsView({ startDate, endDate }) {
|
|
23
|
-
const { values, page, set } = useUrlFilters("mr", FILTERS);
|
|
24
|
-
const [search, setSearch] = useState(values.q);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const timer = setTimeout(() => {
|
|
27
|
-
if (search.trim() !== values.q) set({ q: search.trim() });
|
|
28
|
-
}, 300);
|
|
29
|
-
return () => clearTimeout(timer);
|
|
30
|
-
}, [search]);
|
|
31
|
-
const filters = useMemo(
|
|
32
|
-
() => ({ startDate, endDate, q: values.q, page, limit: PAGE_SIZE }),
|
|
33
|
-
[startDate, endDate, values.q, page]
|
|
34
|
-
);
|
|
35
|
-
const { data, isLoading, isError, refetch } = useMerchantRevenue(filters);
|
|
36
|
-
if (isError) {
|
|
37
|
-
return /* @__PURE__ */ jsx(
|
|
38
|
-
LoadFailed,
|
|
39
|
-
{
|
|
40
|
-
message: "The merchants could not be loaded. This box may not have this view yet.",
|
|
41
|
-
onRetry: () => void refetch()
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (isLoading || !data) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading merchants\u2026" });
|
|
46
|
-
const hasMore = data.rows.length === PAGE_SIZE;
|
|
47
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
48
|
-
/* @__PURE__ */ jsx("p", { className: styles.lede, children: "This shows which merchants received the most money in this period, and how their customers paid them. Open a merchant to see each payment." }),
|
|
49
|
-
/* @__PURE__ */ jsx("div", { className: styles.toolbar, children: /* @__PURE__ */ jsx(Filters, { label: "Filter merchants", children: /* @__PURE__ */ jsx(FilterSearch, { value: search, onChange: setSearch, placeholder: "Search a merchant by name", label: "Search merchants" }) }) }),
|
|
50
|
-
data.rows.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No merchant took a payment in this period. Try a wider date range." }) : /* @__PURE__ */ jsx(
|
|
51
|
-
TableShell,
|
|
52
|
-
{
|
|
53
|
-
minWidth: 760,
|
|
54
|
-
density: "compact",
|
|
55
|
-
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
56
|
-
/* @__PURE__ */ jsx(Th, { children: "Merchant" }),
|
|
57
|
-
/* @__PURE__ */ jsx(Th, { align: "right", children: "Money received" }),
|
|
58
|
-
/* @__PURE__ */ jsx(Th, { align: "right", children: "Payments" }),
|
|
59
|
-
/* @__PURE__ */ jsx(Th, { children: "How customers paid" }),
|
|
60
|
-
/* @__PURE__ */ jsx(Th, { children: " " })
|
|
61
|
-
] }),
|
|
62
|
-
children: data.rows.map((row) => /* @__PURE__ */ jsx(MerchantRow, { row }, `${row.merchant.id}-${row.currency}`))
|
|
63
|
-
}
|
|
64
|
-
),
|
|
65
|
-
/* @__PURE__ */ jsx(
|
|
66
|
-
Pagination,
|
|
67
|
-
{
|
|
68
|
-
page,
|
|
69
|
-
totalPages: hasMore ? page + 1 : page,
|
|
70
|
-
noun: "merchants",
|
|
71
|
-
onPage: (next) => set({ page: next })
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
] });
|
|
75
|
-
}
|
|
76
|
-
function MerchantRow({ row }) {
|
|
77
|
-
const total = Number(row.grossMinor) || 1;
|
|
78
|
-
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
79
|
-
/* @__PURE__ */ jsx("td", { className: table.name, children: row.merchant.name ? /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.merchant.id}`, children: row.merchant.name }) : "Unknown merchant" }),
|
|
80
|
-
/* @__PURE__ */ jsx("td", { className: table.numStrong, children: minorToMoney(row.grossMinor, row.currency) }),
|
|
81
|
-
/* @__PURE__ */ jsx("td", { className: table.num, children: integer.format(row.payments) }),
|
|
82
|
-
/* @__PURE__ */ jsxs("td", { children: [
|
|
83
|
-
/* @__PURE__ */ jsx("div", { className: styles.methodBar, role: "img", "aria-label": row.byMethod.map((m) => `${m.label} ${minorToMoney(m.amountMinor, row.currency)}`).join(", "), children: row.byMethod.map((m) => /* @__PURE__ */ jsx(
|
|
84
|
-
"span",
|
|
85
|
-
{
|
|
86
|
-
className: styles.methodSeg,
|
|
87
|
-
style: { width: `${Number(m.amountMinor) / total * 100}%`, background: METHOD_COLOUR[m.key] ?? "#8a8f85" }
|
|
88
|
-
},
|
|
89
|
-
m.key
|
|
90
|
-
)) }),
|
|
91
|
-
/* @__PURE__ */ jsx("div", { className: styles.methodText, children: row.byMethod.map((m) => `${m.label} ${Math.round(Number(m.amountMinor) / total * 100)}%`).join(" \xB7 ") })
|
|
92
|
-
] }),
|
|
93
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Link, { href: `?tab=merchants&mp_merchant=${row.merchant.id}`, children: "See payments" }) })
|
|
94
|
-
] });
|
|
95
|
-
}
|
|
96
|
-
export {
|
|
97
|
-
MerchantsView
|
|
98
|
-
};
|
|
99
|
-
//# sourceMappingURL=merchants-view.js.map
|