@withpotter/pages-admin 0.13.0-next.104 → 0.13.0-next.106
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/_overview/activation-panel.js +6 -6
- package/dist/admin/_overview/activation-panel.js.map +1 -1
- package/dist/admin/_overview/attention-groups.js +8 -8
- package/dist/admin/_overview/attention-groups.js.map +1 -1
- package/dist/admin/_overview/attention-panel.js +3 -3
- package/dist/admin/_overview/attention-panel.js.map +1 -1
- package/dist/admin/_overview/control-room.js +9 -9
- package/dist/admin/_overview/control-room.js.map +1 -1
- package/dist/admin/_overview/kpi-row.js +7 -7
- package/dist/admin/_overview/kpi-row.js.map +1 -1
- package/dist/admin/_overview/leaders-panel.js +1 -1
- package/dist/admin/_overview/leaders-panel.js.map +1 -1
- package/dist/admin/_overview/portfolio-panel.js +5 -5
- package/dist/admin/_overview/portfolio-panel.js.map +1 -1
- package/dist/admin/_overview/position-panel.js +6 -6
- package/dist/admin/_overview/position-panel.js.map +1 -1
- package/dist/admin/_ui/data-table.js +1 -1
- package/dist/admin/_ui/data-table.js.map +1 -1
- package/dist/admin/_ui/list-card.js +1 -1
- package/dist/admin/_ui/list-card.js.map +1 -1
- package/dist/admin/_ui/trend.js +2 -2
- package/dist/admin/_ui/trend.js.map +1 -1
- package/dist/admin/page.js +6 -6
- package/dist/admin/page.js.map +1 -1
- package/dist/admin/payout-tiers/_tier-dialog.js +31 -21
- package/dist/admin/payout-tiers/_tier-dialog.js.map +1 -1
- package/dist/admin/payout-tiers/requirement-labels.js +70 -0
- package/dist/admin/payout-tiers/requirement-labels.js.map +1 -0
- package/dist/admin/payout-tiers/tier-preview-result.js +10 -9
- package/dist/admin/payout-tiers/tier-preview-result.js.map +1 -1
- package/dist/admin/payout-tiers/tiers-table.js +6 -7
- package/dist/admin/payout-tiers/tiers-table.js.map +1 -1
- package/dist/admin/payout-tiers/tiers-workspace.js +2 -2
- package/dist/admin/payout-tiers/tiers-workspace.js.map +1 -1
- package/dist/admin/revenue/cohort-panel.js +1 -1
- package/dist/admin/revenue/cohort-panel.js.map +1 -1
- package/dist/admin/revenue/plan-mix-panel.js +2 -2
- package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
- package/dist/admin/revenue/revenue-kpis.js +3 -3
- package/dist/admin/revenue/revenue-kpis.js.map +1 -1
- package/dist/admin/revenue/revenue-overview.js +1 -1
- package/dist/admin/revenue/revenue-overview.js.map +1 -1
- package/dist/admin/revenue/subscriptions-view.js +3 -3
- package/dist/admin/revenue/subscriptions-view.js.map +1 -1
- package/dist/admin/revenue/top-tenants-panel.js +3 -3
- package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
- package/package.json +4 -4
|
@@ -6,11 +6,11 @@ function ActivationPanel({
|
|
|
6
6
|
activation
|
|
7
7
|
}) {
|
|
8
8
|
if (!activation) {
|
|
9
|
-
return /* @__PURE__ */ jsx(Panel, { title: "
|
|
9
|
+
return /* @__PURE__ */ jsx(Panel, { title: "From sign-up to first sale", caption: "All time", height: "auto", children: /* @__PURE__ */ jsx(AdmLoading, { label: "Loading sign-up progress\u2026", size: "sm" }) });
|
|
10
10
|
}
|
|
11
11
|
const stages = activation.funnel;
|
|
12
12
|
const top = stages[0]?.count || 1;
|
|
13
|
-
return /* @__PURE__ */ jsxs(Panel, { title: "
|
|
13
|
+
return /* @__PURE__ */ jsxs(Panel, { title: "From sign-up to first sale", caption: "All time", height: "auto", children: [
|
|
14
14
|
/* @__PURE__ */ jsx("ul", { className: styles.bars, children: stages.map((s) => /* @__PURE__ */ jsxs("li", { children: [
|
|
15
15
|
/* @__PURE__ */ jsxs("div", { className: styles.barLabel, children: [
|
|
16
16
|
/* @__PURE__ */ jsx("span", { children: s.stage }),
|
|
@@ -26,24 +26,24 @@ function ActivationPanel({
|
|
|
26
26
|
}
|
|
27
27
|
) })
|
|
28
28
|
] }, s.stage)) }),
|
|
29
|
-
/* @__PURE__ */ jsx("div", { className: styles.subhead, children: "
|
|
29
|
+
/* @__PURE__ */ jsx("div", { className: styles.subhead, children: "How many carry on" }),
|
|
30
30
|
/* @__PURE__ */ jsxs("ul", { className: styles.bars, children: [
|
|
31
31
|
/* @__PURE__ */ jsxs("li", { className: styles.barLabel, children: [
|
|
32
|
-
/* @__PURE__ */ jsx("span", { children: "Signed up
|
|
32
|
+
/* @__PURE__ */ jsx("span", { children: "Signed up, then became active" }),
|
|
33
33
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
34
34
|
activation.activationRatePct,
|
|
35
35
|
"%"
|
|
36
36
|
] })
|
|
37
37
|
] }),
|
|
38
38
|
/* @__PURE__ */ jsxs("li", { className: styles.barLabel, children: [
|
|
39
|
-
/* @__PURE__ */ jsx("span", { children: "Signed up
|
|
39
|
+
/* @__PURE__ */ jsx("span", { children: "Signed up, then took a first order" }),
|
|
40
40
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
41
41
|
activation.orderConversionPct,
|
|
42
42
|
"%"
|
|
43
43
|
] })
|
|
44
44
|
] }),
|
|
45
45
|
/* @__PURE__ */ jsxs("li", { className: styles.barLabel, children: [
|
|
46
|
-
/* @__PURE__ */ jsx("span", { children: "
|
|
46
|
+
/* @__PURE__ */ jsx("span", { children: "Typical days to a first order" }),
|
|
47
47
|
/* @__PURE__ */ jsx("strong", { children: activation.medianDaysToFirstOrder == null ? "\u2013" : activation.medianDaysToFirstOrder })
|
|
48
48
|
] })
|
|
49
49
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/activation-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { AdmLoading, Panel } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/**\n * Signed up → Activated → First order → First payout, lifetime (not\n * range-scoped — a funnel this shallow needs the whole base to read).\n */\nexport function ActivationPanel({\n activation,\n}: {\n activation: PlatformCockpit['activation'];\n}) {\n if (!activation) {\n return (\n <Panel title=\"
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/activation-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { AdmLoading, Panel } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/**\n * Signed up → Activated → First order → First payout, lifetime (not\n * range-scoped — a funnel this shallow needs the whole base to read).\n */\nexport function ActivationPanel({\n activation,\n}: {\n activation: PlatformCockpit['activation'];\n}) {\n if (!activation) {\n return (\n <Panel title=\"From sign-up to first sale\" caption=\"All time\" height=\"auto\">\n <AdmLoading label=\"Loading sign-up progress…\" size=\"sm\" />\n </Panel>\n );\n }\n\n const stages = activation.funnel;\n const top = stages[0]?.count || 1;\n\n return (\n <Panel title=\"From sign-up to first sale\" caption=\"All time\" height=\"auto\">\n <ul className={styles.bars}>\n {stages.map((s) => (\n <li key={s.stage}>\n <div className={styles.barLabel}>\n <span>{s.stage}</span>\n <strong>{integer.format(s.count)}</strong>\n </div>\n <div className={styles.track}>\n <span\n style={{\n width: `${Math.max((s.count / top) * 100, 2)}%`,\n background: 'var(--accent-primary)',\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n\n <div className={styles.subhead}>How many carry on</div>\n <ul className={styles.bars}>\n <li className={styles.barLabel}>\n <span>Signed up, then became active</span>\n <strong>{activation.activationRatePct}%</strong>\n </li>\n <li className={styles.barLabel}>\n <span>Signed up, then took a first order</span>\n <strong>{activation.orderConversionPct}%</strong>\n </li>\n <li className={styles.barLabel}>\n <span>Typical days to a first order</span>\n <strong>\n {activation.medianDaysToFirstOrder == null\n ? '–'\n : activation.medianDaysToFirstOrder}\n </strong>\n </li>\n </ul>\n </Panel>\n );\n}\n"],"mappings":"AAkBQ,cAaI,YAbJ;AAjBR,SAAS,YAAY,aAAa;AAClC,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa;AAM/B,SAAS,gBAAgB;AAAA,EAC9B;AACF,GAEG;AACD,MAAI,CAAC,YAAY;AACf,WACE,oBAAC,SAAM,OAAM,8BAA6B,SAAQ,YAAW,QAAO,QAClE,8BAAC,cAAW,OAAM,kCAA4B,MAAK,MAAK,GAC1D;AAAA,EAEJ;AAEA,QAAM,SAAS,WAAW;AAC1B,QAAM,MAAM,OAAO,CAAC,GAAG,SAAS;AAEhC,SACE,qBAAC,SAAM,OAAM,8BAA6B,SAAQ,YAAW,QAAO,QAClE;AAAA,wBAAC,QAAG,WAAW,OAAO,MACnB,iBAAO,IAAI,CAAC,MACX,qBAAC,QACC;AAAA,2BAAC,SAAI,WAAW,OAAO,UACrB;AAAA,4BAAC,UAAM,YAAE,OAAM;AAAA,QACf,oBAAC,YAAQ,kBAAQ,OAAO,EAAE,KAAK,GAAE;AAAA,SACnC;AAAA,MACA,oBAAC,SAAI,WAAW,OAAO,OACrB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,YACL,OAAO,GAAG,KAAK,IAAK,EAAE,QAAQ,MAAO,KAAK,CAAC,CAAC;AAAA,YAC5C,YAAY;AAAA,UACd;AAAA;AAAA,MACF,GACF;AAAA,SAZO,EAAE,KAaX,CACD,GACH;AAAA,IAEA,oBAAC,SAAI,WAAW,OAAO,SAAS,+BAAiB;AAAA,IACjD,qBAAC,QAAG,WAAW,OAAO,MACpB;AAAA,2BAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,2CAA6B;AAAA,QACnC,qBAAC,YAAQ;AAAA,qBAAW;AAAA,UAAkB;AAAA,WAAC;AAAA,SACzC;AAAA,MACA,qBAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,gDAAkC;AAAA,QACxC,qBAAC,YAAQ;AAAA,qBAAW;AAAA,UAAmB;AAAA,WAAC;AAAA,SAC1C;AAAA,MACA,qBAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,2CAA6B;AAAA,QACnC,oBAAC,YACE,qBAAW,0BAA0B,OAClC,WACA,WAAW,wBACjB;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -13,13 +13,13 @@ function attentionGroups(a) {
|
|
|
13
13
|
tenantId: s.tenantId,
|
|
14
14
|
href: tenant(s.tenantId),
|
|
15
15
|
title: s.tenantName,
|
|
16
|
-
detail: "
|
|
16
|
+
detail: "Payout failed",
|
|
17
17
|
value: inCurrency(s.netAmount, s.currency)
|
|
18
18
|
}))
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
key: "invoices",
|
|
22
|
-
title: "Overdue",
|
|
22
|
+
title: "Overdue bills",
|
|
23
23
|
count: a.overdueInvoices.count,
|
|
24
24
|
note: overdueOutstanding(a.overdueInvoices) ?? void 0,
|
|
25
25
|
items: a.overdueInvoices.sample.map((i) => ({
|
|
@@ -28,13 +28,13 @@ function attentionGroups(a) {
|
|
|
28
28
|
tenantId: i.tenantId,
|
|
29
29
|
href: tenant(i.tenantId),
|
|
30
30
|
title: i.tenantName,
|
|
31
|
-
detail: `
|
|
31
|
+
detail: `Bill ${i.invoiceNumber}`,
|
|
32
32
|
value: inCurrency(i.amountDue, i.currency)
|
|
33
33
|
}))
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
key: "review",
|
|
37
|
-
title: "
|
|
37
|
+
title: "Merchants to review",
|
|
38
38
|
count: a.needsReview.length,
|
|
39
39
|
items: a.needsReview.map((t) => ({
|
|
40
40
|
id: t.id,
|
|
@@ -43,12 +43,12 @@ function attentionGroups(a) {
|
|
|
43
43
|
href: tenant(t.id),
|
|
44
44
|
title: t.businessName,
|
|
45
45
|
detail: t.slug,
|
|
46
|
-
value: t.status
|
|
46
|
+
value: t.status.replace(/_/g, " ")
|
|
47
47
|
}))
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
key: "trials",
|
|
51
|
-
title: "
|
|
51
|
+
title: "Free trials ending",
|
|
52
52
|
count: a.trialsEndingSoon.length,
|
|
53
53
|
items: a.trialsEndingSoon.map((t) => ({
|
|
54
54
|
id: t.id,
|
|
@@ -57,12 +57,12 @@ function attentionGroups(a) {
|
|
|
57
57
|
href: tenant(t.id),
|
|
58
58
|
title: t.businessName,
|
|
59
59
|
detail: t.slug,
|
|
60
|
-
value: t.daysLeft === 0 ? "Today" : `${t.daysLeft}
|
|
60
|
+
value: t.daysLeft === 0 ? "Today" : `${t.daysLeft} ${t.daysLeft === 1 ? "day" : "days"} left`
|
|
61
61
|
}))
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
key: "dormant",
|
|
65
|
-
title: "
|
|
65
|
+
title: "Inactive",
|
|
66
66
|
count: a.dormantTenants.length,
|
|
67
67
|
note: "No orders or sign-ins for 60+ days.",
|
|
68
68
|
items: a.dormantTenants.map((t) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/attention-groups.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { logoOf } from '../commerce/merchant-meta';\nimport { inCurrency, overdueOutstanding } from './overdue-outstanding';\n\nexport interface AttentionItem {\n id: string;\n /** The merchant the row is about, for views that open it in place. */\n tenantId: string;\n href: string;\n title: string;\n detail: string;\n value: string;\n /** The merchant's logo, once the cockpit sends one; initials until then. */\n logo?: string | null;\n}\n\nexport interface AttentionGroup {\n key: string;\n title: string;\n count: number;\n note?: string;\n items: AttentionItem[];\n}\n\nconst tenant = (id: string) => `/admin/tenants/${id}`;\n\n/** The cockpit's exceptions as uniform groups, most urgent kinds first. */\nexport function attentionGroups(\n a: PlatformCockpit['attention'],\n): AttentionGroup[] {\n return [\n {\n key: 'settlements',\n title: 'Failed payouts',\n count: a.failedSettlements.count,\n items: a.failedSettlements.sample.map((s) => ({\n id: s.id,\n logo: logoOf(s),\n tenantId: s.tenantId,\n href: tenant(s.tenantId),\n title: s.tenantName,\n detail: '
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/attention-groups.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { logoOf } from '../commerce/merchant-meta';\nimport { inCurrency, overdueOutstanding } from './overdue-outstanding';\n\nexport interface AttentionItem {\n id: string;\n /** The merchant the row is about, for views that open it in place. */\n tenantId: string;\n href: string;\n title: string;\n detail: string;\n value: string;\n /** The merchant's logo, once the cockpit sends one; initials until then. */\n logo?: string | null;\n}\n\nexport interface AttentionGroup {\n key: string;\n title: string;\n count: number;\n note?: string;\n items: AttentionItem[];\n}\n\nconst tenant = (id: string) => `/admin/tenants/${id}`;\n\n/** The cockpit's exceptions as uniform groups, most urgent kinds first. */\nexport function attentionGroups(\n a: PlatformCockpit['attention'],\n): AttentionGroup[] {\n return [\n {\n key: 'settlements',\n title: 'Failed payouts',\n count: a.failedSettlements.count,\n items: a.failedSettlements.sample.map((s) => ({\n id: s.id,\n logo: logoOf(s),\n tenantId: s.tenantId,\n href: tenant(s.tenantId),\n title: s.tenantName,\n detail: 'Payout failed',\n value: inCurrency(s.netAmount, s.currency),\n })),\n },\n {\n key: 'invoices',\n title: 'Overdue bills',\n count: a.overdueInvoices.count,\n note: overdueOutstanding(a.overdueInvoices) ?? undefined,\n items: a.overdueInvoices.sample.map((i) => ({\n id: i.id,\n logo: logoOf(i),\n tenantId: i.tenantId,\n href: tenant(i.tenantId),\n title: i.tenantName,\n detail: `Bill ${i.invoiceNumber}`,\n value: inCurrency(i.amountDue, i.currency),\n })),\n },\n {\n key: 'review',\n title: 'Merchants to review',\n count: a.needsReview.length,\n items: a.needsReview.map((t) => ({\n id: t.id,\n logo: logoOf(t),\n tenantId: t.id,\n href: tenant(t.id),\n title: t.businessName,\n detail: t.slug,\n value: t.status.replace(/_/g, ' '),\n })),\n },\n {\n key: 'trials',\n title: 'Free trials ending',\n count: a.trialsEndingSoon.length,\n items: a.trialsEndingSoon.map((t) => ({\n id: t.id,\n logo: logoOf(t),\n tenantId: t.id,\n href: tenant(t.id),\n title: t.businessName,\n detail: t.slug,\n value: t.daysLeft === 0 ? 'Today' : `${t.daysLeft} ${t.daysLeft === 1 ? 'day' : 'days'} left`,\n })),\n },\n {\n key: 'dormant',\n title: 'Inactive',\n count: a.dormantTenants.length,\n note: 'No orders or sign-ins for 60+ days.',\n items: a.dormantTenants.map((t) => ({\n id: t.id,\n logo: logoOf(t),\n tenantId: t.id,\n href: tenant(t.id),\n title: t.businessName,\n detail: t.slug,\n value: '60d+',\n })),\n },\n ];\n}\n"],"mappings":"AACA,SAAS,cAAc;AACvB,SAAS,YAAY,0BAA0B;AAsB/C,MAAM,SAAS,CAAC,OAAe,kBAAkB,EAAE;AAG5C,SAAS,gBACd,GACkB;AAClB,SAAO;AAAA,IACL;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO,EAAE,kBAAkB;AAAA,MAC3B,OAAO,EAAE,kBAAkB,OAAO,IAAI,CAAC,OAAO;AAAA,QAC5C,IAAI,EAAE;AAAA,QACN,MAAM,OAAO,CAAC;AAAA,QACd,UAAU,EAAE;AAAA,QACZ,MAAM,OAAO,EAAE,QAAQ;AAAA,QACvB,OAAO,EAAE;AAAA,QACT,QAAQ;AAAA,QACR,OAAO,WAAW,EAAE,WAAW,EAAE,QAAQ;AAAA,MAC3C,EAAE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO,EAAE,gBAAgB;AAAA,MACzB,MAAM,mBAAmB,EAAE,eAAe,KAAK;AAAA,MAC/C,OAAO,EAAE,gBAAgB,OAAO,IAAI,CAAC,OAAO;AAAA,QAC1C,IAAI,EAAE;AAAA,QACN,MAAM,OAAO,CAAC;AAAA,QACd,UAAU,EAAE;AAAA,QACZ,MAAM,OAAO,EAAE,QAAQ;AAAA,QACvB,OAAO,EAAE;AAAA,QACT,QAAQ,QAAQ,EAAE,aAAa;AAAA,QAC/B,OAAO,WAAW,EAAE,WAAW,EAAE,QAAQ;AAAA,MAC3C,EAAE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO,EAAE,YAAY;AAAA,MACrB,OAAO,EAAE,YAAY,IAAI,CAAC,OAAO;AAAA,QAC/B,IAAI,EAAE;AAAA,QACN,MAAM,OAAO,CAAC;AAAA,QACd,UAAU,EAAE;AAAA,QACZ,MAAM,OAAO,EAAE,EAAE;AAAA,QACjB,OAAO,EAAE;AAAA,QACT,QAAQ,EAAE;AAAA,QACV,OAAO,EAAE,OAAO,QAAQ,MAAM,GAAG;AAAA,MACnC,EAAE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO,EAAE,iBAAiB;AAAA,MAC1B,OAAO,EAAE,iBAAiB,IAAI,CAAC,OAAO;AAAA,QACpC,IAAI,EAAE;AAAA,QACN,MAAM,OAAO,CAAC;AAAA,QACd,UAAU,EAAE;AAAA,QACZ,MAAM,OAAO,EAAE,EAAE;AAAA,QACjB,OAAO,EAAE;AAAA,QACT,QAAQ,EAAE;AAAA,QACV,OAAO,EAAE,aAAa,IAAI,UAAU,GAAG,EAAE,QAAQ,IAAI,EAAE,aAAa,IAAI,QAAQ,MAAM;AAAA,MACxF,EAAE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO,EAAE,eAAe;AAAA,MACxB,MAAM;AAAA,MACN,OAAO,EAAE,eAAe,IAAI,CAAC,OAAO;AAAA,QAClC,IAAI,EAAE;AAAA,QACN,MAAM,OAAO,CAAC;AAAA,QACd,UAAU,EAAE;AAAA,QACZ,MAAM,OAAO,EAAE,EAAE;AAAA,QACjB,OAAO,EAAE;AAAA,QACT,QAAQ,EAAE;AAAA,QACV,OAAO;AAAA,MACT,EAAE;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
|
|
@@ -16,14 +16,14 @@ function AttentionPanel({
|
|
|
16
16
|
const active = groups.find((g) => g.key === picked) ?? [...groups].sort((a, b) => b.count - a.count)[0];
|
|
17
17
|
const unknown = attention.unavailable ?? [];
|
|
18
18
|
return /* @__PURE__ */ jsxs(Panel, { title: "Needs attention", count: total, flush: true, children: [
|
|
19
|
-
unknown.length > 0 && /* @__PURE__ */ jsx("p", { className: styles.note, children: "Some
|
|
20
|
-
!active ? /* @__PURE__ */ jsx("div", { className: styles.clear, children: /* @__PURE__ */ jsx(EmptyNote, { children: unknown.length > 0 ? "Nothing to show right now." : "
|
|
19
|
+
unknown.length > 0 && /* @__PURE__ */ jsx("p", { className: styles.note, children: "Some items couldn't be loaded, so this list may be missing things." }),
|
|
20
|
+
!active ? /* @__PURE__ */ jsx("div", { className: styles.clear, children: /* @__PURE__ */ jsx(EmptyNote, { children: unknown.length > 0 ? "Nothing to show right now." : "Nothing needs attention in this period." }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
21
21
|
/* @__PURE__ */ jsx(
|
|
22
22
|
"div",
|
|
23
23
|
{
|
|
24
24
|
className: styles.filters,
|
|
25
25
|
role: "group",
|
|
26
|
-
"aria-label": "Show
|
|
26
|
+
"aria-label": "Show one kind of item",
|
|
27
27
|
children: groups.map((g) => /* @__PURE__ */ jsxs(
|
|
28
28
|
"button",
|
|
29
29
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/attention-panel.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { Panel } from '../_ui';\nimport { attentionGroups } from './attention-groups';\nimport styles from './attention.module.css';\n\n/**\n * Every exception in one card: a filter per kind, the rows scrolling inside.\n * The busiest kind is open first; empty kinds are not offered at all.\n */\nexport function AttentionPanel({\n attention,\n}: {\n attention: PlatformCockpit['attention'];\n}) {\n const groups = attentionGroups(attention).filter((g) => g.count);\n const total = groups.reduce((sum, g) => sum + g.count, 0);\n const [picked, setPicked] = useState<string | null>(null);\n const active =\n groups.find((g) => g.key === picked) ??\n [...groups].sort((a, b) => b.count - a.count)[0];\n\n const unknown = attention.unavailable ?? [];\n\n return (\n <Panel title=\"Needs attention\" count={total} flush>\n {unknown.length > 0 && (\n <p className={styles.note}>\n Some
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/attention-panel.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { EmptyNote } from '@withpotter/ui-kit';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { Panel } from '../_ui';\nimport { attentionGroups } from './attention-groups';\nimport styles from './attention.module.css';\n\n/**\n * Every exception in one card: a filter per kind, the rows scrolling inside.\n * The busiest kind is open first; empty kinds are not offered at all.\n */\nexport function AttentionPanel({\n attention,\n}: {\n attention: PlatformCockpit['attention'];\n}) {\n const groups = attentionGroups(attention).filter((g) => g.count);\n const total = groups.reduce((sum, g) => sum + g.count, 0);\n const [picked, setPicked] = useState<string | null>(null);\n const active =\n groups.find((g) => g.key === picked) ??\n [...groups].sort((a, b) => b.count - a.count)[0];\n\n const unknown = attention.unavailable ?? [];\n\n return (\n <Panel title=\"Needs attention\" count={total} flush>\n {unknown.length > 0 && (\n <p className={styles.note}>\n Some items couldn't be loaded, so this list may be missing things.\n </p>\n )}\n {!active ? (\n <div className={styles.clear}>\n <EmptyNote>\n {unknown.length > 0\n ? 'Nothing to show right now.'\n : 'Nothing needs attention in this period.'}\n </EmptyNote>\n </div>\n ) : (\n <>\n <div\n className={styles.filters}\n role=\"group\"\n aria-label=\"Show one kind of item\"\n >\n {groups.map((g) => (\n <button\n key={g.key}\n type=\"button\"\n aria-pressed={g.key === active.key}\n className={styles.filter}\n onClick={() => setPicked(g.key)}\n >\n {g.title}\n <span>{g.count}</span>\n </button>\n ))}\n </div>\n {active.note && <p className={styles.note}>{active.note}</p>}\n <ul className={styles.rows}>\n {active.items.map((item) => (\n <li key={item.id}>\n <Link href={item.href} className={styles.row}>\n {item.logo && (\n <EntityAvatar name={item.title} src={item.logo} size={32} />\n )}\n <span className={styles.rowMain}>\n <strong>{item.title}</strong>\n <small>{item.detail}</small>\n </span>\n <span className={styles.rowValue}>{item.value}</span>\n </Link>\n </li>\n ))}\n </ul>\n </>\n )}\n </Panel>\n );\n}\n"],"mappings":";AAgCQ,SAaA,UAbA,KAoBM,YApBN;AA9BR,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAE1B,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,uBAAuB;AAChC,OAAO,YAAY;AAMZ,SAAS,eAAe;AAAA,EAC7B;AACF,GAEG;AACD,QAAM,SAAS,gBAAgB,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK;AAC/D,QAAM,QAAQ,OAAO,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AACxD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AACxD,QAAM,SACJ,OAAO,KAAK,CAAC,MAAM,EAAE,QAAQ,MAAM,KACnC,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAEjD,QAAM,UAAU,UAAU,eAAe,CAAC;AAE1C,SACE,qBAAC,SAAM,OAAM,mBAAkB,OAAO,OAAO,OAAK,MAC/C;AAAA,YAAQ,SAAS,KAChB,oBAAC,OAAE,WAAW,OAAO,MAAM,gFAE3B;AAAA,IAED,CAAC,SACA,oBAAC,SAAI,WAAW,OAAO,OACrB,8BAAC,aACE,kBAAQ,SAAS,IACd,+BACA,2CACN,GACF,IAEA,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,OAAO;AAAA,UAClB,MAAK;AAAA,UACL,cAAW;AAAA,UAEV,iBAAO,IAAI,CAAC,MACX;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,gBAAc,EAAE,QAAQ,OAAO;AAAA,cAC/B,WAAW,OAAO;AAAA,cAClB,SAAS,MAAM,UAAU,EAAE,GAAG;AAAA,cAE7B;AAAA,kBAAE;AAAA,gBACH,oBAAC,UAAM,YAAE,OAAM;AAAA;AAAA;AAAA,YAPV,EAAE;AAAA,UAQT,CACD;AAAA;AAAA,MACH;AAAA,MACC,OAAO,QAAQ,oBAAC,OAAE,WAAW,OAAO,MAAO,iBAAO,MAAK;AAAA,MACxD,oBAAC,QAAG,WAAW,OAAO,MACnB,iBAAO,MAAM,IAAI,CAAC,SACjB,oBAAC,QACC,+BAAC,QAAK,MAAM,KAAK,MAAM,WAAW,OAAO,KACtC;AAAA,aAAK,QACJ,oBAAC,gBAAa,MAAM,KAAK,OAAO,KAAK,KAAK,MAAM,MAAM,IAAI;AAAA,QAE5D,qBAAC,UAAK,WAAW,OAAO,SACtB;AAAA,8BAAC,YAAQ,eAAK,OAAM;AAAA,UACpB,oBAAC,WAAO,eAAK,QAAO;AAAA,WACtB;AAAA,QACA,oBAAC,UAAK,WAAW,OAAO,UAAW,eAAK,OAAM;AAAA,SAChD,KAVO,KAAK,EAWd,CACD,GACH;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -13,9 +13,9 @@ function VerificationCard() {
|
|
|
13
13
|
StatCard,
|
|
14
14
|
{
|
|
15
15
|
compact: true,
|
|
16
|
-
label: "
|
|
16
|
+
label: "Identity checks to review",
|
|
17
17
|
value: waiting === void 0 ? "\u2013" : /* @__PURE__ */ jsx(CountUp, { value: waiting }),
|
|
18
|
-
sub: "
|
|
18
|
+
sub: "Merchants waiting for a decision",
|
|
19
19
|
href: "/admin/verification",
|
|
20
20
|
alert: !!waiting
|
|
21
21
|
}
|
|
@@ -28,9 +28,9 @@ function SubscriptionsCard() {
|
|
|
28
28
|
StatCard,
|
|
29
29
|
{
|
|
30
30
|
compact: true,
|
|
31
|
-
label: "
|
|
31
|
+
label: "Plan income",
|
|
32
32
|
value: formatMoney(revenue.recurring.mrr, revenue.currency),
|
|
33
|
-
sub: "
|
|
33
|
+
sub: "What merchants pay for their plans each month",
|
|
34
34
|
href: "/admin/revenue"
|
|
35
35
|
}
|
|
36
36
|
);
|
|
@@ -46,14 +46,14 @@ function ControlRoom({
|
|
|
46
46
|
const overdueUnknown = !!attention.unavailable?.includes("overdueInvoices");
|
|
47
47
|
const withdrawals = useModuleOn("withdrawals");
|
|
48
48
|
const verification = useModuleOn("verification");
|
|
49
|
-
return /* @__PURE__ */ jsxs("section", { className: styles.room, "aria-label": "
|
|
49
|
+
return /* @__PURE__ */ jsxs("section", { className: styles.room, "aria-label": "Things that need action", children: [
|
|
50
50
|
/* @__PURE__ */ jsx(
|
|
51
51
|
StatCard,
|
|
52
52
|
{
|
|
53
53
|
compact: true,
|
|
54
54
|
label: "Failed payouts",
|
|
55
55
|
value: failedUnknown ? "\u2013" : /* @__PURE__ */ jsx(CountUp, { value: failed }),
|
|
56
|
-
sub: failedUnknown ? "Could not be loaded" : "
|
|
56
|
+
sub: failedUnknown ? "Could not be loaded" : "Payouts that did not reach the merchant",
|
|
57
57
|
href: withdrawals ? "/admin/withdrawals?tab=failed" : void 0,
|
|
58
58
|
alert: failedUnknown || failed > 0
|
|
59
59
|
}
|
|
@@ -62,9 +62,9 @@ function ControlRoom({
|
|
|
62
62
|
StatCard,
|
|
63
63
|
{
|
|
64
64
|
compact: true,
|
|
65
|
-
label: "Overdue
|
|
65
|
+
label: "Overdue bills",
|
|
66
66
|
value: overdueUnknown ? "\u2013" : /* @__PURE__ */ jsx(CountUp, { value: overdue }),
|
|
67
|
-
sub: overdueUnknown ? "Could not be loaded" : overdueOutstanding(attention.overdueInvoices, currency) ?? "
|
|
67
|
+
sub: overdueUnknown ? "Could not be loaded" : overdueOutstanding(attention.overdueInvoices, currency) ?? "Bills past their due date",
|
|
68
68
|
href: "/admin/revenue",
|
|
69
69
|
alert: overdueUnknown || overdue > 0
|
|
70
70
|
}
|
|
@@ -76,7 +76,7 @@ function ControlRoom({
|
|
|
76
76
|
compact: true,
|
|
77
77
|
label: "Verified wallets",
|
|
78
78
|
value: /* @__PURE__ */ jsx(CountUp, { value: totals.verifiedWallets }),
|
|
79
|
-
sub: "
|
|
79
|
+
sub: "Merchants who have passed their identity checks"
|
|
80
80
|
}
|
|
81
81
|
),
|
|
82
82
|
/* @__PURE__ */ jsx(SubscriptionsCard, {})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/control-room.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { CountUp } from '../_fx';\nimport { StatCard, formatMoney } from '../_ui';\nimport { usePlatformRevenue } from '../hooks';\nimport { useModuleOn } from '../_shell/modules-context';\nimport styles from './control-room.module.css';\nimport { useWaitingCases } from './use-waiting-cases';\nimport { overdueOutstanding } from './overdue-outstanding';\n\n/** KYC / KYB waiting count; mounted only where Verification is served. */\nfunction VerificationCard() {\n const waiting = useWaitingCases().data;\n\n return (\n <StatCard\n compact\n label=\"
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/control-room.tsx"],"sourcesContent":["'use client';\n\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { CountUp } from '../_fx';\nimport { StatCard, formatMoney } from '../_ui';\nimport { usePlatformRevenue } from '../hooks';\nimport { useModuleOn } from '../_shell/modules-context';\nimport styles from './control-room.module.css';\nimport { useWaitingCases } from './use-waiting-cases';\nimport { overdueOutstanding } from './overdue-outstanding';\n\n/** KYC / KYB waiting count; mounted only where Verification is served. */\nfunction VerificationCard() {\n const waiting = useWaitingCases().data;\n\n return (\n <StatCard\n compact\n label=\"Identity checks to review\"\n value={waiting === undefined ? '–' : <CountUp value={waiting} />}\n sub=\"Merchants waiting for a decision\"\n href=\"/admin/verification\"\n alert={!!waiting}\n />\n );\n}\n\n/** Monthly recurring revenue; shown only where billing feeds the figure, never as a made-up zero. */\nfunction SubscriptionsCard() {\n const revenue = usePlatformRevenue().data;\n\n if (!revenue?.recurring.tracked) return null;\n\n return (\n <StatCard\n compact\n label=\"Plan income\"\n value={formatMoney(revenue.recurring.mrr, revenue.currency)}\n sub=\"What merchants pay for their plans each month\"\n href=\"/admin/revenue\"\n />\n );\n}\n\n/**\n * The control room: every queue that needs a decision, then the revenue and\n * wallet figures, as one row of identical metric cards that wraps. A card opens its screen when this Box\n * serves that screen; a queue whose module is off is not shown.\n */\nexport function ControlRoom({\n attention,\n currency = '',\n totals,\n}: {\n attention: PlatformCockpit['attention'];\n /** Lifetime figures; a card whose figure the api does not send is left out. */\n totals?: PlatformCockpit['totals'];\n /** The currency filter's choice; narrows what overdue invoices owe. */\n currency?: string;\n}) {\n const failed = attention.failedSettlements.count;\n const overdue = attention.overdueInvoices.count;\n // A feed that failed to load is unknown, not zero.\n const failedUnknown = !!attention.unavailable?.includes('failedSettlements');\n const overdueUnknown = !!attention.unavailable?.includes('overdueInvoices');\n const withdrawals = useModuleOn('withdrawals');\n const verification = useModuleOn('verification');\n\n return (\n <section className={styles.room} aria-label=\"Things that need action\">\n <StatCard\n compact\n label=\"Failed payouts\"\n value={failedUnknown ? '–' : <CountUp value={failed} />}\n sub={\n failedUnknown\n ? 'Could not be loaded'\n : 'Payouts that did not reach the merchant'\n }\n href={withdrawals ? '/admin/withdrawals?tab=failed' : undefined}\n alert={failedUnknown || failed > 0}\n />\n <StatCard\n compact\n label=\"Overdue bills\"\n value={overdueUnknown ? '–' : <CountUp value={overdue} />}\n sub={\n overdueUnknown\n ? 'Could not be loaded'\n : (overdueOutstanding(attention.overdueInvoices, currency) ??\n 'Bills past their due date')\n }\n href=\"/admin/revenue\"\n alert={overdueUnknown || overdue > 0}\n />\n {verification && <VerificationCard />}\n {totals?.verifiedWallets !== undefined && (\n <StatCard\n compact\n label=\"Verified wallets\"\n value={<CountUp value={totals.verifiedWallets} />}\n sub=\"Merchants who have passed their identity checks\"\n />\n )}\n <SubscriptionsCard />\n </section>\n );\n}\n"],"mappings":";AAmB2C,cAkDvC,YAlDuC;AAhB3C,SAAS,eAAe;AACxB,SAAS,UAAU,mBAAmB;AACtC,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAC5B,OAAO,YAAY;AACnB,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AAGnC,SAAS,mBAAmB;AAC1B,QAAM,UAAU,gBAAgB,EAAE;AAElC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAO;AAAA,MACP,OAAM;AAAA,MACN,OAAO,YAAY,SAAY,WAAM,oBAAC,WAAQ,OAAO,SAAS;AAAA,MAC9D,KAAI;AAAA,MACJ,MAAK;AAAA,MACL,OAAO,CAAC,CAAC;AAAA;AAAA,EACX;AAEJ;AAGA,SAAS,oBAAoB;AAC3B,QAAM,UAAU,mBAAmB,EAAE;AAErC,MAAI,CAAC,SAAS,UAAU,QAAS,QAAO;AAExC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAO;AAAA,MACP,OAAM;AAAA,MACN,OAAO,YAAY,QAAQ,UAAU,KAAK,QAAQ,QAAQ;AAAA,MAC1D,KAAI;AAAA,MACJ,MAAK;AAAA;AAAA,EACP;AAEJ;AAOO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAMG;AACD,QAAM,SAAS,UAAU,kBAAkB;AAC3C,QAAM,UAAU,UAAU,gBAAgB;AAE1C,QAAM,gBAAgB,CAAC,CAAC,UAAU,aAAa,SAAS,mBAAmB;AAC3E,QAAM,iBAAiB,CAAC,CAAC,UAAU,aAAa,SAAS,iBAAiB;AAC1E,QAAM,cAAc,YAAY,aAAa;AAC7C,QAAM,eAAe,YAAY,cAAc;AAE/C,SACE,qBAAC,aAAQ,WAAW,OAAO,MAAM,cAAW,2BAC1C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,gBAAgB,WAAM,oBAAC,WAAQ,OAAO,QAAQ;AAAA,QACrD,KACE,gBACI,wBACA;AAAA,QAEN,MAAM,cAAc,kCAAkC;AAAA,QACtD,OAAO,iBAAiB,SAAS;AAAA;AAAA,IACnC;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,iBAAiB,WAAM,oBAAC,WAAQ,OAAO,SAAS;AAAA,QACvD,KACE,iBACI,wBACC,mBAAmB,UAAU,iBAAiB,QAAQ,KACvD;AAAA,QAEN,MAAK;AAAA,QACL,OAAO,kBAAkB,UAAU;AAAA;AAAA,IACrC;AAAA,IACC,gBAAgB,oBAAC,oBAAiB;AAAA,IAClC,QAAQ,oBAAoB,UAC3B;AAAA,MAAC;AAAA;AAAA,QACC,SAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,oBAAC,WAAQ,OAAO,OAAO,iBAAiB;AAAA,QAC/C,KAAI;AAAA;AAAA,IACN;AAAA,IAEF,oBAAC,qBAAkB;AAAA,KACrB;AAEJ;","names":[]}
|
|
@@ -8,17 +8,17 @@ function KpiRow({
|
|
|
8
8
|
compare,
|
|
9
9
|
caption
|
|
10
10
|
}) {
|
|
11
|
-
const delta = (kpi) => compare ? { pct: kpi.pct, caption: "vs
|
|
11
|
+
const delta = (kpi) => compare ? { pct: kpi.pct, caption: "vs the period before" } : void 0;
|
|
12
12
|
const gmvKpi = rowFor(data.kpis.gmvByCurrency, focus);
|
|
13
13
|
const gmvSpark = rowFor(
|
|
14
14
|
data.trend.performanceSeriesByCurrency,
|
|
15
15
|
focus
|
|
16
16
|
)?.series.map((p) => p.gmv);
|
|
17
|
-
return /* @__PURE__ */ jsxs("section", { "aria-label": "
|
|
17
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Key numbers", className: "adm-grid", children: [
|
|
18
18
|
/* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
19
19
|
StatCard,
|
|
20
20
|
{
|
|
21
|
-
label: "
|
|
21
|
+
label: "Total sales",
|
|
22
22
|
value: /* @__PURE__ */ jsx(
|
|
23
23
|
MoneyLines,
|
|
24
24
|
{
|
|
@@ -32,7 +32,7 @@ function KpiRow({
|
|
|
32
32
|
delta: gmvKpi ? delta(gmvKpi) : void 0,
|
|
33
33
|
sub: compare && gmvKpi ? void 0 : caption,
|
|
34
34
|
spark: gmvSpark,
|
|
35
|
-
hint: "
|
|
35
|
+
hint: "The total value of paid orders in this period, before refunds. Each currency is shown on its own line and never added together."
|
|
36
36
|
}
|
|
37
37
|
) }),
|
|
38
38
|
/* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
@@ -44,13 +44,13 @@ function KpiRow({
|
|
|
44
44
|
delta: delta(data.kpis.orders),
|
|
45
45
|
sub: compare ? void 0 : "Paid orders, all time",
|
|
46
46
|
spark: data.sparklines.orders,
|
|
47
|
-
hint: "Paid orders across all
|
|
47
|
+
hint: "Paid orders across all merchants, in every currency."
|
|
48
48
|
}
|
|
49
49
|
) }),
|
|
50
50
|
/* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
51
51
|
StatCard,
|
|
52
52
|
{
|
|
53
|
-
label: "New
|
|
53
|
+
label: "New merchants",
|
|
54
54
|
value: integer.format(data.kpis.newTenants.current),
|
|
55
55
|
href: "/admin/tenants",
|
|
56
56
|
delta: delta(data.kpis.newTenants),
|
|
@@ -61,7 +61,7 @@ function KpiRow({
|
|
|
61
61
|
/* @__PURE__ */ jsx("div", { className: "adm-span-3", children: /* @__PURE__ */ jsx(
|
|
62
62
|
StatCard,
|
|
63
63
|
{
|
|
64
|
-
label: "New accounts",
|
|
64
|
+
label: "New user accounts",
|
|
65
65
|
value: integer.format(data.kpis.newAccounts.current),
|
|
66
66
|
href: "/admin/accounts",
|
|
67
67
|
delta: delta(data.kpis.newAccounts),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/kpi-row.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat();\n\n/** The four headline figures for the window, each opening its page. */\nexport function KpiRow({\n data,\n currency,\n focus,\n compare,\n caption,\n}: {\n data: PlatformCockpit;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the change and sparkline follow, or null with several. */\n focus: string | null;\n /** False for \"all time\": there is no earlier period to compare with. */\n compare: boolean;\n caption: string;\n}) {\n const delta = (kpi: { pct: number | null }) =>\n compare ? { pct: kpi.pct, caption: 'vs
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/kpi-row.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { MoneyLines, StatCard, rowFor } from '../_ui';\n\nconst integer = new Intl.NumberFormat();\n\n/** The four headline figures for the window, each opening its page. */\nexport function KpiRow({\n data,\n currency,\n focus,\n compare,\n caption,\n}: {\n data: PlatformCockpit;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the change and sparkline follow, or null with several. */\n focus: string | null;\n /** False for \"all time\": there is no earlier period to compare with. */\n compare: boolean;\n caption: string;\n}) {\n const delta = (kpi: { pct: number | null }) =>\n compare ? { pct: kpi.pct, caption: 'vs the period before' } : undefined;\n // GMV's change and sparkline only mean something within one currency.\n const gmvKpi = rowFor(data.kpis.gmvByCurrency, focus);\n const gmvSpark = rowFor(\n data.trend.performanceSeriesByCurrency,\n focus,\n )?.series.map((p) => p.gmv);\n\n return (\n <section aria-label=\"Key numbers\" className=\"adm-grid\">\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Total sales\"\n value={\n <MoneyLines\n rows={data.kpis.gmvByCurrency}\n currency={currency}\n amount={(k) => k.current}\n empty=\"No sales yet\"\n />\n }\n href=\"/admin/revenue\"\n delta={gmvKpi ? delta(gmvKpi) : undefined}\n sub={compare && gmvKpi ? undefined : caption}\n spark={gmvSpark}\n hint=\"The total value of paid orders in this period, before refunds. Each currency is shown on its own line and never added together.\"\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Orders\"\n value={integer.format(data.kpis.orders.current)}\n href=\"/admin/revenue\"\n delta={delta(data.kpis.orders)}\n sub={compare ? undefined : 'Paid orders, all time'}\n spark={data.sparklines.orders}\n hint=\"Paid orders across all merchants, in every currency.\"\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"New merchants\"\n value={integer.format(data.kpis.newTenants.current)}\n href=\"/admin/tenants\"\n delta={delta(data.kpis.newTenants)}\n sub={\n compare\n ? undefined\n : `${integer.format(data.totals.totalTenants)} in total`\n }\n spark={data.sparklines.tenants}\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"New user accounts\"\n value={integer.format(data.kpis.newAccounts.current)}\n href=\"/admin/accounts\"\n delta={delta(data.kpis.newAccounts)}\n sub={\n compare\n ? undefined\n : `${integer.format(data.totals.totalAccounts)} in total`\n }\n spark={data.sparklines.accounts}\n />\n </div>\n </section>\n );\n}\n"],"mappings":"AAgCI,SAKQ,KALR;AA/BJ,SAAS,YAAY,UAAU,cAAc;AAE7C,MAAM,UAAU,IAAI,KAAK,aAAa;AAG/B,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,QAAM,QAAQ,CAAC,QACb,UAAU,EAAE,KAAK,IAAI,KAAK,SAAS,uBAAuB,IAAI;AAEhE,QAAM,SAAS,OAAO,KAAK,KAAK,eAAe,KAAK;AACpD,QAAM,WAAW;AAAA,IACf,KAAK,MAAM;AAAA,IACX;AAAA,EACF,GAAG,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG;AAE1B,SACE,qBAAC,aAAQ,cAAW,eAAc,WAAU,YAC1C;AAAA,wBAAC,SAAI,WAAU,cACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,KAAK,KAAK;AAAA,YAChB;AAAA,YACA,QAAQ,CAAC,MAAM,EAAE;AAAA,YACjB,OAAM;AAAA;AAAA,QACR;AAAA,QAEF,MAAK;AAAA,QACL,OAAO,SAAS,MAAM,MAAM,IAAI;AAAA,QAChC,KAAK,WAAW,SAAS,SAAY;AAAA,QACrC,OAAO;AAAA,QACP,MAAK;AAAA;AAAA,IACP,GACF;AAAA,IACA,oBAAC,SAAI,WAAU,cACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,QAAQ,OAAO,KAAK,KAAK,OAAO,OAAO;AAAA,QAC9C,MAAK;AAAA,QACL,OAAO,MAAM,KAAK,KAAK,MAAM;AAAA,QAC7B,KAAK,UAAU,SAAY;AAAA,QAC3B,OAAO,KAAK,WAAW;AAAA,QACvB,MAAK;AAAA;AAAA,IACP,GACF;AAAA,IACA,oBAAC,SAAI,WAAU,cACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,QAAQ,OAAO,KAAK,KAAK,WAAW,OAAO;AAAA,QAClD,MAAK;AAAA,QACL,OAAO,MAAM,KAAK,KAAK,UAAU;AAAA,QACjC,KACE,UACI,SACA,GAAG,QAAQ,OAAO,KAAK,OAAO,YAAY,CAAC;AAAA,QAEjD,OAAO,KAAK,WAAW;AAAA;AAAA,IACzB,GACF;AAAA,IACA,oBAAC,SAAI,WAAU,cACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,QAAQ,OAAO,KAAK,KAAK,YAAY,OAAO;AAAA,QACnD,MAAK;AAAA,QACL,OAAO,MAAM,KAAK,KAAK,WAAW;AAAA,QAClC,KACE,UACI,SACA,GAAG,QAAQ,OAAO,KAAK,OAAO,aAAa,CAAC;AAAA,QAElD,OAAO,KAAK,WAAW;AAAA;AAAA,IACzB,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/leaders-panel.tsx"],"sourcesContent":["import Link from 'next/link';\nimport { TableShell, Th } from '@withpotter/ui-kit';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { logoOf } from '../commerce/merchant-meta';\nimport { EntityAvatar } from '../_fx';\nimport { PICK_CURRENCY, Panel, moneyIn } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/** Merchants by volume within one currency: rank, orders, GMV and share. */\nexport function LeadersPanel({\n tenants,\n focus,\n caption,\n}: {\n /** The leaders ranked within `focus` only. */\n tenants: PlatformCockpit['topTenants'];\n /** The one currency ranked, or null when there are several and none is chosen. */\n focus: string | null;\n caption: string;\n}) {\n const top = tenants[0]?.gmv || 1;\n const money = moneyIn(focus ?? '');\n\n return (\n <Panel\n title=\"
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/leaders-panel.tsx"],"sourcesContent":["import Link from 'next/link';\nimport { TableShell, Th } from '@withpotter/ui-kit';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { logoOf } from '../commerce/merchant-meta';\nimport { EntityAvatar } from '../_fx';\nimport { PICK_CURRENCY, Panel, moneyIn } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/** Merchants by volume within one currency: rank, orders, GMV and share. */\nexport function LeadersPanel({\n tenants,\n focus,\n caption,\n}: {\n /** The leaders ranked within `focus` only. */\n tenants: PlatformCockpit['topTenants'];\n /** The one currency ranked, or null when there are several and none is chosen. */\n focus: string | null;\n caption: string;\n}) {\n const top = tenants[0]?.gmv || 1;\n const money = moneyIn(focus ?? '');\n\n return (\n <Panel\n title=\"Top merchants\"\n caption={caption}\n action={<Link href=\"/admin/tenants\">View all</Link>}\n flush\n >\n {tenants.length ? (\n <TableShell\n bare\n minWidth={320}\n density=\"compact\"\n head={\n <tr>\n <Th className=\"w-10\">#</Th>\n <Th>Merchant</Th>\n <Th align=\"right\">Orders</Th>\n <Th align=\"right\">GMV</Th>\n </tr>\n }\n >\n {tenants.map((t, index) => {\n const logo = logoOf(t);\n\n return (\n <tr key={t.id} className={styles.leaderRow}>\n <td className={styles.rank}>{index + 1}</td>\n <td className={styles.leaderName}>\n <span className={styles.leaderId}>\n {logo && (\n <EntityAvatar\n name={t.businessName}\n src={logo}\n size={28}\n />\n )}\n <Link href={`/admin/tenants/${t.id}`}>\n {t.businessName}\n </Link>\n </span>\n <span\n className={styles.share}\n style={{ width: `${Math.max((t.gmv / top) * 100, 2)}%` }}\n />\n </td>\n <td className={styles.num}>{integer.format(t.orders)}</td>\n <td className={styles.numStrong}>{money(t.gmv)}</td>\n </tr>\n );\n })}\n </TableShell>\n ) : (\n <p className={styles.empty}>\n {focus ? `No ${focus} merchant volume yet.` : PICK_CURRENCY}\n </p>\n )}\n </Panel>\n );\n}\n"],"mappings":"AA6Bc,cASF,YATE;AA7Bd,OAAO,UAAU;AACjB,SAAS,YAAY,UAAU;AAE/B,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,eAAe,OAAO,eAAe;AAC9C,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa;AAG/B,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,MAAM,QAAQ,CAAC,GAAG,OAAO;AAC/B,QAAM,QAAQ,QAAQ,SAAS,EAAE;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN;AAAA,MACA,QAAQ,oBAAC,QAAK,MAAK,kBAAiB,sBAAQ;AAAA,MAC5C,OAAK;AAAA,MAEJ,kBAAQ,SACP;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,MACE,qBAAC,QACC;AAAA,gCAAC,MAAG,WAAU,QAAO,eAAC;AAAA,YACtB,oBAAC,MAAG,sBAAQ;AAAA,YACZ,oBAAC,MAAG,OAAM,SAAQ,oBAAM;AAAA,YACxB,oBAAC,MAAG,OAAM,SAAQ,iBAAG;AAAA,aACvB;AAAA,UAGD,kBAAQ,IAAI,CAAC,GAAG,UAAU;AACzB,kBAAM,OAAO,OAAO,CAAC;AAErB,mBACE,qBAAC,QAAc,WAAW,OAAO,WAC/B;AAAA,kCAAC,QAAG,WAAW,OAAO,MAAO,kBAAQ,GAAE;AAAA,cACvC,qBAAC,QAAG,WAAW,OAAO,YACpB;AAAA,qCAAC,UAAK,WAAW,OAAO,UACrB;AAAA,0BACC;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAM,EAAE;AAAA,sBACR,KAAK;AAAA,sBACL,MAAM;AAAA;AAAA,kBACR;AAAA,kBAEF,oBAAC,QAAK,MAAM,kBAAkB,EAAE,EAAE,IAC/B,YAAE,cACL;AAAA,mBACF;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,OAAO;AAAA,oBAClB,OAAO,EAAE,OAAO,GAAG,KAAK,IAAK,EAAE,MAAM,MAAO,KAAK,CAAC,CAAC,IAAI;AAAA;AAAA,gBACzD;AAAA,iBACF;AAAA,cACA,oBAAC,QAAG,WAAW,OAAO,KAAM,kBAAQ,OAAO,EAAE,MAAM,GAAE;AAAA,cACrD,oBAAC,QAAG,WAAW,OAAO,WAAY,gBAAM,EAAE,GAAG,GAAE;AAAA,iBArBxC,EAAE,EAsBX;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH,IAEA,oBAAC,OAAE,WAAW,OAAO,OAClB,kBAAQ,MAAM,KAAK,0BAA0B,eAChD;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -13,8 +13,8 @@ function PortfolioPanel({
|
|
|
13
13
|
return /* @__PURE__ */ jsxs(
|
|
14
14
|
Panel,
|
|
15
15
|
{
|
|
16
|
-
title: "Merchant
|
|
17
|
-
caption: `${integer.format(total)}
|
|
16
|
+
title: "Merchant breakdown",
|
|
17
|
+
caption: `${integer.format(total)} merchants`,
|
|
18
18
|
height: "var(--adm-panel-h)",
|
|
19
19
|
children: [
|
|
20
20
|
/* @__PURE__ */ jsx("div", { className: styles.stack, "aria-hidden": true, children: statuses.map((s) => /* @__PURE__ */ jsx(
|
|
@@ -29,17 +29,17 @@ function PortfolioPanel({
|
|
|
29
29
|
)) }),
|
|
30
30
|
/* @__PURE__ */ jsx("ul", { className: styles.legend, children: statuses.map((s) => /* @__PURE__ */ jsxs("li", { children: [
|
|
31
31
|
/* @__PURE__ */ jsx("i", { style: { background: statusColor(s.status) } }),
|
|
32
|
-
/* @__PURE__ */ jsx("span", { children: s.status }),
|
|
32
|
+
/* @__PURE__ */ jsx("span", { children: s.status.replace(/_/g, " ") }),
|
|
33
33
|
/* @__PURE__ */ jsx("strong", { children: integer.format(s.count) }),
|
|
34
34
|
/* @__PURE__ */ jsxs("small", { children: [
|
|
35
35
|
pct(s.count, total),
|
|
36
36
|
"%"
|
|
37
37
|
] })
|
|
38
38
|
] }, s.status)) }),
|
|
39
|
-
/* @__PURE__ */ jsx("div", { className: styles.subhead, children: "
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: styles.subhead, children: "Biggest industries" }),
|
|
40
40
|
/* @__PURE__ */ jsx("ul", { className: styles.bars, children: industries.map((row, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
41
41
|
/* @__PURE__ */ jsxs("div", { className: styles.barLabel, children: [
|
|
42
|
-
/* @__PURE__ */ jsx("span", { children: row.industry || "
|
|
42
|
+
/* @__PURE__ */ jsx("span", { children: row.industry || "Not given" }),
|
|
43
43
|
/* @__PURE__ */ jsxs("strong", { children: [
|
|
44
44
|
integer.format(row.count),
|
|
45
45
|
/* @__PURE__ */ jsxs("small", { children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/portfolio-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { Panel, categoryColor, statusColor } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\nconst pct = (count: number, total: number) =>\n total ? Math.round((count / total) * 100) : 0;\n\n/**\n * Where the merchant base sits: one status bar, then industries ranked. The\n * card is as tall as its row neighbours and the list scrolls inside it.\n */\nexport function PortfolioPanel({\n statuses,\n industries,\n}: {\n statuses: PlatformCockpit['statusBreakdown'];\n industries: PlatformCockpit['industryBreakdown'];\n}) {\n const total = statuses.reduce((sum, s) => sum + s.count, 0);\n const industryTotal = industries.reduce((sum, i) => sum + i.count, 0);\n const topIndustry = industries[0]?.count || 1;\n\n return (\n <Panel\n title=\"Merchant
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/portfolio-panel.tsx"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { Panel, categoryColor, statusColor } from '../_ui';\nimport styles from './figures.module.css';\n\nconst integer = new Intl.NumberFormat();\n\nconst pct = (count: number, total: number) =>\n total ? Math.round((count / total) * 100) : 0;\n\n/**\n * Where the merchant base sits: one status bar, then industries ranked. The\n * card is as tall as its row neighbours and the list scrolls inside it.\n */\nexport function PortfolioPanel({\n statuses,\n industries,\n}: {\n statuses: PlatformCockpit['statusBreakdown'];\n industries: PlatformCockpit['industryBreakdown'];\n}) {\n const total = statuses.reduce((sum, s) => sum + s.count, 0);\n const industryTotal = industries.reduce((sum, i) => sum + i.count, 0);\n const topIndustry = industries[0]?.count || 1;\n\n return (\n <Panel\n title=\"Merchant breakdown\"\n caption={`${integer.format(total)} merchants`}\n height=\"var(--adm-panel-h)\"\n >\n <div className={styles.stack} aria-hidden>\n {statuses.map((s) => (\n <span\n key={s.status}\n style={{\n width: `${pct(s.count, total)}%`,\n background: statusColor(s.status),\n }}\n />\n ))}\n </div>\n <ul className={styles.legend}>\n {statuses.map((s) => (\n <li key={s.status}>\n <i style={{ background: statusColor(s.status) }} />\n <span>{s.status.replace(/_/g, ' ')}</span>\n <strong>{integer.format(s.count)}</strong>\n <small>{pct(s.count, total)}%</small>\n </li>\n ))}\n </ul>\n\n <div className={styles.subhead}>Biggest industries</div>\n <ul className={styles.bars}>\n {industries.map((row, index) => (\n <li key={row.industry || 'unspecified'}>\n <div className={styles.barLabel}>\n <span>{row.industry || 'Not given'}</span>\n <strong>\n {integer.format(row.count)}\n <small> · {pct(row.count, industryTotal)}%</small>\n </strong>\n </div>\n <div className={styles.track}>\n <span\n style={{\n width: `${(row.count / topIndustry) * 100}%`,\n background: categoryColor(index),\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n </Panel>\n );\n}\n"],"mappings":"AAgCU,cAeE,YAfF;AA/BV,SAAS,OAAO,eAAe,mBAAmB;AAClD,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa;AAEtC,MAAM,MAAM,CAAC,OAAe,UAC1B,QAAQ,KAAK,MAAO,QAAQ,QAAS,GAAG,IAAI;AAMvC,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AACF,GAGG;AACD,QAAM,QAAQ,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAC1D,QAAM,gBAAgB,WAAW,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AACpE,QAAM,cAAc,WAAW,CAAC,GAAG,SAAS;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,SAAS,GAAG,QAAQ,OAAO,KAAK,CAAC;AAAA,MACjC,QAAO;AAAA,MAEP;AAAA,4BAAC,SAAI,WAAW,OAAO,OAAO,eAAW,MACtC,mBAAS,IAAI,CAAC,MACb;AAAA,UAAC;AAAA;AAAA,YAEC,OAAO;AAAA,cACL,OAAO,GAAG,IAAI,EAAE,OAAO,KAAK,CAAC;AAAA,cAC7B,YAAY,YAAY,EAAE,MAAM;AAAA,YAClC;AAAA;AAAA,UAJK,EAAE;AAAA,QAKT,CACD,GACH;AAAA,QACA,oBAAC,QAAG,WAAW,OAAO,QACnB,mBAAS,IAAI,CAAC,MACb,qBAAC,QACC;AAAA,8BAAC,OAAE,OAAO,EAAE,YAAY,YAAY,EAAE,MAAM,EAAE,GAAG;AAAA,UACjD,oBAAC,UAAM,YAAE,OAAO,QAAQ,MAAM,GAAG,GAAE;AAAA,UACnC,oBAAC,YAAQ,kBAAQ,OAAO,EAAE,KAAK,GAAE;AAAA,UACjC,qBAAC,WAAO;AAAA,gBAAI,EAAE,OAAO,KAAK;AAAA,YAAE;AAAA,aAAC;AAAA,aAJtB,EAAE,MAKX,CACD,GACH;AAAA,QAEA,oBAAC,SAAI,WAAW,OAAO,SAAS,gCAAkB;AAAA,QAClD,oBAAC,QAAG,WAAW,OAAO,MACnB,qBAAW,IAAI,CAAC,KAAK,UACpB,qBAAC,QACC;AAAA,+BAAC,SAAI,WAAW,OAAO,UACrB;AAAA,gCAAC,UAAM,cAAI,YAAY,aAAY;AAAA,YACnC,qBAAC,YACE;AAAA,sBAAQ,OAAO,IAAI,KAAK;AAAA,cACzB,qBAAC,WAAM;AAAA;AAAA,gBAAI,IAAI,IAAI,OAAO,aAAa;AAAA,gBAAE;AAAA,iBAAC;AAAA,eAC5C;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAW,OAAO,OACrB;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,OAAO,GAAI,IAAI,QAAQ,cAAe,GAAG;AAAA,gBACzC,YAAY,cAAc,KAAK;AAAA,cACjC;AAAA;AAAA,UACF,GACF;AAAA,aAfO,IAAI,YAAY,aAgBzB,CACD,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -9,18 +9,18 @@ function PositionPanel({
|
|
|
9
9
|
const perf = data.performance;
|
|
10
10
|
const rows = [
|
|
11
11
|
[
|
|
12
|
-
"Active
|
|
12
|
+
"Active merchants",
|
|
13
13
|
integer.format(data.totals.activeTenants),
|
|
14
|
-
`of ${integer.format(data.totals.totalTenants)}
|
|
14
|
+
`of ${integer.format(data.totals.totalTenants)} merchants`
|
|
15
15
|
],
|
|
16
16
|
[
|
|
17
17
|
"Merchants with sales",
|
|
18
18
|
perf ? integer.format(perf.transactingTenants) : "\u2014",
|
|
19
19
|
// The API divides by every workspace, not only the active ones.
|
|
20
|
-
perf ? `${perf.transactingTenantRatePct}% of all
|
|
20
|
+
perf ? `${perf.transactingTenantRatePct}% of all merchants, this period` : void 0
|
|
21
21
|
],
|
|
22
22
|
[
|
|
23
|
-
"
|
|
23
|
+
"All-time sales",
|
|
24
24
|
/* @__PURE__ */ jsx(
|
|
25
25
|
MoneyLines,
|
|
26
26
|
{
|
|
@@ -47,12 +47,12 @@ function PositionPanel({
|
|
|
47
47
|
"this period, scaled to a year"
|
|
48
48
|
],
|
|
49
49
|
[
|
|
50
|
-
"
|
|
50
|
+
"User accounts",
|
|
51
51
|
integer.format(data.totals.totalAccounts),
|
|
52
52
|
data.accounts ? `${integer.format(data.accounts.active)} active and verified` : void 0
|
|
53
53
|
]
|
|
54
54
|
];
|
|
55
|
-
return /* @__PURE__ */ jsx(Panel, { title: "Platform
|
|
55
|
+
return /* @__PURE__ */ jsx(Panel, { title: "Platform totals", caption: "Today", children: /* @__PURE__ */ jsx("dl", { className: styles.position, children: rows.map(([label, value, hint]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
56
56
|
/* @__PURE__ */ jsx("dt", { children: label }),
|
|
57
57
|
/* @__PURE__ */ jsxs("dd", { children: [
|
|
58
58
|
/* @__PURE__ */ jsx("strong", { children: value }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_overview/position-panel.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { MoneyLines, Panel } from '../_ui';\nimport styles from './position.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/** The platform's standing figures — not tied to the selected window. */\nexport function PositionPanel({\n data,\n currency,\n}: {\n data: PlatformCockpit;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n}) {\n const perf = data.performance;\n const rows: Array<[string, ReactNode, string?]> = [\n [\n 'Active
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_overview/position-panel.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { MoneyLines, Panel } from '../_ui';\nimport styles from './position.module.css';\n\nconst integer = new Intl.NumberFormat();\n\n/** The platform's standing figures — not tied to the selected window. */\nexport function PositionPanel({\n data,\n currency,\n}: {\n data: PlatformCockpit;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n}) {\n const perf = data.performance;\n const rows: Array<[string, ReactNode, string?]> = [\n [\n 'Active merchants',\n integer.format(data.totals.activeTenants),\n `of ${integer.format(data.totals.totalTenants)} merchants`,\n ],\n [\n 'Merchants with sales',\n perf ? integer.format(perf.transactingTenants) : '—',\n // The API divides by every workspace, not only the active ones.\n perf\n ? `${perf.transactingTenantRatePct}% of all merchants, this period`\n : undefined,\n ],\n [\n 'All-time sales',\n <MoneyLines\n key=\"gmv\"\n rows={data.totals.lifetimeGmvByCurrency}\n currency={currency}\n amount={(r) => r.amount}\n empty=\"No sales yet\"\n />,\n ],\n [\n 'Yearly sales pace',\n <MoneyLines\n key=\"run\"\n rows={perf?.byCurrency}\n currency={currency}\n amount={(r) => r.annualizedGmvRunRate}\n empty=\"—\"\n />,\n 'this period, scaled to a year',\n ],\n [\n 'User accounts',\n integer.format(data.totals.totalAccounts),\n data.accounts\n ? `${integer.format(data.accounts.active)} active and verified`\n : undefined,\n ],\n ];\n\n return (\n <Panel title=\"Platform totals\" caption=\"Today\">\n <dl className={styles.position}>\n {rows.map(([label, value, hint]) => (\n <div key={label}>\n <dt>{label}</dt>\n <dd>\n <strong>{value}</strong>\n {hint && <small>{hint}</small>}\n </dd>\n </div>\n ))}\n </dl>\n </Panel>\n );\n}\n"],"mappings":"AAiCM,cAkCM,YAlCN;AA/BN,SAAS,YAAY,aAAa;AAClC,OAAO,YAAY;AAEnB,MAAM,UAAU,IAAI,KAAK,aAAa;AAG/B,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,KAAK;AAClB,QAAM,OAA4C;AAAA,IAChD;AAAA,MACE;AAAA,MACA,QAAQ,OAAO,KAAK,OAAO,aAAa;AAAA,MACxC,MAAM,QAAQ,OAAO,KAAK,OAAO,YAAY,CAAC;AAAA,IAChD;AAAA,IACA;AAAA,MACE;AAAA,MACA,OAAO,QAAQ,OAAO,KAAK,kBAAkB,IAAI;AAAA;AAAA,MAEjD,OACI,GAAG,KAAK,wBAAwB,oCAChC;AAAA,IACN;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UAEC,MAAM,KAAK,OAAO;AAAA,UAClB;AAAA,UACA,QAAQ,CAAC,MAAM,EAAE;AAAA,UACjB,OAAM;AAAA;AAAA,QAJF;AAAA,MAKN;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UAEC,MAAM,MAAM;AAAA,UACZ;AAAA,UACA,QAAQ,CAAC,MAAM,EAAE;AAAA,UACjB,OAAM;AAAA;AAAA,QAJF;AAAA,MAKN;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA,QAAQ,OAAO,KAAK,OAAO,aAAa;AAAA,MACxC,KAAK,WACD,GAAG,QAAQ,OAAO,KAAK,SAAS,MAAM,CAAC,yBACvC;AAAA,IACN;AAAA,EACF;AAEA,SACE,oBAAC,SAAM,OAAM,mBAAkB,SAAQ,SACrC,8BAAC,QAAG,WAAW,OAAO,UACnB,eAAK,IAAI,CAAC,CAAC,OAAO,OAAO,IAAI,MAC5B,qBAAC,SACC;AAAA,wBAAC,QAAI,iBAAM;AAAA,IACX,qBAAC,QACC;AAAA,0BAAC,YAAQ,iBAAM;AAAA,MACd,QAAQ,oBAAC,WAAO,gBAAK;AAAA,OACxB;AAAA,OALQ,KAMV,CACD,GACH,GACF;AAEJ;","names":[]}
|
|
@@ -32,7 +32,7 @@ function DataTable({
|
|
|
32
32
|
{
|
|
33
33
|
tone: "error",
|
|
34
34
|
title: errorTitle,
|
|
35
|
-
description: "Please
|
|
35
|
+
description: "Please try again. If it keeps happening, tell the engineering team."
|
|
36
36
|
}
|
|
37
37
|
) }) }),
|
|
38
38
|
!isLoading && !isError && rows && rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: colCount, style: { padding: 0 }, children: /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_ui/data-table.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport { tableStyles } from './styles';\n\nexport interface Column<T> {\n /** Unique key for the column. */\n key: string;\n /** Header label. */\n header: string;\n /** Cell renderer. Defaults to String(row[key]). */\n render?: (row: T) => ReactNode;\n /** Render muted (secondary text colour). */\n muted?: boolean;\n /** Prevent wrapping. */\n nowrap?: boolean;\n width?: number | string;\n}\n\n/**\n * Generic admin data table. Owns the loading (skeleton), error and empty\n * states so list pages never hand-roll a <table> again. Supports an optional\n * expandable sub-row per record.\n */\nexport function DataTable<T>({\n columns,\n rows,\n getRowKey,\n isLoading,\n isError,\n errorTitle = 'Failed to load',\n emptyTitle = 'Nothing here yet',\n emptyDescription,\n emptyIcon,\n skeletonRows = 6,\n renderExpanded,\n isExpanded,\n onRowClick,\n}: {\n columns: Column<T>[];\n rows: T[] | undefined;\n getRowKey: (row: T) => string;\n isLoading?: boolean;\n isError?: boolean;\n errorTitle?: string;\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIcon?: React.ComponentType<{ size?: number; color?: string }>;\n skeletonRows?: number;\n renderExpanded?: (row: T) => ReactNode;\n isExpanded?: (row: T) => boolean;\n /** When provided, the whole row becomes clickable (excluding links/buttons). */\n onRowClick?: (row: T) => void;\n}) {\n const colCount = columns.length;\n\n return (\n <div className=\"adm-table-card\">\n <table style={tableStyles.table}>\n <thead>\n <tr style={tableStyles.headRow}>\n {columns.map((c) => (\n <th key={c.key} style={{ ...tableStyles.th, width: c.width }}>\n {c.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {isLoading &&\n Array.from({ length: skeletonRows }).map((_, i) => (\n <tr key={`sk-${i}`}>\n {columns.map((c) => (\n <td key={c.key} style={tableStyles.cell}>\n <Skeleton\n width={`${50 + ((i + c.key.length) % 4) * 12}%`}\n />\n </td>\n ))}\n </tr>\n ))}\n\n {!isLoading && isError && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_ui/data-table.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport { tableStyles } from './styles';\n\nexport interface Column<T> {\n /** Unique key for the column. */\n key: string;\n /** Header label. */\n header: string;\n /** Cell renderer. Defaults to String(row[key]). */\n render?: (row: T) => ReactNode;\n /** Render muted (secondary text colour). */\n muted?: boolean;\n /** Prevent wrapping. */\n nowrap?: boolean;\n width?: number | string;\n}\n\n/**\n * Generic admin data table. Owns the loading (skeleton), error and empty\n * states so list pages never hand-roll a <table> again. Supports an optional\n * expandable sub-row per record.\n */\nexport function DataTable<T>({\n columns,\n rows,\n getRowKey,\n isLoading,\n isError,\n errorTitle = 'Failed to load',\n emptyTitle = 'Nothing here yet',\n emptyDescription,\n emptyIcon,\n skeletonRows = 6,\n renderExpanded,\n isExpanded,\n onRowClick,\n}: {\n columns: Column<T>[];\n rows: T[] | undefined;\n getRowKey: (row: T) => string;\n isLoading?: boolean;\n isError?: boolean;\n errorTitle?: string;\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIcon?: React.ComponentType<{ size?: number; color?: string }>;\n skeletonRows?: number;\n renderExpanded?: (row: T) => ReactNode;\n isExpanded?: (row: T) => boolean;\n /** When provided, the whole row becomes clickable (excluding links/buttons). */\n onRowClick?: (row: T) => void;\n}) {\n const colCount = columns.length;\n\n return (\n <div className=\"adm-table-card\">\n <table style={tableStyles.table}>\n <thead>\n <tr style={tableStyles.headRow}>\n {columns.map((c) => (\n <th key={c.key} style={{ ...tableStyles.th, width: c.width }}>\n {c.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {isLoading &&\n Array.from({ length: skeletonRows }).map((_, i) => (\n <tr key={`sk-${i}`}>\n {columns.map((c) => (\n <td key={c.key} style={tableStyles.cell}>\n <Skeleton\n width={`${50 + ((i + c.key.length) % 4) * 12}%`}\n />\n </td>\n ))}\n </tr>\n ))}\n\n {!isLoading && isError && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please try again. If it keeps happening, tell the engineering team.\"\n />\n </td>\n </tr>\n )}\n\n {!isLoading && !isError && rows && rows.length === 0 && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n icon={emptyIcon}\n title={emptyTitle}\n description={emptyDescription}\n />\n </td>\n </tr>\n )}\n\n {!isLoading &&\n !isError &&\n rows?.map((row) => (\n <RowGroup\n key={getRowKey(row)}\n row={row}\n columns={columns}\n renderExpanded={renderExpanded}\n expanded={isExpanded?.(row) ?? false}\n colCount={colCount}\n onRowClick={onRowClick}\n />\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction RowGroup<T>({\n row,\n columns,\n renderExpanded,\n expanded,\n colCount,\n onRowClick,\n}: {\n row: T;\n columns: Column<T>[];\n renderExpanded?: (row: T) => ReactNode;\n expanded: boolean;\n colCount: number;\n onRowClick?: (row: T) => void;\n}) {\n const clickable = !!onRowClick;\n return (\n <>\n <tr\n onClick={\n clickable\n ? (e) => {\n // Let inner links/buttons handle their own clicks.\n if ((e.target as HTMLElement).closest('a,button')) return;\n onRowClick!(row);\n }\n : undefined\n }\n style={clickable ? { cursor: 'pointer' } : undefined}\n onMouseEnter={\n clickable\n ? (e) => (e.currentTarget.style.background = 'var(--bg-subtle)')\n : undefined\n }\n onMouseLeave={\n clickable ? (e) => (e.currentTarget.style.background = '') : undefined\n }\n >\n {columns.map((c) => (\n <td\n key={c.key}\n style={{\n ...(c.muted ? tableStyles.cellMuted : tableStyles.cell),\n whiteSpace: c.nowrap ? 'nowrap' : undefined,\n }}\n >\n {c.render\n ? c.render(row)\n : String((row as Record<string, unknown>)[c.key] ?? '—')}\n </td>\n ))}\n </tr>\n {expanded && renderExpanded && (\n <tr>\n <td\n colSpan={colCount}\n style={{ ...tableStyles.cell, background: 'var(--bg-subtle)' }}\n >\n {renderExpanded(row)}\n </td>\n </tr>\n )}\n </>\n );\n}\n"],"mappings":"AA8Dc,SAgFV,UAhFU,KAMN,YANM;AA7Dd,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAqBrB,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAeG;AACD,QAAM,WAAW,QAAQ;AAEzB,SACE,oBAAC,SAAI,WAAU,kBACb,+BAAC,WAAM,OAAO,YAAY,OACxB;AAAA,wBAAC,WACC,8BAAC,QAAG,OAAO,YAAY,SACpB,kBAAQ,IAAI,CAAC,MACZ,oBAAC,QAAe,OAAO,EAAE,GAAG,YAAY,IAAI,OAAO,EAAE,MAAM,GACxD,YAAE,UADI,EAAE,GAEX,CACD,GACH,GACF;AAAA,IACA,qBAAC,WACE;AAAA,mBACC,MAAM,KAAK,EAAE,QAAQ,aAAa,CAAC,EAAE,IAAI,CAAC,GAAG,MAC3C,oBAAC,QACE,kBAAQ,IAAI,CAAC,MACZ,oBAAC,QAAe,OAAO,YAAY,MACjC;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,GAAG,MAAO,IAAI,EAAE,IAAI,UAAU,IAAK,EAAE;AAAA;AAAA,MAC9C,KAHO,EAAE,GAIX,CACD,KAPM,MAAM,CAAC,EAQhB,CACD;AAAA,MAEF,CAAC,aAAa,WACb,oBAAC,QACC,8BAAC,QAAG,SAAS,UAAU,OAAO,EAAE,SAAS,EAAE,GACzC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,aAAY;AAAA;AAAA,MACd,GACF,GACF;AAAA,MAGD,CAAC,aAAa,CAAC,WAAW,QAAQ,KAAK,WAAW,KACjD,oBAAC,QACC,8BAAC,QAAG,SAAS,UAAU,OAAO,EAAE,SAAS,EAAE,GACzC;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,UACP,aAAa;AAAA;AAAA,MACf,GACF,GACF;AAAA,MAGD,CAAC,aACA,CAAC,WACD,MAAM,IAAI,CAAC,QACT;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU,aAAa,GAAG,KAAK;AAAA,UAC/B;AAAA,UACA;AAAA;AAAA,QANK,UAAU,GAAG;AAAA,MAOpB,CACD;AAAA,OACL;AAAA,KACF,GACF;AAEJ;AAEA,SAAS,SAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,YAAY,CAAC,CAAC;AACpB,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,SACE,YACI,CAAC,MAAM;AAEL,cAAK,EAAE,OAAuB,QAAQ,UAAU,EAAG;AACnD,qBAAY,GAAG;AAAA,QACjB,IACA;AAAA,QAEN,OAAO,YAAY,EAAE,QAAQ,UAAU,IAAI;AAAA,QAC3C,cACE,YACI,CAAC,MAAO,EAAE,cAAc,MAAM,aAAa,qBAC3C;AAAA,QAEN,cACE,YAAY,CAAC,MAAO,EAAE,cAAc,MAAM,aAAa,KAAM;AAAA,QAG9D,kBAAQ,IAAI,CAAC,MACZ;AAAA,UAAC;AAAA;AAAA,YAEC,OAAO;AAAA,cACL,GAAI,EAAE,QAAQ,YAAY,YAAY,YAAY;AAAA,cAClD,YAAY,EAAE,SAAS,WAAW;AAAA,YACpC;AAAA,YAEC,YAAE,SACC,EAAE,OAAO,GAAG,IACZ,OAAQ,IAAgC,EAAE,GAAG,KAAK,QAAG;AAAA;AAAA,UARpD,EAAE;AAAA,QAST,CACD;AAAA;AAAA,IACH;AAAA,IACC,YAAY,kBACX,oBAAC,QACC;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,OAAO,EAAE,GAAG,YAAY,MAAM,YAAY,mBAAmB;AAAA,QAE5D,yBAAe,GAAG;AAAA;AAAA,IACrB,GACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -53,7 +53,7 @@ function ListCard({
|
|
|
53
53
|
{
|
|
54
54
|
tone: "error",
|
|
55
55
|
title: errorTitle,
|
|
56
|
-
description: "Please
|
|
56
|
+
description: "Please try again. If it keeps happening, tell the engineering team.",
|
|
57
57
|
action: onRetry && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: onRetry, children: "Try again" })
|
|
58
58
|
}
|
|
59
59
|
)
|