@burdenoff/microfe-billing 2026.624.3 → 2026.625.2

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 (95) hide show
  1. package/dist/billing/BillingAdminRoutes.js +1 -1
  2. package/dist/billing/BillingAdminRoutes.js.map +1 -1
  3. package/dist/billing/modules/addons/pages/AddonCreatePage.js +2 -2
  4. package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
  5. package/dist/billing/modules/addons/pages/AddonDetailPage.js +4 -4
  6. package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
  7. package/dist/billing/modules/addons/pages/AddonEditPage.js +2 -2
  8. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  9. package/dist/billing/modules/addons/pages/AddonsListPage.js +1 -1
  10. package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
  11. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +98 -99
  12. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
  13. package/dist/billing/modules/analytics/pages/AnalyticsPage.js +88 -85
  14. package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
  15. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +6 -6
  16. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
  17. package/dist/billing/modules/billing/pages/InvoicesListPage.js +2 -2
  18. package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
  19. package/dist/billing/modules/billing/pages/RefundsPage.js +360 -370
  20. package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
  21. package/dist/billing/modules/billing/pages/UserRefundsPage.js +2 -2
  22. package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
  23. package/dist/billing/modules/checkout/components/BillingAccountSelector.js +1 -1
  24. package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
  25. package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js +1 -1
  26. package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
  27. package/dist/billing/modules/checkout/components/SubscriptionSelector.js +1 -1
  28. package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
  29. package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js +2 -2
  30. package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
  31. package/dist/billing/modules/coupons/pages/CouponDetailPage.js +102 -104
  32. package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
  33. package/dist/billing/modules/coupons/pages/CouponsListPage.js +5 -5
  34. package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
  35. package/dist/billing/modules/credits/pages/AdminCreditsPage.js +1 -1
  36. package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
  37. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +1 -1
  38. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
  39. package/dist/billing/modules/credits/pages/CreditsPage.js +29 -28
  40. package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
  41. package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js +2 -2
  42. package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
  43. package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +1 -1
  44. package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
  45. package/dist/billing/modules/dashboard/components/BillingAccountCard.js +3 -3
  46. package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
  47. package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js +2 -2
  48. package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
  49. package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js +1 -1
  50. package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
  51. package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +1 -1
  52. package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
  53. package/dist/billing/modules/dashboard/pages/OverviewPage.js +139 -142
  54. package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
  55. package/dist/billing/modules/discounts/pages/DiscountsListPage.js +6 -6
  56. package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
  57. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +4 -4
  58. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
  59. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +1 -1
  60. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
  61. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +142 -133
  62. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
  63. package/dist/billing/modules/plans/pages/PlanCreatePage.js +2 -2
  64. package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
  65. package/dist/billing/modules/plans/pages/PlanDetailPage.js +4 -4
  66. package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
  67. package/dist/billing/modules/plans/pages/PlanEditPage.js +2 -2
  68. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  69. package/dist/billing/modules/plans/pages/PlansBrowsePage.js +4 -4
  70. package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
  71. package/dist/billing/modules/plans/pages/PlansListPage.js +2 -2
  72. package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
  73. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +4 -4
  74. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
  75. package/dist/billing/modules/promotions/pages/PromotionsListPage.js +2 -2
  76. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
  77. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +2 -2
  78. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
  79. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +5 -5
  80. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
  81. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +2 -2
  82. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
  83. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +1 -1
  84. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
  85. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +259 -265
  86. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
  87. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +7 -7
  88. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
  89. package/dist/billing/modules/usage/pages/UsagePage.js +13 -13
  90. package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
  91. package/dist/billing/shared/ui/Card.js +1 -1
  92. package/dist/billing/shared/ui/Card.js.map +1 -1
  93. package/dist/billing/shared/ui/MetricCard.js +28 -21
  94. package/dist/billing/shared/ui/MetricCard.js.map +1 -1
  95. package/package.json +2 -2
@@ -10,8 +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
14
  //#region src/billing/modules/analytics/pages/AnalyticsPage.tsx
