@withpotter/pages-admin 0.13.0-next.84 → 0.13.0-next.86

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 (51) hide show
  1. package/dist/admin/_money/finder-format.js +1 -0
  2. package/dist/admin/_money/finder-format.js.map +1 -1
  3. package/dist/admin/_money/format.js +1 -0
  4. package/dist/admin/_money/format.js.map +1 -1
  5. package/dist/admin/_overview/attention.module.css +0 -3
  6. package/dist/admin/_overview/overdue-outstanding.js +2 -1
  7. package/dist/admin/_overview/overdue-outstanding.js.map +1 -1
  8. package/dist/admin/_ui/list-card.module.css +2 -16
  9. package/dist/admin/_ui/money-view.js +1 -0
  10. package/dist/admin/_ui/money-view.js.map +1 -1
  11. package/dist/admin/_ui/page-header.js +31 -4
  12. package/dist/admin/_ui/page-header.js.map +1 -1
  13. package/dist/admin/_ui/page-header.module.css +45 -0
  14. package/dist/admin/_ui/panel.module.css +0 -10
  15. package/dist/admin/accounts/account-panel.js +43 -11
  16. package/dist/admin/accounts/account-panel.js.map +1 -1
  17. package/dist/admin/accounts/account-panel.module.css +59 -7
  18. package/dist/admin/accounts/account-row.js +2 -2
  19. package/dist/admin/accounts/account-row.js.map +1 -1
  20. package/dist/admin/accounts/account-sessions.js +28 -13
  21. package/dist/admin/accounts/account-sessions.js.map +1 -1
  22. package/dist/admin/accounts/account-sessions.module.css +4 -2
  23. package/dist/admin/accounts/account-stage.js +34 -0
  24. package/dist/admin/accounts/account-stage.js.map +1 -1
  25. package/dist/admin/accounts/account-vitals.js +15 -13
  26. package/dist/admin/accounts/account-vitals.js.map +1 -1
  27. package/dist/admin/audit/status-pill.js +6 -0
  28. package/dist/admin/audit/status-pill.js.map +1 -1
  29. package/dist/admin/audit/status-pill.module.css +4 -0
  30. package/dist/admin/commerce/commerce-overview.js +2 -2
  31. package/dist/admin/commerce/commerce-overview.js.map +1 -1
  32. package/dist/admin/commerce/merchant-meta.js +1 -0
  33. package/dist/admin/commerce/merchant-meta.js.map +1 -1
  34. package/dist/admin/commerce/merchant-view.js +2 -2
  35. package/dist/admin/commerce/merchant-view.js.map +1 -1
  36. package/dist/admin/feedback/prompt-operations.module.css +0 -2
  37. package/dist/admin/feedback/prompt-preview.module.css +0 -2
  38. package/dist/admin/revenue/revenue-kpis.js +1 -1
  39. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  40. package/dist/admin/revenue/revenue-workspace.js +1 -1
  41. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  42. package/dist/admin/support/queue-card.module.css +1 -4
  43. package/dist/admin/tenants/[id]/commerce/format.js +2 -1
  44. package/dist/admin/tenants/[id]/commerce/format.js.map +1 -1
  45. package/dist/admin/tenants/[id]/page.js +13 -6
  46. package/dist/admin/tenants/[id]/page.js.map +1 -1
  47. package/dist/admin/tenants/[id]/tenant-layout.module.css +1 -4
  48. package/dist/admin/tenants/[id]/tenant-overview.module.css +1 -1
  49. package/dist/admin/tenants/tenants-table.js +1 -1
  50. package/dist/admin/tenants/tenants-table.js.map +1 -1
  51. package/package.json +5 -5
