@withpotter/pages-admin 0.13.0-next.96 → 0.13.0-next.98
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/hooks.js +21 -1
- package/dist/admin/hooks.js.map +1 -1
- package/dist/admin/revenue/download-button.js +40 -0
- package/dist/admin/revenue/download-button.js.map +1 -0
- package/dist/admin/revenue/ledger-model.js +84 -0
- package/dist/admin/revenue/ledger-model.js.map +1 -0
- package/dist/admin/revenue/ledger.module.css +101 -0
- package/dist/admin/revenue/merchant-payments-view.js +231 -0
- package/dist/admin/revenue/merchant-payments-view.js.map +1 -0
- package/dist/admin/revenue/method-cards.js +32 -0
- package/dist/admin/revenue/method-cards.js.map +1 -0
- package/dist/admin/revenue/revenue-kpis.js +13 -13
- package/dist/admin/revenue/revenue-kpis.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +19 -2
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +31 -2
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/dist/admin/revenue/subscriptions-view.js +188 -0
- package/dist/admin/revenue/subscriptions-view.js.map +1 -0
- package/dist/admin/tenants/[id]/page.js +6 -1
- package/dist/admin/tenants/[id]/page.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CountUp, Reveal } from "../_fx";
|
|
3
|
+
import { StatCard } from "../_ui";
|
|
4
|
+
import { methodCards, minorToMoney } from "./ledger-model";
|
|
5
|
+
const integer = new Intl.NumberFormat("en-US");
|
|
6
|
+
function MethodCards({
|
|
7
|
+
now,
|
|
8
|
+
before,
|
|
9
|
+
currency
|
|
10
|
+
}) {
|
|
11
|
+
const cards = methodCards(now, before, currency);
|
|
12
|
+
return /* @__PURE__ */ jsx("section", { "aria-label": "How customers paid", className: "adm-grid", children: cards.map((card, index) => /* @__PURE__ */ jsx(Reveal, { index, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
13
|
+
StatCard,
|
|
14
|
+
{
|
|
15
|
+
label: card.label,
|
|
16
|
+
value: /* @__PURE__ */ jsx(
|
|
17
|
+
CountUp,
|
|
18
|
+
{
|
|
19
|
+
value: Number(card.amountMinor) / 100,
|
|
20
|
+
format: (v) => minorToMoney(Math.round(v * 100), currency)
|
|
21
|
+
}
|
|
22
|
+
),
|
|
23
|
+
sub: card.payments === 0 ? "No payments this way yet" : `${integer.format(card.payments)} ${card.payments === 1 ? "payment" : "payments"} \xB7 ${card.share}% of the money received`,
|
|
24
|
+
hint: `Money customers paid by ${card.label.toLowerCase()}, before fees.`,
|
|
25
|
+
delta: card.change === null ? void 0 : { pct: card.change, caption: "vs the period before" }
|
|
26
|
+
}
|
|
27
|
+
) }, card.key)) });
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
MethodCards
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=method-cards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/method-cards.tsx"],"sourcesContent":["import type { GatewayChannelTotal } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal } from '../_fx';\nimport { StatCard } from '../_ui';\nimport { methodCards, minorToMoney } from './ledger-model';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/**\n * One card for each way customers pay (Card, USSD, Bank transfer, Opay): how\n * much came in that way, how many payments, its share of all the money\n * received, and how it moved since the period before. A quiet method shows\n * zero, so it is seen rather than missing.\n */\nexport function MethodCards({\n now,\n before,\n currency,\n}: {\n now: readonly GatewayChannelTotal[];\n before: readonly GatewayChannelTotal[] | null;\n currency: string;\n}) {\n const cards = methodCards(now, before, currency);\n\n return (\n <section aria-label=\"How customers paid\" className=\"adm-grid\">\n {cards.map((card, index) => (\n <Reveal key={card.key} index={index} className=\"adm-span-3\">\n <StatCard\n label={card.label}\n value={\n <CountUp\n value={Number(card.amountMinor) / 100}\n format={(v) => minorToMoney(Math.round(v * 100), currency)}\n />\n }\n sub={\n card.payments === 0\n ? 'No payments this way yet'\n : `${integer.format(card.payments)} ${card.payments === 1 ? 'payment' : 'payments'} · ${card.share}% of the money received`\n }\n hint={`Money customers paid by ${card.label.toLowerCase()}, before fees.`}\n delta={\n card.change === null\n ? undefined\n : { pct: card.change, caption: 'vs the period before' }\n }\n />\n </Reveal>\n ))}\n </section>\n );\n}\n"],"mappings":"AA+Bc;AA9Bd,SAAS,SAAS,cAAc;AAChC,SAAS,gBAAgB;AACzB,SAAS,aAAa,oBAAoB;AAE1C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAQtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,YAAY,KAAK,QAAQ,QAAQ;AAE/C,SACE,oBAAC,aAAQ,cAAW,sBAAqB,WAAU,YAChD,gBAAM,IAAI,CAAC,MAAM,UAChB,oBAAC,UAAsB,OAAc,WAAU,cAC7C;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,KAAK;AAAA,MACZ,OACE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,OAAO,KAAK,WAAW,IAAI;AAAA,UAClC,QAAQ,CAAC,MAAM,aAAa,KAAK,MAAM,IAAI,GAAG,GAAG,QAAQ;AAAA;AAAA,MAC3D;AAAA,MAEF,KACE,KAAK,aAAa,IACd,6BACA,GAAG,QAAQ,OAAO,KAAK,QAAQ,CAAC,IAAI,KAAK,aAAa,IAAI,YAAY,UAAU,SAAM,KAAK,KAAK;AAAA,MAEtG,MAAM,2BAA2B,KAAK,MAAM,YAAY,CAAC;AAAA,MACzD,OACE,KAAK,WAAW,OACZ,SACA,EAAE,KAAK,KAAK,QAAQ,SAAS,uBAAuB;AAAA;AAAA,EAE5D,KApBW,KAAK,GAqBlB,CACD,GACH;AAEJ;","names":[]}
|
|
@@ -13,42 +13,42 @@ function RevenueKpis({
|
|
|
13
13
|
}) {
|
|
14
14
|
const { recurring } = data;
|
|
15
15
|
const tracked = recurring.tracked !== false;
|
|
16
|
-
const notTracked = "Not
|
|
17
|
-
const noSource = "
|
|
16
|
+
const notTracked = "Not set up yet";
|
|
17
|
+
const noSource = "The plan payments are not connected yet";
|
|
18
18
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
19
19
|
const lifetimeShare = gmv && gmv.lifetime > 0 ? `${Math.round(gmv.inRange / gmv.lifetime * 100)}% of lifetime` : void 0;
|
|
20
20
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Revenue indicators", className: "adm-grid", children: [
|
|
21
21
|
/* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
22
22
|
StatCard,
|
|
23
23
|
{
|
|
24
|
-
label: "
|
|
24
|
+
label: "Plan income each month",
|
|
25
25
|
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
|
|
26
|
-
hint: `
|
|
27
|
-
sub: tracked ? `${integer.format(recurring.activeSubscriptions)} paying` : noSource
|
|
26
|
+
hint: `What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || "NGN"}. Free plans, trials and pauses are not counted. (MRR)`,
|
|
27
|
+
sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : noSource
|
|
28
28
|
}
|
|
29
29
|
) }),
|
|
30
30
|
/* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
31
31
|
StatCard,
|
|
32
32
|
{
|
|
33
|
-
label: "
|
|
33
|
+
label: "Plan income over a year",
|
|
34
34
|
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
|
|
35
|
-
hint: "
|
|
36
|
-
sub: tracked ? "
|
|
35
|
+
hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)",
|
|
36
|
+
sub: tracked ? "Each month times 12" : noSource
|
|
37
37
|
}
|
|
38
38
|
) }),
|
|
39
39
|
/* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
40
40
|
StatCard,
|
|
41
41
|
{
|
|
42
|
-
label: "Average
|
|
42
|
+
label: "Average per paying merchant",
|
|
43
43
|
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
|
|
44
|
-
hint: "What
|
|
45
|
-
sub: tracked ? "
|
|
44
|
+
hint: "What one paying merchant pays in a month, on average. (ARPA)",
|
|
45
|
+
sub: tracked ? "Each month" : noSource
|
|
46
46
|
}
|
|
47
47
|
) }),
|
|
48
48
|
/* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
49
49
|
StatCard,
|
|
50
50
|
{
|
|
51
|
-
label: "Merchant sales
|
|
51
|
+
label: "Merchant sales",
|
|
52
52
|
value: /* @__PURE__ */ jsx(
|
|
53
53
|
MoneyLines,
|
|
54
54
|
{
|
|
@@ -60,7 +60,7 @@ function RevenueKpis({
|
|
|
60
60
|
),
|
|
61
61
|
href: "/admin/tenants",
|
|
62
62
|
sub: lifetimeShare,
|
|
63
|
-
hint: `
|
|
63
|
+
hint: `Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`,
|
|
64
64
|
spark: focus ? gmvSpark : void 0,
|
|
65
65
|
sparkColor: TONE.money.fg
|
|
66
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The four headline figures: recurring revenue first, then merchant GMV. */\nexport function RevenueKpis({\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n data: PlatformRevenue;\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 share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { CountUp, Reveal, TONE } from '../_fx';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat('en-US');\n\n/** The four headline figures: recurring revenue first, then merchant GMV. */\nexport function RevenueKpis({\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n data: PlatformRevenue;\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 share and sparkline follow, or null. */\n focus: string | null;\n gmvSpark: number[];\n}) {\n const { recurring } = data;\n // Without the plan payments connected the API sends 0s; say so rather than show a zero.\n const tracked = recurring.tracked !== false;\n const notTracked = 'Not set up yet';\n const noSource = 'The plan payments are not connected yet';\n const gmv = rowFor(data.gmvByCurrency, focus);\n const lifetimeShare =\n gmv && gmv.lifetime > 0\n ? `${Math.round((gmv.inRange / gmv.lifetime) * 100)}% of lifetime`\n : undefined;\n\n return (\n <section aria-label=\"Revenue indicators\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={\n tracked ? (\n <CountUp value={recurring.mrr} format={money} />\n ) : (\n notTracked\n )\n }\n hint={`What merchants on a paid plan are expected to pay ${ADMIN_NAME} every month, in ${data.currency || 'NGN'}. Free plans, trials and pauses are not counted. (MRR)`}\n sub={\n tracked\n ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying`\n : noSource\n }\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={\n tracked ? (\n <CountUp value={recurring.arr} format={money} />\n ) : (\n notTracked\n )\n }\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)\"\n sub={tracked ? 'Each month times 12' : noSource}\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={\n tracked ? (\n <CountUp value={recurring.arpa} format={money2} />\n ) : (\n notTracked\n )\n }\n hint=\"What one paying merchant pays in a month, on average. (ARPA)\"\n sub={tracked ? 'Each month' : noSource}\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Merchant sales\"\n value={\n <MoneyLines\n rows={data.gmvByCurrency}\n currency={currency}\n amount={(g) => g.inRange}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/tenants\"\n sub={lifetimeShare}\n hint={`Everything merchants sold through ${ADMIN_NAME}. This is the merchants' money, not ${ADMIN_NAME}'s own income. One line per currency; currencies are never added together. (GMV)`}\n spark={focus ? gmvSpark : undefined}\n sparkColor={TONE.money.fg}\n />\n </Reveal>\n </section>\n );\n}\n"],"mappings":"AAsCI,SAMU,KANV;AAtCJ,SAAS,kBAAkB;AAE3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAGtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,UAAU,IAAI;AAEtB,QAAM,UAAU,UAAU,YAAY;AACtC,QAAM,aAAa;AACnB,QAAM,WAAW;AACjB,QAAM,MAAM,OAAO,KAAK,eAAe,KAAK;AAC5C,QAAM,gBACJ,OAAO,IAAI,WAAW,IAClB,GAAG,KAAK,MAAO,IAAI,UAAU,IAAI,WAAY,GAAG,CAAC,kBACjD;AAEN,SACE,qBAAC,aAAQ,cAAW,sBAAqB,WAAU,YACjD;AAAA,wBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,QAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,IAER,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,IACzC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,QAGJ,MAAK;AAAA,QACL,KAAK,UAAU,eAAe;AAAA;AAAA,IAChC,GACF;AAAA,IACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX;AAAA,YACA,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,OAAM;AAAA;AAAA,QACR;AAAA,QAEF,MAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,QACtG,OAAO,QAAQ,WAAW;AAAA,QAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,IACzB,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -6,6 +6,7 @@ import { MoneyLines, PICK_CURRENCY, Panel, StatCard, moneyIn, rowFor, toTrend }
|
|
|
6
6
|
import { Reveal } from "../_fx";
|
|
7
7
|
import { Tinted } from "../audit/tinted";
|
|
8
8
|
import { ChannelPanel } from "./channel-panel";
|
|
9
|
+
import { MethodCards } from "./method-cards";
|
|
9
10
|
import { HealthPanel } from "./health-panel";
|
|
10
11
|
import { PlanMixPanel } from "./plan-mix-panel";
|
|
11
12
|
import { platformFeeRows } from "./platform-fees";
|
|
@@ -13,6 +14,7 @@ import { RevenueKpis } from "./revenue-kpis";
|
|
|
13
14
|
import { SubscriptionsPanel } from "./subscriptions-panel";
|
|
14
15
|
import { TopTenantsPanel } from "./top-tenants-panel";
|
|
15
16
|
import styles from "./revenue.module.css";
|
|
17
|
+
import ledgerStyles from "./ledger.module.css";
|
|
16
18
|
function RevenueOverview({
|
|
17
19
|
data,
|
|
18
20
|
health,
|
|
@@ -23,7 +25,8 @@ function RevenueOverview({
|
|
|
23
25
|
period,
|
|
24
26
|
span,
|
|
25
27
|
recurringCaption,
|
|
26
|
-
channels
|
|
28
|
+
channels,
|
|
29
|
+
ledger
|
|
27
30
|
}) {
|
|
28
31
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
29
32
|
const gmvSeries = useMemo(() => toTrend(gmv?.series, "gmv"), [gmv]);
|
|
@@ -77,7 +80,21 @@ function RevenueOverview({
|
|
|
77
80
|
) }) }) : /* @__PURE__ */ jsx("p", { className: styles.empty, children: PICK_CURRENCY })
|
|
78
81
|
}
|
|
79
82
|
) }) }),
|
|
80
|
-
|
|
83
|
+
ledger && focus ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
+
/* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "How customers paid" }),
|
|
85
|
+
/* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
|
|
86
|
+
"Money customers paid to merchants, by way of paying. ",
|
|
87
|
+
gmvCaption
|
|
88
|
+
] }),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
MethodCards,
|
|
91
|
+
{
|
|
92
|
+
now: ledger.byChannel.current,
|
|
93
|
+
before: ledger.byChannel.previous,
|
|
94
|
+
currency: focus
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] }) : 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 }) }) }),
|
|
81
98
|
recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
82
99
|
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
|
|
83
100
|
HealthPanel,
|
|
@@ -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 PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\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 { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-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';\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}: {\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}) {\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 <RevenueKpis\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\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=\"Platform fees earned\"\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, settlements and subscriptions.\"\n />\n </Reveal>\n </section>\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"GMV trend\"\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 {
|
|
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 { 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 { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-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}: {\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}) {\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 <RevenueKpis\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\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=\"Platform fees earned\"\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, settlements and subscriptions.\"\n />\n </Reveal>\n </section>\n )}\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"GMV trend\"\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 <h2 className={ledgerStyles.sectionTitle}>How customers paid</h2>\n <p className={ledgerStyles.sectionNote}>\n Money customers paid to merchants, by way of paying. {gmvCaption}\n </p>\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 {recurringTracked && (\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 <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n </div>\n </div>\n )}\n\n <div className=\"adm-grid\">\n <div className={recurringTracked ? 'adm-span-8' : 'adm-span-12'}>\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n {recurringTracked && (\n <div className=\"adm-span-4\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n )}\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import { useMemo } from "react";
|
|
|
4
4
|
import { ADMIN_NAME } from "../_shell/admin-brand";
|
|
5
5
|
import {
|
|
6
6
|
useGatewayChannels,
|
|
7
|
+
useMerchantPayments,
|
|
7
8
|
usePlatformCockpit,
|
|
8
9
|
usePlatformRevenue
|
|
9
10
|
} from "../hooks";
|
|
@@ -23,10 +24,14 @@ import {
|
|
|
23
24
|
usePersistedDateRange,
|
|
24
25
|
useTabParam
|
|
25
26
|
} from "../_ui";
|
|
27
|
+
import { MerchantPaymentsView } from "./merchant-payments-view";
|
|
26
28
|
import { RevenueOverview } from "./revenue-overview";
|
|
27
29
|
import { RevenueRetention } from "./revenue-retention";
|
|
30
|
+
import { SubscriptionsView } from "./subscriptions-view";
|
|
28
31
|
const TABS = [
|
|
29
32
|
{ id: "overview", label: "Overview" },
|
|
33
|
+
{ id: "merchants", label: "Merchant payments" },
|
|
34
|
+
{ id: "subscriptions", label: "Subscriptions" },
|
|
30
35
|
{ id: "retention", label: "Retention" }
|
|
31
36
|
];
|
|
32
37
|
const KEYS = TABS.map((t) => t.id);
|
|
@@ -56,6 +61,11 @@ function RevenueWorkspace() {
|
|
|
56
61
|
startDate: range.startDate,
|
|
57
62
|
endDate: range.endDate
|
|
58
63
|
});
|
|
64
|
+
const ledger = useMerchantPayments({
|
|
65
|
+
startDate: range.startDate,
|
|
66
|
+
endDate: range.endDate,
|
|
67
|
+
limit: 1
|
|
68
|
+
});
|
|
59
69
|
const currencies = useMemo(() => data?.currencies ?? [], [data]);
|
|
60
70
|
const focus = focusCurrency(currencies, currency);
|
|
61
71
|
const recurringCurrency = data?.currency || "NGN";
|
|
@@ -77,6 +87,24 @@ function RevenueWorkspace() {
|
|
|
77
87
|
);
|
|
78
88
|
} else if (isLoading || !data) {
|
|
79
89
|
body = /* @__PURE__ */ jsx(AdmLoading, { label: "Loading revenue metrics\u2026" });
|
|
90
|
+
} else if (tab === "merchants") {
|
|
91
|
+
body = /* @__PURE__ */ jsx(
|
|
92
|
+
MerchantPaymentsView,
|
|
93
|
+
{
|
|
94
|
+
startDate: range.startDate,
|
|
95
|
+
endDate: range.endDate,
|
|
96
|
+
currency
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
} else if (tab === "subscriptions") {
|
|
100
|
+
body = /* @__PURE__ */ jsx(
|
|
101
|
+
SubscriptionsView,
|
|
102
|
+
{
|
|
103
|
+
startDate: range.startDate,
|
|
104
|
+
endDate: range.endDate,
|
|
105
|
+
revenue: data
|
|
106
|
+
}
|
|
107
|
+
);
|
|
80
108
|
} else if (tab === "retention") {
|
|
81
109
|
body = /* @__PURE__ */ jsx(
|
|
82
110
|
RevenueRetention,
|
|
@@ -100,7 +128,8 @@ function RevenueWorkspace() {
|
|
|
100
128
|
period,
|
|
101
129
|
span,
|
|
102
130
|
recurringCaption: `${period} \xB7 ${recurringCurrency}`,
|
|
103
|
-
channels: channels.data?.byChannel
|
|
131
|
+
channels: channels.data?.byChannel,
|
|
132
|
+
ledger: ledger.data
|
|
104
133
|
}
|
|
105
134
|
);
|
|
106
135
|
}
|
|
@@ -109,7 +138,7 @@ function RevenueWorkspace() {
|
|
|
109
138
|
PageHeader,
|
|
110
139
|
{
|
|
111
140
|
title: "Revenue",
|
|
112
|
-
description: `
|
|
141
|
+
description: `All the money moving through ${ADMIN_NAME}: what customers pay merchants, and what merchants pay ${ADMIN_NAME} for their plan.`,
|
|
113
142
|
actions: /* @__PURE__ */ jsxs("div", { className: "adm-toolbar", children: [
|
|
114
143
|
/* @__PURE__ */ jsx(
|
|
115
144
|
CurrencyFilter,
|
|
@@ -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 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 { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\n\nconst TABS = [\n { id: 'overview', label: 'Overview' },\n { id: 'retention', label: 'Retention' },\n] as const;\n\nconst KEYS = TABS.map((t) => t.id);\n\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 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 { RevenueOverview } from './revenue-overview';\nimport { RevenueRetention } from './revenue-retention';\nimport { SubscriptionsView } from './subscriptions-view';\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 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 />\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 <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":[]}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { EmptyNote, TableRow, TableShell, Th } from "@withpotter/ui-kit";
|
|
5
|
+
import {
|
|
6
|
+
platformAdminApi
|
|
7
|
+
} from "@withpotter/lib/admin-api";
|
|
8
|
+
import { useSubscriptionPayments } from "../hooks";
|
|
9
|
+
import { CountUp, Reveal } from "../_fx";
|
|
10
|
+
import { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from "../_ui";
|
|
11
|
+
import { DownloadButton } from "./download-button";
|
|
12
|
+
import {
|
|
13
|
+
PURPOSE_WORDS,
|
|
14
|
+
STATUS_WORDS,
|
|
15
|
+
SUBSCRIPTION_METHOD_WORDS,
|
|
16
|
+
downloadName,
|
|
17
|
+
minorToMoney,
|
|
18
|
+
word
|
|
19
|
+
} from "./ledger-model";
|
|
20
|
+
import { PlanMixPanel } from "./plan-mix-panel";
|
|
21
|
+
import styles from "./ledger.module.css";
|
|
22
|
+
import table from "./revenue.module.css";
|
|
23
|
+
const PAGE_SIZE = 25;
|
|
24
|
+
const integer = new Intl.NumberFormat("en-US");
|
|
25
|
+
const STATUSES = [
|
|
26
|
+
{ value: "", label: "Every result" },
|
|
27
|
+
{ value: "succeeded", label: "Paid" },
|
|
28
|
+
{ value: "failed", label: "Failed" },
|
|
29
|
+
{ value: "pending", label: "Waiting" }
|
|
30
|
+
];
|
|
31
|
+
const METHODS = [
|
|
32
|
+
{ value: "", label: "Every way of paying" },
|
|
33
|
+
{ value: "gateway", label: "Card or bank" },
|
|
34
|
+
{ value: "wallet", label: "Wallet" },
|
|
35
|
+
{ value: "manual", label: "Recorded by hand" }
|
|
36
|
+
];
|
|
37
|
+
const tone = (status) => status === "succeeded" ? styles.statusGood : status === "failed" ? styles.statusBad : styles.statusWait;
|
|
38
|
+
function SubscriptionsView({
|
|
39
|
+
startDate,
|
|
40
|
+
endDate,
|
|
41
|
+
revenue
|
|
42
|
+
}) {
|
|
43
|
+
const [search, setSearch] = useState("");
|
|
44
|
+
const [q, setQ] = useState("");
|
|
45
|
+
const [status, setStatus] = useState("");
|
|
46
|
+
const [method, setMethod] = useState("");
|
|
47
|
+
const [page, setPage] = useState(1);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const timer = setTimeout(() => setQ(search.trim()), 300);
|
|
50
|
+
return () => clearTimeout(timer);
|
|
51
|
+
}, [search]);
|
|
52
|
+
const filters = useMemo(
|
|
53
|
+
() => ({ startDate, endDate, status, method, q, page, limit: PAGE_SIZE }),
|
|
54
|
+
[startDate, endDate, status, method, q, page]
|
|
55
|
+
);
|
|
56
|
+
const change = (set) => (value) => {
|
|
57
|
+
set(value);
|
|
58
|
+
setPage(1);
|
|
59
|
+
};
|
|
60
|
+
const { recurring } = revenue;
|
|
61
|
+
const tracked = recurring.tracked !== false;
|
|
62
|
+
const currency = recurring.currency ?? revenue.currency ?? "NGN";
|
|
63
|
+
const money = moneyIn(currency);
|
|
64
|
+
const money2 = moneyIn(currency, 2);
|
|
65
|
+
const notSet = "Not set up yet";
|
|
66
|
+
const why = "The plan payments have not been connected to this page yet.";
|
|
67
|
+
const payments = useSubscriptionPayments(filters);
|
|
68
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
69
|
+
/* @__PURE__ */ jsx("p", { className: styles.lede, children: "This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants." }),
|
|
70
|
+
/* @__PURE__ */ jsxs("section", { "aria-label": "Plan income", className: "adm-grid", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
72
|
+
StatCard,
|
|
73
|
+
{
|
|
74
|
+
label: "Plan income each month",
|
|
75
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notSet,
|
|
76
|
+
sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why,
|
|
77
|
+
hint: "What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)"
|
|
78
|
+
}
|
|
79
|
+
) }),
|
|
80
|
+
/* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
81
|
+
StatCard,
|
|
82
|
+
{
|
|
83
|
+
label: "Plan income over a year",
|
|
84
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notSet,
|
|
85
|
+
sub: tracked ? "Each month times 12" : why,
|
|
86
|
+
hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)"
|
|
87
|
+
}
|
|
88
|
+
) }),
|
|
89
|
+
/* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
90
|
+
StatCard,
|
|
91
|
+
{
|
|
92
|
+
label: "Average per paying merchant",
|
|
93
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notSet,
|
|
94
|
+
sub: tracked ? "Each month" : why,
|
|
95
|
+
hint: "What one paying merchant pays Olive in a month, on average. (ARPA)"
|
|
96
|
+
}
|
|
97
|
+
) }),
|
|
98
|
+
/* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
99
|
+
StatCard,
|
|
100
|
+
{
|
|
101
|
+
label: "Joined and left",
|
|
102
|
+
value: tracked ? `${integer.format(recurring.newInRange)} joined` : notSet,
|
|
103
|
+
sub: tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why,
|
|
104
|
+
hint: "Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off."
|
|
105
|
+
}
|
|
106
|
+
) })
|
|
107
|
+
] }),
|
|
108
|
+
tracked && recurring.planBreakdown.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
109
|
+
/* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "Who pays what" }),
|
|
110
|
+
/* @__PURE__ */ jsx("p", { className: styles.sectionNote, children: "How the monthly plan income splits across plans." }),
|
|
111
|
+
/* @__PURE__ */ jsx(PlanMixPanel, { planBreakdown: recurring.planBreakdown, money, caption: `Each month \xB7 ${currency}` })
|
|
112
|
+
] }) : null,
|
|
113
|
+
/* @__PURE__ */ jsx("h2", { className: styles.sectionTitle, children: "Every plan payment" }),
|
|
114
|
+
payments.isError ? /* @__PURE__ */ jsx("div", { className: styles.notice, children: "Plan payments are not available on this box yet. Once the subscription module is connected, every payment will be listed here." }) : payments.isLoading || !payments.data ? /* @__PURE__ */ jsx(AdmLoading, { label: "Loading plan payments\u2026" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
|
|
116
|
+
/* @__PURE__ */ jsxs(Filters, { label: "Filter plan payments", children: [
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
FilterSearch,
|
|
119
|
+
{
|
|
120
|
+
value: search,
|
|
121
|
+
onChange: change(setSearch),
|
|
122
|
+
placeholder: "Search a merchant's name or email, or a reference",
|
|
123
|
+
label: "Search plan payments"
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
/* @__PURE__ */ jsx(FilterSelect, { value: status, onChange: change(setStatus), options: STATUSES, label: "Payment result" }),
|
|
127
|
+
/* @__PURE__ */ jsx(FilterSelect, { value: method, onChange: change(setMethod), options: METHODS, label: "Way of paying" })
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ jsx(
|
|
130
|
+
DownloadButton,
|
|
131
|
+
{
|
|
132
|
+
filename: downloadName("plan-payments", startDate, endDate),
|
|
133
|
+
load: () => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: void 0, limit: void 0 })
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
] }),
|
|
137
|
+
payments.data.items.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { className: table.empty, children: "No plan payments match. Try a wider date range or clear a filter." }) : /* @__PURE__ */ jsx(
|
|
138
|
+
TableShell,
|
|
139
|
+
{
|
|
140
|
+
minWidth: 860,
|
|
141
|
+
density: "compact",
|
|
142
|
+
head: /* @__PURE__ */ jsxs("tr", { children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Th, { children: "When" }),
|
|
144
|
+
/* @__PURE__ */ jsx(Th, { children: "Merchant" }),
|
|
145
|
+
/* @__PURE__ */ jsx(Th, { children: "Plan" }),
|
|
146
|
+
/* @__PURE__ */ jsx(Th, { children: "For" }),
|
|
147
|
+
/* @__PURE__ */ jsx(Th, { children: "How" }),
|
|
148
|
+
/* @__PURE__ */ jsx(Th, { children: "Result" }),
|
|
149
|
+
/* @__PURE__ */ jsx(Th, { align: "right", children: "Amount" })
|
|
150
|
+
] }),
|
|
151
|
+
children: payments.data.items.map((row) => /* @__PURE__ */ jsx(PlanPaymentRow, { row }, row.id))
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
Pagination,
|
|
156
|
+
{
|
|
157
|
+
page,
|
|
158
|
+
totalPages: Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1),
|
|
159
|
+
total: payments.data.total,
|
|
160
|
+
noun: "plan payments",
|
|
161
|
+
onPage: setPage
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] })
|
|
165
|
+
] });
|
|
166
|
+
}
|
|
167
|
+
function PlanPaymentRow({ row }) {
|
|
168
|
+
const when = row.paidAt ?? row.createdAt;
|
|
169
|
+
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
170
|
+
/* @__PURE__ */ jsx("td", { className: table.num, children: new Date(when).toLocaleString(void 0, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) }),
|
|
171
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: styles.whoName, children: row.merchantName || "Unknown merchant" }),
|
|
173
|
+
row.merchantEmail ? /* @__PURE__ */ jsx("span", { className: styles.whoSub, children: row.merchantEmail }) : null
|
|
174
|
+
] }) }),
|
|
175
|
+
/* @__PURE__ */ jsx("td", { className: table.name, children: row.plan.charAt(0).toUpperCase() + row.plan.slice(1) }),
|
|
176
|
+
/* @__PURE__ */ jsx("td", { children: word(PURPOSE_WORDS, row.purpose) }),
|
|
177
|
+
/* @__PURE__ */ jsx("td", { children: word(SUBSCRIPTION_METHOD_WORDS, row.method) }),
|
|
178
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("span", { className: styles.who, children: [
|
|
179
|
+
/* @__PURE__ */ jsx("span", { className: `${styles.status} ${tone(row.status)}`, children: word(STATUS_WORDS, row.status) }),
|
|
180
|
+
row.status === "failed" && row.failureReason ? /* @__PURE__ */ jsx("span", { className: styles.whoSub, children: row.failureReason }) : null
|
|
181
|
+
] }) }),
|
|
182
|
+
/* @__PURE__ */ jsx("td", { className: table.numStrong, children: minorToMoney(row.amountKobo, "NGN") })
|
|
183
|
+
] });
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
SubscriptionsView
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=subscriptions-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/subscriptions-view.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { EmptyNote, TableRow, TableShell, Th } from '@withpotter/ui-kit';\nimport {\n platformAdminApi,\n type PlatformRevenue,\n type SubscriptionPaymentRow,\n} from '@withpotter/lib/admin-api';\nimport { useSubscriptionPayments } from '../hooks';\nimport { LoadFailed } from '../audit/load-failed';\nimport { CountUp, Reveal } from '../_fx';\nimport { AdmLoading, FilterSearch, FilterSelect, Filters, Pagination, StatCard, moneyIn } from '../_ui';\nimport { DownloadButton } from './download-button';\nimport {\n PURPOSE_WORDS,\n STATUS_WORDS,\n SUBSCRIPTION_METHOD_WORDS,\n downloadName,\n minorToMoney,\n word,\n} from './ledger-model';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport styles from './ledger.module.css';\nimport table from './revenue.module.css';\n\nconst PAGE_SIZE = 25;\nconst integer = new Intl.NumberFormat('en-US');\n\nconst STATUSES = [\n { value: '', label: 'Every result' },\n { value: 'succeeded', label: 'Paid' },\n { value: 'failed', label: 'Failed' },\n { value: 'pending', label: 'Waiting' },\n];\nconst METHODS = [\n { value: '', label: 'Every way of paying' },\n { value: 'gateway', label: 'Card or bank' },\n { value: 'wallet', label: 'Wallet' },\n { value: 'manual', label: 'Recorded by hand' },\n];\n\nconst tone = (status: string) =>\n status === 'succeeded' ? styles.statusGood : status === 'failed' ? styles.statusBad : styles.statusWait;\n\n/** What merchants pay Olive for their plan: the income figures, who pays what, and every plan payment. */\nexport function SubscriptionsView({\n startDate,\n endDate,\n revenue,\n}: {\n startDate?: string;\n endDate?: string;\n revenue: PlatformRevenue;\n}) {\n const [search, setSearch] = useState('');\n const [q, setQ] = useState('');\n const [status, setStatus] = useState('');\n const [method, setMethod] = useState('');\n const [page, setPage] = useState(1);\n\n useEffect(() => {\n const timer = setTimeout(() => setQ(search.trim()), 300);\n\n return () => clearTimeout(timer);\n }, [search]);\n\n const filters = useMemo(\n () => ({ startDate, endDate, status, method, q, page, limit: PAGE_SIZE }),\n [startDate, endDate, status, method, q, page],\n );\n const change = <T,>(set: (v: T) => void) => (value: T) => {\n set(value);\n setPage(1);\n };\n\n const { recurring } = revenue;\n const tracked = recurring.tracked !== false;\n const currency = recurring.currency ?? revenue.currency ?? 'NGN';\n const money = moneyIn(currency);\n const money2 = moneyIn(currency, 2);\n const notSet = 'Not set up yet';\n const why = 'The plan payments have not been connected to this page yet.';\n const payments = useSubscriptionPayments(filters);\n\n return (\n <div>\n <p className={styles.lede}>\n This is money that merchants pay Olive for their plan. It is kept apart from the money customers pay merchants.\n </p>\n\n <section aria-label=\"Plan income\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income each month\"\n value={tracked ? <CountUp value={recurring.mrr} format={money} /> : notSet}\n sub={tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : why}\n hint=\"What merchants on a paid plan are expected to pay Olive every month. Merchants on a free plan, a trial or a pause are not counted. (MRR)\"\n />\n </Reveal>\n <Reveal index={1} className=\"adm-span-3\">\n <StatCard\n label=\"Plan income over a year\"\n value={tracked ? <CountUp value={recurring.arr} format={money} /> : notSet}\n sub={tracked ? 'Each month times 12' : why}\n hint=\"What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)\"\n />\n </Reveal>\n <Reveal index={2} className=\"adm-span-3\">\n <StatCard\n label=\"Average per paying merchant\"\n value={tracked ? <CountUp value={recurring.arpa} format={money2} /> : notSet}\n sub={tracked ? 'Each month' : why}\n hint=\"What one paying merchant pays Olive in a month, on average. (ARPA)\"\n />\n </Reveal>\n <Reveal index={3} className=\"adm-span-3\">\n <StatCard\n label=\"Joined and left\"\n value={tracked ? `${integer.format(recurring.newInRange)} joined` : notSet}\n sub={tracked ? `${integer.format(recurring.churnedInRange)} left or cancelled in this period` : why}\n hint=\"Merchants who started a plan in this period, and merchants whose plan was cancelled, ended or switched off.\"\n />\n </Reveal>\n </section>\n\n {tracked && recurring.planBreakdown.length > 0 ? (\n <>\n <h2 className={styles.sectionTitle}>Who pays what</h2>\n <p className={styles.sectionNote}>How the monthly plan income splits across plans.</p>\n <PlanMixPanel planBreakdown={recurring.planBreakdown} money={money} caption={`Each month · ${currency}`} />\n </>\n ) : null}\n\n <h2 className={styles.sectionTitle}>Every plan payment</h2>\n {payments.isError ? (\n <div className={styles.notice}>\n Plan payments are not available on this box yet. Once the subscription module is connected, every payment will\n be listed here.\n </div>\n ) : payments.isLoading || !payments.data ? (\n <AdmLoading label=\"Loading plan payments…\" />\n ) : (\n <>\n <div className={styles.toolbar}>\n <Filters label=\"Filter plan payments\">\n <FilterSearch\n value={search}\n onChange={change(setSearch)}\n placeholder=\"Search a merchant's name or email, or a reference\"\n label=\"Search plan payments\"\n />\n <FilterSelect value={status} onChange={change(setStatus)} options={STATUSES} label=\"Payment result\" />\n <FilterSelect value={method} onChange={change(setMethod)} options={METHODS} label=\"Way of paying\" />\n </Filters>\n <DownloadButton\n filename={downloadName('plan-payments', startDate, endDate)}\n load={() => platformAdminApi.subscriptionPaymentsCsv({ ...filters, page: undefined, limit: undefined })}\n />\n </div>\n\n {payments.data.items.length === 0 ? (\n <EmptyNote className={table.empty}>No plan payments match. Try a wider date range or clear a filter.</EmptyNote>\n ) : (\n <TableShell\n minWidth={860}\n density=\"compact\"\n head={\n <tr>\n <Th>When</Th>\n <Th>Merchant</Th>\n <Th>Plan</Th>\n <Th>For</Th>\n <Th>How</Th>\n <Th>Result</Th>\n <Th align=\"right\">Amount</Th>\n </tr>\n }\n >\n {payments.data.items.map((row) => (\n <PlanPaymentRow key={row.id} row={row} />\n ))}\n </TableShell>\n )}\n <Pagination\n page={page}\n totalPages={Math.max(Math.ceil(payments.data.total / PAGE_SIZE), 1)}\n total={payments.data.total}\n noun=\"plan payments\"\n onPage={setPage}\n />\n </>\n )}\n </div>\n );\n}\n\nfunction PlanPaymentRow({ row }: { row: SubscriptionPaymentRow }) {\n const when = row.paidAt ?? row.createdAt;\n\n return (\n <TableRow>\n <td className={table.num}>\n {new Date(when).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}\n </td>\n <td>\n <span className={styles.who}>\n <span className={styles.whoName}>{row.merchantName || 'Unknown merchant'}</span>\n {row.merchantEmail ? <span className={styles.whoSub}>{row.merchantEmail}</span> : null}\n </span>\n </td>\n <td className={table.name}>{row.plan.charAt(0).toUpperCase() + row.plan.slice(1)}</td>\n <td>{word(PURPOSE_WORDS, row.purpose)}</td>\n <td>{word(SUBSCRIPTION_METHOD_WORDS, row.method)}</td>\n <td>\n <span className={styles.who}>\n <span className={`${styles.status} ${tone(row.status)}`}>{word(STATUS_WORDS, row.status)}</span>\n {row.status === 'failed' && row.failureReason ? <span className={styles.whoSub}>{row.failureReason}</span> : null}\n </span>\n </td>\n <td className={table.numStrong}>{minorToMoney(row.amountKobo, 'NGN')}</td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2240,6 +2240,7 @@ function ActivityTable({
|
|
|
2240
2240
|
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Customer" }),
|
|
2241
2241
|
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Total" }),
|
|
2242
2242
|
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Payment" }),
|
|
2243
|
+
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Paid by" }),
|
|
2243
2244
|
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Status" }),
|
|
2244
2245
|
/* @__PURE__ */ jsx("th", { style: tableStyles.th, children: "Date" })
|
|
2245
2246
|
] }) }),
|
|
@@ -2248,7 +2249,10 @@ function ActivityTable({
|
|
|
2248
2249
|
"#",
|
|
2249
2250
|
o.orderNumber
|
|
2250
2251
|
] }),
|
|
2251
|
-
/* @__PURE__ */
|
|
2252
|
+
/* @__PURE__ */ jsxs("td", { style: tableStyles.cell, children: [
|
|
2253
|
+
o.customerName ?? o.customerEmail,
|
|
2254
|
+
o.customerName ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "var(--text-tertiary)" }, children: [o.customerEmail, o.customerPhone].filter(Boolean).join(" \xB7 ") }) : null
|
|
2255
|
+
] }),
|
|
2252
2256
|
/* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: moneyCur(o.total, o.currency) }),
|
|
2253
2257
|
/* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: /* @__PURE__ */ jsx(
|
|
2254
2258
|
Badge,
|
|
@@ -2257,6 +2261,7 @@ function ActivityTable({
|
|
|
2257
2261
|
children: o.paymentStatus
|
|
2258
2262
|
}
|
|
2259
2263
|
) }),
|
|
2264
|
+
/* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: o.paymentMethod ?? /* @__PURE__ */ jsx("span", { style: { color: "var(--text-tertiary)" }, children: "Not recorded" }) }),
|
|
2260
2265
|
/* @__PURE__ */ jsx("td", { style: tableStyles.cell, children: /* @__PURE__ */ jsx(
|
|
2261
2266
|
Badge,
|
|
2262
2267
|
{
|