@burdenoff/microfe-billing 2026.625.3 → 2026.625.5

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 (98) hide show
  1. package/dist/billing/lib/native-purchases.js +2 -44
  2. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +192 -187
  3. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
  4. package/dist/billing/modules/addons/pages/AddonCreatePage.js +322 -317
  5. package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
  6. package/dist/billing/modules/addons/pages/AddonDetailPage.js +190 -185
  7. package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
  8. package/dist/billing/modules/addons/pages/AddonEditPage.js +297 -292
  9. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  10. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +60 -75
  11. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
  12. package/dist/billing/modules/addons/pages/AddonsListPage.js +86 -101
  13. package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
  14. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +124 -121
  15. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
  16. package/dist/billing/modules/analytics/pages/AnalyticsPage.js +103 -111
  17. package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
  18. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +196 -160
  19. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
  20. package/dist/billing/modules/billing/pages/InvoicesListPage.js +58 -73
  21. package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
  22. package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +65 -75
  23. package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
  24. package/dist/billing/modules/billing/pages/RefundsPage.js +399 -399
  25. package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
  26. package/dist/billing/modules/billing/pages/TransactionsPage.js +49 -48
  27. package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
  28. package/dist/billing/modules/billing/pages/UserRefundsPage.js +144 -151
  29. package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
  30. package/dist/billing/modules/checkout/pages/CheckoutPage.js +430 -425
  31. package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
  32. package/dist/billing/modules/coupons/pages/CouponDetailPage.js +121 -93
  33. package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
  34. package/dist/billing/modules/coupons/pages/CouponsListPage.js +154 -152
  35. package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
  36. package/dist/billing/modules/coupons/pages/CreateCouponPage.js +467 -462
  37. package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
  38. package/dist/billing/modules/coupons/pages/EditCouponPage.js +294 -286
  39. package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
  40. package/dist/billing/modules/credits/pages/AdminCreditsPage.js +123 -117
  41. package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
  42. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +43 -42
  43. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
  44. package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +192 -207
  45. package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
  46. package/dist/billing/modules/credits/pages/CreditsPage.js +40 -56
  47. package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
  48. package/dist/billing/modules/dashboard/pages/OverviewPage.js +128 -121
  49. package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
  50. package/dist/billing/modules/discounts/pages/DiscountsListPage.js +165 -180
  51. package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
  52. package/dist/billing/modules/earnings/pages/EarningsPage.js +103 -100
  53. package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
  54. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +175 -175
  55. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
  56. package/dist/billing/modules/earnings/pages/PayoutsPage.js +96 -96
  57. package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
  58. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +42 -40
  59. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
  60. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +45 -38
  61. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
  62. package/dist/billing/modules/plans/pages/PlanCreatePage.js +339 -334
  63. package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
  64. package/dist/billing/modules/plans/pages/PlanDetailPage.js +80 -74
  65. package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
  66. package/dist/billing/modules/plans/pages/PlanEditPage.js +342 -337
  67. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  68. package/dist/billing/modules/plans/pages/PlansBrowsePage.js +38 -53
  69. package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
  70. package/dist/billing/modules/plans/pages/PlansListPage.js +60 -75
  71. package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
  72. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +242 -237
  73. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
  74. package/dist/billing/modules/promotions/pages/PromotionsListPage.js +29 -39
  75. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
  76. package/dist/billing/modules/quota/pages/QuotaPage.js +1 -0
  77. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +299 -294
  78. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
  79. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +191 -186
  80. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
  81. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +308 -303
  82. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
  83. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +57 -78
  84. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
  85. package/dist/billing/modules/settings/pages/SettingsPage.js +79 -78
  86. package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
  87. package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +15 -13
  88. package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
  89. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +338 -299
  90. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
  91. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +82 -103
  92. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
  93. package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +59 -54
  94. package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
  95. package/dist/billing/modules/usage/pages/UsagePage.js +172 -215
  96. package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
  97. package/package.json +2 -2
  98. package/dist/billing/lib/native-purchases.js.map +0 -1
@@ -10,9 +10,9 @@ import { useMemo as l, useState as u } from "react";
10
10
  import { AlertCircle as d, BarChart3 as f, RefreshCw as p, TrendingUp as m, Users as h, Wallet as g } from "lucide-react";
11
11
  import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
12
12
  import { useI18n as b } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
13
- import { CTAOverflowMenu as x } from "@burdenoff/fe-libs/ui";
13
+ import { CTAOverflowMenu as x, EmphasisPanel as S, IllustratedEmptyState as C, PagePurpose as w } from "@burdenoff/fe-libs/ui";
14
14
  //#region src/billing/modules/analytics/pages/AnalyticsPage.tsx