@@ -9,6 +9,7 @@ function compactAmount(amount, currency) {
9
9
  return new Intl.NumberFormat("en-US", {
10
10
  ...base,
11
11
  style: "currency",
12
+ currencyDisplay: "narrowSymbol",
12
13
  currency
13
14
  }).format(amount);
14
15
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_money/finder-format.ts"],"sourcesContent":["/** Formatting for the merchant finder and its quick view. */\n\n/**\n * The merchant's currency when the row carries one, flat or on the profile.\n * The tenants list does not send it today, so callers fall back to plain.\n */\nexport function currencyOf(row: object): string | null {\n const r = row as {\n currency?: string | null;\n profile?: { currency?: string | null } | null;\n };\n\n return r.currency ?? r.profile?.currency ?? null;\n}\n\n/** A short figure: \"₦1.2M\" in a currency, \"1.2M\" without one. */\nexport function compactAmount(amount: number, currency: string | null): string {\n const base = { notation: 'compact', maximumFractionDigits: 1 } as const;\n\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n ...base,\n style: 'currency',\n currency,\n }).format(amount);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return new Intl.NumberFormat('en-US', base).format(amount ?? 0);\n}\n\n/** An account number cut to its last four: \"•••• 4821\". */\nexport function maskAccount(accountNumber: string): string {\n const last = accountNumber.replace(/\\s/g, '').slice(-4);\n\n return last ? `•••• ${last}` : '–';\n}\n\n/** True when a minor-unit string holds something other than zero. */\nexport function nonZero(minor: string): boolean {\n try {\n return BigInt(minor) !== BigInt(0);\n } catch {\n return false;\n }\n}\n\n/** The wallet ID when the tenant detail sends one (it is not typed yet). */\nexport function walletIdOf(wallet: object | null | undefined): string | null {\n const id = (wallet as { id?: unknown } | null | undefined)?.id;\n\n return typeof id === 'string' && id ? id : null;\n}\n"],"mappings":"AAMO,SAAS,WAAW,KAA4B;AACrD,QAAM,IAAI;AAKV,SAAO,EAAE,YAAY,EAAE,SAAS,YAAY;AAC9C;AAGO,SAAS,cAAc,QAAgB,UAAiC;AAC7E,QAAM,OAAO,EAAE,UAAU,WAAW,uBAAuB,EAAE;AAE7D,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,GAAG;AAAA,QACH,OAAO;AAAA,QACP;AAAA,MACF,CAAC,EAAE,OAAO,MAAM;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,aAAa,SAAS,IAAI,EAAE,OAAO,UAAU,CAAC;AAChE;AAGO,SAAS,YAAY,eAA+B;AACzD,QAAM,OAAO,cAAc,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE;AAEtD,SAAO,OAAO,4BAAQ,IAAI,KAAK;AACjC;AAGO,SAAS,QAAQ,OAAwB;AAC9C,MAAI;AACF,WAAO,OAAO,KAAK,MAAM,OAAO,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,QAAkD;AAC3E,QAAM,KAAM,QAAgD;AAE5D,SAAO,OAAO,OAAO,YAAY,KAAK,KAAK;AAC7C;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_money/finder-format.ts"],"sourcesContent":["/** Formatting for the merchant finder and its quick view. */\n\n/**\n * The merchant's currency when the row carries one, flat or on the profile.\n * The tenants list does not send it today, so callers fall back to plain.\n */\nexport function currencyOf(row: object): string | null {\n const r = row as {\n currency?: string | null;\n profile?: { currency?: string | null } | null;\n };\n\n return r.currency ?? r.profile?.currency ?? null;\n}\n\n/** A short figure: \"₦1.2M\" in a currency, \"1.2M\" without one. */\nexport function compactAmount(amount: number, currency: string | null): string {\n const base = { notation: 'compact', maximumFractionDigits: 1 } as const;\n\n if (currency) {\n try {\n return new Intl.NumberFormat('en-US', {\n ...base,\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n }).format(amount);\n } catch {\n // An unknown code falls through to the plain number.\n }\n }\n\n return new Intl.NumberFormat('en-US', base).format(amount ?? 0);\n}\n\n/** An account number cut to its last four: \"•••• 4821\". */\nexport function maskAccount(accountNumber: string): string {\n const last = accountNumber.replace(/\\s/g, '').slice(-4);\n\n return last ? `•••• ${last}` : '–';\n}\n\n/** True when a minor-unit string holds something other than zero. */\nexport function nonZero(minor: string): boolean {\n try {\n return BigInt(minor) !== BigInt(0);\n } catch {\n return false;\n }\n}\n\n/** The wallet ID when the tenant detail sends one (it is not typed yet). */\nexport function walletIdOf(wallet: object | null | undefined): string | null {\n const id = (wallet as { id?: unknown } | null | undefined)?.id;\n\n return typeof id === 'string' && id ? id : null;\n}\n"],"mappings":"AAMO,SAAS,WAAW,KAA4B;AACrD,QAAM,IAAI;AAKV,SAAO,EAAE,YAAY,EAAE,SAAS,YAAY;AAC9C;AAGO,SAAS,cAAc,QAAgB,UAAiC;AAC7E,QAAM,OAAO,EAAE,UAAU,WAAW,uBAAuB,EAAE;AAE7D,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,KAAK,aAAa,SAAS;AAAA,QACpC,GAAG;AAAA,QACH,OAAO;AAAA,QACP,iBAAiB;AAAA,QACjB;AAAA,MACF,CAAC,EAAE,OAAO,MAAM;AAAA,IAClB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,aAAa,SAAS,IAAI,EAAE,OAAO,UAAU,CAAC;AAChE;AAGO,SAAS,YAAY,eAA+B;AACzD,QAAM,OAAO,cAAc,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE;AAEtD,SAAO,OAAO,4BAAQ,IAAI,KAAK;AACjC;AAGO,SAAS,QAAQ,OAAwB;AAC9C,MAAI;AACF,WAAO,OAAO,KAAK,MAAM,OAAO,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,QAAkD;AAC3E,QAAM,KAAM,QAAgD;AAE5D,SAAO,OAAO,OAAO,YAAY,KAAK,KAAK;AAC7C;","names":[]}
@@ -3,6 +3,7 @@ function money(amount, currency) {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
+ currencyDisplay: "narrowSymbol",
6
7
  currency: currency || "NGN",
7
8
  minimumFractionDigits: 2,
8
9
  maximumFractionDigits: 2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_money/format.ts"],"sourcesContent":["import { formatMinor } from '@withpotter/lib/money-minor';\n\n/** Money and time formatting for the money screens. Amounts are major units. */\n\nexport function money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: currency || 'NGN',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount ?? 0);\n } catch {\n return `${currency} ${(amount ?? 0).toFixed(2)}`;\n }\n}\n\n/**\n * Minor units held as a string, shown in the currency (\"150000\" NGN → ₦1,500.00,\n * \"1500\" JPY → ¥1,500). Split at the currency's own exponent, never ÷ 100.\n */\nexport function moneyMinor(minor: string, currency: string): string {\n return /^-?\\d+$/.test(minor)\n ? formatMinor(minor, currency || 'NGN')\n : `${minor} ${currency}`;\n}\n\nexport function when(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n return d.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function day(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n // \"15 Oct 2026\" everywhere, whatever the browser's locale.\n return d.toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"3h\", \"2d\": how long a record has been open. */\nexport function age(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n const h = Math.floor(ms / 3_600_000);\n\n if (h < 1) return `${Math.max(1, Math.floor(ms / 60_000))}m`;\n\n if (h < 48) return `${h}h`;\n\n return `${Math.floor(h / 24)}d`;\n}\n\n/** Keep the start and end of a long ID: \"8f2c1a…9b0d\". */\nexport function middle(id: string, keep = 6): string {\n return id.length <= keep * 2 + 1\n ? id\n : `${id.slice(0, keep)}…${id.slice(-4)}`;\n}\n\n/** An API error's own words when it has some, a plain line when not. */\nexport function errorText(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n\n return fallback;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAIrB,SAAS,MAAM,QAAgB,UAA0B;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,UAAU,CAAC;AAAA,EACvB,QAAQ;AACN,WAAO,GAAG,QAAQ,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC;AAAA,EAChD;AACF;AAMO,SAAS,WAAW,OAAe,UAA0B;AAClE,SAAO,UAAU,KAAK,KAAK,IACvB,YAAY,OAAO,YAAY,KAAK,IACpC,GAAG,KAAK,IAAI,QAAQ;AAC1B;AAEO,SAAS,KAAK,KAAwC;AAC3D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAEtC,SAAO,EAAE,eAAe,QAAW;AAAA,IACjC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;AAEO,SAAS,IAAI,KAAwC;AAC1D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAGtC,SAAO,EAAE,mBAAmB,SAAS;AAAA,IACnC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC;AACH;AAGO,SAAS,IAAI,KAAa,MAAM,KAAK,IAAI,GAAW;AACzD,QAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,CAAC;AACpD,QAAM,IAAI,KAAK,MAAM,KAAK,IAAS;AAEnC,MAAI,IAAI,EAAG,QAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC,CAAC;AAEzD,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AAEvB,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B;AAGO,SAAS,OAAO,IAAY,OAAO,GAAW;AACnD,SAAO,GAAG,UAAU,OAAO,IAAI,IAC3B,KACA,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,SAAI,GAAG,MAAM,EAAE,CAAC;AAC1C;AAGO,SAAS,UAAU,OAAgB,UAA0B;AAClE,MAAI,iBAAiB,SAAS,MAAM,QAAS,QAAO,MAAM;AAE1D,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_money/format.ts"],"sourcesContent":["import { formatMinor } from '@withpotter/lib/money-minor';\n\n/** Money and time formatting for the money screens. Amounts are major units. */\n\nexport function money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency: currency || 'NGN',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount ?? 0);\n } catch {\n return `${currency} ${(amount ?? 0).toFixed(2)}`;\n }\n}\n\n/**\n * Minor units held as a string, shown in the currency (\"150000\" NGN → ₦1,500.00,\n * \"1500\" JPY → ¥1,500). Split at the currency's own exponent, never ÷ 100.\n */\nexport function moneyMinor(minor: string, currency: string): string {\n return /^-?\\d+$/.test(minor)\n ? formatMinor(minor, currency || 'NGN')\n : `${minor} ${currency}`;\n}\n\nexport function when(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n return d.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function day(iso: string | null | undefined): string {\n if (!iso) return '–';\n\n const d = new Date(iso);\n\n if (Number.isNaN(d.getTime())) return '–';\n\n // \"15 Oct 2026\" everywhere, whatever the browser's locale.\n return d.toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\n/** \"3h\", \"2d\": how long a record has been open. */\nexport function age(iso: string, now = Date.now()): string {\n const ms = Math.max(0, now - new Date(iso).getTime());\n const h = Math.floor(ms / 3_600_000);\n\n if (h < 1) return `${Math.max(1, Math.floor(ms / 60_000))}m`;\n\n if (h < 48) return `${h}h`;\n\n return `${Math.floor(h / 24)}d`;\n}\n\n/** Keep the start and end of a long ID: \"8f2c1a…9b0d\". */\nexport function middle(id: string, keep = 6): string {\n return id.length <= keep * 2 + 1\n ? id\n : `${id.slice(0, keep)}…${id.slice(-4)}`;\n}\n\n/** An API error's own words when it has some, a plain line when not. */\nexport function errorText(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n\n return fallback;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAIrB,SAAS,MAAM,QAAgB,UAA0B;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,UAAU,YAAY;AAAA,MACtB,uBAAuB;AAAA,MACvB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,UAAU,CAAC;AAAA,EACvB,QAAQ;AACN,WAAO,GAAG,QAAQ,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC;AAAA,EAChD;AACF;AAMO,SAAS,WAAW,OAAe,UAA0B;AAClE,SAAO,UAAU,KAAK,KAAK,IACvB,YAAY,OAAO,YAAY,KAAK,IACpC,GAAG,KAAK,IAAI,QAAQ;AAC1B;AAEO,SAAS,KAAK,KAAwC;AAC3D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAEtC,SAAO,EAAE,eAAe,QAAW;AAAA,IACjC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;AAEO,SAAS,IAAI,KAAwC;AAC1D,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,IAAI,IAAI,KAAK,GAAG;AAEtB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAGtC,SAAO,EAAE,mBAAmB,SAAS;AAAA,IACnC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC;AACH;AAGO,SAAS,IAAI,KAAa,MAAM,KAAK,IAAI,GAAW;AACzD,QAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,CAAC;AACpD,QAAM,IAAI,KAAK,MAAM,KAAK,IAAS;AAEnC,MAAI,IAAI,EAAG,QAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC,CAAC;AAEzD,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AAEvB,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B;AAGO,SAAS,OAAO,IAAY,OAAO,GAAW;AACnD,SAAO,GAAG,UAAU,OAAO,IAAI,IAC3B,KACA,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,SAAI,GAAG,MAAM,EAAE,CAAC;AAC1C;AAGO,SAAS,UAAU,OAAgB,UAA0B;AAClE,MAAI,iBAAiB,SAAS,MAAM,QAAS,QAAO,MAAM;AAE1D,SAAO;AACT;","names":[]}
@@ -1,9 +1,6 @@
1
1
  /* Attention panel: filter per kind, rows scroll inside the card. */
2
2
 
3
3
  .filters {
4
- position: sticky;
5
- top: 0;
6
- z-index: 1;
7
4
  display: flex;
8
5
  gap: 4px;
9
6
  padding: 0 20px 12px;
@@ -3,7 +3,8 @@ function inCurrency(value, code) {
3
3
  try {
4
4
  return new Intl.NumberFormat("en-US", {
5
5
  style: "currency",
6
- currency: code || "USD",
6
+ currencyDisplay: "narrowSymbol",
7
+ currency: code || "NGN",
7
8
  maximumFractionDigits: 0
8
9
  }).format(value);
9
10
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_overview/overdue-outstanding.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { totalsByCurrency } from '../_ui/currency-filter-model';\n\ntype Overdue = PlatformCockpit['attention']['overdueInvoices'];\n\n/** A main-unit amount in its own currency: 120000 NGN → \"NGN 120,000\". */\nexport function inCurrency(value: number, code: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code || 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n } catch {\n return `${code} ${Math.round(value)}`;\n }\n}\n\n/**\n * What the overdue invoices still owe, one amount per currency, or null when\n * it cannot be said honestly. Amounts are never added across currencies.\n *\n * The engine sends `totalByCurrency` (every overdue invoice, per currency);\n * `selected` narrows it to the currency filter's choice. An engine without it\n * only has the sample: when the sample IS every overdue invoice it is added\n * up per currency; when there are more, nothing is shown rather than a\n * partial figure. The mixed `total` is never shown.\n */\nexport function overdueOutstanding(\n overdue: Overdue,\n selected = '',\n): string | null {\n if (!overdue.count) return null;\n\n let totals: Array<{ currency: string; amount: number }>;\n if (overdue.totalByCurrency) {\n totals = overdue.totalByCurrency.filter(\n (t) => !selected || t.currency === selected,\n );\n if (!totals.length) return selected ? `No ${selected} outstanding` : null;\n } else {\n if (overdue.sample.length < overdue.count) return null;\n totals = totalsByCurrency(\n overdue.sample,\n (i) => i.currency,\n (i) => i.amountDue,\n ).filter((t) => !selected || t.currency === selected);\n }\n\n const lines = totals.map((t) => inCurrency(t.amount, t.currency));\n\n return lines.length ? `${lines.join(' · ')} outstanding` : null;\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AAK1B,SAAS,WAAW,OAAe,MAAsB;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,UAAU,QAAQ;AAAA,MAClB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO,GAAG,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AACF;AAYO,SAAS,mBACd,SACA,WAAW,IACI;AACf,MAAI,CAAC,QAAQ,MAAO,QAAO;AAE3B,MAAI;AACJ,MAAI,QAAQ,iBAAiB;AAC3B,aAAS,QAAQ,gBAAgB;AAAA,MAC/B,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa;AAAA,IACrC;AACA,QAAI,CAAC,OAAO,OAAQ,QAAO,WAAW,MAAM,QAAQ,iBAAiB;AAAA,EACvE,OAAO;AACL,QAAI,QAAQ,OAAO,SAAS,QAAQ,MAAO,QAAO;AAClD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,CAAC,MAAM,EAAE;AAAA,MACT,CAAC,MAAM,EAAE;AAAA,IACX,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AAAA,EACtD;AAEA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAEhE,SAAO,MAAM,SAAS,GAAG,MAAM,KAAK,QAAK,CAAC,iBAAiB;AAC7D;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_overview/overdue-outstanding.ts"],"sourcesContent":["import type { PlatformCockpit } from '@withpotter/lib/admin-api';\nimport { totalsByCurrency } from '../_ui/currency-filter-model';\n\ntype Overdue = PlatformCockpit['attention']['overdueInvoices'];\n\n/** A main-unit amount in its own currency: 120000 NGN → \"₦120,000\". */\nexport function inCurrency(value: number, code: string): string {\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency: code || 'NGN',\n maximumFractionDigits: 0,\n }).format(value);\n } catch {\n return `${code} ${Math.round(value)}`;\n }\n}\n\n/**\n * What the overdue invoices still owe, one amount per currency, or null when\n * it cannot be said honestly. Amounts are never added across currencies.\n *\n * The engine sends `totalByCurrency` (every overdue invoice, per currency);\n * `selected` narrows it to the currency filter's choice. An engine without it\n * only has the sample: when the sample IS every overdue invoice it is added\n * up per currency; when there are more, nothing is shown rather than a\n * partial figure. The mixed `total` is never shown.\n */\nexport function overdueOutstanding(\n overdue: Overdue,\n selected = '',\n): string | null {\n if (!overdue.count) return null;\n\n let totals: Array<{ currency: string; amount: number }>;\n if (overdue.totalByCurrency) {\n totals = overdue.totalByCurrency.filter(\n (t) => !selected || t.currency === selected,\n );\n if (!totals.length) return selected ? `No ${selected} outstanding` : null;\n } else {\n if (overdue.sample.length < overdue.count) return null;\n totals = totalsByCurrency(\n overdue.sample,\n (i) => i.currency,\n (i) => i.amountDue,\n ).filter((t) => !selected || t.currency === selected);\n }\n\n const lines = totals.map((t) => inCurrency(t.amount, t.currency));\n\n return lines.length ? `${lines.join(' · ')} outstanding` : null;\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AAK1B,SAAS,WAAW,OAAe,MAAsB;AAC9D,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,UAAU,QAAQ;AAAA,MAClB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO,GAAG,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AACF;AAYO,SAAS,mBACd,SACA,WAAW,IACI;AACf,MAAI,CAAC,QAAQ,MAAO,QAAO;AAE3B,MAAI;AACJ,MAAI,QAAQ,iBAAiB;AAC3B,aAAS,QAAQ,gBAAgB;AAAA,MAC/B,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa;AAAA,IACrC;AACA,QAAI,CAAC,OAAO,OAAQ,QAAO,WAAW,MAAM,QAAQ,iBAAiB;AAAA,EACvE,OAAO;AACL,QAAI,QAAQ,OAAO,SAAS,QAAQ,MAAO,QAAO;AAClD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,CAAC,MAAM,EAAE;AAAA,MACT,CAAC,MAAM,EAAE;AAAA,IACX,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AAAA,EACtD;AAEA,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;AAEhE,SAAO,MAAM,SAAS,GAAG,MAAM,KAAK,QAAK,CAAC,iBAAiB;AAC7D;","names":[]}
@@ -31,23 +31,13 @@
31
31
  color: var(--text-secondary);
32
32
  }
33
33
 
34
- /* The table's own scroller holds both axes, so the header can stick to it. */
34
+ /* The list grows with its rows; only a wide table scrolls, sideways. */
35
35
  .card div:has(> table),
36
36
  .fill div:has(> table) {
37
- max-height: calc(var(--adm-panel-h) * 1.4);
38
- overflow: auto;
39
- overscroll-behavior: contain;
37
+ overflow-x: auto;
40
38
  scrollbar-width: thin;
41
39
  }
42
40
 
43
- /* Inside a Panel: the table fills the body and scrolls on its own. */
44
- .fill,
45
- .fill > div,
46
- .fill div:has(> table) {
47
- height: 100%;
48
- max-height: 100%;
49
- }
50
-
51
41
  .note {
52
42
  margin: 0;
53
43
  padding: 0 20px;
@@ -55,12 +45,8 @@
55
45
 
56
46
  .card thead th,
57
47
  .fill thead th {
58
- position: sticky;
59
- top: 0;
60
- z-index: 1;
61
48
  white-space: nowrap;
62
49
  background: var(--bg-base);
63
- box-shadow: inset 0 -1px 0 var(--border-subtle);
64
50
  }
65
51
 
66
52
  .footer {
@@ -4,6 +4,7 @@ function formatMoney(amount, currency, decimals = 0) {
4
4
  try {
5
5
  return new Intl.NumberFormat("en-US", {
6
6
  style: "currency",
7
+ currencyDisplay: "narrowSymbol",
7
8
  currency,
8
9
  maximumFractionDigits: decimals
9
10
  }).format(amount);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"NGN 120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"₦120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}
@@ -1,6 +1,33 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as Popover from "@radix-ui/react-popover";
4
+ import { Info } from "lucide-react";
5
+ import styles from "./page-header.module.css";
3
6
  import { useInWorkspaceTab } from "./workspace-tab";
7
+ function PageInfo({ title, text }) {
8
+ return /* @__PURE__ */ jsx("span", { className: `adm-page-desc ${styles.info}`, children: /* @__PURE__ */ jsxs(Popover.Root, { children: [
9
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
10
+ "button",
11
+ {
12
+ type: "button",
13
+ className: styles.trigger,
14
+ "aria-label": `About ${title}`,
15
+ children: /* @__PURE__ */ jsx(Info, { size: 16, "aria-hidden": true })
16
+ }
17
+ ) }),
18
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
19
+ Popover.Content,
20
+ {
21
+ side: "bottom",
22
+ align: "start",
23
+ sideOffset: 6,
24
+ collisionPadding: 16,
25
+ className: styles.popover,
26
+ children: text
27
+ }
28
+ ) })
29
+ ] }) });
30
+ }
4
31
  function PageHeader({
5
32
  title,
6
33
  description,
@@ -9,15 +36,15 @@ function PageHeader({
9
36
  if (useInWorkspaceTab()) {
10
37
  if (!description && !actions) return null;
11
38
  return /* @__PURE__ */ jsxs("div", { className: "adm-page-header adm-tab-lead", children: [
12
- description && /* @__PURE__ */ jsx("p", { className: "adm-page-desc", children: description }),
39
+ description && /* @__PURE__ */ jsx(PageInfo, { title, text: description }),
13
40
  actions && /* @__PURE__ */ jsx("div", { className: "adm-page-actions", children: actions })
14
41
  ] });
15
42
  }
16
43
  return /* @__PURE__ */ jsxs("div", { className: "adm-page-header", children: [
17
- /* @__PURE__ */ jsx("div", { className: "adm-page-lead", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
44
+ /* @__PURE__ */ jsxs("div", { className: "adm-page-lead", children: [
18
45
  /* @__PURE__ */ jsx("h1", { className: "adm-page-title", children: title }),
19
- description && /* @__PURE__ */ jsx("p", { className: "adm-page-desc", children: description })
20
- ] }) }),
46
+ description && /* @__PURE__ */ jsx(PageInfo, { title, text: description })
47
+ ] }),
21
48
  actions && /* @__PURE__ */ jsx("div", { className: "adm-page-actions", children: actions })
22
49
  ] });
23
50
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/page-header.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport type { Tone } from '../_fx';\nimport { useInWorkspaceTab } from './workspace-tab';\n\n/**\n * Standard page header: title, one line of context, actions on the right.\n * No icon: the nav already says where you are. `icon`/`tone` are accepted\n * and ignored so existing callers compile.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string;\n description?: string;\n actions?: ReactNode;\n icon?: LucideIcon;\n tone?: Tone;\n}) {\n // Inside a workspace tab the tab is the title: keep the context and actions.\n if (useInWorkspaceTab()) {\n if (!description && !actions) return null;\n return (\n <div className=\"adm-page-header adm-tab-lead\">\n {description && <p className=\"adm-page-desc\">{description}</p>}\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"adm-page-header\">\n <div className=\"adm-page-lead\">\n <div style={{ minWidth: 0 }}>\n <h1 className=\"adm-page-title\">{title}</h1>\n {description && <p className=\"adm-page-desc\">{description}</p>}\n </div>\n </div>\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n}\n"],"mappings":";AA2BM,SACkB,KADlB;AAtBN,SAAS,yBAAyB;AAO3B,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAMG;AAED,MAAI,kBAAkB,GAAG;AACvB,QAAI,CAAC,eAAe,CAAC,QAAS,QAAO;AACrC,WACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,qBAAe,oBAAC,OAAE,WAAU,iBAAiB,uBAAY;AAAA,MACzD,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,OACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,wBAAC,SAAI,WAAU,iBACb,+BAAC,SAAI,OAAO,EAAE,UAAU,EAAE,GACxB;AAAA,0BAAC,QAAG,WAAU,kBAAkB,iBAAM;AAAA,MACrC,eAAe,oBAAC,OAAE,WAAU,iBAAiB,uBAAY;AAAA,OAC5D,GACF;AAAA,IACC,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,KACzD;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/page-header.tsx"],"sourcesContent":["'use client';\n\nimport * as Popover from '@radix-ui/react-popover';\nimport { Info } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\nimport type { Tone } from '../_fx';\nimport styles from './page-header.module.css';\nimport { useInWorkspaceTab } from './workspace-tab';\n\n/**\n * The page's explanation: an info icon with the text in a popover, never a\n * line under the title. It keeps the `adm-page-desc` class as a global hook so\n * an app that draws its own info icon can hide this one with CSS.\n */\nfunction PageInfo({ title, text }: { title: string; text: string }) {\n return (\n <span className={`adm-page-desc ${styles.info}`}>\n <Popover.Root>\n <Popover.Trigger asChild>\n <button\n type=\"button\"\n className={styles.trigger}\n aria-label={`About ${title}`}\n >\n <Info size={16} aria-hidden />\n </button>\n </Popover.Trigger>\n <Popover.Portal>\n <Popover.Content\n side=\"bottom\"\n align=\"start\"\n sideOffset={6}\n collisionPadding={16}\n className={styles.popover}\n >\n {text}\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n </span>\n );\n}\n\n/**\n * Standard page header: title, an info icon for context, actions on the right.\n * No icon: the nav already says where you are. `icon`/`tone` are accepted\n * and ignored so existing callers compile.\n */\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string;\n description?: string;\n actions?: ReactNode;\n icon?: LucideIcon;\n tone?: Tone;\n}) {\n // Inside a workspace tab the tab is the title: keep the context and actions.\n if (useInWorkspaceTab()) {\n if (!description && !actions) return null;\n return (\n <div className=\"adm-page-header adm-tab-lead\">\n {description && <PageInfo title={title} text={description} />}\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n }\n\n return (\n <div className=\"adm-page-header\">\n <div className=\"adm-page-lead\">\n <h1 className=\"adm-page-title\">{title}</h1>\n {description && <PageInfo title={title} text={description} />}\n </div>\n {actions && <div className=\"adm-page-actions\">{actions}</div>}\n </div>\n );\n}\n"],"mappings":";AAkBM,SAOM,KAPN;AAhBN,YAAY,aAAa;AACzB,SAAS,YAAY;AAIrB,OAAO,YAAY;AACnB,SAAS,yBAAyB;AAOlC,SAAS,SAAS,EAAE,OAAO,KAAK,GAAoC;AAClE,SACE,oBAAC,UAAK,WAAW,iBAAiB,OAAO,IAAI,IAC3C,+BAAC,QAAQ,MAAR,EACC;AAAA,wBAAC,QAAQ,SAAR,EAAgB,SAAO,MACtB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,cAAY,SAAS,KAAK;AAAA,QAE1B,8BAAC,QAAK,MAAM,IAAI,eAAW,MAAC;AAAA;AAAA,IAC9B,GACF;AAAA,IACA,oBAAC,QAAQ,QAAR,EACC;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,YAAY;AAAA,QACZ,kBAAkB;AAAA,QAClB,WAAW,OAAO;AAAA,QAEjB;AAAA;AAAA,IACH,GACF;AAAA,KACF,GACF;AAEJ;AAOO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAMG;AAED,MAAI,kBAAkB,GAAG;AACvB,QAAI,CAAC,eAAe,CAAC,QAAS,QAAO;AACrC,WACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,qBAAe,oBAAC,YAAS,OAAc,MAAM,aAAa;AAAA,MAC1D,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,OACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,SAAI,WAAU,iBACb;AAAA,0BAAC,QAAG,WAAU,kBAAkB,iBAAM;AAAA,MACrC,eAAe,oBAAC,YAAS,OAAc,MAAM,aAAa;AAAA,OAC7D;AAAA,IACC,WAAW,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA,KACzD;AAEJ;","names":[]}
@@ -0,0 +1,45 @@
1
+ /* The page description as an info icon with a popover. */
2
+ .info {
3
+ display: inline-flex;
4
+ flex-shrink: 0;
5
+ margin: 0;
6
+ }
7
+
8
+ .trigger {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ width: 24px;
13
+ height: 24px;
14
+ padding: 0;
15
+ border: 0;
16
+ border-radius: 6px;
17
+ background: transparent;
18
+ color: var(--text-tertiary);
19
+ cursor: pointer;
20
+ transition: background var(--adm-fast);
21
+ }
22
+
23
+ .trigger:hover,
24
+ .trigger[data-state='open'] {
25
+ background: var(--adm-hover);
26
+ color: var(--text-primary);
27
+ }
28
+
29
+ .trigger:focus-visible {
30
+ outline: 2px solid var(--accent-primary);
31
+ outline-offset: 2px;
32
+ }
33
+
34
+ .popover {
35
+ z-index: 70;
36
+ width: min(320px, calc(100vw - 32px));
37
+ padding: 12px;
38
+ border: 1px solid var(--border-subtle);
39
+ border-radius: var(--adm-card-radius);
40
+ background: var(--bg-base);
41
+ box-shadow: var(--adm-shadow-pop);
42
+ font-size: 13px;
43
+ line-height: 20px;
44
+ color: var(--text-secondary);
45
+ }
@@ -75,17 +75,7 @@
75
75
  padding: 0 0 24px;
76
76
  }
