@withpotter/pages-admin 0.13.0-next.101 → 0.13.0-next.103
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/tenant-operators.js +73 -0
- package/dist/admin/_pos/tenant-operators.js.map +1 -0
- package/dist/admin/_pos/tenant-pos-figures.js +82 -0
- package/dist/admin/_pos/tenant-pos-figures.js.map +1 -0
- package/dist/admin/_pos/use-pos.js +12 -0
- package/dist/admin/_pos/use-pos.js.map +1 -1
- package/dist/admin/hooks.js +19 -0
- package/dist/admin/hooks.js.map +1 -1
- package/dist/admin/pos/shifts-tab.js +15 -8
- package/dist/admin/pos/shifts-tab.js.map +1 -1
- package/dist/admin/revenue/cohort-panel.js +5 -5
- package/dist/admin/revenue/cohort-panel.js.map +1 -1
- package/dist/admin/revenue/health-panel.js +17 -17
- package/dist/admin/revenue/health-panel.js.map +1 -1
- package/dist/admin/revenue/ledger-model.js +7 -0
- package/dist/admin/revenue/ledger-model.js.map +1 -1
- package/dist/admin/revenue/ledger.module.css +43 -0
- package/dist/admin/revenue/merchant-headline.js +37 -0
- package/dist/admin/revenue/merchant-headline.js.map +1 -0
- package/dist/admin/revenue/merchant-payments-view.js +46 -22
- package/dist/admin/revenue/merchant-payments-view.js.map +1 -1
- package/dist/admin/revenue/merchants-view.js +99 -0
- package/dist/admin/revenue/merchants-view.js.map +1 -0
- package/dist/admin/revenue/payment-drawer.js +53 -0
- package/dist/admin/revenue/payment-drawer.js.map +1 -0
- package/dist/admin/revenue/plan-mix-panel.js +1 -1
- package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
- package/dist/admin/revenue/plan-payment-drawer.js +42 -0
- package/dist/admin/revenue/plan-payment-drawer.js.map +1 -0
- package/dist/admin/revenue/print-summary-button.js +34 -0
- package/dist/admin/revenue/print-summary-button.js.map +1 -0
- package/dist/admin/revenue/quality-panel.js +19 -19
- package/dist/admin/revenue/quality-panel.js.map +1 -1
- package/dist/admin/revenue/revenue-kpis.js +64 -50
- package/dist/admin/revenue/revenue-kpis.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +68 -50
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/revenue-retention.js +17 -13
- package/dist/admin/revenue/revenue-retention.js.map +1 -1
- package/dist/admin/revenue/revenue-workspace.js +16 -0
- package/dist/admin/revenue/revenue-workspace.js.map +1 -1
- package/dist/admin/revenue/subscriptions-panel.js +14 -14
- package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
- package/dist/admin/revenue/subscriptions-view.js +20 -20
- package/dist/admin/revenue/subscriptions-view.js.map +1 -1
- package/dist/admin/revenue/summary-download.js +34 -0
- package/dist/admin/revenue/summary-download.js.map +1 -0
- package/dist/admin/revenue/summary-print.js +82 -0
- package/dist/admin/revenue/summary-print.js.map +1 -0
- package/dist/admin/revenue/summary-rows.js +44 -0
- package/dist/admin/revenue/summary-rows.js.map +1 -0
- package/dist/admin/revenue/top-tenants-panel.js +1 -1
- package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
- package/dist/admin/revenue/url-filters.js +36 -0
- package/dist/admin/revenue/url-filters.js.map +1 -0
- package/dist/admin/revenue/use-url-filters.js +24 -0
- package/dist/admin/revenue/use-url-filters.js.map +1 -0
- package/dist/admin/tenants/[id]/tenant-pos.js +53 -16
- package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Fact, FactList } from "@withpotter/ui-kit";
|
|
5
|
+
import { StatusPill } from "../audit/status-pill";
|
|
6
|
+
import { Drawer } from "../_ui";
|
|
7
|
+
import { PURPOSE_WORDS, STATUS_WORDS, SUBSCRIPTION_METHOD_WORDS, minorToMoney, word } from "./ledger-model";
|
|
8
|
+
import styles from "./ledger.module.css";
|
|
9
|
+
const when = (iso) => new Date(iso).toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
|
|
10
|
+
const TONE = { succeeded: "success", pending: "pending", failed: "failed" };
|
|
11
|
+
function PlanPaymentDrawer({ row, onClose }) {
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
Drawer,
|
|
14
|
+
{
|
|
15
|
+
open: Boolean(row),
|
|
16
|
+
onClose,
|
|
17
|
+
title: row ? minorToMoney(row.amountKobo, "NGN") : "Plan payment",
|
|
18
|
+
subtitle: row ? row.merchantName ?? "Unknown merchant" : void 0,
|
|
19
|
+
meta: row ? /* @__PURE__ */ jsx(StatusPill, { status: TONE[row.status] ?? "neutral", label: word(STATUS_WORDS, row.status) }) : void 0,
|
|
20
|
+
children: row ? /* @__PURE__ */ jsx("div", { className: styles.drawerBody, children: /* @__PURE__ */ jsxs(FactList, { columns: 2, children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Fact, { label: "Paid on", value: when(row.paidAt ?? row.createdAt) }),
|
|
22
|
+
/* @__PURE__ */ jsx(Fact, { label: "Plan", value: row.plan.charAt(0).toUpperCase() + row.plan.slice(1) }),
|
|
23
|
+
/* @__PURE__ */ jsx(Fact, { label: "What it was for", value: word(PURPOSE_WORDS, row.purpose) }),
|
|
24
|
+
/* @__PURE__ */ jsx(Fact, { label: "How they paid", value: word(SUBSCRIPTION_METHOD_WORDS, row.method) }),
|
|
25
|
+
/* @__PURE__ */ jsx(
|
|
26
|
+
Fact,
|
|
27
|
+
{
|
|
28
|
+
label: "Merchant",
|
|
29
|
+
value: /* @__PURE__ */ jsx(Link, { href: `/admin/tenants/${row.tenantId}`, children: row.merchantName ?? "Open merchant" })
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
/* @__PURE__ */ jsx(Fact, { label: "Email", value: row.merchantEmail ?? "\u2013" }),
|
|
33
|
+
/* @__PURE__ */ jsx(Fact, { label: "Reference", value: row.reference }),
|
|
34
|
+
row.status === "failed" ? /* @__PURE__ */ jsx(Fact, { label: "Why it failed", value: row.failureReason ?? "Not recorded" }) : null
|
|
35
|
+
] }) }) : null
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
PlanPaymentDrawer
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=plan-payment-drawer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/plan-payment-drawer.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { Fact, FactList } from '@withpotter/ui-kit';\nimport type { SubscriptionPaymentRow } from '@withpotter/lib/admin-api';\nimport { StatusPill } from '../audit/status-pill';\nimport { Drawer } from '../_ui';\nimport { PURPOSE_WORDS, STATUS_WORDS, SUBSCRIPTION_METHOD_WORDS, minorToMoney, word } from './ledger-model';\nimport styles from './ledger.module.css';\n\nconst when = (iso: string): string =>\n new Date(iso).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' });\n\nconst TONE: Record<string, string> = { succeeded: 'success', pending: 'pending', failed: 'failed' };\n\n/** One plan payment in full: which merchant paid, for which plan, and how it went. */\nexport function PlanPaymentDrawer({ row, onClose }: { row: SubscriptionPaymentRow | null; onClose: () => void }) {\n return (\n <Drawer\n open={Boolean(row)}\n onClose={onClose}\n title={row ? minorToMoney(row.amountKobo, 'NGN') : 'Plan payment'}\n subtitle={row ? (row.merchantName ?? 'Unknown merchant') : undefined}\n meta={\n row ? <StatusPill status={TONE[row.status] ?? 'neutral'} label={word(STATUS_WORDS, row.status)} /> : undefined\n }\n >\n {row ? (\n <div className={styles.drawerBody}>\n <FactList columns={2}>\n <Fact label=\"Paid on\" value={when(row.paidAt ?? row.createdAt)} />\n <Fact label=\"Plan\" value={row.plan.charAt(0).toUpperCase() + row.plan.slice(1)} />\n <Fact label=\"What it was for\" value={word(PURPOSE_WORDS, row.purpose)} />\n <Fact label=\"How they paid\" value={word(SUBSCRIPTION_METHOD_WORDS, row.method)} />\n <Fact\n label=\"Merchant\"\n value={<Link href={`/admin/tenants/${row.tenantId}`}>{row.merchantName ?? 'Open merchant'}</Link>}\n />\n <Fact label=\"Email\" value={row.merchantEmail ?? '–'} />\n <Fact label=\"Reference\" value={row.reference} />\n {row.status === 'failed' ? <Fact label=\"Why it failed\" value={row.failureReason ?? 'Not recorded'} /> : null}\n </FactList>\n </div>\n ) : null}\n </Drawer>\n );\n}\n"],"mappings":";AAwBc,cAKJ,YALI;AAtBd,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAE/B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,eAAe,cAAc,2BAA2B,cAAc,YAAY;AAC3F,OAAO,YAAY;AAEnB,MAAM,OAAO,CAAC,QACZ,IAAI,KAAK,GAAG,EAAE,eAAe,SAAS,EAAE,WAAW,UAAU,WAAW,QAAQ,CAAC;AAEnF,MAAM,OAA+B,EAAE,WAAW,WAAW,SAAS,WAAW,QAAQ,SAAS;AAG3F,SAAS,kBAAkB,EAAE,KAAK,QAAQ,GAAgE;AAC/G,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,QAAQ,GAAG;AAAA,MACjB;AAAA,MACA,OAAO,MAAM,aAAa,IAAI,YAAY,KAAK,IAAI;AAAA,MACnD,UAAU,MAAO,IAAI,gBAAgB,qBAAsB;AAAA,MAC3D,MACE,MAAM,oBAAC,cAAW,QAAQ,KAAK,IAAI,MAAM,KAAK,WAAW,OAAO,KAAK,cAAc,IAAI,MAAM,GAAG,IAAK;AAAA,MAGtG,gBACC,oBAAC,SAAI,WAAW,OAAO,YACrB,+BAAC,YAAS,SAAS,GACjB;AAAA,4BAAC,QAAK,OAAM,WAAU,OAAO,KAAK,IAAI,UAAU,IAAI,SAAS,GAAG;AAAA,QAChE,oBAAC,QAAK,OAAM,QAAO,OAAO,IAAI,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,KAAK,MAAM,CAAC,GAAG;AAAA,QAChF,oBAAC,QAAK,OAAM,mBAAkB,OAAO,KAAK,eAAe,IAAI,OAAO,GAAG;AAAA,QACvE,oBAAC,QAAK,OAAM,iBAAgB,OAAO,KAAK,2BAA2B,IAAI,MAAM,GAAG;AAAA,QAChF;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO,oBAAC,QAAK,MAAM,kBAAkB,IAAI,QAAQ,IAAK,cAAI,gBAAgB,iBAAgB;AAAA;AAAA,QAC5F;AAAA,QACA,oBAAC,QAAK,OAAM,SAAQ,OAAO,IAAI,iBAAiB,UAAK;AAAA,QACrD,oBAAC,QAAK,OAAM,aAAY,OAAO,IAAI,WAAW;AAAA,QAC7C,IAAI,WAAW,WAAW,oBAAC,QAAK,OAAM,iBAAgB,OAAO,IAAI,iBAAiB,gBAAgB,IAAK;AAAA,SAC1G,GACF,IACE;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Button, useToast } from "@withpotter/ui-kit";
|
|
4
|
+
import { ADMIN_NAME } from "../_shell/admin-brand";
|
|
5
|
+
import { summaryHtml } from "./summary-print";
|
|
6
|
+
function PrintSummaryButton({ model }) {
|
|
7
|
+
const { toast } = useToast();
|
|
8
|
+
const run = () => {
|
|
9
|
+
const frame = document.createElement("iframe");
|
|
10
|
+
frame.setAttribute("aria-hidden", "true");
|
|
11
|
+
frame.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
|
|
12
|
+
document.body.appendChild(frame);
|
|
13
|
+
const doc = frame.contentDocument;
|
|
14
|
+
const view = frame.contentWindow;
|
|
15
|
+
if (!doc || !view) {
|
|
16
|
+
frame.remove();
|
|
17
|
+
toast({ title: "The summary could not be opened. Please try again." });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
doc.open();
|
|
21
|
+
doc.write(summaryHtml(model, ADMIN_NAME, (/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { dateStyle: "long" })));
|
|
22
|
+
doc.close();
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
view.focus();
|
|
25
|
+
view.print();
|
|
26
|
+
setTimeout(() => frame.remove(), 1e3);
|
|
27
|
+
}, 150);
|
|
28
|
+
};
|
|
29
|
+
return /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: run, children: "Save as PDF" });
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
PrintSummaryButton
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=print-summary-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/print-summary-button.tsx"],"sourcesContent":["'use client';\n\nimport { Button, useToast } from '@withpotter/ui-kit';\nimport { 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.\n */\nexport function PrintSummaryButton({ model }: { model: SummaryModel }) {\n const { toast } = useToast();\n\n const run = () => {\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 toast({ title: 'The summary could not be opened. Please try again.' });\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\n return (\n <Button variant=\"secondary\" size=\"sm\" onClick={run}>\n Save as PDF\n </Button>\n );\n}\n"],"mappings":";AA0CI;AAxCJ,SAAS,QAAQ,gBAAgB;AACjC,SAAS,kBAAkB;AAC3B,SAAS,mBAAsC;AAMxC,SAAS,mBAAmB,EAAE,MAAM,GAA4B;AACrE,QAAM,EAAE,MAAM,IAAI,SAAS;AAE3B,QAAM,MAAM,MAAM;AAChB,UAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,UAAM,aAAa,eAAe,MAAM;AACxC,UAAM,MAAM,UAAU;AACtB,aAAS,KAAK,YAAY,KAAK;AAE/B,UAAM,MAAM,MAAM;AAClB,UAAM,OAAO,MAAM;AAEnB,QAAI,CAAC,OAAO,CAAC,MAAM;AACjB,YAAM,OAAO;AACb,YAAM,EAAE,OAAO,qDAAqD,CAAC;AAErE;AAAA,IACF;AAEA,QAAI,KAAK;AACT,QAAI,MAAM,YAAY,OAAO,aAAY,oBAAI,KAAK,GAAE,mBAAmB,SAAS,EAAE,WAAW,OAAO,CAAC,CAAC,CAAC;AACvG,QAAI,MAAM;AAGV,eAAW,MAAM;AACf,WAAK,MAAM;AACX,WAAK,MAAM;AACX,iBAAW,MAAM,MAAM,OAAO,GAAG,GAAI;AAAA,IACvC,GAAG,GAAG;AAAA,EACR;AAEA,SACE,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,KAAK,yBAEpD;AAEJ;","names":[]}
|
|
@@ -14,56 +14,56 @@ function QualityPanel({
|
|
|
14
14
|
const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));
|
|
15
15
|
const rows = [
|
|
16
16
|
{
|
|
17
|
-
label: "
|
|
18
|
-
note: "
|
|
17
|
+
label: "Income kept from existing merchants",
|
|
18
|
+
note: "Plan income from merchants who were here at the start, after upgrades, downgrades and those who left",
|
|
19
19
|
value: quality.nrrPct == null ? "\u2013" : `${quality.nrrPct}%`,
|
|
20
|
-
hint: `+${money(quality.expansionMrr)}
|
|
20
|
+
hint: `+${money(quality.expansionMrr)} upgrades \xB7 \u2212${money(
|
|
21
21
|
quality.contractionMrr
|
|
22
|
-
)}
|
|
22
|
+
)} downgrades`
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
label: "
|
|
26
|
-
note: "
|
|
25
|
+
label: "Growth against loss",
|
|
26
|
+
note: "Plan income gained (new merchants and upgrades) for every \u20A61 lost (merchants who left and downgrades)",
|
|
27
27
|
value: quality.quickRatio == null ? "\u2013" : `${quality.quickRatio}\xD7`
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
label: "
|
|
31
|
-
note: "
|
|
30
|
+
label: "Plan income lost",
|
|
31
|
+
note: "Share of plan income lost to merchants who left",
|
|
32
32
|
value: quality.mrrChurnRatePct == null ? "\u2013" : `${quality.mrrChurnRatePct}%`,
|
|
33
33
|
tone: quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0 ? "danger" : "default"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
label: "
|
|
37
|
-
note: "
|
|
36
|
+
label: "Merchant sales after refunds",
|
|
37
|
+
note: "What merchants sold, less what was refunded",
|
|
38
38
|
value: lines((q) => q.netGmv),
|
|
39
39
|
hint: `Gross ${lines((q) => q.grossGmv)}`
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
|
-
label: "Refunded",
|
|
43
|
-
note: "
|
|
42
|
+
label: "Refunded sales",
|
|
43
|
+
note: "Value of sales that were refunded in this period",
|
|
44
44
|
value: lines((q) => q.refundedGmv)
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
label: "
|
|
48
|
-
note: "
|
|
47
|
+
label: "Share refunded",
|
|
48
|
+
note: "Refunded sales as a share of all sales",
|
|
49
49
|
value: one ? `${one.refundRatePct}%` : "\u2013",
|
|
50
50
|
hint: one ? one.currency : "Pick a currency"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
|
-
label: "
|
|
54
|
-
note: "
|
|
53
|
+
label: "Orders refunded",
|
|
54
|
+
note: "How many orders were refunded",
|
|
55
55
|
value: integer.format(
|
|
56
56
|
(gmv ?? []).filter((q) => !currency || q.currency === currency).reduce((n, q) => n + q.refundedOrders, 0)
|
|
57
57
|
)
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
label: "
|
|
61
|
-
note: "
|
|
60
|
+
label: "Paying merchants lost",
|
|
61
|
+
note: "Share of paying merchants who stopped paying in this period",
|
|
62
62
|
value: quality.logoChurnRatePct == null ? "\u2013" : `${quality.logoChurnRatePct}%`,
|
|
63
63
|
tone: quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0 ? "danger" : "default"
|
|
64
64
|
}
|
|
65
65
|
];
|
|
66
|
-
return /* @__PURE__ */ jsx(Panel, { title: "
|
|
66
|
+
return /* @__PURE__ */ jsx(Panel, { title: "How healthy the income is", caption, children: /* @__PURE__ */ jsx(FigureList, { rows }) });
|
|
67
67
|
}
|
|
68
68
|
export {
|
|
69
69
|
QualityPanel
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: '
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/quality-panel.tsx"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport { Panel, moneyLines, noneText, rowFor } from '../_ui';\nimport { FigureList, type Figure } from './figure-list';\n\nconst integer = new Intl.NumberFormat('en-US');\n\ntype Quality = NonNullable<PlatformRevenue['quality']>;\n\n/** Retention and refund health over the selected window. */\nexport function QualityPanel({\n quality,\n money,\n currency,\n caption,\n}: {\n quality: Quality;\n /** Formats recurring revenue (MRR), in the currency the API names for it. */\n money: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n caption: string;\n}) {\n // Merchant GMV figures are per currency, joined line by line, never added.\n const gmv = quality.gmvByCurrency;\n const lines = (amount: (q: NonNullable<typeof gmv>[number]) => number) =>\n moneyLines(gmv, currency, amount).join(' · ') || noneText(currency);\n const codes = (gmv ?? []).map((q) => q.currency);\n // A rate only reads within one currency: the chosen one, or the only one.\n const one = rowFor(gmv, currency || (codes.length === 1 ? codes[0] : null));\n const rows: Figure[] = [\n {\n label: 'Income kept from existing merchants',\n note: 'Plan income from merchants who were here at the start, after upgrades, downgrades and those who left',\n value: quality.nrrPct == null ? '–' : `${quality.nrrPct}%`,\n hint: `+${money(quality.expansionMrr)} upgrades · −${money(\n quality.contractionMrr,\n )} downgrades`,\n },\n {\n label: 'Growth against loss',\n note: 'Plan income gained (new merchants and upgrades) for every ₦1 lost (merchants who left and downgrades)',\n value: quality.quickRatio == null ? '–' : `${quality.quickRatio}×`,\n },\n {\n label: 'Plan income lost',\n note: 'Share of plan income lost to merchants who left',\n value:\n quality.mrrChurnRatePct == null ? '–' : `${quality.mrrChurnRatePct}%`,\n tone:\n quality.mrrChurnRatePct != null && quality.mrrChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n {\n label: 'Merchant sales after refunds',\n note: 'What merchants sold, less what was refunded',\n value: lines((q) => q.netGmv),\n hint: `Gross ${lines((q) => q.grossGmv)}`,\n },\n {\n label: 'Refunded sales',\n note: 'Value of sales that were refunded in this period',\n value: lines((q) => q.refundedGmv),\n },\n {\n label: 'Share refunded',\n note: 'Refunded sales as a share of all sales',\n value: one ? `${one.refundRatePct}%` : '–',\n hint: one ? one.currency : 'Pick a currency',\n },\n {\n label: 'Orders refunded',\n note: 'How many orders were refunded',\n value: integer.format(\n (gmv ?? [])\n .filter((q) => !currency || q.currency === currency)\n .reduce((n, q) => n + q.refundedOrders, 0),\n ),\n },\n {\n label: 'Paying merchants lost',\n note: 'Share of paying merchants who stopped paying in this period',\n value:\n quality.logoChurnRatePct == null ? '–' : `${quality.logoChurnRatePct}%`,\n tone:\n quality.logoChurnRatePct != null && quality.logoChurnRatePct > 0\n ? 'danger'\n : 'default',\n },\n ];\n\n return (\n <Panel title=\"How healthy the income is\" caption={caption}>\n <FigureList rows={rows} />\n </Panel>\n );\n}\n"],"mappings":"AA6FM;AA5FN,SAAS,OAAO,YAAY,UAAU,cAAc;AACpD,SAAS,kBAA+B;AAExC,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAKtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AAED,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,CAAC,WACb,WAAW,KAAK,UAAU,MAAM,EAAE,KAAK,QAAK,KAAK,SAAS,QAAQ;AACpE,QAAM,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ;AAE/C,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK;AAC1E,QAAM,OAAiB;AAAA,IACrB;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,UAAU,OAAO,WAAM,GAAG,QAAQ,MAAM;AAAA,MACvD,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC,wBAAgB;AAAA,QACnD,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ,cAAc,OAAO,WAAM,GAAG,QAAQ,UAAU;AAAA,IACjE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,mBAAmB,OAAO,WAAM,GAAG,QAAQ,eAAe;AAAA,MACpE,MACE,QAAQ,mBAAmB,QAAQ,QAAQ,kBAAkB,IACzD,WACA;AAAA,IACR;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM;AAAA,MAC5B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,CAAC,MAAM,EAAE,WAAW;AAAA,IACnC;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,MAAM,GAAG,IAAI,aAAa,MAAM;AAAA,MACvC,MAAM,MAAM,IAAI,WAAW;AAAA,IAC7B;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,SACZ,OAAO,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ,EAClD,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,gBAAgB,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OACE,QAAQ,oBAAoB,OAAO,WAAM,GAAG,QAAQ,gBAAgB;AAAA,MACtE,MACE,QAAQ,oBAAoB,QAAQ,QAAQ,mBAAmB,IAC3D,WACA;AAAA,IACR;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,6BAA4B,SACvC,8BAAC,cAAW,MAAY,GAC1B;AAEJ;","names":[]}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ADMIN_NAME } from "../_shell/admin-brand";
|
|
3
3
|
import { CountUp, Reveal, TONE } from "../_fx";
|
|
4
4
|
import { MoneyLines, StatCard, rowFor } from "../_ui";
|
|
5
5
|
const integer = new Intl.NumberFormat("en-US");
|
|
6
6
|
function RevenueKpis({
|
|
7
|
+
part,
|
|
8
|
+
extra,
|
|
7
9
|
data,
|
|
8
10
|
money,
|
|
9
11
|
money2,
|
|
@@ -17,55 +19,67 @@ function RevenueKpis({
|
|
|
17
19
|
const noSource = "The plan payments are not connected yet";
|
|
18
20
|
const gmv = rowFor(data.gmvByCurrency, focus);
|
|
19
21
|
const lifetimeShare = gmv && gmv.lifetime > 0 ? `${Math.round(gmv.inRange / gmv.lifetime * 100)}% of lifetime` : void 0;
|
|
20
|
-
return /* @__PURE__ */ jsxs(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"section",
|
|
24
|
+
{
|
|
25
|
+
"aria-label": part === "olive" ? "What Olive earns" : "What merchants sold",
|
|
26
|
+
className: "adm-grid",
|
|
27
|
+
children: [
|
|
28
|
+
part === "olive" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
/* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
30
|
+
StatCard,
|
|
31
|
+
{
|
|
32
|
+
label: "Plan income each month",
|
|
33
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.mrr, format: money }) : notTracked,
|
|
34
|
+
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)`,
|
|
35
|
+
sub: tracked ? `${integer.format(recurring.payingMerchants ?? recurring.activeSubscriptions)} merchants paying` : noSource
|
|
36
|
+
}
|
|
37
|
+
) }),
|
|
38
|
+
/* @__PURE__ */ jsx(Reveal, { index: 1, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
39
|
+
StatCard,
|
|
40
|
+
{
|
|
41
|
+
label: "Plan income over a year",
|
|
42
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arr, format: money }) : notTracked,
|
|
43
|
+
hint: "What plan income adds up to over a year if nothing changes: the monthly figure times 12. (ARR)",
|
|
44
|
+
sub: tracked ? "Each month times 12" : noSource
|
|
45
|
+
}
|
|
46
|
+
) }),
|
|
47
|
+
/* @__PURE__ */ jsx(Reveal, { index: 2, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
48
|
+
StatCard,
|
|
49
|
+
{
|
|
50
|
+
label: "Average per paying merchant",
|
|
51
|
+
value: tracked ? /* @__PURE__ */ jsx(CountUp, { value: recurring.arpa, format: money2 }) : notTracked,
|
|
52
|
+
hint: "What one paying merchant pays in a month, on average. (ARPA)",
|
|
53
|
+
sub: tracked ? "Each month" : noSource
|
|
54
|
+
}
|
|
55
|
+
) })
|
|
56
|
+
] }),
|
|
57
|
+
part === "merchant" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
58
|
+
/* @__PURE__ */ jsx(Reveal, { index: 3, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
59
|
+
StatCard,
|
|
60
|
+
{
|
|
61
|
+
label: "Merchant sales",
|
|
62
|
+
value: /* @__PURE__ */ jsx(
|
|
63
|
+
MoneyLines,
|
|
64
|
+
{
|
|
65
|
+
rows: data.gmvByCurrency,
|
|
66
|
+
currency,
|
|
67
|
+
amount: (g) => g.inRange,
|
|
68
|
+
empty: "No sales yet"
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
href: "/admin/tenants",
|
|
72
|
+
sub: lifetimeShare,
|
|
73
|
+
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)`,
|
|
74
|
+
spark: focus ? gmvSpark : void 0,
|
|
75
|
+
sparkColor: TONE.money.fg
|
|
76
|
+
}
|
|
77
|
+
) }),
|
|
78
|
+
extra
|
|
79
|
+
] })
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
);
|
|
69
83
|
}
|
|
70
84
|
export {
|
|
71
85
|
RevenueKpis
|
|
@@ -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
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-kpis.tsx"],"sourcesContent":["import { ADMIN_NAME } from '../_shell/admin-brand';\nimport type { ReactNode } from 'react';\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/**\n * The headline figures, in two groups that are never added together: what\n * merchants sold (`merchant`), and what Olive itself earns from plans (`olive`).\n */\nexport function RevenueKpis({\n part,\n extra,\n data,\n money,\n money2,\n currency,\n focus,\n gmvSpark,\n}: {\n part: 'merchant' | 'olive';\n /** More cards for the merchant group, in the same row. */\n extra?: ReactNode;\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\n aria-label={part === 'olive' ? 'What Olive earns' : 'What merchants sold'}\n className=\"adm-grid\"\n >\n {part === 'olive' && (\n <>\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 </>\n )}\n {part === 'merchant' && (\n <>\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 {extra}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"AAoDQ,mBAMM,KANN;AApDR,SAAS,kBAAkB;AAG3B,SAAS,SAAS,QAAQ,YAAY;AACtC,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAMtC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAaG;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;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,UAAU,qBAAqB;AAAA,MACpD,WAAU;AAAA,MAET;AAAA,iBAAS,WACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAM,qDAAqD,UAAU,oBAAoB,KAAK,YAAY,KAAK;AAAA,cAC/G,KACE,UACI,GAAG,QAAQ,OAAO,UAAU,mBAAmB,UAAU,mBAAmB,CAAC,sBAC7E;AAAA;AAAA,UAER,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,IAE9C;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,wBAAwB;AAAA;AAAA,UACzC,GACF;AAAA,UACA,oBAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE,UACE,oBAAC,WAAQ,OAAO,UAAU,MAAM,QAAQ,QAAQ,IAEhD;AAAA,cAGJ,MAAK;AAAA,cACL,KAAK,UAAU,eAAe;AAAA;AAAA,UAChC,GACF;AAAA,WACE;AAAA,QAED,SAAS,cACR,iCACF;AAAA,8BAAC,UAAO,OAAO,GAAG,WAAU,cAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,OACE;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,QAAQ,CAAC,MAAM,EAAE;AAAA,kBACjB,OAAM;AAAA;AAAA,cACR;AAAA,cAEF,MAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM,qCAAqC,UAAU,uCAAuC,UAAU;AAAA,cACtG,OAAO,QAAQ,WAAW;AAAA,cAC1B,YAAY,KAAK,MAAM;AAAA;AAAA,UACzB,GACF;AAAA,UACK;AAAA,WACH;AAAA;AAAA;AAAA,EAEJ;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 { MerchantHeadline } from "./merchant-headline";
|
|
9
10
|
import { MethodCards } from "./method-cards";
|
|
10
11
|
import { HealthPanel } from "./health-panel";
|
|
11
12
|
import { PlanMixPanel } from "./plan-mix-panel";
|
|
@@ -34,37 +35,25 @@ function RevenueOverview({
|
|
|
34
35
|
const recurringTracked = data.recurring.tracked !== false;
|
|
35
36
|
const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);
|
|
36
37
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
38
|
+
/* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Money merchants earned" }),
|
|
39
|
+
/* @__PURE__ */ jsx("p", { className: ledgerStyles.sectionNote, children: "What customers paid to merchants. This is the merchants' money, not Olive's own income." }),
|
|
37
40
|
/* @__PURE__ */ jsx(
|
|
38
41
|
RevenueKpis,
|
|
39
42
|
{
|
|
43
|
+
part: "merchant",
|
|
40
44
|
data,
|
|
41
45
|
money,
|
|
42
46
|
money2,
|
|
43
47
|
currency,
|
|
44
48
|
focus,
|
|
45
|
-
gmvSpark: gmvSeries.map((p) => p.value)
|
|
49
|
+
gmvSpark: gmvSeries.map((p) => p.value),
|
|
50
|
+
extra: ledger && focus ? /* @__PURE__ */ jsx(MerchantHeadline, { ledger, focus }) : null
|
|
46
51
|
}
|
|
47
52
|
),
|
|
48
|
-
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
49
|
-
StatCard,
|
|
50
|
-
{
|
|
51
|
-
label: "Platform fees earned",
|
|
52
|
-
value: /* @__PURE__ */ jsx(
|
|
53
|
-
MoneyLines,
|
|
54
|
-
{
|
|
55
|
-
rows: platformFees,
|
|
56
|
-
currency: "",
|
|
57
|
-
amount: (r) => r.amount,
|
|
58
|
-
decimals: 2
|
|
59
|
-
}
|
|
60
|
-
),
|
|
61
|
-
hint: "What Olive keeps from payouts, settlements and subscriptions."
|
|
62
|
-
}
|
|
63
|
-
) }) }),
|
|
64
53
|
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
|
|
65
54
|
Panel,
|
|
66
55
|
{
|
|
67
|
-
title: "
|
|
56
|
+
title: "Merchant sales over time",
|
|
68
57
|
caption: gmvCaption,
|
|
69
58
|
height: "calc(var(--adm-panel-h) + 32px)",
|
|
70
59
|
children: focus ? /* @__PURE__ */ jsx("div", { className: styles.flatChart, children: /* @__PURE__ */ jsx(Tinted, { tone: "money", children: /* @__PURE__ */ jsx(
|
|
@@ -81,11 +70,7 @@ function RevenueOverview({
|
|
|
81
70
|
}
|
|
82
71
|
) }) }),
|
|
83
72
|
ledger && focus ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
-
/* @__PURE__ */ jsx("
|
|
85
|
-
/* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
|
|
86
|
-
"Money customers paid to merchants, by way of paying. ",
|
|
87
|
-
gmvCaption
|
|
88
|
-
] }),
|
|
73
|
+
/* @__PURE__ */ jsx("h3", { className: ledgerStyles.subTitle, children: "How customers paid" }),
|
|
89
74
|
/* @__PURE__ */ jsx(
|
|
90
75
|
MethodCards,
|
|
91
76
|
{
|
|
@@ -95,34 +80,67 @@ function RevenueOverview({
|
|
|
95
80
|
}
|
|
96
81
|
)
|
|
97
82
|
] }) : 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 }) }) }),
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
planBreakdown: data.recurring.planBreakdown,
|
|
111
|
-
money,
|
|
112
|
-
caption: recurringCaption
|
|
113
|
-
}
|
|
114
|
-
) })
|
|
83
|
+
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(
|
|
84
|
+
TopTenantsPanel,
|
|
85
|
+
{
|
|
86
|
+
tenants: gmv?.topTenants ?? [],
|
|
87
|
+
lifetime: gmv?.lifetime ?? 0,
|
|
88
|
+
focus
|
|
89
|
+
}
|
|
90
|
+
) }) }),
|
|
91
|
+
/* @__PURE__ */ jsx("h2", { className: ledgerStyles.sectionTitle, children: "Money Olive earns" }),
|
|
92
|
+
/* @__PURE__ */ jsxs("p", { className: ledgerStyles.sectionNote, children: [
|
|
93
|
+
"What merchants pay Olive: their plan payments, and the fees Olive keeps.",
|
|
94
|
+
recurringTracked ? "" : " Plan income is not set up on this box yet."
|
|
115
95
|
] }),
|
|
116
|
-
/* @__PURE__ */
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
96
|
+
/* @__PURE__ */ jsx(
|
|
97
|
+
RevenueKpis,
|
|
98
|
+
{
|
|
99
|
+
part: "olive",
|
|
100
|
+
data,
|
|
101
|
+
money,
|
|
102
|
+
money2,
|
|
103
|
+
currency,
|
|
104
|
+
focus,
|
|
105
|
+
gmvSpark: []
|
|
106
|
+
}
|
|
107
|
+
),
|
|
108
|
+
platformFees && /* @__PURE__ */ jsx("section", { "aria-label": "Platform fees", className: "adm-grid", children: /* @__PURE__ */ jsx(Reveal, { index: 0, className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
109
|
+
StatCard,
|
|
110
|
+
{
|
|
111
|
+
label: "Fees Olive keeps",
|
|
112
|
+
value: /* @__PURE__ */ jsx(
|
|
113
|
+
MoneyLines,
|
|
114
|
+
{
|
|
115
|
+
rows: platformFees,
|
|
116
|
+
currency: "",
|
|
117
|
+
amount: (r) => r.amount,
|
|
118
|
+
decimals: 2
|
|
119
|
+
}
|
|
120
|
+
),
|
|
121
|
+
hint: "What Olive keeps from payouts, settlements and plan payments, after costs."
|
|
122
|
+
}
|
|
123
|
+
) }) }),
|
|
124
|
+
recurringTracked && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
125
|
+
/* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
126
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(
|
|
127
|
+
HealthPanel,
|
|
128
|
+
{
|
|
129
|
+
health,
|
|
130
|
+
money,
|
|
131
|
+
caption: recurringCaption
|
|
132
|
+
}
|
|
133
|
+
) }),
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: /* @__PURE__ */ jsx(
|
|
135
|
+
PlanMixPanel,
|
|
136
|
+
{
|
|
137
|
+
planBreakdown: data.recurring.planBreakdown,
|
|
138
|
+
money,
|
|
139
|
+
caption: recurringCaption
|
|
140
|
+
}
|
|
141
|
+
) })
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsx("div", { className: "adm-grid", children: /* @__PURE__ */ jsx("div", { className: "adm-span-12", children: /* @__PURE__ */ jsx(SubscriptionsPanel, { recurring: data.recurring, span }) }) })
|
|
126
144
|
] })
|
|
127
145
|
] });
|
|
128
146
|
}
|
|
@@ -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 { 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
|
|
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 { MerchantHeadline } from './merchant-headline';\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 <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, settlements 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 <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\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":[]}
|