@burdenoff/microfe-billing 2026.821.1 → 2026.825.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.
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingRoot.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/hooks/useBillingAccountSelection.js.map +1 -1
- package/dist/billing/hooks/useBillingEventEmitter.js.map +1 -1
- package/dist/billing/hooks/useBillingNavigate.js.map +1 -1
- package/dist/billing/hooks/useBillingPermissions.js.map +1 -1
- package/dist/billing/hooks/useDefaultBillingCurrency.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/hooks/useAddons.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/addons/store/addonCartStore.js.map +1 -1
- package/dist/billing/modules/affiliate/api.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +107 -107
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +80 -81
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useInvoices.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useTransactions.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +127 -127
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StoreShippingSection.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js +0 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useNativeCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentProviders.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/usePaymentStatusPolling.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useStoreShipping.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +412 -395
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/checkout/utils/storeOrder.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/hooks/useCredits.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useBillingOverviewData.js.map +1 -1
- package/dist/billing/modules/dashboard/hooks/useDashboard.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/api.js.map +1 -1
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +145 -145
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +133 -133
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +7 -9
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +95 -95
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -1
- package/dist/billing/modules/plans/hooks/usePlans.js.map +1 -1
- package/dist/billing/modules/plans/hooks/useQuotas.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
- package/dist/billing/modules/quota/hooks/useQuotaTemplates.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaAssignFreePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js +5 -6
- package/dist/billing/modules/store/pages/StoreCheckoutPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/hooks/useUsage.js +14 -9
- package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +481 -453
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/providers/BillingProvider.js.map +1 -1
- package/dist/billing/shared/components/AccessDenied.js.map +1 -1
- package/dist/billing/shared/components/ActorIdentity.js.map +1 -1
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
- package/dist/billing/shared/components/EmptyState.js.map +1 -1
- package/dist/billing/shared/components/PageHeader.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/StatCard.js.map +1 -1
- package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -1
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/billing/shared/ui/Card.js.map +1 -1
- package/dist/billing/shared/ui/MetricCard.js.map +1 -1
- package/dist/billing/shared/ui/ProgressBar.js.map +1 -1
- package/dist/billing/shared/ui/StatusPill.js.map +1 -1
- package/dist/billing/shared/utils/directBillingGraphql.js.map +1 -1
- package/dist/billing/shared/utils/error.js.map +1 -1
- package/dist/billing/shared/utils/format.js.map +1 -1
- package/dist/billing/shared/utils/gatewayGraphql.js.map +1 -1
- package/dist/billing/shared/utils/navigation.js.map +1 -1
- package/dist/billing/shared/utils/paymentTheme.js.map +1 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
- package/dist/billing/shared/utils/status.js.map +1 -1
- package/dist/billing/shared/utils/tokens.js.map +1 -1
- package/dist/generated/global-operations.js +5 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/generated/wspace-operations.js +1 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js +1 -1
- package/dist/node_modules/@capacitor/core/dist/index.js.map +1 -1
- package/package.json +13 -10
|
@@ -9,10 +9,9 @@ import { PlanDuration as s, SubscriptionStatus as c } from "../../../../generate
|
|
|
9
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
|
-
import {
|
|
13
|
-
import { CTAOverflowMenu as x, EmphasisPanel as S, IllustratedEmptyState as C, PagePurpose as w } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { CTAOverflowMenu as b, EmphasisPanel as x, IllustratedEmptyState as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
|
|
14
13
|
//#region src/billing/modules/analytics/pages/AnalyticsPage.tsx
|
|
15
|
-
var
|
|
14
|
+
var w = [
|
|
16
15
|
{
|
|
17
16
|
value: "LAST_7_DAYS",
|
|
18
17
|
label: "Last 7 days"
|
|
@@ -33,37 +32,37 @@ var T = [
|
|
|
33
32
|
value: "LAST_MONTH",
|
|
34
33
|
label: "Last month"
|
|
35
34
|
}
|
|
36
|
-
],
|
|
35
|
+
], T = new Set([
|
|
37
36
|
c.Active,
|
|
38
37
|
c.Trialing,
|
|
39
38
|
c.PastDue,
|
|
40
39
|
c.Paused
|
|
41
40
|
]);
|
|
42
|
-
function
|
|
41
|
+
function E(e) {
|
|
43
42
|
let t = new Date(e);
|
|
44
43
|
return t.setHours(0, 0, 0, 0), t;
|
|
45
44
|
}
|
|
46
|
-
function
|
|
45
|
+
function D(e) {
|
|
47
46
|
let t = /* @__PURE__ */ new Date(), n = t;
|
|
48
47
|
switch (e) {
|
|
49
48
|
case "LAST_7_DAYS": {
|
|
50
49
|
let e = new Date(t);
|
|
51
50
|
return e.setDate(e.getDate() - 6), {
|
|
52
|
-
start:
|
|
51
|
+
start: E(e),
|
|
53
52
|
end: n
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
case "LAST_30_DAYS": {
|
|
57
56
|
let e = new Date(t);
|
|
58
57
|
return e.setDate(e.getDate() - 29), {
|
|
59
|
-
start:
|
|
58
|
+
start: E(e),
|
|
60
59
|
end: n
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
case "LAST_90_DAYS": {
|
|
64
63
|
let e = new Date(t);
|
|
65
64
|
return e.setDate(e.getDate() - 89), {
|
|
66
|
-
start:
|
|
65
|
+
start: E(e),
|
|
67
66
|
end: n
|
|
68
67
|
};
|
|
69
68
|
}
|
|
@@ -77,45 +76,45 @@ function O(e) {
|
|
|
77
76
|
};
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
|
-
function
|
|
79
|
+
function O(e, t, n) {
|
|
81
80
|
if (!e) return !1;
|
|
82
81
|
let r = new Date(e);
|
|
83
82
|
return r >= t && r <= n;
|
|
84
83
|
}
|
|
85
|
-
function
|
|
84
|
+
function k(e, t) {
|
|
86
85
|
return t === s.Yearly ? e / 12 : e;
|
|
87
86
|
}
|
|
88
|
-
function
|
|
87
|
+
function A(e, t) {
|
|
89
88
|
return new Intl.DateTimeFormat("en-US", {
|
|
90
89
|
month: "short",
|
|
91
90
|
day: "numeric"
|
|
92
91
|
}).format(e);
|
|
93
92
|
}
|
|
94
|
-
function
|
|
93
|
+
function j(e, t, n, r) {
|
|
95
94
|
let i = /* @__PURE__ */ new Map();
|
|
96
95
|
return e.forEach((e) => {
|
|
97
96
|
let a = new Date(e.paidAt || e.createdAt);
|
|
98
97
|
if (a < n || a > r) return;
|
|
99
|
-
let o =
|
|
98
|
+
let o = A(a, t);
|
|
100
99
|
i.set(o, (i.get(o) ?? 0) + e.amount);
|
|
101
100
|
}), Array.from(i.entries()).map(([e, t]) => ({
|
|
102
101
|
label: e,
|
|
103
102
|
value: t
|
|
104
103
|
}));
|
|
105
104
|
}
|
|
106
|
-
function
|
|
105
|
+
function M(e, t, n, r) {
|
|
107
106
|
let i = /* @__PURE__ */ new Map();
|
|
108
107
|
return e.forEach((e) => {
|
|
109
108
|
let a = new Date(e.createdAt);
|
|
110
109
|
if (a < n || a > r) return;
|
|
111
|
-
let o =
|
|
110
|
+
let o = A(a, t);
|
|
112
111
|
i.set(o, (i.get(o) ?? 0) + 1);
|
|
113
112
|
}), Array.from(i.entries()).map(([e, t]) => ({
|
|
114
113
|
label: e,
|
|
115
114
|
value: t
|
|
116
115
|
}));
|
|
117
116
|
}
|
|
118
|
-
function
|
|
117
|
+
function N(e, t) {
|
|
119
118
|
try {
|
|
120
119
|
return r(e, t);
|
|
121
120
|
} catch {
|
|
@@ -126,12 +125,12 @@ function P(e, t) {
|
|
|
126
125
|
}).format(e);
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
|
-
var
|
|
130
|
-
let
|
|
131
|
-
let e =
|
|
128
|
+
var P = () => {
|
|
129
|
+
let r = a(), [s, E] = u("LAST_30_DAYS"), { start: A, end: P } = l(() => D(s), [s]), { data: F, loading: I, error: L, refetch: R } = o({ fetchPolicy: "cache-and-network" }), z = l(() => F?.getBillingAccountsByOrg ?? [], [F?.getBillingAccountsByOrg]), B = l(() => F?.getPayments ?? [], [F?.getPayments]), V = l(() => z.flatMap((e) => (e.subscriptions ?? []).filter((e) => !!e)), [z]), H = l(() => V.filter((e) => T.has(e.status)), [V]), U = H.find((e) => e.plan?.currency)?.plan?.currency || B[0]?.currency || z[0]?.currency || "USD", W = l(() => {
|
|
130
|
+
let e = H.reduce((e, t) => {
|
|
132
131
|
let n = t.plan?.price ?? 0, r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
|
|
133
|
-
return e +
|
|
134
|
-
}, 0), t = e * 12, n =
|
|
132
|
+
return e + k(n, t.plan?.duration) * r;
|
|
133
|
+
}, 0), t = e * 12, n = z.length, r = z.filter((e) => O(e.createdAt, A, P)).length, i = V.filter((e) => (e.status === c.Canceled || e.status === c.Expired) && O(e.canceledAt || e.endDate, A, P)).length, a = H.length + i, o = a > 0 ? i / a * 100 : 0, s = n > 0 ? e / n : 0;
|
|
135
134
|
return {
|
|
136
135
|
mrr: e,
|
|
137
136
|
arr: t,
|
|
@@ -140,38 +139,38 @@ var F = () => {
|
|
|
140
139
|
ltv: o > 0 ? s / (o / 100) : t,
|
|
141
140
|
totalCustomers: n,
|
|
142
141
|
newCustomers: r,
|
|
143
|
-
activeSubscriptions:
|
|
142
|
+
activeSubscriptions: H.length
|
|
144
143
|
};
|
|
145
144
|
}, [
|
|
146
|
-
W,
|
|
147
|
-
V,
|
|
148
|
-
F,
|
|
149
|
-
I,
|
|
150
|
-
U
|
|
151
|
-
]), q = l(() => M(H, D, F, I), [
|
|
152
145
|
H,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
146
|
+
z,
|
|
147
|
+
A,
|
|
148
|
+
P,
|
|
149
|
+
V
|
|
150
|
+
]), G = l(() => j(B, s, A, P), [
|
|
151
|
+
B,
|
|
152
|
+
s,
|
|
153
|
+
A,
|
|
154
|
+
P
|
|
155
|
+
]), K = l(() => M(V, s, A, P), [
|
|
156
|
+
s,
|
|
157
|
+
A,
|
|
158
|
+
P,
|
|
159
|
+
V
|
|
160
|
+
]), q = l(() => {
|
|
162
161
|
let e = /* @__PURE__ */ new Map();
|
|
163
|
-
return
|
|
164
|
-
let n = t.plan?.name || "Unknown Plan", r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1, i =
|
|
162
|
+
return H.forEach((t) => {
|
|
163
|
+
let n = t.plan?.name || "Unknown Plan", r = t.seatCount && t.seatCount > 0 ? t.seatCount : 1, i = k(t.plan?.price ?? 0, t.plan?.duration) * r, a = e.get(n);
|
|
165
164
|
e.set(n, {
|
|
166
165
|
name: n,
|
|
167
166
|
activeSubscriptions: (a?.activeSubscriptions ?? 0) + 1,
|
|
168
167
|
estimatedMrr: (a?.estimatedMrr ?? 0) + i
|
|
169
168
|
});
|
|
170
169
|
}), Array.from(e.values()).sort((e, t) => t.estimatedMrr - e.estimatedMrr);
|
|
171
|
-
}, [
|
|
172
|
-
let t = (e.subscriptions ?? []).filter((e) => e ?
|
|
170
|
+
}, [H]), J = l(() => z.map((e) => {
|
|
171
|
+
let t = (e.subscriptions ?? []).filter((e) => e ? T.has(e.status) : !1), n = t.reduce((e, t) => {
|
|
173
172
|
let n = t.seatCount && t.seatCount > 0 ? t.seatCount : 1;
|
|
174
|
-
return e +
|
|
173
|
+
return e + k(t.plan?.price ?? 0, t.plan?.duration) * n;
|
|
175
174
|
}, 0);
|
|
176
175
|
return {
|
|
177
176
|
id: e.id ?? e.name ?? e.email ?? "unknown-account",
|
|
@@ -180,8 +179,8 @@ var F = () => {
|
|
|
180
179
|
activeSubscriptions: t.length,
|
|
181
180
|
estimatedMrr: n
|
|
182
181
|
};
|
|
183
|
-
}).sort((e, t) => t.estimatedMrr - e.estimatedMrr).slice(0, 5), [
|
|
184
|
-
return
|
|
182
|
+
}).sort((e, t) => t.estimatedMrr - e.estimatedMrr).slice(0, 5), [z]), Y = Math.max(...G.map((e) => e.value), 0), X = Math.max(...K.map((e) => e.value), 0);
|
|
183
|
+
return r.canViewAnalytics ? /* @__PURE__ */ y("div", {
|
|
185
184
|
className: "space-y-6 p-4 sm:p-6",
|
|
186
185
|
children: [
|
|
187
186
|
/* @__PURE__ */ v(t, {
|
|
@@ -190,35 +189,35 @@ var F = () => {
|
|
|
190
189
|
actions: /* @__PURE__ */ y("div", {
|
|
191
190
|
className: "flex flex-col gap-2 sm:flex-row sm:items-center",
|
|
192
191
|
children: [/* @__PURE__ */ v("select", {
|
|
193
|
-
value:
|
|
194
|
-
onChange: (e) =>
|
|
192
|
+
value: s,
|
|
193
|
+
onChange: (e) => E(e.target.value),
|
|
195
194
|
"aria-label": "Analytics period",
|
|
196
195
|
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:
|
|
196
|
+
children: w.map((e) => /* @__PURE__ */ v("option", {
|
|
198
197
|
value: e.value,
|
|
199
198
|
children: e.label
|
|
200
199
|
}, e.value))
|
|
201
|
-
}), /* @__PURE__ */ v(
|
|
200
|
+
}), /* @__PURE__ */ v(b, { actions: [{
|
|
202
201
|
label: "Refresh",
|
|
203
202
|
onSelect: () => {
|
|
204
|
-
|
|
203
|
+
R();
|
|
205
204
|
},
|
|
206
205
|
icon: /* @__PURE__ */ v(p, { className: "size-4" })
|
|
207
206
|
}] })]
|
|
208
207
|
})
|
|
209
208
|
}),
|
|
210
|
-
/* @__PURE__ */ v(
|
|
211
|
-
|
|
209
|
+
/* @__PURE__ */ v(C, { 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." }),
|
|
210
|
+
L && /* @__PURE__ */ y("div", {
|
|
212
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",
|
|
213
212
|
children: [/* @__PURE__ */ v(d, { className: "mt-0.5 size-4 shrink-0" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
|
|
214
213
|
className: "font-medium",
|
|
215
214
|
children: "Unable to load billing analytics"
|
|
216
215
|
}), /* @__PURE__ */ v("p", {
|
|
217
216
|
className: "mt-1 text-sm",
|
|
218
|
-
children:
|
|
217
|
+
children: L.message
|
|
219
218
|
})] })]
|
|
220
219
|
}),
|
|
221
|
-
|
|
220
|
+
I && z.length === 0 ? /* @__PURE__ */ v("div", {
|
|
222
221
|
className: "flex min-h-[320px] items-center justify-center rounded-lg border border-border-subtle bg-bg-surface",
|
|
223
222
|
children: /* @__PURE__ */ y("div", {
|
|
224
223
|
className: "flex flex-col items-center gap-3 text-text-muted",
|
|
@@ -227,34 +226,34 @@ var F = () => {
|
|
|
227
226
|
children: "Loading live billing analytics…"
|
|
228
227
|
})]
|
|
229
228
|
})
|
|
230
|
-
}) :
|
|
229
|
+
}) : z.length === 0 ? /* @__PURE__ */ v(S, {
|
|
231
230
|
illustration: "empty-data",
|
|
232
231
|
title: "No billing data available yet",
|
|
233
232
|
description: "Create a billing account and complete a subscription or payment to unlock live analytics."
|
|
234
233
|
}) : /* @__PURE__ */ y(_, { children: [
|
|
235
|
-
/* @__PURE__ */ v(
|
|
234
|
+
/* @__PURE__ */ v(x, {
|
|
236
235
|
className: "p-4 sm:p-5",
|
|
237
236
|
children: /* @__PURE__ */ y("div", {
|
|
238
237
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
|
|
239
238
|
children: [
|
|
240
239
|
/* @__PURE__ */ v(n, {
|
|
241
240
|
label: "MRR",
|
|
242
|
-
value:
|
|
241
|
+
value: N(W.mrr, U),
|
|
243
242
|
icon: /* @__PURE__ */ v(m, { className: "size-5" })
|
|
244
243
|
}),
|
|
245
244
|
/* @__PURE__ */ v(n, {
|
|
246
245
|
label: "ARR",
|
|
247
|
-
value:
|
|
246
|
+
value: N(W.arr, U),
|
|
248
247
|
icon: /* @__PURE__ */ v(f, { className: "size-5" })
|
|
249
248
|
}),
|
|
250
249
|
/* @__PURE__ */ v(n, {
|
|
251
250
|
label: "ARPU",
|
|
252
|
-
value:
|
|
251
|
+
value: N(W.arpu, U),
|
|
253
252
|
icon: /* @__PURE__ */ v(g, { className: "size-5" })
|
|
254
253
|
}),
|
|
255
254
|
/* @__PURE__ */ v(n, {
|
|
256
255
|
label: "Churn Rate",
|
|
257
|
-
value: i(
|
|
256
|
+
value: i(W.churnRate),
|
|
258
257
|
icon: /* @__PURE__ */ v(h, { className: "size-5" })
|
|
259
258
|
})
|
|
260
259
|
]
|
|
@@ -265,19 +264,19 @@ var F = () => {
|
|
|
265
264
|
children: [
|
|
266
265
|
/* @__PURE__ */ v(n, {
|
|
267
266
|
label: "Total Customers",
|
|
268
|
-
value:
|
|
267
|
+
value: W.totalCustomers
|
|
269
268
|
}),
|
|
270
269
|
/* @__PURE__ */ v(n, {
|
|
271
270
|
label: "New Customers",
|
|
272
|
-
value:
|
|
271
|
+
value: W.newCustomers
|
|
273
272
|
}),
|
|
274
273
|
/* @__PURE__ */ v(n, {
|
|
275
274
|
label: "Active Subscriptions",
|
|
276
|
-
value:
|
|
275
|
+
value: W.activeSubscriptions
|
|
277
276
|
}),
|
|
278
277
|
/* @__PURE__ */ v(n, {
|
|
279
278
|
label: "Estimated LTV",
|
|
280
|
-
value:
|
|
279
|
+
value: N(W.ltv, U)
|
|
281
280
|
})
|
|
282
281
|
]
|
|
283
282
|
}),
|
|
@@ -296,17 +295,17 @@ var F = () => {
|
|
|
296
295
|
})] }), /* @__PURE__ */ y("span", {
|
|
297
296
|
className: "rounded-full bg-status-info-bg-subtle px-2.5 py-1 text-xs font-medium text-status-info-text",
|
|
298
297
|
children: [
|
|
299
|
-
|
|
298
|
+
G.length,
|
|
300
299
|
" data point",
|
|
301
|
-
|
|
300
|
+
G.length === 1 ? "" : "s"
|
|
302
301
|
]
|
|
303
302
|
})]
|
|
304
|
-
}),
|
|
303
|
+
}), G.length === 0 ? /* @__PURE__ */ v("p", {
|
|
305
304
|
className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
|
|
306
305
|
children: "No successful payments were recorded during this period."
|
|
307
306
|
}) : /* @__PURE__ */ v("div", {
|
|
308
307
|
className: "space-y-3",
|
|
309
|
-
children:
|
|
308
|
+
children: G.map((e) => /* @__PURE__ */ y("div", {
|
|
310
309
|
className: "space-y-1",
|
|
311
310
|
children: [/* @__PURE__ */ y("div", {
|
|
312
311
|
className: "flex items-center justify-between gap-3 text-sm",
|
|
@@ -315,13 +314,13 @@ var F = () => {
|
|
|
315
314
|
children: e.label
|
|
316
315
|
}), /* @__PURE__ */ v("span", {
|
|
317
316
|
className: "text-text-muted",
|
|
318
|
-
children:
|
|
317
|
+
children: N(e.value, U)
|
|
319
318
|
})]
|
|
320
319
|
}), /* @__PURE__ */ v("div", {
|
|
321
320
|
className: "h-2 rounded-full bg-bg-sunken",
|
|
322
321
|
children: /* @__PURE__ */ v("div", {
|
|
323
322
|
className: "h-2 rounded-full bg-action-primary-bg transition-[width]",
|
|
324
|
-
style: { width: `${
|
|
323
|
+
style: { width: `${Y > 0 ? e.value / Y * 100 : 0}%` }
|
|
325
324
|
})
|
|
326
325
|
})]
|
|
327
326
|
}, e.label))
|
|
@@ -338,14 +337,14 @@ var F = () => {
|
|
|
338
337
|
children: "New subscriptions created during the selected period."
|
|
339
338
|
})] }), /* @__PURE__ */ y("span", {
|
|
340
339
|
className: "rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text",
|
|
341
|
-
children: [
|
|
340
|
+
children: [K.reduce((e, t) => e + t.value, 0), " created"]
|
|
342
341
|
})]
|
|
343
|
-
}),
|
|
342
|
+
}), K.length === 0 ? /* @__PURE__ */ v("p", {
|
|
344
343
|
className: "rounded-lg bg-bg-sunken/40 p-4 text-sm text-text-muted",
|
|
345
344
|
children: "No new subscriptions were created during this period."
|
|
346
345
|
}) : /* @__PURE__ */ v("div", {
|
|
347
346
|
className: "space-y-3",
|
|
348
|
-
children:
|
|
347
|
+
children: K.map((e) => /* @__PURE__ */ y("div", {
|
|
349
348
|
className: "space-y-1",
|
|
350
349
|
children: [/* @__PURE__ */ y("div", {
|
|
351
350
|
className: "flex items-center justify-between gap-3 text-sm",
|
|
@@ -360,7 +359,7 @@ var F = () => {
|
|
|
360
359
|
className: "h-2 rounded-full bg-bg-sunken",
|
|
361
360
|
children: /* @__PURE__ */ v("div", {
|
|
362
361
|
className: "h-2 rounded-full bg-status-success-bg transition-[width]",
|
|
363
|
-
style: { width: `${
|
|
362
|
+
style: { width: `${X > 0 ? e.value / X * 100 : 0}%` }
|
|
364
363
|
})
|
|
365
364
|
})]
|
|
366
365
|
}, e.label))
|
|
@@ -382,12 +381,12 @@ var F = () => {
|
|
|
382
381
|
})]
|
|
383
382
|
}), /* @__PURE__ */ v("div", {
|
|
384
383
|
className: "p-4",
|
|
385
|
-
children:
|
|
384
|
+
children: q.length === 0 ? /* @__PURE__ */ v("p", {
|
|
386
385
|
className: "text-sm text-text-muted",
|
|
387
386
|
children: "No active plans to analyse yet."
|
|
388
387
|
}) : /* @__PURE__ */ v("div", {
|
|
389
388
|
className: "space-y-3",
|
|
390
|
-
children:
|
|
389
|
+
children: q.map((e) => /* @__PURE__ */ y("div", {
|
|
391
390
|
className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
|
|
392
391
|
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
|
|
393
392
|
className: "font-medium text-text-primary",
|
|
@@ -401,7 +400,7 @@ var F = () => {
|
|
|
401
400
|
]
|
|
402
401
|
})] }), /* @__PURE__ */ y("p", {
|
|
403
402
|
className: "text-sm font-semibold text-text-primary",
|
|
404
|
-
children: [
|
|
403
|
+
children: [N(e.estimatedMrr, U), " / month"]
|
|
405
404
|
})]
|
|
406
405
|
}, e.name))
|
|
407
406
|
})
|
|
@@ -419,12 +418,12 @@ var F = () => {
|
|
|
419
418
|
})]
|
|
420
419
|
}), /* @__PURE__ */ v("div", {
|
|
421
420
|
className: "p-4",
|
|
422
|
-
children:
|
|
421
|
+
children: J.length === 0 ? /* @__PURE__ */ v("p", {
|
|
423
422
|
className: "text-sm text-text-muted",
|
|
424
423
|
children: "No customer revenue data available yet."
|
|
425
424
|
}) : /* @__PURE__ */ v("div", {
|
|
426
425
|
className: "space-y-3",
|
|
427
|
-
children:
|
|
426
|
+
children: J.map((e) => /* @__PURE__ */ y("div", {
|
|
428
427
|
className: "flex flex-col gap-2 rounded-lg border border-border-subtle p-3 sm:flex-row sm:items-center sm:justify-between",
|
|
429
428
|
children: [/* @__PURE__ */ y("div", {
|
|
430
429
|
className: "min-w-0",
|
|
@@ -439,7 +438,7 @@ var F = () => {
|
|
|
439
438
|
className: "text-left sm:text-right",
|
|
440
439
|
children: [/* @__PURE__ */ y("p", {
|
|
441
440
|
className: "text-sm font-semibold text-text-primary",
|
|
442
|
-
children: [
|
|
441
|
+
children: [N(e.estimatedMrr, U), " / month"]
|
|
443
442
|
}), /* @__PURE__ */ y("p", {
|
|
444
443
|
className: "text-xs text-text-muted",
|
|
445
444
|
children: [
|
|
@@ -459,6 +458,6 @@ var F = () => {
|
|
|
459
458
|
}) : /* @__PURE__ */ v(e, { message: "You don't have permission to view analytics." });
|
|
460
459
|
};
|
|
461
460
|
//#endregion
|
|
462
|
-
export {
|
|
461
|
+
export { P as AnalyticsPage };
|
|
463
462
|
|
|
464
463
|
//# 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 {\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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"}
|
|
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 {\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 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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"}
|