77
77
 
78
- /* A table's own x-scroll wrapper would capture the sticky header; let the
79
- panel body be the one scroller so column headers stay pinned. */
80
- .body div:has(> table) {
81
- overflow: visible;
82
- }
83
-
84
- /* Sticky column headers when a table scrolls inside the card. */
85
78
  .body thead th {
86
- position: sticky;
87
- top: 0;
88
- z-index: 1;
89
79
  background: var(--bg-base);
90
80
  }
91
81
 
@@ -7,7 +7,14 @@ import { StatusPill } from "../audit/status-pill";
7
7
  import { useAccountTenants } from "../hooks";
8
8
  import { CopyText } from "../tenants/[id]/copy-text";
9
9
  import { AccountActivity } from "./account-activity";
10
- import { ACTIVE_HINT, STAGE_LABELS, stoppedAt } from "./account-stage";
10
+ import {
11
+ ACTIVE_HINT,
12
+ providerLabel,
13
+ STAGE_LABELS,
14
+ stageStatus,
15
+ stepStates,
16
+ stoppedAt
17
+ } from "./account-stage";
11
18
  import { AccountSessions } from "./account-sessions";
12
19
  import { Hint } from "./hint";
13
20
  import { AccountVitals } from "./account-vitals";
@@ -20,12 +27,14 @@ function Section({
20
27
  title,
21
28
  index,
22
29
  count,
30
+ hint,
23
31
  children
24
32
  }) {
25
33
  return /* @__PURE__ */ jsxs(Reveal, { as: "section", index, className: styles.section, children: [
26
34
  /* @__PURE__ */ jsxs("h3", { className: styles.heading, children: [
27
35
  title,
28
- count !== void 0 && /* @__PURE__ */ jsx("span", { className: styles.count, children: count })
36
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: styles.count, children: count }),
37
+ hint && /* @__PURE__ */ jsx(Hint, { label: hint })
29
38
  ] }),
30
39
  children
31
40
  ] });