14
- var x = [
15
+ var S = [
15
16
  {
16
17
  value: "LAST_7_DAYS",
17
18
  label: "Last 7 days"
@@ -32,37 +33,37 @@ var x = [
32
33
  value: "LAST_MONTH",
33
34
  label: "Last month"
34
35
  }
35
- ], S = new Set([
36
+ ], C = new Set([
36
37
  c.Active,
37
38
  c.Trialing,
38
39
  c.PastDue,
39
40
  c.Paused
40
41
  ]);
41
- function C(e) {
42
+ function w(e) {
42
43
  let t = new Date(e);
43
44
  return t.setHours(0, 0, 0, 0), t;
44
45
  }
45
- function w(e) {
46
+ function T(e) {
46
47
  let t = /* @__PURE__ */ new Date(), n = t;
47
48
  switch (e) {
48
49
  case "LAST_7_DAYS": {
49
50
  let e = new Date(t);
50
51
  return e.setDate(e.getDate() - 6), {
51
- start: C(e),
52
+ start: w(e),
52
53
  end: n
53
54
  };
54
55
  }
55
56
  case "LAST_30_DAYS": {
56
57
  let e = new Date(t);
57
58
  return e.setDate(e.getDate() - 29), {
58
- start: C(e),
59
+ start: w(e),
59
60
  end: n
60
61
  };
61
62
  }
62
63
  case "LAST_90_DAYS": {
63
64
  let e = new Date(t);
64
65
  return e.setDate(e.getDate() - 89), {
65
- start: C(e),
66
+ start: w(e),
66
67
  end: n
67
68
  };
68
69
  }
@@ -76,45 +77,45 @@ function w(e) {
76
77
  };
77
78
  }
78
79
  }
79
- function T(e, t, n) {
80
+ function E(e, t, n) {
80
81
  if (!e) return !1;
81
82
  let r = new Date(e);
82
83
  return r >= t && r <= n;
83
84
  }
84
- function E(e, t) {
85
+ function D(e, t) {
85
86
  return t === s.Yearly ? e / 12 : e;
86
87
  }
87
- function D(e, t) {
88
+ function O(e, t) {
88
89
  return new Intl.DateTimeFormat("en-US", {
89
90
  month: "short",
90
91
  day: "numeric"
91
92
  }).format(e);
92
93
  }
93
- function O(e, t, n, r) {
94
+ function k(e, t, n, r) {
94
95
  let i = /* @__PURE__ */ new Map();
95
96
  return e.forEach((e) => {
96
97
  let a = new Date(e.paidAt || e.createdAt);
97
98
  if (a < n || a > r) return;
98
- let o = D(a, t);
99
+ let o = O(a, t);
99
100
  i.set(o, (i.get(o) ?? 0) + e.amount);
100
101
  }), Array.from(i.entries()).map(([e, t]) => ({
101
102
  label: e,
102
103
  value: t
103
104
  }));
104
105
  }
105
- function k(e, t, n, r) {
106
+ function A(e, t, n, r) {
106
107
  let i = /* @__PURE__ */ new Map();
107
108
  return e.forEach((e) => {
108
109
  let a = new Date(e.createdAt);
109
110
  if (a < n || a > r) return;
110
- let o = D(a, t);
111
+ let o = O(a, t);
111
112
  i.set(o, (i.get(o) ?? 0) + 1);
112
113
  }), Array.from(i.entries()).map(([e, t]) => ({
113
114
  label: e,
114
115
  value: t
115
116
  }));
116
117
  }
117
- function A(e, t) {
118
+ function j(e, t) {
118
119
  try {
119
120
  return r(e, t);
120
121
  } catch {
@@ -125,12 +126,12 @@ function A(e, t) {
125
126
  }).format(e);
126
127
  }
127
128
  }
128
- var j = () => {
129
- let { t: r } = b(), s = a(), [C, D] = u("LAST_30_DAYS"), { start: j, end: M } = l(() => w(C), [C]), { data: N, loading: P, error: F, refetch: I } = o({ fetchPolicy: "cache-and-network" }), L = l(() => N?.getBillingAccountsByOrg ?? [], [N?.getBillingAccountsByOrg]), R = l(() => N?.getPayments ?? [], [N?.getPayments]), z = l(() => L.flatMap((e) => (e.subscriptions ?? []).filter((e) => !!e)), [L]), B = l(() => z.filter((e) => S.has(e.status)), [z]), V = B.find((e) => e.plan?.currency)?.plan?.currency || R[0]?.currency || L[0]?.currency || "USD", H = l(() => {
130
- let e = B.reduce((e, t) => {
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) => {
131
132
  let n = t.plan?.price ?? 0, r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
132
- return e + E(n, t.plan?.duration) * r;
133
- }, 0), t = e * 12, n = L.length, r = L.filter((e) => T(e.createdAt, j, M)).length, i = z.filter((e) => (e.status === c.Canceled || e.status === c.Expired) && T(e.canceledAt || e.endDate, j, M)).length, a = B.length + i, o = a > 0 ? i / a * 100 : 0, s = n > 0 ? e / n : 0;
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;
134
135
  return {
135
136
  mrr: e,
136
137
  arr: t,
@@ -139,38 +140,38 @@ var j = () => {
139
140
  ltv: o > 0 ? s / (o / 100) : t,
140
141
  totalCustomers: n,
141
142
  newCustomers: r,
142
- activeSubscriptions: B.length
143
+ activeSubscriptions: V.length
143
144
  };
144
145
  }, [
145
- B,
146
- L,
147
- j,
148
- M,
149
- z
150
- ]), U = l(() => O(R, C, j, M), [
146
+ V,
151
147
  R,
152
- C,
153
- j,
154
- M
155
- ]), W = l(() => k(z, C, j, M), [
156
- C,
157
- j,
158
148
  M,
159
- z
160
- ]), G = l(() => {
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(() => {
161
162
  let e = /* @__PURE__ */ new Map();
162
- return B.forEach((t) => {
163
- let n = t.plan?.name || "Unknown Plan", r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1, i = E(t.plan?.price ?? 0, t.plan?.duration) * r, a = e.get(n);
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);
164
165
  e.set(n, {
165
166
  name: n,
166
167
  activeSubscriptions: (a?.activeSubscriptions ?? 0) + 1,
167
168
  estimatedMrr: (a?.estimatedMrr ?? 0) + i
168
169
  });
169
170
  }), Array.from(e.values()).sort((e, t) => t.estimatedMrr - e.estimatedMrr);
170
- }, [B]), K = l(() => L.map((e) => {
171
- let t = (e.subscriptions ?? []).filter((e) => e ? S.has(e.status) : !1), n = t.reduce((e, t) => {
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) => {
172
173
  let n = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
173
- return e + E(t.plan?.price ?? 0, t.plan?.duration) * n;
174
+ return e + D(t.plan?.price ?? 0, t.plan?.duration) * n;
174
175
  }, 0);
175
176
  return {
176
177
  id: e.id ?? e.name ?? e.email ?? "unknown-account",
@@ -179,7 +180,7 @@ var j = () => {
179
180
  activeSubscriptions: t.length,
180
181
  estimatedMrr: n
181
182
  };
182
- }).sort((e, t) => t.estimatedMrr - e.estimatedMrr).slice(0, 5), [L]), q = Math.max(...U.map((e) => e.value), 0), J = Math.max(...W.map((e) => e.value), 0);
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
184
  return s.canViewAnalytics ? /* @__PURE__ */ y("div", {
184
185
  className: "space-y-6 p-4 sm:p-6",
185
186
  children: [
@@ -188,33 +189,35 @@ var j = () => {
188
189
  description: "Track recurring revenue, customer growth, and billing performance using live billing data.",
189
190
  actions: /* @__PURE__ */ y("div", {
190
191
  className: "flex flex-col gap-2 sm:flex-row sm:items-center",
191
- children: [/* @__PURE__ */ y("button", {
192
- type: "button",
193
- onClick: () => I(),
194
- className: "inline-flex items-center justify-center gap-2 rounded-button border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
195
- children: [/* @__PURE__ */ v(p, { className: "size-4" }), "Refresh"]
196
- }), /* @__PURE__ */ v("select", {
197
- value: C,
198
- onChange: (e) => D(e.target.value),
199
- className: "rounded-input border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
200
- children: x.map((e) => /* @__PURE__ */ v("option", {
192
+ children: [/* @__PURE__ */ v("select", {
193
+ value: w,
194
+ onChange: (e) => O(e.target.value),
195
+ "aria-label": "Analytics period",
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", {
201
198
  value: e.value,
202
199
  children: e.label
203
200
  }, e.value))
204
- })]
201
+ }), /* @__PURE__ */ v(x, { actions: [{
202
+ label: "Refresh",
203
+ onSelect: () => {
204
+ L();
205
+ },
206
+ icon: /* @__PURE__ */ v(p, { className: "size-4" })
207
+ }] })]
205
208
  })
206
209
  }),
207
- F && /* @__PURE__ */ y("div", {
210
+ I && /* @__PURE__ */ y("div", {
208
211
  className: "flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text",
209
212
  children: [/* @__PURE__ */ v(d, { className: "mt-0.5 size-4 shrink-0" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
210
213
  className: "font-medium",
211
214
  children: "Unable to load billing analytics"
212
215
  }), /* @__PURE__ */ v("p", {
213
216
  className: "mt-1 text-sm",
214
- children: F.message
217
+ children: I.message
215
218
  })] })]
216
219
  }),
217
- P && L.length === 0 ? /* @__PURE__ */ v("div", {
220
+ F && R.length === 0 ? /* @__PURE__ */ v("div", {
218
221
  className: "flex min-h-[320px] items-center justify-center rounded-lg border border-border-subtle bg-bg-surface",
219
222
  children: /* @__PURE__ */ y("div", {
220
223
  className: "flex flex-col items-center gap-3 text-text-muted",
@@ -223,7 +226,7 @@ var j = () => {
223
226
  children: "Loading live billing analytics…"
224
227
  })]
225
228
  })
226
- }) : L.length === 0 ? /* @__PURE__ */ y("div", {
229
+ }) : R.length === 0 ? /* @__PURE__ */ y("div", {
227
230
  className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface p-8 text-center",
228
231
  children: [
229
232
  /* @__PURE__ */ v(f, { className: "mx-auto size-10 text-text-muted" }),
@@ -242,24 +245,24 @@ var j = () => {
242
245
  children: [
243
246
  /* @__PURE__ */ v(n, {
244
247
  label: "MRR",
245
- value: A(H.mrr, V),
248
+ value: j(U.mrr, H),
246
249
  icon: /* @__PURE__ */ v(m, { className: "size-5" }),
247
250
  className: "bg-status-info-bg-subtle"
248
251
  }),
249
252
  /* @__PURE__ */ v(n, {
250
253
  label: "ARR",
251
- value: A(H.arr, V),
254
+ value: j(U.arr, H),
252
255
  icon: /* @__PURE__ */ v(f, { className: "size-5" }),
253
256
  className: "bg-status-success-bg-subtle"
254
257
  }),
255
258
  /* @__PURE__ */ v(n, {
256
259
  label: "ARPU",
257
- value: A(H.arpu, V),
260
+ value: j(U.arpu, H),
258
261
  icon: /* @__PURE__ */ v(g, { className: "size-5" })
259
262
  }),
260
263
  /* @__PURE__ */ v(n, {
261
264
  label: "Churn Rate",
262
- value: i(H.churnRate),
265
+ value: i(U.churnRate),
263
266
  icon: /* @__PURE__ */ v(h, { className: "size-5" }),
264
267
  className: "bg-status-warning-bg-subtle"
265
268
  })
@@ -270,26 +273,26 @@ var j = () => {
270
273
  children: [
271
274
  /* @__PURE__ */ v(n, {
272
275
  label: "Total Customers",
273
- value: H.totalCustomers
276
+ value: U.totalCustomers
274
277
  }),
275
278
  /* @__PURE__ */ v(n, {
276
279
  label: "New Customers",
277
- value: H.newCustomers
280
+ value: U.newCustomers
278
281
  }),
279
282
  /* @__PURE__ */ v(n, {
280
283
  label: "Active Subscriptions",
281
- value: H.activeSubscriptions
284
+ value: U.activeSubscriptions
282
285
  }),
283
286
  /* @__PURE__ */ v(n, {
284
287
  label: "Estimated LTV",
285
- value: A(H.ltv, V)
288
+ value: j(U.ltv, H)
286
289
  })
287
290
  ]
288
291
  }),
289
292
  /* @__PURE__ */ y("div", {
290
293
  className: "grid grid-cols-1 gap-6 xl:grid-cols-2",
291
294
  children: [/* @__PURE__ */ y("section", {
292
- className: "rounded-card border border-border-subtle bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]",
295
+ className: "rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]",
293
296
  children: [/* @__PURE__ */ y("div", {
294
297
  className: "mb-4 flex items-center justify-between gap-3",
295
298
  children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
@@ -301,17 +304,17 @@ var j = () => {
301
304
  })] }), /* @__PURE__ */ y("span", {
302
305
  className: "rounded-full bg-status-info-bg-subtle px-2.5 py-1 text-xs font-medium text-status-info-text",
303
306
  children: [
304
- U.length,
307
+ W.length,
305
308
  " data point",
306
- U.length === 1 ? "" : "s"
309
+ W.length === 1 ? "" : "s"
307
310
  ]
308
311
  })]
309
- }), U.length === 0 ? /* @__PURE__ */ v("p", {
312
+ }), W.length === 0 ? /* @__PURE__ */ v("p", {
310
313
  className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
311
314
  children: "No successful payments were recorded during this period."
312
315
  }) : /* @__PURE__ */ v("div", {
313
316
  className: "space-y-3",
314
- children: U.map((e) => /* @__PURE__ */ y("div", {
317
+ children: W.map((e) => /* @__PURE__ */ y("div", {
315
318
  className: "space-y-1",
316
319
  children: [/* @__PURE__ */ y("div", {
317
320
  className: "flex items-center justify-between gap-3 text-sm",
@@ -320,19 +323,19 @@ var j = () => {
320
323
  children: e.label
321
324
  }), /* @__PURE__ */ v("span", {
322
325
  className: "text-text-muted",
323
- children: A(e.value, V)
326
+ children: j(e.value, H)
324
327
  })]
325
328
  }), /* @__PURE__ */ v("div", {
326
329
  className: "h-2 rounded-full bg-bg-sunken",
327
330
  children: /* @__PURE__ */ v("div", {
328
331
  className: "h-2 rounded-full bg-action-primary-bg transition-[width]",
329
- style: { width: `${q > 0 ? e.value / q * 100 : 0}%` }
332
+ style: { width: `${J > 0 ? e.value / J * 100 : 0}%` }
330
333
  })
331
334
  })]
332
335
  }, e.label))
333
336
  })]
334
337
  }), /* @__PURE__ */ y("section", {
335
- className: "rounded-card border border-border-subtle bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]",
338
+ className: "rounded-card border border-border-seam bg-bg-surface p-4 sm:p-6 shadow-[var(--shadow-elevation-1)]",
336
339
  children: [/* @__PURE__ */ y("div", {
337
340
  className: "mb-4 flex items-center justify-between gap-3",
338
341
  children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
@@ -343,14 +346,14 @@ var j = () => {
343
346
  children: "New subscriptions created during the selected period."
344
347
  })] }), /* @__PURE__ */ y("span", {
345
348
  className: "rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text",
346
- children: [W.reduce((e, t) => e + t.value, 0), " created"]
349
+ children: [G.reduce((e, t) => e + t.value, 0), " created"]
347
350
  })]
348
- }), W.length === 0 ? /* @__PURE__ */ v("p", {
351
+ }), G.length === 0 ? /* @__PURE__ */ v("p", {
349
352
  className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
350
353
  children: "No new subscriptions were created during this period."
351
354
  }) : /* @__PURE__ */ v("div", {
352
355
  className: "space-y-3",
353
- children: W.map((e) => /* @__PURE__ */ y("div", {
356
+ children: G.map((e) => /* @__PURE__ */ y("div", {
354
357
  className: "space-y-1",
355
358
  children: [/* @__PURE__ */ y("div", {
356
359
  className: "flex items-center justify-between gap-3 text-sm",
@@ -365,7 +368,7 @@ var j = () => {
365
368
  className: "h-2 rounded-full bg-bg-sunken",
366
369
  children: /* @__PURE__ */ v("div", {
367
370
  className: "h-2 rounded-full bg-status-success-bg transition-[width]",
368
- style: { width: `${J > 0 ? e.value / J * 100 : 0}%` }
371
+ style: { width: `${Y > 0 ? e.value / Y * 100 : 0}%` }
369
372
  })
370
373
  })]
371
374
  }, e.label))
@@ -375,7 +378,7 @@ var j = () => {
375
378
  /* @__PURE__ */ y("div", {
376
379
  className: "grid grid-cols-1 gap-6 xl:grid-cols-2",
377
380
  children: [/* @__PURE__ */ y("section", {
378
- className: "rounded-card border border-border-subtle bg-bg-surface shadow-[var(--shadow-elevation-1)]",
381
+ className: "rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]",
379
382
  children: [/* @__PURE__ */ y("div", {
380
383
  className: "border-b border-border-subtle p-4",
381
384
  children: [/* @__PURE__ */ v("h3", {
@@ -387,12 +390,12 @@ var j = () => {
387
390
  })]
388
391
  }), /* @__PURE__ */ v("div", {
389
392
  className: "p-4",
390
- children: G.length === 0 ? /* @__PURE__ */ v("p", {
393
+ children: K.length === 0 ? /* @__PURE__ */ v("p", {
391
394
  className: "text-sm text-text-muted",
392
395
  children: "No active plans to analyse yet."
393
396
  }) : /* @__PURE__ */ v("div", {
394
397
  className: "space-y-3",
395
- children: G.map((e) => /* @__PURE__ */ y("div", {
398
+ children: K.map((e) => /* @__PURE__ */ y("div", {
396
399
  className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
397
400
  children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
398
401
  className: "font-medium text-text-primary",
@@ -406,13 +409,13 @@ var j = () => {
406
409
  ]
407
410
  })] }), /* @__PURE__ */ y("p", {
408
411
  className: "text-sm font-semibold text-text-primary",
409
- children: [A(e.estimatedMrr, V), " / month"]
412
+ children: [j(e.estimatedMrr, H), " / month"]
410
413
  })]
411
414
  }, e.name))
412
415
  })
413
416
  })]
414
417
  }), /* @__PURE__ */ y("section", {
415
- className: "rounded-card border border-border-subtle bg-bg-surface shadow-[var(--shadow-elevation-1)]",
418
+ className: "rounded-card border border-border-seam bg-bg-surface shadow-[var(--shadow-elevation-1)]",
416
419
  children: [/* @__PURE__ */ y("div", {
417
420
  className: "border-b border-border-subtle p-4",
418
421
  children: [/* @__PURE__ */ v("h3", {
@@ -424,12 +427,12 @@ var j = () => {
424
427
  })]
425
428
  }), /* @__PURE__ */ v("div", {
426
429
  className: "p-4",
427
- children: K.length === 0 ? /* @__PURE__ */ v("p", {
430
+ children: q.length === 0 ? /* @__PURE__ */ v("p", {
428
431
  className: "text-sm text-text-muted",
429
432
  children: "No customer revenue data available yet."
430
433
  }) : /* @__PURE__ */ v("div", {
431
434
  className: "space-y-3",
432
- children: K.map((e) => /* @__PURE__ */ y("div", {
435
+ children: q.map((e) => /* @__PURE__ */ y("div", {
433
436
  className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
434
437
  children: [/* @__PURE__ */ y("div", {
435
438
  className: "min-w-0",
@@ -444,7 +447,7 @@ var j = () => {
444
447
  className: "text-left sm:text-right",
445
448
  children: [/* @__PURE__ */ y("p", {
446
449
  className: "text-sm font-semibold text-text-primary",
447
- children: [A(e.estimatedMrr, V), " / month"]
450
+ children: [j(e.estimatedMrr, H), " / month"]
448
451
  }), /* @__PURE__ */ y("p", {
449
452
  className: "text-xs text-text-muted",
450
453
  children: [
@@ -464,6 +467,6 @@ var j = () => {
464
467
  }) : /* @__PURE__ */ v(e, { message: "You don't have permission to view analytics." });
465
468
  };
466
469
  //#endregion
467
- export { j as AnalyticsPage };
470
+ export { M as AnalyticsPage };
468
471
 
469
472
  //# 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 { 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 <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"inline-flex items-center justify-center gap-2 rounded-button border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <RefreshCw className=\"size-4\" />\n Refresh\n </button>\n <select\n value={period}\n onChange={(event) => setPeriod(event.target.value as AnalyticsPeriod)}\n className=\"rounded-input border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus: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 </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-subtle 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-subtle 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-subtle 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-subtle 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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"}
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":"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"}