@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.
Files changed (47) hide show
  1. package/dist/admin/_overview/activation-panel.js +6 -6
  2. package/dist/admin/_overview/activation-panel.js.map +1 -1
  3. package/dist/admin/_overview/attention-groups.js +8 -8
  4. package/dist/admin/_overview/attention-groups.js.map +1 -1
  5. package/dist/admin/_overview/attention-panel.js +3 -3
  6. package/dist/admin/_overview/attention-panel.js.map +1 -1
  7. package/dist/admin/_overview/control-room.js +9 -9
  8. package/dist/admin/_overview/control-room.js.map +1 -1
  9. package/dist/admin/_overview/kpi-row.js +7 -7
  10. package/dist/admin/_overview/kpi-row.js.map +1 -1
  11. package/dist/admin/_overview/leaders-panel.js +1 -1
  12. package/dist/admin/_overview/leaders-panel.js.map +1 -1
  13. package/dist/admin/_overview/portfolio-panel.js +5 -5
  14. package/dist/admin/_overview/portfolio-panel.js.map +1 -1
  15. package/dist/admin/_overview/position-panel.js +6 -6
  16. package/dist/admin/_overview/position-panel.js.map +1 -1
  17. package/dist/admin/_ui/data-table.js +1 -1
  18. package/dist/admin/_ui/data-table.js.map +1 -1
  19. package/dist/admin/_ui/list-card.js +1 -1
  20. package/dist/admin/_ui/list-card.js.map +1 -1
  21. package/dist/admin/_ui/trend.js +2 -2
  22. package/dist/admin/_ui/trend.js.map +1 -1
  23. package/dist/admin/page.js +6 -6
  24. package/dist/admin/page.js.map +1 -1
  25. package/dist/admin/payout-tiers/_tier-dialog.js +31 -21
  26. package/dist/admin/payout-tiers/_tier-dialog.js.map +1 -1
  27. package/dist/admin/payout-tiers/requirement-labels.js +70 -0
  28. package/dist/admin/payout-tiers/requirement-labels.js.map +1 -0
  29. package/dist/admin/payout-tiers/tier-preview-result.js +10 -9
  30. package/dist/admin/payout-tiers/tier-preview-result.js.map +1 -1
  31. package/dist/admin/payout-tiers/tiers-table.js +6 -7
  32. package/dist/admin/payout-tiers/tiers-table.js.map +1 -1
  33. package/dist/admin/payout-tiers/tiers-workspace.js +2 -2
  34. package/dist/admin/payout-tiers/tiers-workspace.js.map +1 -1
  35. package/dist/admin/revenue/cohort-panel.js +1 -1
  36. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  37. package/dist/admin/revenue/plan-mix-panel.js +2 -2
  38. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  39. package/dist/admin/revenue/revenue-kpis.js +3 -3
  40. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  41. package/dist/admin/revenue/revenue-overview.js +1 -1
  42. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  43. package/dist/admin/revenue/subscriptions-view.js +3 -3
  44. package/dist/admin/revenue/subscriptions-view.js.map +1 -1
  45. package/dist/admin/revenue/top-tenants-panel.js +3 -3
  46. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  47. 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: "Activation funnel", caption: "All time", height: "auto", children: /* @__PURE__ */ jsx(AdmLoading, { label: "Loading activation funnel\u2026", size: "sm" }) });
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: "Activation funnel", caption: "All time", height: "auto", children: [
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: "Conversion" }),
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 \u2192 activated" }),
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 \u2192 first order" }),
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: "Median days to first order" }),
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=\"Activation funnel\" caption=\"All time\" height=\"auto\">\n <AdmLoading label=\"Loading activation funnel…\" 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=\"Activation funnel\" 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}>Conversion</div>\n <ul className={styles.bars}>\n <li className={styles.barLabel}>\n <span>Signed up → activated</span>\n <strong>{activation.activationRatePct}%</strong>\n </li>\n <li className={styles.barLabel}>\n <span>Signed up → first order</span>\n <strong>{activation.orderConversionPct}%</strong>\n </li>\n <li className={styles.barLabel}>\n <span>Median days to 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,qBAAoB,SAAQ,YAAW,QAAO,QACzD,8BAAC,cAAW,OAAM,mCAA6B,MAAK,MAAK,GAC3D;AAAA,EAEJ;AAEA,QAAM,SAAS,WAAW;AAC1B,QAAM,MAAM,OAAO,CAAC,GAAG,SAAS;AAEhC,SACE,qBAAC,SAAM,OAAM,qBAAoB,SAAQ,YAAW,QAAO,QACzD;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,wBAAU;AAAA,IAC1C,qBAAC,QAAG,WAAW,OAAO,MACpB;AAAA,2BAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,wCAAqB;AAAA,QAC3B,qBAAC,YAAQ;AAAA,qBAAW;AAAA,UAAkB;AAAA,WAAC;AAAA,SACzC;AAAA,MACA,qBAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,0CAAuB;AAAA,QAC7B,qBAAC,YAAQ;AAAA,qBAAW;AAAA,UAAmB;AAAA,WAAC;AAAA,SAC1C;AAAA,MACA,qBAAC,QAAG,WAAW,OAAO,UACpB;AAAA,4BAAC,UAAK,wCAA0B;AAAA,QAChC,oBAAC,YACE,qBAAW,0BAA0B,OAClC,WACA,WAAW,wBACjB;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
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: "Settlement failed",
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: `Invoice ${i.invoiceNumber}`,
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: "Review",
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: "Trials ending",
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}d left`
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: "Dormant",
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: 'Settlement failed',\n value: inCurrency(s.netAmount, s.currency),\n })),\n },\n {\n key: 'invoices',\n title: 'Overdue',\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: `Invoice ${i.invoiceNumber}`,\n value: inCurrency(i.amountDue, i.currency),\n })),\n },\n {\n key: 'review',\n title: '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,\n })),\n },\n {\n key: 'trials',\n title: '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}d left`,\n })),\n },\n {\n key: 'dormant',\n title: 'Dormant',\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,WAAW,EAAE,aAAa;AAAA,QAClC,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;AAAA,MACX,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;AAAA,MACnD,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":[]}
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 exceptions could not be loaded, so this list may be incomplete." }),
20
- !active ? /* @__PURE__ */ jsx("div", { className: styles.clear, children: /* @__PURE__ */ jsx(EmptyNote, { children: unknown.length > 0 ? "Nothing to show right now." : "No exceptions in this period." }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
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 exceptions of one kind",
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 exceptions could not be loaded, so this list may be incomplete.\n </p>\n )}\n {!active ? (\n <div className={styles.clear}>\n <EmptyNote>\n {unknown.length > 0\n ? 'Nothing to show right now.'\n : 'No exceptions in this period.'}\n </EmptyNote>\n </div>\n ) : (\n <>\n <div\n className={styles.filters}\n role=\"group\"\n aria-label=\"Show exceptions of one kind\"\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,kFAE3B;AAAA,IAED,CAAC,SACA,oBAAC,SAAI,WAAW,OAAO,OACrB,8BAAC,aACE,kBAAQ,SAAS,IACd,+BACA,iCACN,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":[]}
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&apos;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: "KYC / KYB decisions",
16
+ label: "Identity checks to review",
17
17
  value: waiting === void 0 ? "\u2013" : /* @__PURE__ */ jsx(CountUp, { value: waiting }),
18
- sub: "Businesses waiting for review",
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: "Subscriptions",
31
+ label: "Plan income",
32
32
  value: formatMoney(revenue.recurring.mrr, revenue.currency),
33
- sub: "Monthly recurring revenue",
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": "Control room", children: [
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" : "Settlements that did not land",
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 invoices",
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) ?? "Invoices past their due date",
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: "Businesses that passed KYC or KYB"
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=\"KYC / KYB decisions\"\n value={waiting === undefined ? '–' : <CountUp value={waiting} />}\n sub=\"Businesses waiting for review\"\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=\"Subscriptions\"\n value={formatMoney(revenue.recurring.mrr, revenue.currency)}\n sub=\"Monthly recurring revenue\"\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=\"Control room\">\n <StatCard\n compact\n label=\"Failed payouts\"\n value={failedUnknown ? '–' : <CountUp value={failed} />}\n sub={\n failedUnknown\n ? 'Could not be loaded'\n : 'Settlements that did not land'\n }\n href={withdrawals ? '/admin/withdrawals?tab=failed' : undefined}\n alert={failedUnknown || failed > 0}\n />\n <StatCard\n compact\n label=\"Overdue invoices\"\n value={overdueUnknown ? '–' : <CountUp value={overdue} />}\n sub={\n overdueUnknown\n ? 'Could not be loaded'\n : (overdueOutstanding(attention.overdueInvoices, currency) ??\n 'Invoices 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=\"Businesses that passed KYC or KYB\"\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,gBAC1C;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":[]}
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 prior period" } : void 0;
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": "Performance indicators", className: "adm-grid", children: [
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: "Merchant GMV",
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: "Gross value of paid orders in the selected period, one line per currency. Currencies are never added together."
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 merchant workspaces, in every currency."
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 workspaces",
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 prior period' } : 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=\"Performance indicators\" className=\"adm-grid\">\n <div className=\"adm-span-3\">\n <StatCard\n label=\"Merchant GMV\"\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=\"Gross value of paid orders in the selected period, one line per currency. Currencies are 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 merchant workspaces, in every currency.\"\n />\n </div>\n <div className=\"adm-span-3\">\n <StatCard\n label=\"New workspaces\"\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 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,kBAAkB,IAAI;AAE3D,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,0BAAyB,WAAU,YACrD;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
+ {"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":[]}
@@ -16,7 +16,7 @@ function LeadersPanel({
16
16
  return /* @__PURE__ */ jsx(
17
17
  Panel,
18
18
  {
19
- title: "Leading merchants",
19
+ title: "Top merchants",
20
20
  caption,
21
21
  action: /* @__PURE__ */ jsx(Link, { href: "/admin/tenants", children: "View all" }),
22
22
  flush: true,
@@ -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=\"Leading 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":[]}
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 portfolio",
17
- caption: `${integer.format(total)} workspaces`,
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: "Industries" }),
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 || "Unspecified" }),
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 portfolio\"\n caption={`${integer.format(total)} workspaces`}\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}</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}>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 || 'Unspecified'}</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,QAAO;AAAA,UAChB,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,wBAAU;AAAA,QAC1C,oBAAC,QAAG,WAAW,OAAO,MACnB,qBAAW,IAAI,CAAC,KAAK,UACpB,qBAAC,QACC;AAAA,+BAAC,SAAI,WAAW,OAAO,UACrB;AAAA,gCAAC,UAAM,cAAI,YAAY,eAAc;AAAA,YACrC,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":[]}
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 workspaces",
12
+ "Active merchants",
13
13
  integer.format(data.totals.activeTenants),
14
- `of ${integer.format(data.totals.totalTenants)} \xB7 status active`
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 workspaces, this period` : void 0
20
+ perf ? `${perf.transactingTenantRatePct}% of all merchants, this period` : void 0
21
21
  ],
22
22
  [
23
- "Lifetime GMV",
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
- "Accounts",
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 position", caption: "Today", children: /* @__PURE__ */ jsx("dl", { className: styles.position, children: rows.map(([label, value, hint]) => /* @__PURE__ */ jsxs("div", { children: [
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 workspaces',\n integer.format(data.totals.activeTenants),\n `of ${integer.format(data.totals.totalTenants)} · status active`,\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 workspaces, this period`\n : undefined,\n ],\n [\n 'Lifetime GMV',\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 '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 position\" 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,qCAChC;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,qBAAoB,SAAQ,SACvC,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":[]}
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 retry. If this persists, check the API logs."
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 retry. If this persists, check the API logs.\"\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":"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","names":[]}
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":"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","names":[]}
@@ -53,7 +53,7 @@ function ListCard({
53
53
  {
54
54
  tone: "error",
55
55
  title: errorTitle,
56
- description: "Please retry. If this persists, check the API logs.",
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
  )