@@ -62,11 +71,11 @@ function AccountPanel({
62
71
  account.stage && /* @__PURE__ */ jsx(
63
72
  StatusPill,
64
73
  {
65
- status: account.stage === "active" ? "active" : "pending",
74
+ status: stageStatus(account.stage),
66
75
  label: STAGE_LABELS[account.stage]
67
76
  }
68
77
  ),
69
- account.authProvider && /* @__PURE__ */ jsx("span", { className: styles.muted, children: account.authProvider })
78
+ account.authProvider && /* @__PURE__ */ jsx("span", { className: styles.muted, children: providerLabel(account.authProvider) })
70
79
  ] }),
71
80
  footer: account && /* @__PURE__ */ jsxs(Fragment, { children: [
72
81
  account.status && /* @__PURE__ */ jsx(
@@ -105,17 +114,31 @@ function AccountPanel({
105
114
  )
106
115
  ] }),
107
116
  children: account && /* @__PURE__ */ jsxs("div", { className: styles.panel, children: [
108
- [verify, impersonate, setStatus].map(
109
- (action, i) => action.isError && /* @__PURE__ */ jsx(Banner, { tone: "error", title: "That didn\u2019t work", children: action.error.message }, i)
110
- ),
111
- /* @__PURE__ */ jsx(AccountVitals, { account, workspaces: liveWorkspaces }),
112
- account.stage && /* @__PURE__ */ jsxs("p", { className: styles.stopped, children: [
117
+ account.stage && /* @__PURE__ */ jsxs("div", { className: styles.stopped, children: [
113
118
  /* @__PURE__ */ jsxs("span", { className: styles.stoppedLabel, children: [
114
119
  "Where this account stopped",
115
120
  /* @__PURE__ */ jsx(Hint, { label: ACTIVE_HINT })
116
121
  ] }),
117
- /* @__PURE__ */ jsx("span", { children: stoppedAt(account) ?? "Active" })
122
+ /* @__PURE__ */ jsx("ol", { className: styles.steps, children: stepStates(account).map(
123
+ (step) => /* @__PURE__ */ jsxs(
124
+ "li",
125
+ {
126
+ className: styles.step,
127
+ "data-state": step.state,
128
+ children: [
129
+ /* @__PURE__ */ jsx("span", { className: styles.stepDot, "aria-hidden": true }),
130
+ step.label
131
+ ]
132
+ },
133
+ step.label
134
+ )
135
+ ) }),
136
+ /* @__PURE__ */ jsx("span", { className: styles.stoppedText, children: stoppedAt(account) ?? "Active" })
118
137
  ] }),
138
+ [verify, impersonate, setStatus].map(
139
+ (action, i) => action.isError && /* @__PURE__ */ jsx(Banner, { tone: "error", title: "That didn\u2019t work", children: action.error.message }, i)
140
+ ),
141
+ /* @__PURE__ */ jsx(AccountVitals, { account, workspaces: liveWorkspaces }),
119
142
  /* @__PURE__ */ jsx(
120
143
  Section,
121
144
  {
@@ -133,7 +156,16 @@ function AccountPanel({
133
156
  }
134
157
  ),
135
158
  /* @__PURE__ */ jsx(Section, { title: "Workspaces", index: 1, count: liveWorkspaces, children: /* @__PURE__ */ jsx(Workspaces, { accountId: account.id }) }),
136
- /* @__PURE__ */ jsx(Section, { title: "Sessions", index: 2, children: /* @__PURE__ */ jsx(AccountSessions, { accountId: account.id }) }),
159
+ /* @__PURE__ */ jsx(
160
+ Section,
161
+ {
162
+ title: "Sessions",
163
+ index: 2,
164
+ count: account.activeSessions,
165
+ hint: "The number is devices signed in now.",
166
+ children: /* @__PURE__ */ jsx(AccountSessions, { accountId: account.id })
167
+ }
168
+ ),
137
169
  /* @__PURE__ */ jsx(Section, { title: "Account ID", index: 3, children: /* @__PURE__ */ jsx(CopyText, { value: account.id }) })
138
170
  ] })
139
171
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/accounts/account-panel.tsx"],"sourcesContent":["'use client';\n\nimport { Banner, Button } from '@withpotter/ui-kit';\nimport type { AccountRow } from '@withpotter/lib/admin-api';\nimport { Reveal } from '../_fx';\nimport { Drawer } from '../_ui';\nimport { StatusPill } from '../audit/status-pill';\nimport { useAccountTenants } from '../hooks';\nimport { CopyText } from '../tenants/[id]/copy-text';\nimport { AccountActivity } from './account-activity';\nimport { ACTIVE_HINT, STAGE_LABELS, stoppedAt } from './account-stage';\nimport { AccountSessions } from './account-sessions';\nimport { Hint } from './hint';\nimport { AccountVitals } from './account-vitals';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport { useAccountActivity } from './use-account-activity';\nimport { Workspaces } from './account-workspaces';\nimport styles from './account-panel.module.css';\n\n/** Enough to open the panel from anywhere; the table row adds the rest. */\nexport type AccountSummary = Pick<AccountRow, 'id' | 'email' | 'name'> &\n Partial<AccountRow>;\n\nfunction Section({\n title,\n index,\n count,\n children,\n}: {\n title: string;\n index: number;\n count?: number;\n children: React.ReactNode;\n}) {\n return (\n <Reveal as=\"section\" index={index} className={styles.section}>\n <h3 className={styles.heading}>\n {title}\n {count !== undefined && <span className={styles.count}>{count}</span>}\n </h3>\n {children}\n </Reveal>\n );\n}\n\nexport function AccountPanel({\n account,\n onClose,\n}: {\n account: AccountSummary | null;\n onClose: () => void;\n}) {\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const tenants = useAccountTenants(account?.id ?? null);\n const activity = useAccountActivity(account?.id ?? null, tenants.data);\n const suspended = account?.status === 'suspended';\n const liveWorkspaces = tenants.data?.filter(\n (w) => w.status !== 'deleted' && w.memberStatus !== 'removed',\n ).length;\n\n return (\n <Drawer\n open={Boolean(account)}\n onClose={onClose}\n width={560}\n title={account?.name || account?.email || 'Account'}\n subtitle={account?.name ? account.email : undefined}\n meta={\n account && (\n <>\n {account.status && <StatusPill status={account.status} />}\n {account.emailVerified !== undefined && (\n <StatusPill\n status={account.emailVerified ? 'verified' : 'unverified'}\n label={\n account.emailVerified ? 'Email verified' : 'Email unverified'\n }\n />\n )}\n {account.stage && (\n <StatusPill\n status={account.stage === 'active' ? 'active' : 'pending'}\n label={STAGE_LABELS[account.stage]}\n />\n )}\n {account.authProvider && (\n <span className={styles.muted}>{account.authProvider}</span>\n )}\n </>\n )\n }\n footer={\n account && (\n <>\n {account.status && (\n <Button\n size=\"sm\"\n variant={suspended ? 'secondary' : 'ghostDanger'}\n loading={setStatus.isPending}\n onClick={() =>\n setStatus.mutate({\n id: account.id,\n status: suspended ? 'active' : 'suspended',\n })\n }\n >\n {suspended ? 'Restore' : 'Suspend'}\n </Button>\n )}\n <span className={styles.spacer} />\n {account.emailVerified === false && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n loading={verify.isPending}\n onClick={() => verify.mutate(account.id)}\n >\n Verify email\n </Button>\n )}\n {ownerView && (\n <Button\n size=\"sm\"\n variant=\"primary\"\n loading={impersonate.isPending}\n onClick={() => impersonate.mutate(account.id)}\n >\n View as user\n </Button>\n )}\n </>\n )\n }\n >\n {account && (\n <div className={styles.panel}>\n {[verify, impersonate, setStatus].map(\n (action, i) =>\n action.isError && (\n <Banner key={i} tone=\"error\" title=\"That didn’t work\">\n {action.error.message}\n </Banner>\n ),\n )}\n\n <AccountVitals account={account} workspaces={liveWorkspaces} />\n\n {account.stage && (\n <p className={styles.stopped}>\n <span className={styles.stoppedLabel}>\n Where this account stopped\n <Hint label={ACTIVE_HINT} />\n </span>\n <span>{stoppedAt(account) ?? 'Active'}</span>\n </p>\n )}\n\n <Section\n title=\"Recent activity\"\n index={0}\n count={activity.entries.length}\n >\n <AccountActivity\n entries={activity.entries}\n isLoading={activity.isLoading}\n skipped={activity.skipped}\n />\n </Section>\n\n <Section title=\"Workspaces\" index={1} count={liveWorkspaces}>\n <Workspaces accountId={account.id} />\n </Section>\n\n <Section title=\"Sessions\" index={2}>\n <AccountSessions accountId={account.id} />\n </Section>\n\n <Section title=\"Account ID\" index={3}>\n <CopyText value={account.id} />\n </Section>\n </div>\n )}\n </Drawer>\n );\n}\n"],"mappings":";AAqCM,SAkCI,UAhCsB,KAF1B;AAnCN,SAAS,QAAQ,cAAc;AAE/B,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAChC,SAAS,aAAa,cAAc,iBAAiB;AACrD,SAAS,uBAAuB;AAChC,SAAS,YAAY;AACrB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAMnB,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACE,qBAAC,UAAO,IAAG,WAAU,OAAc,WAAW,OAAO,SACnD;AAAA,yBAAC,QAAG,WAAW,OAAO,SACnB;AAAA;AAAA,MACA,UAAU,UAAa,oBAAC,UAAK,WAAW,OAAO,OAAQ,iBAAM;AAAA,OAChE;AAAA,IACC;AAAA,KACH;AAEJ;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,WAAW,QAAQ,YAAY,IAAI,kBAAkB;AAC7D,QAAM,YAAY,YAAY,WAAW;AACzC,QAAM,UAAU,kBAAkB,SAAS,MAAM,IAAI;AACrD,QAAM,WAAW,mBAAmB,SAAS,MAAM,MAAM,QAAQ,IAAI;AACrE,QAAM,YAAY,SAAS,WAAW;AACtC,QAAM,iBAAiB,QAAQ,MAAM;AAAA,IACnC,CAAC,MAAM,EAAE,WAAW,aAAa,EAAE,iBAAiB;AAAA,EACtD,EAAE;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,QAAQ,OAAO;AAAA,MACrB;AAAA,MACA,OAAO;AAAA,MACP,OAAO,SAAS,QAAQ,SAAS,SAAS;AAAA,MAC1C,UAAU,SAAS,OAAO,QAAQ,QAAQ;AAAA,MAC1C,MACE,WACE,iCACG;AAAA,gBAAQ,UAAU,oBAAC,cAAW,QAAQ,QAAQ,QAAQ;AAAA,QACtD,QAAQ,kBAAkB,UACzB;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,QAAQ,gBAAgB,aAAa;AAAA,YAC7C,OACE,QAAQ,gBAAgB,mBAAmB;AAAA;AAAA,QAE/C;AAAA,QAED,QAAQ,SACP;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,QAAQ,UAAU,WAAW,WAAW;AAAA,YAChD,OAAO,aAAa,QAAQ,KAAK;AAAA;AAAA,QACnC;AAAA,QAED,QAAQ,gBACP,oBAAC,UAAK,WAAW,OAAO,OAAQ,kBAAQ,cAAa;AAAA,SAEzD;AAAA,MAGJ,QACE,WACE,iCACG;AAAA,gBAAQ,UACP;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,YAAY,cAAc;AAAA,YACnC,SAAS,UAAU;AAAA,YACnB,SAAS,MACP,UAAU,OAAO;AAAA,cACf,IAAI,QAAQ;AAAA,cACZ,QAAQ,YAAY,WAAW;AAAA,YACjC,CAAC;AAAA,YAGF,sBAAY,YAAY;AAAA;AAAA,QAC3B;AAAA,QAEF,oBAAC,UAAK,WAAW,OAAO,QAAQ;AAAA,QAC/B,QAAQ,kBAAkB,SACzB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,OAAO;AAAA,YAChB,SAAS,MAAM,OAAO,OAAO,QAAQ,EAAE;AAAA,YACxC;AAAA;AAAA,QAED;AAAA,QAED,aACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,YAAY;AAAA,YACrB,SAAS,MAAM,YAAY,OAAO,QAAQ,EAAE;AAAA,YAC7C;AAAA;AAAA,QAED;AAAA,SAEJ;AAAA,MAIH,qBACC,qBAAC,SAAI,WAAW,OAAO,OACpB;AAAA,SAAC,QAAQ,aAAa,SAAS,EAAE;AAAA,UAChC,CAAC,QAAQ,MACP,OAAO,WACL,oBAAC,UAAe,MAAK,SAAQ,OAAM,yBAChC,iBAAO,MAAM,WADH,CAEb;AAAA,QAEN;AAAA,QAEA,oBAAC,iBAAc,SAAkB,YAAY,gBAAgB;AAAA,QAE5D,QAAQ,SACP,qBAAC,OAAE,WAAW,OAAO,SACnB;AAAA,+BAAC,UAAK,WAAW,OAAO,cAAc;AAAA;AAAA,YAEpC,oBAAC,QAAK,OAAO,aAAa;AAAA,aAC5B;AAAA,UACA,oBAAC,UAAM,oBAAU,OAAO,KAAK,UAAS;AAAA,WACxC;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,OAAO;AAAA,YACP,OAAO,SAAS,QAAQ;AAAA,YAExB;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,SAAS;AAAA,gBAClB,WAAW,SAAS;AAAA,gBACpB,SAAS,SAAS;AAAA;AAAA,YACpB;AAAA;AAAA,QACF;AAAA,QAEA,oBAAC,WAAQ,OAAM,cAAa,OAAO,GAAG,OAAO,gBAC3C,8BAAC,cAAW,WAAW,QAAQ,IAAI,GACrC;AAAA,QAEA,oBAAC,WAAQ,OAAM,YAAW,OAAO,GAC/B,8BAAC,mBAAgB,WAAW,QAAQ,IAAI,GAC1C;AAAA,QAEA,oBAAC,WAAQ,OAAM,cAAa,OAAO,GACjC,8BAAC,YAAS,OAAO,QAAQ,IAAI,GAC/B;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/accounts/account-panel.tsx"],"sourcesContent":["'use client';\n\nimport { Banner, Button } from '@withpotter/ui-kit';\nimport type { AccountRow } from '@withpotter/lib/admin-api';\nimport { Reveal } from '../_fx';\nimport { Drawer } from '../_ui';\nimport { StatusPill } from '../audit/status-pill';\nimport { useAccountTenants } from '../hooks';\nimport { CopyText } from '../tenants/[id]/copy-text';\nimport { AccountActivity } from './account-activity';\nimport {\n ACTIVE_HINT,\n providerLabel,\n STAGE_LABELS,\n stageStatus,\n stepStates,\n stoppedAt,\n} from './account-stage';\nimport type { StepState } from './account-stage';\nimport { AccountSessions } from './account-sessions';\nimport { Hint } from './hint';\nimport { AccountVitals } from './account-vitals';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport { useAccountActivity } from './use-account-activity';\nimport { Workspaces } from './account-workspaces';\nimport styles from './account-panel.module.css';\n\n/** Enough to open the panel from anywhere; the table row adds the rest. */\nexport type AccountSummary = Pick<AccountRow, 'id' | 'email' | 'name'> &\n Partial<AccountRow>;\n\nfunction Section({\n title,\n index,\n count,\n hint,\n children,\n}: {\n title: string;\n index: number;\n count?: number;\n hint?: string;\n children: React.ReactNode;\n}) {\n return (\n <Reveal as=\"section\" index={index} className={styles.section}>\n <h3 className={styles.heading}>\n {title}\n {count !== undefined && <span className={styles.count}>{count}</span>}\n {hint && <Hint label={hint} />}\n </h3>\n {children}\n </Reveal>\n );\n}\n\nexport function AccountPanel({\n account,\n onClose,\n}: {\n account: AccountSummary | null;\n onClose: () => void;\n}) {\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const tenants = useAccountTenants(account?.id ?? null);\n const activity = useAccountActivity(account?.id ?? null, tenants.data);\n const suspended = account?.status === 'suspended';\n const liveWorkspaces = tenants.data?.filter(\n (w) => w.status !== 'deleted' && w.memberStatus !== 'removed',\n ).length;\n\n return (\n <Drawer\n open={Boolean(account)}\n onClose={onClose}\n width={560}\n title={account?.name || account?.email || 'Account'}\n subtitle={account?.name ? account.email : undefined}\n meta={\n account && (\n <>\n {account.status && <StatusPill status={account.status} />}\n {account.emailVerified !== undefined && (\n <StatusPill\n status={account.emailVerified ? 'verified' : 'unverified'}\n label={\n account.emailVerified ? 'Email verified' : 'Email unverified'\n }\n />\n )}\n {account.stage && (\n <StatusPill\n status={stageStatus(account.stage)}\n label={STAGE_LABELS[account.stage]}\n />\n )}\n {account.authProvider && (\n <span className={styles.muted}>\n {providerLabel(account.authProvider)}\n </span>\n )}\n </>\n )\n }\n footer={\n account && (\n <>\n {account.status && (\n <Button\n size=\"sm\"\n variant={suspended ? 'secondary' : 'ghostDanger'}\n loading={setStatus.isPending}\n onClick={() =>\n setStatus.mutate({\n id: account.id,\n status: suspended ? 'active' : 'suspended',\n })\n }\n >\n {suspended ? 'Restore' : 'Suspend'}\n </Button>\n )}\n <span className={styles.spacer} />\n {account.emailVerified === false && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n loading={verify.isPending}\n onClick={() => verify.mutate(account.id)}\n >\n Verify email\n </Button>\n )}\n {ownerView && (\n <Button\n size=\"sm\"\n variant=\"primary\"\n loading={impersonate.isPending}\n onClick={() => impersonate.mutate(account.id)}\n >\n View as user\n </Button>\n )}\n </>\n )\n }\n >\n {account && (\n <div className={styles.panel}>\n {account.stage && (\n <div className={styles.stopped}>\n <span className={styles.stoppedLabel}>\n Where this account stopped\n <Hint label={ACTIVE_HINT} />\n </span>\n <ol className={styles.steps}>\n {stepStates(account).map(\n (step: { label: string; state: StepState }) => (\n <li\n key={step.label}\n className={styles.step}\n data-state={step.state}\n >\n <span className={styles.stepDot} aria-hidden />\n {step.label}\n </li>\n ),\n )}\n </ol>\n <span className={styles.stoppedText}>\n {stoppedAt(account) ?? 'Active'}\n </span>\n </div>\n )}\n\n {[verify, impersonate, setStatus].map(\n (action, i) =>\n action.isError && (\n <Banner key={i} tone=\"error\" title=\"That didn’t work\">\n {action.error.message}\n </Banner>\n ),\n )}\n\n <AccountVitals account={account} workspaces={liveWorkspaces} />\n\n <Section\n title=\"Recent activity\"\n index={0}\n count={activity.entries.length}\n >\n <AccountActivity\n entries={activity.entries}\n isLoading={activity.isLoading}\n skipped={activity.skipped}\n />\n </Section>\n\n <Section title=\"Workspaces\" index={1} count={liveWorkspaces}>\n <Workspaces accountId={account.id} />\n </Section>\n\n <Section\n title=\"Sessions\"\n index={2}\n count={account.activeSessions}\n hint=\"The number is devices signed in now.\"\n >\n <AccountSessions accountId={account.id} />\n </Section>\n\n <Section title=\"Account ID\" index={3}>\n <CopyText value={account.id} />\n </Section>\n </div>\n )}\n </Drawer>\n );\n}\n"],"mappings":"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","names":[]}
@@ -16,6 +16,10 @@
16
16
  background: var(--bg-base);
17
17
  }
18
18
 
19
+ .vitals[data-count='3'] {
20
+ grid-template-columns: repeat(3, minmax(0, 1fr));
21
+ }
22
+
19
23
  .vital {
20
24
  display: grid;
21
25
  gap: 4px;
@@ -141,21 +145,69 @@
141
145
 
142
146
  .stopped {
143
147
  display: grid;
144
- gap: 4px;
145
- margin: 0;
146
- padding: 12px 16px;
148
+ gap: 12px;
149
+ padding: 16px;
147
150
  border-radius: var(--adm-card-radius);
148
151
  border: 1px solid var(--adm-line);
149
152
  background: var(--bg-base);
150
- font-size: 14px;
151
- font-weight: 500;
152
- line-height: 20px;
153
- color: var(--text-primary);
154
153
  }
155
154
 
156
155
  .stoppedLabel {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 4px;
157
159
  font-size: 12px;
158
160
  font-weight: 400;
159
161
  line-height: 16px;
160
162
  color: var(--text-tertiary);
161
163
  }
164
+
165
+ .steps {
166
+ display: grid;
167
+ grid-template-columns: repeat(4, minmax(0, 1fr));
168
+ gap: 8px;
169
+ margin: 0;
170
+ padding: 0;
171
+ list-style: none;
172
+ }
173
+
174
+ .step {
175
+ display: grid;
176
+ gap: 8px;
177
+ font-size: 12px;
178
+ line-height: 16px;
179
+ color: var(--text-tertiary);
180
+ }
181
+
182
+ .stepDot {
183
+ width: 8px;
184
+ height: 8px;
185
+ border-radius: 999px;
186
+ box-shadow: inset 0 0 0 1px var(--border-subtle);
187
+ }
188
+
189
+ .step[data-state='done'] {
190
+ color: var(--text-secondary);
191
+ }
192
+
193
+ .step[data-state='done'] .stepDot {
194
+ background: var(--adm-ok);
195
+ box-shadow: none;
196
+ }
197
+
198
+ .step[data-state='current'] {
199
+ font-weight: 600;
200
+ color: var(--text-primary);
201
+ }
202
+
203
+ .step[data-state='current'] .stepDot {
204
+ background: var(--adm-wait);
205
+ box-shadow: none;
206
+ }
207
+
208
+ .stoppedText {
209
+ font-size: 14px;
210
+ font-weight: 500;
211
+ line-height: 20px;
212
+ color: var(--text-primary);
213
+ }
@@ -11,7 +11,7 @@ import { EntityAvatar } from "../_fx";
11
11
  import { EMPTY } from "../_ui/list-card";
12
12
  import { StatusPill } from "../audit/status-pill";
13
13
  import { When } from "../audit/when";
14
- import { kycLabel, STAGE_LABELS } from "./account-stage";
14
+ import { kycLabel, STAGE_LABELS, stageStatus } from "./account-stage";
15
15
  import { useAccountActions } from "./use-account-actions";
16
16
  import { useModuleOn } from "../_shell/modules-context";
17
17
  import list from "../_ui/list-card.module.css";
@@ -81,7 +81,7 @@ function AccountListRow({
81
81
  visible.has("stage") && /* @__PURE__ */ jsx("td", { className: list.nowrap, children: a.stage ? /* @__PURE__ */ jsx(
82
82
  StatusPill,
83
83
  {
84
- status: a.stage === "active" ? "active" : "pending",
84
+ status: stageStatus(a.stage),
85
85
  label: STAGE_LABELS[a.stage]
86
86
  }
87
87
  ) : /* @__PURE__ */ jsx("span", { className: list.muted, children: EMPTY }) }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/accounts/account-row.tsx"],"sourcesContent":["'use client';\n\nimport {\n OverflowItem,\n OverflowMenu,\n TableRow,\n useConfirm,\n useToast,\n} from '@withpotter/ui-kit';\nimport type { AccountRow as Account } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { EMPTY } from '../_ui/list-card';\nimport { StatusPill } from '../audit/status-pill';\nimport { When } from '../audit/when';\nimport { kycLabel, STAGE_LABELS } from './account-stage';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport list from '../_ui/list-card.module.css';\nimport styles from './rows.module.css';\n\nexport const PROVIDER_LABELS: Record<string, string> = {\n local: 'Email',\n google: 'Google',\n github: 'GitHub',\n};\n\nconst day = (iso?: string | null) =>\n iso ? new Date(iso).toLocaleDateString() : null;\n\n/** One account: who, how they sign in, status, activity and a ••• menu. */\nexport function AccountListRow({\n a,\n active,\n onOpen,\n visible,\n}: {\n a: Account;\n active: boolean;\n onOpen: () => void;\n /** Which optional columns are showing, in the order the header renders them. */\n visible: Set<string>;\n}) {\n const confirm = useConfirm();\n const { toast } = useToast();\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const suspended = a.status === 'suspended';\n const label = a.name ?? a.email;\n\n const toggleStatus = async () => {\n const ok = await confirm({\n title: suspended ? `Restore ${label}?` : `Suspend ${label}?`,\n description: suspended\n ? 'They can sign in and use their workspaces again.'\n : 'They are signed out everywhere and cannot sign in until restored.',\n confirmLabel: suspended ? 'Restore' : 'Suspend',\n tone: suspended ? 'default' : 'danger',\n });\n if (!ok) return;\n setStatus.mutate(\n { id: a.id, status: suspended ? 'active' : 'suspended' },\n {\n onSuccess: () =>\n toast({ title: `${label} ${suspended ? 'restored' : 'suspended'}` }),\n onError: (e) =>\n toast({\n title: 'The status was not changed',\n description: e.message,\n variant: 'destructive',\n }),\n },\n );\n };\n\n return (\n <TableRow active={active} onClick={onOpen}>\n <td>\n <span className={styles.identity}>\n <EntityAvatar name={label} size={32} round />\n <span className={styles.identityText}>\n <button\n type=\"button\"\n className={styles.nameButton}\n title={label}\n aria-label={`Open ${label}`}\n onClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n >\n {a.name ?? EMPTY}\n </button>\n <span className={styles.secondary} title={a.email}>\n {a.email}\n </span>\n </span>\n </span>\n </td>\n {visible.has('provider') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {PROVIDER_LABELS[a.authProvider] ?? (a.authProvider || EMPTY)}\n </td>\n )}\n <td className={list.nowrap}>\n <StatusPill status={a.status} />\n </td>\n {visible.has('kyc') && (\n <td className={list.nowrap}>\n {kycLabel(a.kycStatus) ?? <span className={list.muted}>{EMPTY}</span>}\n </td>\n )}\n {visible.has('stage') && (\n <td className={list.nowrap}>\n {a.stage ? (\n <StatusPill\n status={a.stage === 'active' ? 'active' : 'pending'}\n label={STAGE_LABELS[a.stage]}\n />\n ) : (\n <span className={list.muted}>{EMPTY}</span>\n )}\n </td>\n )}\n {visible.has('emailVerified') && (\n <td className={list.nowrap}>\n <StatusPill status={a.emailVerified ? 'verified' : 'unverified'} />\n </td>\n )}\n {visible.has('workspaces') && (\n <td className={list.num}>{a.workspaceCount ?? EMPTY}</td>\n )}\n {visible.has('lastLogin') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.lastLoginAt) ?? 'Never'}\n </td>\n )}\n {visible.has('joined') && (\n <td className={`${list.nowrap} ${list.muted}`}>{day(a.createdAt)}</td>\n )}\n {visible.has('firstSale') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.firstSaleAt) ?? 'None yet'}\n </td>\n )}\n {visible.has('activeSessions') && (\n <td className={list.num}>{a.activeSessions ?? EMPTY}</td>\n )}\n {visible.has('lastSeen') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {a.lastSeenAt ? (\n <When iso={a.lastSeenAt} className={styles.when} />\n ) : (\n 'Never'\n )}\n </td>\n )}\n <td className={styles.menuCell} onClick={(e) => e.stopPropagation()}>\n <OverflowMenu label={`Actions for ${label}`}>\n <OverflowItem onClick={onOpen}>View account</OverflowItem>\n {ownerView && (\n <OverflowItem onClick={() => impersonate.mutate(a.id)}>\n View as user\n </OverflowItem>\n )}\n {!a.emailVerified && (\n <OverflowItem\n onClick={() =>\n verify.mutate(a.id, {\n onSuccess: () => toast({ title: `${a.email} verified` }),\n })\n }\n >\n Verify email\n </OverflowItem>\n )}\n <OverflowItem\n onClick={() => void toggleStatus()}\n destructive={!suspended}\n >\n {suspended ? 'Restore' : 'Suspend'}\n </OverflowItem>\n </OverflowMenu>\n </td>\n </TableRow>\n );\n}\n"],"mappings":";AA8EU,cACA,YADA;AA5EV;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,UAAU,oBAAoB;AACvC,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAC5B,OAAO,UAAU;AACjB,OAAO,YAAY;AAEZ,MAAM,kBAA0C;AAAA,EACrD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AACV;AAEA,MAAM,MAAM,CAAC,QACX,MAAM,IAAI,KAAK,GAAG,EAAE,mBAAmB,IAAI;AAGtC,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,UAAU,WAAW;AAC3B,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,EAAE,WAAW,QAAQ,YAAY,IAAI,kBAAkB;AAC7D,QAAM,YAAY,YAAY,WAAW;AACzC,QAAM,YAAY,EAAE,WAAW;AAC/B,QAAM,QAAQ,EAAE,QAAQ,EAAE;AAE1B,QAAM,eAAe,YAAY;AAC/B,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,YAAY,WAAW,KAAK,MAAM,WAAW,KAAK;AAAA,MACzD,aAAa,YACT,qDACA;AAAA,MACJ,cAAc,YAAY,YAAY;AAAA,MACtC,MAAM,YAAY,YAAY;AAAA,IAChC,CAAC;AACD,QAAI,CAAC,GAAI;AACT,cAAU;AAAA,MACR,EAAE,IAAI,EAAE,IAAI,QAAQ,YAAY,WAAW,YAAY;AAAA,MACvD;AAAA,QACE,WAAW,MACT,MAAM,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,aAAa,WAAW,GAAG,CAAC;AAAA,QACrE,SAAS,CAAC,MACR,MAAM;AAAA,UACJ,OAAO;AAAA,UACP,aAAa,EAAE;AAAA,UACf,SAAS;AAAA,QACX,CAAC;AAAA,MACL;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,YAAS,QAAgB,SAAS,QACjC;AAAA,wBAAC,QACC,+BAAC,UAAK,WAAW,OAAO,UACtB;AAAA,0BAAC,gBAAa,MAAM,OAAO,MAAM,IAAI,OAAK,MAAC;AAAA,MAC3C,qBAAC,UAAK,WAAW,OAAO,cACtB;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAW,OAAO;AAAA,YAClB,OAAO;AAAA,YACP,cAAY,QAAQ,KAAK;AAAA,YACzB,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,qBAAO;AAAA,YACT;AAAA,YAEC,YAAE,QAAQ;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,UAAK,WAAW,OAAO,WAAW,OAAO,EAAE,OACzC,YAAE,OACL;AAAA,SACF;AAAA,OACF,GACF;AAAA,IACC,QAAQ,IAAI,UAAU,KACrB,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,0BAAgB,EAAE,YAAY,MAAM,EAAE,gBAAgB,QACzD;AAAA,IAEF,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,cAAW,QAAQ,EAAE,QAAQ,GAChC;AAAA,IACC,QAAQ,IAAI,KAAK,KAChB,oBAAC,QAAG,WAAW,KAAK,QACjB,mBAAS,EAAE,SAAS,KAAK,oBAAC,UAAK,WAAW,KAAK,OAAQ,iBAAM,GAChE;AAAA,IAED,QAAQ,IAAI,OAAO,KAClB,oBAAC,QAAG,WAAW,KAAK,QACjB,YAAE,QACD;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,EAAE,UAAU,WAAW,WAAW;AAAA,QAC1C,OAAO,aAAa,EAAE,KAAK;AAAA;AAAA,IAC7B,IAEA,oBAAC,UAAK,WAAW,KAAK,OAAQ,iBAAM,GAExC;AAAA,IAED,QAAQ,IAAI,eAAe,KAC1B,oBAAC,QAAG,WAAW,KAAK,QAClB,8BAAC,cAAW,QAAQ,EAAE,gBAAgB,aAAa,cAAc,GACnE;AAAA,IAED,QAAQ,IAAI,YAAY,KACvB,oBAAC,QAAG,WAAW,KAAK,KAAM,YAAE,kBAAkB,OAAM;AAAA,IAErD,QAAQ,IAAI,WAAW,KACtB,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,cAAI,EAAE,WAAW,KAAK,SACzB;AAAA,IAED,QAAQ,IAAI,QAAQ,KACnB,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IAAK,cAAI,EAAE,SAAS,GAAE;AAAA,IAElE,QAAQ,IAAI,WAAW,KACtB,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,cAAI,EAAE,WAAW,KAAK,YACzB;AAAA,IAED,QAAQ,IAAI,gBAAgB,KAC3B,oBAAC,QAAG,WAAW,KAAK,KAAM,YAAE,kBAAkB,OAAM;AAAA,IAErD,QAAQ,IAAI,UAAU,KACrB,oBAAC,QAAG,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,IACxC,YAAE,aACD,oBAAC,QAAK,KAAK,EAAE,YAAY,WAAW,OAAO,MAAM,IAEjD,SAEJ;AAAA,IAEF,oBAAC,QAAG,WAAW,OAAO,UAAU,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAChE,+BAAC,gBAAa,OAAO,eAAe,KAAK,IACvC;AAAA,0BAAC,gBAAa,SAAS,QAAQ,0BAAY;AAAA,MAC1C,aACC,oBAAC,gBAAa,SAAS,MAAM,YAAY,OAAO,EAAE,EAAE,GAAG,0BAEvD;AAAA,MAED,CAAC,EAAE,iBACF;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MACP,OAAO,OAAO,EAAE,IAAI;AAAA,YAClB,WAAW,MAAM,MAAM,EAAE,OAAO,GAAG,EAAE,KAAK,YAAY,CAAC;AAAA,UACzD,CAAC;AAAA,UAEJ;AAAA;AAAA,MAED;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,KAAK,aAAa;AAAA,UACjC,aAAa,CAAC;AAAA,UAEb,sBAAY,YAAY;AAAA;AAAA,MAC3B;AAAA,OACF,GACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/accounts/account-row.tsx"],"sourcesContent":["'use client';\n\nimport {\n OverflowItem,\n OverflowMenu,\n TableRow,\n useConfirm,\n useToast,\n} from '@withpotter/ui-kit';\nimport type { AccountRow as Account } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { EMPTY } from '../_ui/list-card';\nimport { StatusPill } from '../audit/status-pill';\nimport { When } from '../audit/when';\nimport { kycLabel, STAGE_LABELS, stageStatus } from './account-stage';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport list from '../_ui/list-card.module.css';\nimport styles from './rows.module.css';\n\nexport const PROVIDER_LABELS: Record<string, string> = {\n local: 'Email',\n google: 'Google',\n github: 'GitHub',\n};\n\nconst day = (iso?: string | null) =>\n iso ? new Date(iso).toLocaleDateString() : null;\n\n/** One account: who, how they sign in, status, activity and a ••• menu. */\nexport function AccountListRow({\n a,\n active,\n onOpen,\n visible,\n}: {\n a: Account;\n active: boolean;\n onOpen: () => void;\n /** Which optional columns are showing, in the order the header renders them. */\n visible: Set<string>;\n}) {\n const confirm = useConfirm();\n const { toast } = useToast();\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const suspended = a.status === 'suspended';\n const label = a.name ?? a.email;\n\n const toggleStatus = async () => {\n const ok = await confirm({\n title: suspended ? `Restore ${label}?` : `Suspend ${label}?`,\n description: suspended\n ? 'They can sign in and use their workspaces again.'\n : 'They are signed out everywhere and cannot sign in until restored.',\n confirmLabel: suspended ? 'Restore' : 'Suspend',\n tone: suspended ? 'default' : 'danger',\n });\n if (!ok) return;\n setStatus.mutate(\n { id: a.id, status: suspended ? 'active' : 'suspended' },\n {\n onSuccess: () =>\n toast({ title: `${label} ${suspended ? 'restored' : 'suspended'}` }),\n onError: (e) =>\n toast({\n title: 'The status was not changed',\n description: e.message,\n variant: 'destructive',\n }),\n },\n );\n };\n\n return (\n <TableRow active={active} onClick={onOpen}>\n <td>\n <span className={styles.identity}>\n <EntityAvatar name={label} size={32} round />\n <span className={styles.identityText}>\n <button\n type=\"button\"\n className={styles.nameButton}\n title={label}\n aria-label={`Open ${label}`}\n onClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n >\n {a.name ?? EMPTY}\n </button>\n <span className={styles.secondary} title={a.email}>\n {a.email}\n </span>\n </span>\n </span>\n </td>\n {visible.has('provider') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {PROVIDER_LABELS[a.authProvider] ?? (a.authProvider || EMPTY)}\n </td>\n )}\n <td className={list.nowrap}>\n <StatusPill status={a.status} />\n </td>\n {visible.has('kyc') && (\n <td className={list.nowrap}>\n {kycLabel(a.kycStatus) ?? <span className={list.muted}>{EMPTY}</span>}\n </td>\n )}\n {visible.has('stage') && (\n <td className={list.nowrap}>\n {a.stage ? (\n <StatusPill\n status={stageStatus(a.stage)}\n label={STAGE_LABELS[a.stage]}\n />\n ) : (\n <span className={list.muted}>{EMPTY}</span>\n )}\n </td>\n )}\n {visible.has('emailVerified') && (\n <td className={list.nowrap}>\n <StatusPill status={a.emailVerified ? 'verified' : 'unverified'} />\n </td>\n )}\n {visible.has('workspaces') && (\n <td className={list.num}>{a.workspaceCount ?? EMPTY}</td>\n )}\n {visible.has('lastLogin') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.lastLoginAt) ?? 'Never'}\n </td>\n )}\n {visible.has('joined') && (\n <td className={`${list.nowrap} ${list.muted}`}>{day(a.createdAt)}</td>\n )}\n {visible.has('firstSale') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.firstSaleAt) ?? 'None yet'}\n </td>\n )}\n {visible.has('activeSessions') && (\n <td className={list.num}>{a.activeSessions ?? EMPTY}</td>\n )}\n {visible.has('lastSeen') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {a.lastSeenAt ? (\n <When iso={a.lastSeenAt} className={styles.when} />\n ) : (\n 'Never'\n )}\n </td>\n )}\n <td className={styles.menuCell} onClick={(e) => e.stopPropagation()}>\n <OverflowMenu label={`Actions for ${label}`}>\n <OverflowItem onClick={onOpen}>View account</OverflowItem>\n {ownerView && (\n <OverflowItem onClick={() => impersonate.mutate(a.id)}>\n View as user\n </OverflowItem>\n )}\n {!a.emailVerified && (\n <OverflowItem\n onClick={() =>\n verify.mutate(a.id, {\n onSuccess: () => toast({ title: `${a.email} verified` }),\n })\n }\n >\n Verify email\n </OverflowItem>\n )}\n <OverflowItem\n onClick={() => void toggleStatus()}\n destructive={!suspended}\n >\n {suspended ? 'Restore' : 'Suspend'}\n </OverflowItem>\n </OverflowMenu>\n </td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}