15
- var S = [
15
+ var T = [
16
16
  {
17
17
  value: "LAST_7_DAYS",
18
18
  label: "Last 7 days"
@@ -33,37 +33,37 @@ var S = [
33
33
  value: "LAST_MONTH",
34
34
  label: "Last month"
35
35
  }
36
- ], C = new Set([
36
+ ], E = new Set([
37
37
  c.Active,
38
38
  c.Trialing,
39
39
  c.PastDue,
40
40
  c.Paused
41
41
  ]);
42
- function w(e) {
42
+ function D(e) {
43
43
  let t = new Date(e);
44
44
  return t.setHours(0, 0, 0, 0), t;
45
45
  }
46
- function T(e) {
46
+ function O(e) {
47
47
  let t = /* @__PURE__ */ new Date(), n = t;
48
48
  switch (e) {
49
49
  case "LAST_7_DAYS": {
50
50
  let e = new Date(t);
51
51
  return e.setDate(e.getDate() - 6), {
52
- start: w(e),
52
+ start: D(e),
53
53
  end: n
54
54
  };
55
55
  }
56
56
  case "LAST_30_DAYS": {
57
57
  let e = new Date(t);
58
58
  return e.setDate(e.getDate() - 29), {
59
- start: w(e),
59
+ start: D(e),
60
60
  end: n
61
61
  };
62
62
  }
63
63
  case "LAST_90_DAYS": {
64
64
  let e = new Date(t);
65
65
  return e.setDate(e.getDate() - 89), {
66
- start: w(e),
66
+ start: D(e),
67
67
  end: n
68
68
  };
69
69
  }
@@ -77,45 +77,45 @@ function T(e) {
77
77
  };
78
78
  }
79
79
  }
80
- function E(e, t, n) {
80
+ function k(e, t, n) {
81
81
  if (!e) return !1;
82
82
  let r = new Date(e);
83
83
  return r >= t && r <= n;
84
84
  }
85
- function D(e, t) {
85
+ function A(e, t) {
86
86
  return t === s.Yearly ? e / 12 : e;
87
87
  }
88
- function O(e, t) {
88
+ function j(e, t) {
89
89
  return new Intl.DateTimeFormat("en-US", {
90
90
  month: "short",
91
91
  day: "numeric"
92
92
  }).format(e);
93
93
  }
94
- function k(e, t, n, r) {
94
+ function M(e, t, n, r) {
95
95
  let i = /* @__PURE__ */ new Map();
96
96
  return e.forEach((e) => {
97
97
  let a = new Date(e.paidAt || e.createdAt);
98
98
  if (a < n || a > r) return;
99
- let o = O(a, t);
99
+ let o = j(a, t);
100
100
  i.set(o, (i.get(o) ?? 0) + e.amount);
101
101
  }), Array.from(i.entries()).map(([e, t]) => ({
102
102
  label: e,
103
103
  value: t
104
104
  }));
105
105
  }
106
- function A(e, t, n, r) {
106
+ function N(e, t, n, r) {
107
107
  let i = /* @__PURE__ */ new Map();
108
108
  return e.forEach((e) => {
109
109
  let a = new Date(e.createdAt);
110
110
  if (a < n || a > r) return;
111
- let o = O(a, t);
111
+ let o = j(a, t);
112
112
  i.set(o, (i.get(o) ?? 0) + 1);
113
113
  }), Array.from(i.entries()).map(([e, t]) => ({
114
114
  label: e,
115
115
  value: t
116
116
  }));
117
117
  }
118
- function j(e, t) {
118
+ function P(e, t) {
119
119
  try {
120
120
  return r(e, t);
121
121
  } catch {
@@ -126,12 +126,12 @@ function j(e, t) {
126
126
  }).format(e);
127
127
  }
128
128
  }
129
- var M = () => {
130
- let { t: r } = b(), s = a(), [w, O] = u("LAST_30_DAYS"), { start: M, end: N } = l(() => T(w), [w]), { data: P, loading: F, error: I, refetch: L } = o({ fetchPolicy: "cache-and-network" }), R = l(() => P?.getBillingAccountsByOrg ?? [], [P?.getBillingAccountsByOrg]), z = l(() => P?.getPayments ?? [], [P?.getPayments]), B = l(() => R.flatMap((e) => (e.subscriptions ?? []).filter((e) => !!e)), [R]), V = l(() => B.filter((e) => C.has(e.status)), [B]), H = V.find((e) => e.plan?.currency)?.plan?.currency || z[0]?.currency || R[0]?.currency || "USD", U = l(() => {
131
- let e = V.reduce((e, t) => {
129
+ var F = () => {
130
+ let { t: r } = b(), s = a(), [D, j] = u("LAST_30_DAYS"), { start: F, end: I } = l(() => O(D), [D]), { data: L, loading: R, error: z, refetch: B } = o({ fetchPolicy: "cache-and-network" }), V = l(() => L?.getBillingAccountsByOrg ?? [], [L?.getBillingAccountsByOrg]), H = l(() => L?.getPayments ?? [], [L?.getPayments]), U = l(() => V.flatMap((e) => (e.subscriptions ?? []).filter((e) => !!e)), [V]), W = l(() => U.filter((e) => E.has(e.status)), [U]), G = W.find((e) => e.plan?.currency)?.plan?.currency || H[0]?.currency || V[0]?.currency || "USD", K = l(() => {
131
+ let e = W.reduce((e, t) => {
132
132
  let n = t.plan?.price ?? 0, r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
133
- return e + D(n, t.plan?.duration) * r;
134
- }, 0), t = e * 12, n = R.length, r = R.filter((e) => E(e.createdAt, M, N)).length, i = B.filter((e) => (e.status === c.Canceled || e.status === c.Expired) && E(e.canceledAt || e.endDate, M, N)).length, a = V.length + i, o = a > 0 ? i / a * 100 : 0, s = n > 0 ? e / n : 0;
133
+ return e + A(n, t.plan?.duration) * r;
134
+ }, 0), t = e * 12, n = V.length, r = V.filter((e) => k(e.createdAt, F, I)).length, i = U.filter((e) => (e.status === c.Canceled || e.status === c.Expired) && k(e.canceledAt || e.endDate, F, I)).length, a = W.length + i, o = a > 0 ? i / a * 100 : 0, s = n > 0 ? e / n : 0;
135
135
  return {
136
136
  mrr: e,
137
137
  arr: t,
@@ -140,38 +140,38 @@ var M = () => {
140
140
  ltv: o > 0 ? s / (o / 100) : t,
141
141
  totalCustomers: n,
142
142
  newCustomers: r,
143
- activeSubscriptions: V.length
143
+ activeSubscriptions: W.length
144
144
  };
145
145
  }, [
146
+ W,
146
147
  V,
147
- R,
148
- M,
149
- N,
150
- B
151
- ]), W = l(() => k(z, w, M, N), [
152
- z,
153
- w,
154
- M,
155
- N
156
- ]), G = l(() => A(B, w, M, N), [
157
- w,
158
- M,
159
- N,
160
- B
161
- ]), K = l(() => {
148
+ F,
149
+ I,
150
+ U
151
+ ]), q = l(() => M(H, D, F, I), [
152
+ H,
153
+ D,
154
+ F,
155
+ I
156
+ ]), J = l(() => N(U, D, F, I), [
157
+ D,
158
+ F,
159
+ I,
160
+ U
161
+ ]), Y = l(() => {
162
162
  let e = /* @__PURE__ */ new Map();
163
- return V.forEach((t) => {
164
- let n = t.plan?.name || "Unknown Plan", r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1, i = D(t.plan?.price ?? 0, t.plan?.duration) * r, a = e.get(n);
163
+ return W.forEach((t) => {
164
+ let n = t.plan?.name || "Unknown Plan", r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1, i = A(t.plan?.price ?? 0, t.plan?.duration) * r, a = e.get(n);
165
165
  e.set(n, {
166
166
  name: n,
167
167
  activeSubscriptions: (a?.activeSubscriptions ?? 0) + 1,
168
168
  estimatedMrr: (a?.estimatedMrr ?? 0) + i
169
169
  });
170
170
  }), Array.from(e.values()).sort((e, t) => t.estimatedMrr - e.estimatedMrr);
171
- }, [V]), q = l(() => R.map((e) => {
172
- let t = (e.subscriptions ?? []).filter((e) => e ? C.has(e.status) : !1), n = t.reduce((e, t) => {
171
+ }, [W]), X = l(() => V.map((e) => {
172
+ let t = (e.subscriptions ?? []).filter((e) => e ? E.has(e.status) : !1), n = t.reduce((e, t) => {
173
173
  let n = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
174
- return e + D(t.plan?.price ?? 0, t.plan?.duration) * n;
174
+ return e + A(t.plan?.price ?? 0, t.plan?.duration) * n;
175
175
  }, 0);
176
176
  return {
177
177
  id: e.id ?? e.name ?? e.email ?? "unknown-account",
@@ -180,7 +180,7 @@ var M = () => {
180
180
  activeSubscriptions: t.length,
181
181
  estimatedMrr: n
182
182
  };
183
- }).sort((e, t) => t.estimatedMrr - e.estimatedMrr).slice(0, 5), [R]), J = Math.max(...W.map((e) => e.value), 0), Y = Math.max(...G.map((e) => e.value), 0);
183
+ }).sort((e, t) => t.estimatedMrr - e.estimatedMrr).slice(0, 5), [V]), Z = Math.max(...q.map((e) => e.value), 0), Q = Math.max(...J.map((e) => e.value), 0);
184
184
  return s.canViewAnalytics ? /* @__PURE__ */ y("div", {
185
185
  className: "space-y-6 p-4 sm:p-6",
186
186
  children: [
@@ -190,34 +190,35 @@ var M = () => {
190
190
  actions: /* @__PURE__ */ y("div", {
191
191
  className: "flex flex-col gap-2 sm:flex-row sm:items-center",
192
192
  children: [/* @__PURE__ */ v("select", {
193
- value: w,
194
- onChange: (e) => O(e.target.value),
193
+ value: D,
194
+ onChange: (e) => j(e.target.value),
195
195
  "aria-label": "Analytics period",
196
196
  className: "rounded-input border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
197
- children: S.map((e) => /* @__PURE__ */ v("option", {
197
+ children: T.map((e) => /* @__PURE__ */ v("option", {
198
198
  value: e.value,
199
199
  children: e.label
200
200
  }, e.value))
201
201
  }), /* @__PURE__ */ v(x, { actions: [{
202
202
  label: "Refresh",
203
203
  onSelect: () => {
204
- L();
204
+ B();
205
205
  },
206
206
  icon: /* @__PURE__ */ v(p, { className: "size-4" })
207
207
  }] })]
208
208
  })
209
209
  }),
210
- I && /* @__PURE__ */ y("div", {
210
+ /* @__PURE__ */ v(w, { children: "Understand the financial health of your business at a glance — recurring revenue (MRR/ARR), customer growth, churn and your highest-value customers — all computed from live billing data. Use the period selector to compare timeframes and spot trends." }),
211
+ z && /* @__PURE__ */ y("div", {
211
212
  className: "flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text",
212
213
  children: [/* @__PURE__ */ v(d, { className: "mt-0.5 size-4 shrink-0" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
213
214
  className: "font-medium",
214
215
  children: "Unable to load billing analytics"
215
216
  }), /* @__PURE__ */ v("p", {
216
217
  className: "mt-1 text-sm",
217
- children: I.message
218
+ children: z.message
218
219
  })] })]
219
220
  }),
220
- F && R.length === 0 ? /* @__PURE__ */ v("div", {
221
+ R && V.length === 0 ? /* @__PURE__ */ v("div", {
221
222
  className: "flex min-h-[320px] items-center justify-center rounded-lg border border-border-subtle bg-bg-surface",
222
223
  children: /* @__PURE__ */ y("div", {
223
224
  className: "flex flex-col items-center gap-3 text-text-muted",
@@ -226,66 +227,57 @@ var M = () => {
226
227
  children: "Loading live billing analytics…"
227
228
  })]
228
229
  })
229
- }) : R.length === 0 ? /* @__PURE__ */ y("div", {
230
- className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface p-8 text-center",
231
- children: [
232
- /* @__PURE__ */ v(f, { className: "mx-auto size-10 text-text-muted" }),
233
- /* @__PURE__ */ v("h2", {
234
- className: "mt-4 text-lg font-semibold text-text-primary",
235
- children: "No billing data available yet"
236
- }),
237
- /* @__PURE__ */ v("p", {
238
- className: "mt-2 text-sm text-text-muted",
239
- children: "Create a billing account and complete a subscription or payment to unlock live analytics."
240
- })
241
- ]
230
+ }) : V.length === 0 ? /* @__PURE__ */ v(C, {
231
+ illustration: "empty-data",
232
+ title: "No billing data available yet",
233
+ description: "Create a billing account and complete a subscription or payment to unlock live analytics."
242
234
  }) : /* @__PURE__ */ y(_, { children: [
243
- /* @__PURE__ */ y("div", {
244
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
245
- children: [
246
- /* @__PURE__ */ v(n, {
247
- label: "MRR",
248
- value: j(U.mrr, H),
249
- icon: /* @__PURE__ */ v(m, { className: "size-5" }),
250
- className: "bg-status-info-bg-subtle"
251
- }),
252
- /* @__PURE__ */ v(n, {
253
- label: "ARR",
254
- value: j(U.arr, H),
255
- icon: /* @__PURE__ */ v(f, { className: "size-5" }),
256
- className: "bg-status-success-bg-subtle"
257
- }),
258
- /* @__PURE__ */ v(n, {
259
- label: "ARPU",
260
- value: j(U.arpu, H),
261
- icon: /* @__PURE__ */ v(g, { className: "size-5" })
262
- }),
263
- /* @__PURE__ */ v(n, {
264
- label: "Churn Rate",
265
- value: i(U.churnRate),
266
- icon: /* @__PURE__ */ v(h, { className: "size-5" }),
267
- className: "bg-status-warning-bg-subtle"
268
- })
269
- ]
235
+ /* @__PURE__ */ v(S, {
236
+ className: "p-4 sm:p-5",
237
+ children: /* @__PURE__ */ y("div", {
238
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
239
+ children: [
240
+ /* @__PURE__ */ v(n, {
241
+ label: "MRR",
242
+ value: P(K.mrr, G),
243
+ icon: /* @__PURE__ */ v(m, { className: "size-5" })
244
+ }),
245
+ /* @__PURE__ */ v(n, {
246
+ label: "ARR",
247
+ value: P(K.arr, G),
248
+ icon: /* @__PURE__ */ v(f, { className: "size-5" })
249
+ }),
250
+ /* @__PURE__ */ v(n, {
251
+ label: "ARPU",
252
+ value: P(K.arpu, G),
253
+ icon: /* @__PURE__ */ v(g, { className: "size-5" })
254
+ }),
255
+ /* @__PURE__ */ v(n, {
256
+ label: "Churn Rate",
257
+ value: i(K.churnRate),
258
+ icon: /* @__PURE__ */ v(h, { className: "size-5" })
259
+ })
260
+ ]
261
+ })
270
262
  }),
271
263
  /* @__PURE__ */ y("div", {
272
264
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
273
265
  children: [
274
266
  /* @__PURE__ */ v(n, {
275
267
  label: "Total Customers",
276
- value: U.totalCustomers
268
+ value: K.totalCustomers
277
269
  }),
278
270
  /* @__PURE__ */ v(n, {
279
271
  label: "New Customers",
280
- value: U.newCustomers
272
+ value: K.newCustomers
281
273
  }),
282
274
  /* @__PURE__ */ v(n, {
283
275
  label: "Active Subscriptions",
284
- value: U.activeSubscriptions
276
+ value: K.activeSubscriptions
285
277
  }),
286
278
  /* @__PURE__ */ v(n, {
287
279
  label: "Estimated LTV",
288
- value: j(U.ltv, H)
280
+ value: P(K.ltv, G)
289
281
  })
290
282
  ]
291
283
  }),
@@ -304,17 +296,17 @@ var M = () => {
304
296
  })] }), /* @__PURE__ */ y("span", {
305
297
  className: "rounded-full bg-status-info-bg-subtle px-2.5 py-1 text-xs font-medium text-status-info-text",
306
298
  children: [
307
- W.length,
299
+ q.length,
308
300
  " data point",
309
- W.length === 1 ? "" : "s"
301
+ q.length === 1 ? "" : "s"
310
302
  ]
311
303
  })]
312
- }), W.length === 0 ? /* @__PURE__ */ v("p", {
304
+ }), q.length === 0 ? /* @__PURE__ */ v("p", {
313
305
  className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
314
306
  children: "No successful payments were recorded during this period."
315
307
  }) : /* @__PURE__ */ v("div", {
316
308
  className: "space-y-3",
317
- children: W.map((e) => /* @__PURE__ */ y("div", {
309
+ children: q.map((e) => /* @__PURE__ */ y("div", {
318
310
  className: "space-y-1",
319
311
  children: [/* @__PURE__ */ y("div", {
320
312
  className: "flex items-center justify-between gap-3 text-sm",
@@ -323,13 +315,13 @@ var M = () => {
323
315
  children: e.label
324
316
  }), /* @__PURE__ */ v("span", {
325
317
  className: "text-text-muted",
326
- children: j(e.value, H)
318
+ children: P(e.value, G)
327
319
  })]
328
320
  }), /* @__PURE__ */ v("div", {
329
321
  className: "h-2 rounded-full bg-bg-sunken",
330
322
  children: /* @__PURE__ */ v("div", {
331
323
  className: "h-2 rounded-full bg-action-primary-bg transition-[width]",
332
- style: { width: `${J > 0 ? e.value / J * 100 : 0}%` }
324
+ style: { width: `${Z > 0 ? e.value / Z * 100 : 0}%` }
333
325
  })
334
326
  })]
335
327
  }, e.label))
@@ -346,14 +338,14 @@ var M = () => {
346
338
  children: "New subscriptions created during the selected period."
347
339
  })] }), /* @__PURE__ */ y("span", {
348
340
  className: "rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text",
349
- children: [G.reduce((e, t) => e + t.value, 0), " created"]
341
+ children: [J.reduce((e, t) => e + t.value, 0), " created"]
350
342
  })]
351
- }), G.length === 0 ? /* @__PURE__ */ v("p", {
343
+ }), J.length === 0 ? /* @__PURE__ */ v("p", {
352
344
  className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
353
345
  children: "No new subscriptions were created during this period."
354
346
  }) : /* @__PURE__ */ v("div", {
355
347
  className: "space-y-3",
356
- children: G.map((e) => /* @__PURE__ */ y("div", {
348
+ children: J.map((e) => /* @__PURE__ */ y("div", {
357
349
  className: "space-y-1",
358
350
  children: [/* @__PURE__ */ y("div", {
359
351
  className: "flex items-center justify-between gap-3 text-sm",
@@ -368,7 +360,7 @@ var M = () => {
368
360
  className: "h-2 rounded-full bg-bg-sunken",
369
361
  children: /* @__PURE__ */ v("div", {
370
362
  className: "h-2 rounded-full bg-status-success-bg transition-[width]",
371
- style: { width: `${Y > 0 ? e.value / Y * 100 : 0}%` }
363
+ style: { width: `${Q > 0 ? e.value / Q * 100 : 0}%` }
372
364
  })
373
365
  })]
374
366
  }, e.label))
@@ -390,12 +382,12 @@ var M = () => {
390
382
  })]
391
383
  }), /* @__PURE__ */ v("div", {
392
384
  className: "p-4",
393
- children: K.length === 0 ? /* @__PURE__ */ v("p", {
385
+ children: Y.length === 0 ? /* @__PURE__ */ v("p", {
394
386
  className: "text-sm text-text-muted",
395
387
  children: "No active plans to analyse yet."
396
388
  }) : /* @__PURE__ */ v("div", {
397
389
  className: "space-y-3",
398
- children: K.map((e) => /* @__PURE__ */ y("div", {
390
+ children: Y.map((e) => /* @__PURE__ */ y("div", {
399
391
  className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
400
392
  children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
401
393
  className: "font-medium text-text-primary",
@@ -409,7 +401,7 @@ var M = () => {
409
401
  ]
410
402
  })] }), /* @__PURE__ */ y("p", {
411
403
  className: "text-sm font-semibold text-text-primary",
412
- children: [j(e.estimatedMrr, H), " / month"]
404
+ children: [P(e.estimatedMrr, G), " / month"]
413
405
  })]
414
406
  }, e.name))
415
407
  })
@@ -427,12 +419,12 @@ var M = () => {
427
419
  })]
428
420
  }), /* @__PURE__ */ v("div", {
429
421
  className: "p-4",
430
- children: q.length === 0 ? /* @__PURE__ */ v("p", {
422
+ children: X.length === 0 ? /* @__PURE__ */ v("p", {
431
423
  className: "text-sm text-text-muted",
432
424
  children: "No customer revenue data available yet."
433
425
  }) : /* @__PURE__ */ v("div", {
434
426
  className: "space-y-3",
435
- children: q.map((e) => /* @__PURE__ */ y("div", {
427
+ children: X.map((e) => /* @__PURE__ */ y("div", {
436
428
  className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
437
429
  children: [/* @__PURE__ */ y("div", {
438
430
  className: "min-w-0",
@@ -447,7 +439,7 @@ var M = () => {
447
439
  className: "text-left sm:text-right",
448
440
  children: [/* @__PURE__ */ y("p", {
449
441
  className: "text-sm font-semibold text-text-primary",
450
- children: [j(e.estimatedMrr, H), " / month"]
442
+ children: [P(e.estimatedMrr, G), " / month"]
451
443
  }), /* @__PURE__ */ y("p", {
452
444
  className: "text-xs text-text-muted",
453
445
  children: [
@@ -467,6 +459,6 @@ var M = () => {
467
459
  }) : /* @__PURE__ */ v(e, { message: "You don't have permission to view analytics." });
468
460
  };
469
461
  //#endregion
470
- export { M as AnalyticsPage };
462
+ export { F as AnalyticsPage };
471
463
 
472
464
  //# sourceMappingURL=AnalyticsPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../../src/billing/modules/analytics/pages/AnalyticsPage.tsx"],"sourcesContent":["/**\n * Analytics Module - Revenue Analytics Page\n * Uses real billing data aggregated from billing accounts, subscriptions, and payments.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { AlertCircle, BarChart3, RefreshCw, TrendingUp, Users, Wallet } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CTAOverflowMenu } from '@burdenoff/fe-libs/ui';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, StatCard } from '../../../shared/components';\nimport { formatCurrency, formatPercentage } from '../../../shared/utils/format';\nimport type { AnalyticsPeriod } from '../../../shared/types';\nimport {\n type GetBillingAnalyticsPageDataQuery,\n useGetBillingAnalyticsPageDataQuery,\n} from '../../../../generated/global-operations';\nimport { PlanDuration, SubscriptionStatus } from '../../../../generated/global-types';\n\nconst PERIOD_OPTIONS: { value: AnalyticsPeriod; label: string }[] = [\n { value: 'LAST_7_DAYS' as AnalyticsPeriod, label: 'Last 7 days' },\n { value: 'LAST_30_DAYS' as AnalyticsPeriod, label: 'Last 30 days' },\n { value: 'LAST_90_DAYS' as AnalyticsPeriod, label: 'Last 90 days' },\n { value: 'CURRENT_MONTH' as AnalyticsPeriod, label: 'This month' },\n { value: 'LAST_MONTH' as AnalyticsPeriod, label: 'Last month' },\n];\n\nconst ACTIVE_SUBSCRIPTION_STATUSES = new Set<SubscriptionStatus>([\n SubscriptionStatus.Active,\n SubscriptionStatus.Trialing,\n SubscriptionStatus.PastDue,\n SubscriptionStatus.Paused,\n]);\n\ntype RevenuePoint = {\n label: string;\n value: number;\n};\n\ntype PlanBreakdown = {\n name: string;\n activeSubscriptions: number;\n estimatedMrr: number;\n};\n\ntype CustomerSummary = {\n id: string;\n name: string;\n email: string;\n activeSubscriptions: number;\n estimatedMrr: number;\n};\n\ntype BillingAnalyticsAccount = GetBillingAnalyticsPageDataQuery['getBillingAccountsByOrg'][number];\ntype BillingAnalyticsSubscription = NonNullable<\n NonNullable<BillingAnalyticsAccount['subscriptions']>[number]\n>;\ntype BillingAnalyticsPayment = GetBillingAnalyticsPageDataQuery['getPayments'][number];\n\nfunction startOfDay(date: Date): Date {\n const value = new Date(date);\n value.setHours(0, 0, 0, 0);\n return value;\n}\n\nfunction getPeriodRange(period: AnalyticsPeriod): { start: Date; end: Date } {\n const now = new Date();\n const end = now;\n\n switch (period) {\n case 'LAST_7_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 6);\n return { start: startOfDay(start), end };\n }\n case 'LAST_30_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 29);\n return { start: startOfDay(start), end };\n }\n case 'LAST_90_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 89);\n return { start: startOfDay(start), end };\n }\n case 'LAST_MONTH': {\n const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const monthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);\n return { start, end: monthEnd };\n }\n case 'CURRENT_MONTH':\n default:\n return {\n start: new Date(now.getFullYear(), now.getMonth(), 1),\n end,\n };\n }\n}\n\nfunction isWithinRange(dateValue: string | null | undefined, start: Date, end: Date): boolean {\n if (!dateValue) {\n return false;\n }\n\n const date = new Date(dateValue);\n return date >= start && date <= end;\n}\n\nfunction normalizeRecurringAmount(\n amount: number,\n duration: PlanDuration | null | undefined\n): number {\n if (duration === PlanDuration.Yearly) {\n return amount / 12;\n }\n\n return amount;\n}\n\nfunction getBucketLabel(dateValue: Date, period: AnalyticsPeriod): string {\n if (period === 'LAST_90_DAYS') {\n return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(dateValue);\n }\n\n return new Intl.DateTimeFormat('en-US', {\n month: 'short',\n day: 'numeric',\n }).format(dateValue);\n}\n\nfunction buildRevenueSeries(\n payments: Array<{ amount: number; paidAt?: string | null; createdAt: string }>,\n period: AnalyticsPeriod,\n start: Date,\n end: Date\n): RevenuePoint[] {\n const buckets = new Map<string, number>();\n\n payments.forEach((payment) => {\n const paymentDate = new Date(payment.paidAt || payment.createdAt);\n if (paymentDate < start || paymentDate > end) {\n return;\n }\n\n const label = getBucketLabel(paymentDate, period);\n buckets.set(label, (buckets.get(label) ?? 0) + payment.amount);\n });\n\n return Array.from(buckets.entries()).map(([label, value]) => ({ label, value }));\n}\n\nfunction buildSubscriptionGrowthSeries(\n subscriptions: Array<{ createdAt: string }>,\n period: AnalyticsPeriod,\n start: Date,\n end: Date\n): RevenuePoint[] {\n const buckets = new Map<string, number>();\n\n subscriptions.forEach((subscription) => {\n const createdAt = new Date(subscription.createdAt);\n if (createdAt < start || createdAt > end) {\n return;\n }\n\n const label = getBucketLabel(createdAt, period);\n buckets.set(label, (buckets.get(label) ?? 0) + 1);\n });\n\n return Array.from(buckets.entries()).map(([label, value]) => ({ label, value }));\n}\n\nfunction formatMetricCurrency(value: number, currency: string): string {\n try {\n return formatCurrency(value, currency);\n } catch {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nexport const AnalyticsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const permissions = useBillingPermissions();\n const [period, setPeriod] = useState<AnalyticsPeriod>('LAST_30_DAYS' as AnalyticsPeriod);\n const { start, end } = useMemo(() => getPeriodRange(period), [period]);\n\n const { data, loading, error, refetch } = useGetBillingAnalyticsPageDataQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const billingAccounts = useMemo<BillingAnalyticsAccount[]>(\n () => data?.getBillingAccountsByOrg ?? [],\n [data?.getBillingAccountsByOrg]\n );\n const successfulPayments = useMemo<BillingAnalyticsPayment[]>(\n () => data?.getPayments ?? [],\n [data?.getPayments]\n );\n\n const subscriptions = useMemo<BillingAnalyticsSubscription[]>(\n () =>\n billingAccounts.flatMap((account: BillingAnalyticsAccount) =>\n (account.subscriptions ?? []).filter(\n (subscription): subscription is BillingAnalyticsSubscription => Boolean(subscription)\n )\n ),\n [billingAccounts]\n );\n\n const activeSubscriptions = useMemo<BillingAnalyticsSubscription[]>(\n () =>\n subscriptions.filter((subscription: BillingAnalyticsSubscription) =>\n ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status)\n ),\n [subscriptions]\n );\n\n const displayCurrency =\n activeSubscriptions.find((subscription) => subscription.plan?.currency)?.plan?.currency ||\n successfulPayments[0]?.currency ||\n billingAccounts[0]?.currency ||\n 'USD';\n\n const metrics = useMemo(() => {\n const mrr = activeSubscriptions.reduce(\n (sum: number, subscription: BillingAnalyticsSubscription) => {\n const planPrice = subscription.plan?.price ?? 0;\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n return sum + normalizeRecurringAmount(planPrice, subscription.plan?.duration) * seatCount;\n },\n 0\n );\n\n const arr = mrr * 12;\n const totalCustomers = billingAccounts.length;\n const newCustomers = billingAccounts.filter((account: BillingAnalyticsAccount) =>\n isWithinRange(account.createdAt, start, end)\n ).length;\n const churnedSubscriptions = subscriptions.filter(\n (subscription: BillingAnalyticsSubscription) =>\n (subscription.status === SubscriptionStatus.Canceled ||\n subscription.status === SubscriptionStatus.Expired) &&\n isWithinRange(subscription.canceledAt || subscription.endDate, start, end)\n ).length;\n\n const churnBase = activeSubscriptions.length + churnedSubscriptions;\n const churnRate = churnBase > 0 ? (churnedSubscriptions / churnBase) * 100 : 0;\n const arpu = totalCustomers > 0 ? mrr / totalCustomers : 0;\n const ltv = churnRate > 0 ? arpu / (churnRate / 100) : arr;\n\n return {\n mrr,\n arr,\n arpu,\n churnRate,\n ltv,\n totalCustomers,\n newCustomers,\n activeSubscriptions: activeSubscriptions.length,\n };\n }, [activeSubscriptions, billingAccounts, start, end, subscriptions]);\n\n const revenueSeries = useMemo(\n () => buildRevenueSeries(successfulPayments, period, start, end),\n [successfulPayments, period, start, end]\n );\n\n const subscriptionGrowthSeries = useMemo(\n () => buildSubscriptionGrowthSeries(subscriptions, period, start, end),\n [period, start, end, subscriptions]\n );\n\n const revenueByPlan = useMemo<PlanBreakdown[]>(() => {\n const planMap = new Map<string, PlanBreakdown>();\n\n activeSubscriptions.forEach((subscription: BillingAnalyticsSubscription) => {\n const planName = subscription.plan?.name || 'Unknown Plan';\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n const estimatedMrr =\n normalizeRecurringAmount(subscription.plan?.price ?? 0, subscription.plan?.duration) *\n seatCount;\n\n const current = planMap.get(planName);\n planMap.set(planName, {\n name: planName,\n activeSubscriptions: (current?.activeSubscriptions ?? 0) + 1,\n estimatedMrr: (current?.estimatedMrr ?? 0) + estimatedMrr,\n });\n });\n\n return Array.from(planMap.values()).sort(\n (left: PlanBreakdown, right: PlanBreakdown) => right.estimatedMrr - left.estimatedMrr\n );\n }, [activeSubscriptions]);\n\n const topCustomers = useMemo<CustomerSummary[]>(() => {\n return billingAccounts\n .map((account: BillingAnalyticsAccount) => {\n const accountSubscriptions = (account.subscriptions ?? []).filter(\n (subscription): subscription is BillingAnalyticsSubscription => {\n if (!subscription) {\n return false;\n }\n\n return ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status);\n }\n );\n\n const estimatedMrr = accountSubscriptions.reduce(\n (sum: number, subscription: BillingAnalyticsSubscription) => {\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n return (\n sum +\n normalizeRecurringAmount(subscription.plan?.price ?? 0, subscription.plan?.duration) *\n seatCount\n );\n },\n 0\n );\n\n return {\n id: account.id ?? account.name ?? account.email ?? 'unknown-account',\n name: account.name ?? 'Unnamed account',\n email: account.email ?? 'No contact email',\n activeSubscriptions: accountSubscriptions.length,\n estimatedMrr,\n };\n })\n .sort(\n (left: CustomerSummary, right: CustomerSummary) => right.estimatedMrr - left.estimatedMrr\n )\n .slice(0, 5);\n }, [billingAccounts]);\n\n const maxRevenuePoint = Math.max(...revenueSeries.map((item) => item.value), 0);\n const maxGrowthPoint = Math.max(...subscriptionGrowthSeries.map((item) => item.value), 0);\n\n if (!permissions.canViewAnalytics) {\n return <AccessDenied message=\"You don't have permission to view analytics.\" />;\n }\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <PageHeader\n title=\"Revenue Analytics\"\n description=\"Track recurring revenue, customer growth, and billing performance using live billing data.\"\n actions={\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <select\n value={period}\n onChange={(event) => setPeriod(event.target.value as AnalyticsPeriod)}\n aria-label=\"Analytics period\"\n className=\"rounded-input border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {PERIOD_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: 'Refresh',\n onSelect: () => {\n void refetch();\n },\n icon: <RefreshCw className=\"size-4\" />,\n },\n ]}\n />\n </div>\n }\n />\n\n {error && (\n <div className=\"flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text\">\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n <div>\n <p className=\"font-medium\">Unable to load billing analytics</p>\n <p className=\"mt-1 text-sm\">{error.message}</p>\n </div>\n </div>\n )}\n\n {loading && billingAccounts.length === 0 ? (\n <div className=\"flex min-h-[320px] items-center justify-center rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"flex flex-col items-center gap-3 text-text-muted\">\n <RefreshCw className=\"size-6 animate-spin\" />\n <p className=\"text-sm\">Loading live billing analytics…</p>\n </div>\n </div>\n ) : billingAccounts.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle bg-bg-surface p-8 text-center\">\n <BarChart3 className=\"mx-auto size-10 text-text-muted\" />\n <h2 className=\"mt-4 text-lg font-semibold text-text-primary\">\n No billing data available yet\n </h2>\n <p className=\"mt-2 text-sm text-text-muted\">\n Create a billing account and complete a subscription or payment to unlock live\n analytics.\n </p>\n </div>\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label=\"MRR\"\n value={formatMetricCurrency(metrics.mrr, displayCurrency)}\n icon={<TrendingUp className=\"size-5\" />}\n className=\"bg-status-info-bg-subtle\"\n />\n <StatCard\n label=\"ARR\"\n value={formatMetricCurrency(metrics.arr, displayCurrency)}\n icon={<BarChart3 className=\"size-5\" />}\n className=\"bg-status-success-bg-subtle\"\n />\n <StatCard\n label=\"ARPU\"\n value={formatMetricCurrency(metrics.arpu, displayCurrency)}\n icon={<Wallet className=\"size-5\" />}\n />\n <StatCard\n label=\"Churn Rate\"\n value={formatPercentage(metrics.churnRate)}\n icon={<Users className=\"size-5\" />}\n className=\"bg-status-warning-bg-subtle\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard label=\"Total Customers\" value={metrics.totalCustomers} />\n <StatCard label=\"New Customers\" value={metrics.newCustomers} />\n <StatCard label=\"Active Subscriptions\" value={metrics.activeSubscriptions} />\n <StatCard\n label=\"Estimated LTV\"\n value={formatMetricCurrency(metrics.ltv, displayCurrency)}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-2\">\n <section className=\"rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Revenue over time</h3>\n <p className=\"text-sm text-text-muted\">\n Successful payments received during the selected period.\n </p>\n </div>\n <span className=\"rounded-full bg-status-info-bg-subtle px-2.5 py-1 text-xs font-medium text-status-info-text\">\n {revenueSeries.length} data point{revenueSeries.length === 1 ? '' : 's'}\n </span>\n </div>\n\n {revenueSeries.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted\">\n No successful payments were recorded during this period.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {revenueSeries.map((point) => (\n <div key={point.label} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium text-text-primary\">{point.label}</span>\n <span className=\"text-text-muted\">\n {formatMetricCurrency(point.value, displayCurrency)}\n </span>\n </div>\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg transition-[width]\"\n style={{\n width: `${maxRevenuePoint > 0 ? (point.value / maxRevenuePoint) * 100 : 0}%`,\n }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Subscription growth</h3>\n <p className=\"text-sm text-text-muted\">\n New subscriptions created during the selected period.\n </p>\n </div>\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {subscriptionGrowthSeries.reduce((sum, point) => sum + point.value, 0)} created\n </span>\n </div>\n\n {subscriptionGrowthSeries.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted\">\n No new subscriptions were created during this period.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {subscriptionGrowthSeries.map((point) => (\n <div key={point.label} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium text-text-primary\">{point.label}</span>\n <span className=\"text-text-muted\">{point.value} new</span>\n </div>\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-status-success-bg transition-[width]\"\n style={{\n width: `${maxGrowthPoint > 0 ? (point.value / maxGrowthPoint) * 100 : 0}%`,\n }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-2\">\n <section className=\"rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]\">\n <div className=\"border-b border-border-subtle p-4\">\n <h3 className=\"font-semibold text-text-primary\">Revenue by plan</h3>\n <p className=\"mt-1 text-sm text-text-muted\">\n Estimated monthly recurring revenue based on currently active subscriptions.\n </p>\n </div>\n <div className=\"p-4\">\n {revenueByPlan.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No active plans to analyse yet.</p>\n ) : (\n <div className=\"space-y-3\">\n {revenueByPlan.map((plan) => (\n <div\n key={plan.name}\n className=\"flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div>\n <p className=\"font-medium text-text-primary\">{plan.name}</p>\n <p className=\"text-sm text-text-muted\">\n {plan.activeSubscriptions} active subscription\n {plan.activeSubscriptions === 1 ? '' : 's'}\n </p>\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {formatMetricCurrency(plan.estimatedMrr, displayCurrency)} / month\n </p>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n\n <section className=\"rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]\">\n <div className=\"border-b border-border-subtle p-4\">\n <h3 className=\"font-semibold text-text-primary\">Top customers</h3>\n <p className=\"mt-1 text-sm text-text-muted\">\n Ranked by estimated monthly recurring revenue from active subscriptions.\n </p>\n </div>\n <div className=\"p-4\">\n {topCustomers.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No customer revenue data available yet.</p>\n ) : (\n <div className=\"space-y-3\">\n {topCustomers.map((customer) => (\n <div\n key={customer.id}\n className=\"flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{customer.name}</p>\n <p className=\"truncate text-sm text-text-muted\">{customer.email}</p>\n </div>\n <div className=\"text-left sm:text-right\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {formatMetricCurrency(customer.estimatedMrr, displayCurrency)} / month\n </p>\n <p className=\"text-xs text-text-muted\">\n {customer.activeSubscriptions} active subscription\n {customer.activeSubscriptions === 1 ? '' : 's'}\n </p>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n </div>\n </>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAmBA,IAAM,IAA8D;CAClE;EAAE,OAAO;EAAkC,OAAO;EAAe;CACjE;EAAE,OAAO;EAAmC,OAAO;EAAgB;CACnE;EAAE,OAAO;EAAmC,OAAO;EAAgB;CACnE;EAAE,OAAO;EAAoC,OAAO;EAAc;CAClE;EAAE,OAAO;EAAiC,OAAO;EAAc;CAChE,EAEK,IAA+B,IAAI,IAAwB;CAC/D,EAAmB;CACnB,EAAmB;CACnB,EAAmB;CACnB,EAAmB;CACpB,CAAC;AA2BF,SAAS,EAAW,GAAkB;CACpC,IAAM,IAAQ,IAAI,KAAK,EAAK;AAE5B,QADA,EAAM,SAAS,GAAG,GAAG,GAAG,EAAE,EACnB;;AAGT,SAAS,EAAe,GAAqD;CAC3E,IAAM,oBAAM,IAAI,MAAM,EAChB,IAAM;AAEZ,SAAQ,GAAR;EACE,KAAK,eAAe;GAClB,IAAM,IAAQ,IAAI,KAAK,EAAI;AAE3B,UADA,EAAM,QAAQ,EAAM,SAAS,GAAG,EAAE,EAC3B;IAAE,OAAO,EAAW,EAAM;IAAE;IAAK;;EAE1C,KAAK,gBAAgB;GACnB,IAAM,IAAQ,IAAI,KAAK,EAAI;AAE3B,UADA,EAAM,QAAQ,EAAM,SAAS,GAAG,GAAG,EAC5B;IAAE,OAAO,EAAW,EAAM;IAAE;IAAK;;EAE1C,KAAK,gBAAgB;GACnB,IAAM,IAAQ,IAAI,KAAK,EAAI;AAE3B,UADA,EAAM,QAAQ,EAAM,SAAS,GAAG,GAAG,EAC5B;IAAE,OAAO,EAAW,EAAM;IAAE;IAAK;;EAE1C,KAAK,aAGH,QAAO;GAAE,OAFK,IAAI,KAAK,EAAI,aAAa,EAAE,EAAI,UAAU,GAAG,GAAG,EAAE;GAEhD,KADC,IAAI,KAAK,EAAI,aAAa,EAAE,EAAI,UAAU,EAAE,GAAG,IAAI,IAAI,IAAI,IAAI;GACjD;EAGjC,QACE,QAAO;GACL,OAAO,IAAI,KAAK,EAAI,aAAa,EAAE,EAAI,UAAU,EAAE,EAAE;GACrD;GACD;;;AAIP,SAAS,EAAc,GAAsC,GAAa,GAAoB;AAC5F,KAAI,CAAC,EACH,QAAO;CAGT,IAAM,IAAO,IAAI,KAAK,EAAU;AAChC,QAAO,KAAQ,KAAS,KAAQ;;AAGlC,SAAS,EACP,GACA,GACQ;AAKR,QAJI,MAAa,EAAa,SACrB,IAAS,KAGX;;AAGT,SAAS,EAAe,GAAiB,GAAiC;AAKxE,QAHS,IAAI,KAAK,eAAe,SAAS;EAAE,OAAO;EAAS,KAAK;EAAW,CAAC,CAAC,OAAO,EAAU;;AASjG,SAAS,EACP,GACA,GACA,GACA,GACgB;CAChB,IAAM,oBAAU,IAAI,KAAqB;AAYzC,QAVA,EAAS,SAAS,MAAY;EAC5B,IAAM,IAAc,IAAI,KAAK,EAAQ,UAAU,EAAQ,UAAU;AACjE,MAAI,IAAc,KAAS,IAAc,EACvC;EAGF,IAAM,IAAQ,EAAe,GAAa,EAAO;AACjD,IAAQ,IAAI,IAAQ,EAAQ,IAAI,EAAM,IAAI,KAAK,EAAQ,OAAO;GAC9D,EAEK,MAAM,KAAK,EAAQ,SAAS,CAAC,CAAC,KAAK,CAAC,GAAO,QAAY;EAAE;EAAO;EAAO,EAAE;;AAGlF,SAAS,EACP,GACA,GACA,GACA,GACgB;CAChB,IAAM,oBAAU,IAAI,KAAqB;AAYzC,QAVA,EAAc,SAAS,MAAiB;EACtC,IAAM,IAAY,IAAI,KAAK,EAAa,UAAU;AAClD,MAAI,IAAY,KAAS,IAAY,EACnC;EAGF,IAAM,IAAQ,EAAe,GAAW,EAAO;AAC/C,IAAQ,IAAI,IAAQ,EAAQ,IAAI,EAAM,IAAI,KAAK,EAAE;GACjD,EAEK,MAAM,KAAK,EAAQ,SAAS,CAAC,CAAC,KAAK,CAAC,GAAO,QAAY;EAAE;EAAO;EAAO,EAAE;;AAGlF,SAAS,EAAqB,GAAe,GAA0B;AACrE,KAAI;AACF,SAAO,EAAe,GAAO,EAAS;SAChC;AACN,SAAO,IAAI,KAAK,aAAa,SAAS;GACpC,OAAO;GACP;GACA,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;;;AAIpB,IAAa,UAA0B;CACrC,IAAM,EAAE,SAAM,GAAS,EAKjB,IAAc,GAAuB,EACrC,CAAC,GAAQ,KAAa,EAA0B,eAAkC,EAClF,EAAE,UAAO,WAAQ,QAAc,EAAe,EAAO,EAAE,CAAC,EAAO,CAAC,EAEhE,EAAE,SAAM,YAAS,UAAO,eAAY,EAAoC,EAC5E,aAAa,qBACd,CAAC,EAEI,IAAkB,QAChB,GAAM,2BAA2B,EAAE,EACzC,CAAC,GAAM,wBAAwB,CAChC,EACK,IAAqB,QACnB,GAAM,eAAe,EAAE,EAC7B,CAAC,GAAM,YAAY,CACpB,EAEK,IAAgB,QAElB,EAAgB,SAAS,OACtB,EAAQ,iBAAiB,EAAE,EAAE,QAC3B,MAA+D,EAAQ,EACzE,CACF,EACH,CAAC,EAAgB,CAClB,EAEK,IAAsB,QAExB,EAAc,QAAQ,MACpB,EAA6B,IAAI,EAAa,OAAO,CACtD,EACH,CAAC,EAAc,CAChB,EAEK,IACJ,EAAoB,MAAM,MAAiB,EAAa,MAAM,SAAS,EAAE,MAAM,YAC/E,EAAmB,IAAI,YACvB,EAAgB,IAAI,YACpB,OAEI,IAAU,QAAc;EAC5B,IAAM,IAAM,EAAoB,QAC7B,GAAa,MAA+C;GAC3D,IAAM,IAAY,EAAa,MAAM,SAAS,GACxC,IACJ,EAAa,aAAa,EAAa,YAAY,IAAI,EAAa,YAAY;AAClF,UAAO,IAAM,EAAyB,GAAW,EAAa,MAAM,SAAS,GAAG;KAElF,EACD,EAEK,IAAM,IAAM,IACZ,IAAiB,EAAgB,QACjC,IAAe,EAAgB,QAAQ,MAC3C,EAAc,EAAQ,WAAW,GAAO,EAAI,CAC7C,CAAC,QACI,IAAuB,EAAc,QACxC,OACE,EAAa,WAAW,EAAmB,YAC1C,EAAa,WAAW,EAAmB,YAC7C,EAAc,EAAa,cAAc,EAAa,SAAS,GAAO,EAAI,CAC7E,CAAC,QAEI,IAAY,EAAoB,SAAS,GACzC,IAAY,IAAY,IAAK,IAAuB,IAAa,MAAM,GACvE,IAAO,IAAiB,IAAI,IAAM,IAAiB;AAGzD,SAAO;GACL;GACA;GACA;GACA;GACA,KAPU,IAAY,IAAI,KAAQ,IAAY,OAAO;GAQrD;GACA;GACA,qBAAqB,EAAoB;GAC1C;IACA;EAAC;EAAqB;EAAiB;EAAO;EAAK;EAAc,CAAC,EAE/D,IAAgB,QACd,EAAmB,GAAoB,GAAQ,GAAO,EAAI,EAChE;EAAC;EAAoB;EAAQ;EAAO;EAAI,CACzC,EAEK,IAA2B,QACzB,EAA8B,GAAe,GAAQ,GAAO,EAAI,EACtE;EAAC;EAAQ;EAAO;EAAK;EAAc,CACpC,EAEK,IAAgB,QAA+B;EACnD,IAAM,oBAAU,IAAI,KAA4B;AAkBhD,SAhBA,EAAoB,SAAS,MAA+C;GAC1E,IAAM,IAAW,EAAa,MAAM,QAAQ,gBACtC,IACJ,EAAa,aAAa,EAAa,YAAY,IAAI,EAAa,YAAY,GAC5E,IACJ,EAAyB,EAAa,MAAM,SAAS,GAAG,EAAa,MAAM,SAAS,GACpF,GAEI,IAAU,EAAQ,IAAI,EAAS;AACrC,KAAQ,IAAI,GAAU;IACpB,MAAM;IACN,sBAAsB,GAAS,uBAAuB,KAAK;IAC3D,eAAe,GAAS,gBAAgB,KAAK;IAC9C,CAAC;IACF,EAEK,MAAM,KAAK,EAAQ,QAAQ,CAAC,CAAC,MACjC,GAAqB,MAAyB,EAAM,eAAe,EAAK,aAC1E;IACA,CAAC,EAAoB,CAAC,EAEnB,IAAe,QACZ,EACJ,KAAK,MAAqC;EACzC,IAAM,KAAwB,EAAQ,iBAAiB,EAAE,EAAE,QACxD,MACM,IAIE,EAA6B,IAAI,EAAa,OAAO,GAHnD,GAKZ,EAEK,IAAe,EAAqB,QACvC,GAAa,MAA+C;GAC3D,IAAM,IACJ,EAAa,aAAa,EAAa,YAAY,IAAI,EAAa,YAAY;AAClF,UACE,IACA,EAAyB,EAAa,MAAM,SAAS,GAAG,EAAa,MAAM,SAAS,GAClF;KAGN,EACD;AAED,SAAO;GACL,IAAI,EAAQ,MAAM,EAAQ,QAAQ,EAAQ,SAAS;GACnD,MAAM,EAAQ,QAAQ;GACtB,OAAO,EAAQ,SAAS;GACxB,qBAAqB,EAAqB;GAC1C;GACD;GACD,CACD,MACE,GAAuB,MAA2B,EAAM,eAAe,EAAK,aAC9E,CACA,MAAM,GAAG,EAAE,EACb,CAAC,EAAgB,CAAC,EAEf,IAAkB,KAAK,IAAI,GAAG,EAAc,KAAK,MAAS,EAAK,MAAM,EAAE,EAAE,EACzE,IAAiB,KAAK,IAAI,GAAG,EAAyB,KAAK,MAAS,EAAK,MAAM,EAAE,EAAE;AAMzF,QAJK,EAAY,mBAKf,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,SACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,OAAO;MACP,WAAW,MAAU,EAAU,EAAM,OAAO,MAAyB;MACrE,cAAW;MACX,WAAU;gBAET,EAAe,KAAK,MACnB,kBAAC,UAAD;OAA2B,OAAO,EAAO;iBACtC,EAAO;OACD,EAFI,EAAO,MAEX,CACT;MACK,CAAA,EACT,kBAAC,GAAD,EACE,SAAS,CACP;MACE,OAAO;MACP,gBAAgB;AACT,UAAS;;MAEhB,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;MACvC,CACF,EACD,CAAA,CACE;;IAER,CAAA;GAED,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAa,WAAU,0BAA2B,CAAA,EAClD,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;KAAG,WAAU;eAAc;KAAoC,CAAA,EAC/D,kBAAC,KAAD;KAAG,WAAU;eAAgB,EAAM;KAAY,CAAA,CAC3C,EAAA,CAAA,CACF;;GAGP,KAAW,EAAgB,WAAW,IACrC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAW,WAAU,uBAAwB,CAAA,EAC7C,kBAAC,KAAD;MAAG,WAAU;gBAAU;MAAmC,CAAA,CACtD;;IACF,CAAA,GACJ,EAAgB,WAAW,IAC7B,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,mCAAoC,CAAA;KACzD,kBAAC,MAAD;MAAI,WAAU;gBAA+C;MAExD,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAA+B;MAGxC,CAAA;KACA;QAEN,kBAAA,GAAA,EAAA,UAAA;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAqB,EAAQ,KAAK,EAAgB;OACzD,MAAM,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;OACvC,WAAU;OACV,CAAA;MACF,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAqB,EAAQ,KAAK,EAAgB;OACzD,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;OACtC,WAAU;OACV,CAAA;MACF,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAqB,EAAQ,MAAM,EAAgB;OAC1D,MAAM,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;OACnC,CAAA;MACF,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAiB,EAAQ,UAAU;OAC1C,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;OAClC,WAAU;OACV,CAAA;MACE;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OAAU,OAAM;OAAkB,OAAO,EAAQ;OAAkB,CAAA;MACnE,kBAAC,GAAD;OAAU,OAAM;OAAgB,OAAO,EAAQ;OAAgB,CAAA;MAC/D,kBAAC,GAAD;OAAU,OAAM;OAAuB,OAAO,EAAQ;OAAuB,CAAA;MAC7E,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAqB,EAAQ,KAAK,EAAgB;OACzD,CAAA;MACE;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAsB,CAAA,EACtE,kBAAC,KAAD;QAAG,WAAU;kBAA0B;QAEnC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAc;SAAO;SAAY,EAAc,WAAW,IAAI,KAAK;SAC/D;UACH;UAEL,EAAc,WAAW,IACxB,kBAAC,KAAD;OAAG,WAAU;iBAAyD;OAElE,CAAA,GAEJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,KAAK,MAClB,kBAAC,OAAD;QAAuB,WAAU;kBAAjC,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAiC,EAAM;UAAa,CAAA,EACpE,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAqB,EAAM,OAAO,EAAgB;UAC9C,CAAA,CACH;YACN,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,OAAD;UACE,WAAU;UACV,OAAO,EACL,OAAO,GAAG,IAAkB,IAAK,EAAM,QAAQ,IAAmB,MAAM,EAAE,IAC3E;UACD,CAAA;SACE,CAAA,CACF;UAfI,EAAM,MAeV,CACN;OACE,CAAA,CAEA;SAEV,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAwB,CAAA,EACxE,kBAAC,KAAD;QAAG,WAAU;kBAA0B;QAEnC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,EAAyB,QAAQ,GAAK,MAAU,IAAM,EAAM,OAAO,EAAE,EAAC,WAClE;UACH;UAEL,EAAyB,WAAW,IACnC,kBAAC,KAAD;OAAG,WAAU;iBAAyD;OAElE,CAAA,GAEJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAyB,KAAK,MAC7B,kBAAC,OAAD;QAAuB,WAAU;kBAAjC,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAiC,EAAM;UAAa,CAAA,EACpE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CAAmC,EAAM,OAAM,OAAW;YACtD;YACN,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,OAAD;UACE,WAAU;UACV,OAAO,EACL,OAAO,GAAG,IAAiB,IAAK,EAAM,QAAQ,IAAkB,MAAM,EAAE,IACzE;UACD,CAAA;SACE,CAAA,CACF;UAbI,EAAM,MAaV,CACN;OACE,CAAA,CAEA;QACN;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAoB,CAAA,EACpE,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAExC,CAAA,CACA;UACN,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,WAAW,IACxB,kBAAC,KAAD;QAAG,WAAU;kBAA0B;QAAmC,CAAA,GAE1E,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAc,KAAK,MAClB,kBAAC,OAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAiC,EAAK;UAAS,CAAA,EAC5D,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAK;WAAoB;WACzB,EAAK,wBAAwB,IAAI,KAAK;WACrC;YACA,EAAA,CAAA,EACN,kBAAC,KAAD;UAAG,WAAU;oBAAb,CACG,EAAqB,EAAK,cAAc,EAAgB,EAAC,WACxD;YACA;WAbC,EAAK,KAaN,CACN;QACE,CAAA;OAEJ,CAAA,CACE;SAEV,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAkB,CAAA,EAClE,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAExC,CAAA,CACA;UACN,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAa,WAAW,IACvB,kBAAC,KAAD;QAAG,WAAU;kBAA0B;QAA2C,CAAA,GAElF,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAa,KAAK,MACjB,kBAAC,OAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAA0C,EAAS;WAAS,CAAA,EACzE,kBAAC,KAAD;WAAG,WAAU;qBAAoC,EAAS;WAAU,CAAA,CAChE;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb,CACG,EAAqB,EAAS,cAAc,EAAgB,EAAC,WAC5D;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAS;YAAoB;YAC7B,EAAS,wBAAwB,IAAI,KAAK;YACzC;aACA;YACF;WAhBC,EAAS,GAgBV,CACN;QACE,CAAA;OAEJ,CAAA,CACE;QACN;;IACL,EAAA,CAAA;GAED;MAnQC,kBAAC,GAAD,EAAc,SAAQ,gDAAiD,CAAA"}
1
+ {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../../src/billing/modules/analytics/pages/AnalyticsPage.tsx"],"sourcesContent":["/**\n * Analytics Module - Revenue Analytics Page\n * Uses real billing data aggregated from billing accounts, subscriptions, and payments.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { AlertCircle, BarChart3, RefreshCw, TrendingUp, Users, Wallet } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n CTAOverflowMenu,\n PagePurpose,\n EmphasisPanel,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader, StatCard } from '../../../shared/components';\nimport { formatCurrency, formatPercentage } from '../../../shared/utils/format';\nimport type { AnalyticsPeriod } from '../../../shared/types';\nimport {\n type GetBillingAnalyticsPageDataQuery,\n useGetBillingAnalyticsPageDataQuery,\n} from '../../../../generated/global-operations';\nimport { PlanDuration, SubscriptionStatus } from '../../../../generated/global-types';\n\nconst PERIOD_OPTIONS: { value: AnalyticsPeriod; label: string }[] = [\n { value: 'LAST_7_DAYS' as AnalyticsPeriod, label: 'Last 7 days' },\n { value: 'LAST_30_DAYS' as AnalyticsPeriod, label: 'Last 30 days' },\n { value: 'LAST_90_DAYS' as AnalyticsPeriod, label: 'Last 90 days' },\n { value: 'CURRENT_MONTH' as AnalyticsPeriod, label: 'This month' },\n { value: 'LAST_MONTH' as AnalyticsPeriod, label: 'Last month' },\n];\n\nconst ACTIVE_SUBSCRIPTION_STATUSES = new Set<SubscriptionStatus>([\n SubscriptionStatus.Active,\n SubscriptionStatus.Trialing,\n SubscriptionStatus.PastDue,\n SubscriptionStatus.Paused,\n]);\n\ntype RevenuePoint = {\n label: string;\n value: number;\n};\n\ntype PlanBreakdown = {\n name: string;\n activeSubscriptions: number;\n estimatedMrr: number;\n};\n\ntype CustomerSummary = {\n id: string;\n name: string;\n email: string;\n activeSubscriptions: number;\n estimatedMrr: number;\n};\n\ntype BillingAnalyticsAccount = GetBillingAnalyticsPageDataQuery['getBillingAccountsByOrg'][number];\ntype BillingAnalyticsSubscription = NonNullable<\n NonNullable<BillingAnalyticsAccount['subscriptions']>[number]\n>;\ntype BillingAnalyticsPayment = GetBillingAnalyticsPageDataQuery['getPayments'][number];\n\nfunction startOfDay(date: Date): Date {\n const value = new Date(date);\n value.setHours(0, 0, 0, 0);\n return value;\n}\n\nfunction getPeriodRange(period: AnalyticsPeriod): { start: Date; end: Date } {\n const now = new Date();\n const end = now;\n\n switch (period) {\n case 'LAST_7_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 6);\n return { start: startOfDay(start), end };\n }\n case 'LAST_30_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 29);\n return { start: startOfDay(start), end };\n }\n case 'LAST_90_DAYS': {\n const start = new Date(now);\n start.setDate(start.getDate() - 89);\n return { start: startOfDay(start), end };\n }\n case 'LAST_MONTH': {\n const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const monthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);\n return { start, end: monthEnd };\n }\n case 'CURRENT_MONTH':\n default:\n return {\n start: new Date(now.getFullYear(), now.getMonth(), 1),\n end,\n };\n }\n}\n\nfunction isWithinRange(dateValue: string | null | undefined, start: Date, end: Date): boolean {\n if (!dateValue) {\n return false;\n }\n\n const date = new Date(dateValue);\n return date >= start && date <= end;\n}\n\nfunction normalizeRecurringAmount(\n amount: number,\n duration: PlanDuration | null | undefined\n): number {\n if (duration === PlanDuration.Yearly) {\n return amount / 12;\n }\n\n return amount;\n}\n\nfunction getBucketLabel(dateValue: Date, period: AnalyticsPeriod): string {\n if (period === 'LAST_90_DAYS') {\n return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(dateValue);\n }\n\n return new Intl.DateTimeFormat('en-US', {\n month: 'short',\n day: 'numeric',\n }).format(dateValue);\n}\n\nfunction buildRevenueSeries(\n payments: Array<{ amount: number; paidAt?: string | null; createdAt: string }>,\n period: AnalyticsPeriod,\n start: Date,\n end: Date\n): RevenuePoint[] {\n const buckets = new Map<string, number>();\n\n payments.forEach((payment) => {\n const paymentDate = new Date(payment.paidAt || payment.createdAt);\n if (paymentDate < start || paymentDate > end) {\n return;\n }\n\n const label = getBucketLabel(paymentDate, period);\n buckets.set(label, (buckets.get(label) ?? 0) + payment.amount);\n });\n\n return Array.from(buckets.entries()).map(([label, value]) => ({ label, value }));\n}\n\nfunction buildSubscriptionGrowthSeries(\n subscriptions: Array<{ createdAt: string }>,\n period: AnalyticsPeriod,\n start: Date,\n end: Date\n): RevenuePoint[] {\n const buckets = new Map<string, number>();\n\n subscriptions.forEach((subscription) => {\n const createdAt = new Date(subscription.createdAt);\n if (createdAt < start || createdAt > end) {\n return;\n }\n\n const label = getBucketLabel(createdAt, period);\n buckets.set(label, (buckets.get(label) ?? 0) + 1);\n });\n\n return Array.from(buckets.entries()).map(([label, value]) => ({ label, value }));\n}\n\nfunction formatMetricCurrency(value: number, currency: string): string {\n try {\n return formatCurrency(value, currency);\n } catch {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nexport const AnalyticsPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const permissions = useBillingPermissions();\n const [period, setPeriod] = useState<AnalyticsPeriod>('LAST_30_DAYS' as AnalyticsPeriod);\n const { start, end } = useMemo(() => getPeriodRange(period), [period]);\n\n const { data, loading, error, refetch } = useGetBillingAnalyticsPageDataQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const billingAccounts = useMemo<BillingAnalyticsAccount[]>(\n () => data?.getBillingAccountsByOrg ?? [],\n [data?.getBillingAccountsByOrg]\n );\n const successfulPayments = useMemo<BillingAnalyticsPayment[]>(\n () => data?.getPayments ?? [],\n [data?.getPayments]\n );\n\n const subscriptions = useMemo<BillingAnalyticsSubscription[]>(\n () =>\n billingAccounts.flatMap((account: BillingAnalyticsAccount) =>\n (account.subscriptions ?? []).filter(\n (subscription): subscription is BillingAnalyticsSubscription => Boolean(subscription)\n )\n ),\n [billingAccounts]\n );\n\n const activeSubscriptions = useMemo<BillingAnalyticsSubscription[]>(\n () =>\n subscriptions.filter((subscription: BillingAnalyticsSubscription) =>\n ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status)\n ),\n [subscriptions]\n );\n\n const displayCurrency =\n activeSubscriptions.find((subscription) => subscription.plan?.currency)?.plan?.currency ||\n successfulPayments[0]?.currency ||\n billingAccounts[0]?.currency ||\n 'USD';\n\n const metrics = useMemo(() => {\n const mrr = activeSubscriptions.reduce(\n (sum: number, subscription: BillingAnalyticsSubscription) => {\n const planPrice = subscription.plan?.price ?? 0;\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n return sum + normalizeRecurringAmount(planPrice, subscription.plan?.duration) * seatCount;\n },\n 0\n );\n\n const arr = mrr * 12;\n const totalCustomers = billingAccounts.length;\n const newCustomers = billingAccounts.filter((account: BillingAnalyticsAccount) =>\n isWithinRange(account.createdAt, start, end)\n ).length;\n const churnedSubscriptions = subscriptions.filter(\n (subscription: BillingAnalyticsSubscription) =>\n (subscription.status === SubscriptionStatus.Canceled ||\n subscription.status === SubscriptionStatus.Expired) &&\n isWithinRange(subscription.canceledAt || subscription.endDate, start, end)\n ).length;\n\n const churnBase = activeSubscriptions.length + churnedSubscriptions;\n const churnRate = churnBase > 0 ? (churnedSubscriptions / churnBase) * 100 : 0;\n const arpu = totalCustomers > 0 ? mrr / totalCustomers : 0;\n const ltv = churnRate > 0 ? arpu / (churnRate / 100) : arr;\n\n return {\n mrr,\n arr,\n arpu,\n churnRate,\n ltv,\n totalCustomers,\n newCustomers,\n activeSubscriptions: activeSubscriptions.length,\n };\n }, [activeSubscriptions, billingAccounts, start, end, subscriptions]);\n\n const revenueSeries = useMemo(\n () => buildRevenueSeries(successfulPayments, period, start, end),\n [successfulPayments, period, start, end]\n );\n\n const subscriptionGrowthSeries = useMemo(\n () => buildSubscriptionGrowthSeries(subscriptions, period, start, end),\n [period, start, end, subscriptions]\n );\n\n const revenueByPlan = useMemo<PlanBreakdown[]>(() => {\n const planMap = new Map<string, PlanBreakdown>();\n\n activeSubscriptions.forEach((subscription: BillingAnalyticsSubscription) => {\n const planName = subscription.plan?.name || 'Unknown Plan';\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n const estimatedMrr =\n normalizeRecurringAmount(subscription.plan?.price ?? 0, subscription.plan?.duration) *\n seatCount;\n\n const current = planMap.get(planName);\n planMap.set(planName, {\n name: planName,\n activeSubscriptions: (current?.activeSubscriptions ?? 0) + 1,\n estimatedMrr: (current?.estimatedMrr ?? 0) + estimatedMrr,\n });\n });\n\n return Array.from(planMap.values()).sort(\n (left: PlanBreakdown, right: PlanBreakdown) => right.estimatedMrr - left.estimatedMrr\n );\n }, [activeSubscriptions]);\n\n const topCustomers = useMemo<CustomerSummary[]>(() => {\n return billingAccounts\n .map((account: BillingAnalyticsAccount) => {\n const accountSubscriptions = (account.subscriptions ?? []).filter(\n (subscription): subscription is BillingAnalyticsSubscription => {\n if (!subscription) {\n return false;\n }\n\n return ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status);\n }\n );\n\n const estimatedMrr = accountSubscriptions.reduce(\n (sum: number, subscription: BillingAnalyticsSubscription) => {\n const seatCount =\n subscription.seatCount && subscription.seatCount > 0 ? subscription.seatCount : 1;\n return (\n sum +\n normalizeRecurringAmount(subscription.plan?.price ?? 0, subscription.plan?.duration) *\n seatCount\n );\n },\n 0\n );\n\n return {\n id: account.id ?? account.name ?? account.email ?? 'unknown-account',\n name: account.name ?? 'Unnamed account',\n email: account.email ?? 'No contact email',\n activeSubscriptions: accountSubscriptions.length,\n estimatedMrr,\n };\n })\n .sort(\n (left: CustomerSummary, right: CustomerSummary) => right.estimatedMrr - left.estimatedMrr\n )\n .slice(0, 5);\n }, [billingAccounts]);\n\n const maxRevenuePoint = Math.max(...revenueSeries.map((item) => item.value), 0);\n const maxGrowthPoint = Math.max(...subscriptionGrowthSeries.map((item) => item.value), 0);\n\n if (!permissions.canViewAnalytics) {\n return <AccessDenied message=\"You don't have permission to view analytics.\" />;\n }\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <PageHeader\n title=\"Revenue Analytics\"\n description=\"Track recurring revenue, customer growth, and billing performance using live billing data.\"\n actions={\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <select\n value={period}\n onChange={(event) => setPeriod(event.target.value as AnalyticsPeriod)}\n aria-label=\"Analytics period\"\n className=\"rounded-input border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {PERIOD_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <CTAOverflowMenu\n actions={[\n {\n label: 'Refresh',\n onSelect: () => {\n void refetch();\n },\n icon: <RefreshCw className=\"size-4\" />,\n },\n ]}\n />\n </div>\n }\n />\n\n <PagePurpose>\n Understand the financial health of your business at a glance — recurring revenue (MRR/ARR),\n customer growth, churn and your highest-value customers — all computed from live billing\n data. Use the period selector to compare timeframes and spot trends.\n </PagePurpose>\n\n {error && (\n <div className=\"flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text\">\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n <div>\n <p className=\"font-medium\">Unable to load billing analytics</p>\n <p className=\"mt-1 text-sm\">{error.message}</p>\n </div>\n </div>\n )}\n\n {loading && billingAccounts.length === 0 ? (\n <div className=\"flex min-h-[320px] items-center justify-center rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"flex flex-col items-center gap-3 text-text-muted\">\n <RefreshCw className=\"size-6 animate-spin\" />\n <p className=\"text-sm\">Loading live billing analytics…</p>\n </div>\n </div>\n ) : billingAccounts.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No billing data available yet\"\n description=\"Create a billing account and complete a subscription or payment to unlock live analytics.\"\n />\n ) : (\n <>\n {/* Headline revenue metrics — the page's primary emphasis zone */}\n <EmphasisPanel className=\"p-4 sm:p-5\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label=\"MRR\"\n value={formatMetricCurrency(metrics.mrr, displayCurrency)}\n icon={<TrendingUp className=\"size-5\" />}\n />\n <StatCard\n label=\"ARR\"\n value={formatMetricCurrency(metrics.arr, displayCurrency)}\n icon={<BarChart3 className=\"size-5\" />}\n />\n <StatCard\n label=\"ARPU\"\n value={formatMetricCurrency(metrics.arpu, displayCurrency)}\n icon={<Wallet className=\"size-5\" />}\n />\n <StatCard\n label=\"Churn Rate\"\n value={formatPercentage(metrics.churnRate)}\n icon={<Users className=\"size-5\" />}\n />\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard label=\"Total Customers\" value={metrics.totalCustomers} />\n <StatCard label=\"New Customers\" value={metrics.newCustomers} />\n <StatCard label=\"Active Subscriptions\" value={metrics.activeSubscriptions} />\n <StatCard\n label=\"Estimated LTV\"\n value={formatMetricCurrency(metrics.ltv, displayCurrency)}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-2\">\n <section className=\"rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Revenue over time</h3>\n <p className=\"text-sm text-text-muted\">\n Successful payments received during the selected period.\n </p>\n </div>\n <span className=\"rounded-full bg-status-info-bg-subtle px-2.5 py-1 text-xs font-medium text-status-info-text\">\n {revenueSeries.length} data point{revenueSeries.length === 1 ? '' : 's'}\n </span>\n </div>\n\n {revenueSeries.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted\">\n No successful payments were recorded during this period.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {revenueSeries.map((point) => (\n <div key={point.label} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium text-text-primary\">{point.label}</span>\n <span className=\"text-text-muted\">\n {formatMetricCurrency(point.value, displayCurrency)}\n </span>\n </div>\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg transition-[width]\"\n style={{\n width: `${maxRevenuePoint > 0 ? (point.value / maxRevenuePoint) * 100 : 0}%`,\n }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div>\n <h3 className=\"font-semibold text-text-primary\">Subscription growth</h3>\n <p className=\"text-sm text-text-muted\">\n New subscriptions created during the selected period.\n </p>\n </div>\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {subscriptionGrowthSeries.reduce((sum, point) => sum + point.value, 0)} created\n </span>\n </div>\n\n {subscriptionGrowthSeries.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted\">\n No new subscriptions were created during this period.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {subscriptionGrowthSeries.map((point) => (\n <div key={point.label} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"font-medium text-text-primary\">{point.label}</span>\n <span className=\"text-text-muted\">{point.value} new</span>\n </div>\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-status-success-bg transition-[width]\"\n style={{\n width: `${maxGrowthPoint > 0 ? (point.value / maxGrowthPoint) * 100 : 0}%`,\n }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-2\">\n <section className=\"rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]\">\n <div className=\"border-b border-border-subtle p-4\">\n <h3 className=\"font-semibold text-text-primary\">Revenue by plan</h3>\n <p className=\"mt-1 text-sm text-text-muted\">\n Estimated monthly recurring revenue based on currently active subscriptions.\n </p>\n </div>\n <div className=\"p-4\">\n {revenueByPlan.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No active plans to analyse yet.</p>\n ) : (\n <div className=\"space-y-3\">\n {revenueByPlan.map((plan) => (\n <div\n key={plan.name}\n className=\"flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div>\n <p className=\"font-medium text-text-primary\">{plan.name}</p>\n <p className=\"text-sm text-text-muted\">\n {plan.activeSubscriptions} active subscription\n {plan.activeSubscriptions === 1 ? '' : 's'}\n </p>\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {formatMetricCurrency(plan.estimatedMrr, displayCurrency)} / month\n </p>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n\n <section className=\"rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]\">\n <div className=\"border-b border-border-subtle p-4\">\n <h3 className=\"font-semibold text-text-primary\">Top customers</h3>\n <p className=\"mt-1 text-sm text-text-muted\">\n Ranked by estimated monthly recurring revenue from active subscriptions.\n </p>\n </div>\n <div className=\"p-4\">\n {topCustomers.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No customer revenue data available yet.</p>\n ) : (\n <div className=\"space-y-3\">\n {topCustomers.map((customer) => (\n <div\n key={customer.id}\n className=\"flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{customer.name}</p>\n <p className=\"truncate text-sm text-text-muted\">{customer.email}</p>\n </div>\n <div className=\"text-left sm:text-right\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {formatMetricCurrency(customer.estimatedMrr, displayCurrency)} / month\n </p>\n <p className=\"text-xs text-text-muted\">\n {customer.activeSubscriptions} active subscription\n {customer.activeSubscriptions === 1 ? '' : 's'}\n </p>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n </div>\n </>\n )}\n </div>\n );\n};\n"],"mappings":"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"}