@burdenoff/microfe-billing 2026.526.1 → 2026.528.1
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 +1 -1
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js +1 -1
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js +15 -8
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +13 -6
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js +8 -4
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +3 -2
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +44 -23
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js +60 -31
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +9 -9
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +8 -8
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +16 -10
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +24 -16
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +5 -1
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js +46 -29
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/TransactionsPage.js +34 -22
- package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js +10 -10
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js +3 -3
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js +7 -0
- package/dist/billing/modules/checkout/components/NativeCheckoutView.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js +13 -13
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js +5 -5
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js +5 -5
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js +4 -4
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js +11 -11
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js +3 -3
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js +8 -8
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +61 -52
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js +22 -15
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js +30 -21
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js +9 -8
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js +11 -8
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +22 -17
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +15 -8
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js +17 -11
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js +15 -11
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +21 -17
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js +14 -11
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js +32 -26
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js +14 -10
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +18 -11
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js +44 -32
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js +35 -26
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +15 -15
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +11 -11
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +5 -5
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +9 -4
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +13 -8
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +3 -2
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +14 -7
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js +23 -9
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaPage.js +11 -7
- package/dist/billing/modules/quota/pages/QuotaPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +3 -3
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +6 -2
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +5 -4
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +26 -12
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js +356 -349
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +157 -123
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js +13 -8
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +15 -7
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +30 -20
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +25 -16
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +50 -34
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/shared/components/AccessDenied.js +2 -2
- package/dist/billing/shared/components/AccessDenied.js.map +1 -1
- package/dist/billing/shared/components/ActorIdentity.js +67 -0
- package/dist/billing/shared/components/ActorIdentity.js.map +1 -0
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js +6 -2
- package/dist/billing/shared/components/CurrencyPriceOverridesEditor.js.map +1 -1
- package/dist/billing/shared/components/EmptyState.js +7 -21
- package/dist/billing/shared/components/EmptyState.js.map +1 -1
- package/dist/billing/shared/components/PageHeader.js +7 -19
- package/dist/billing/shared/components/PageHeader.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js +5 -4
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/index.js +1 -0
- package/dist/billing/shared/hooks/index.js +1 -0
- package/dist/billing/shared/hooks/useActorProfiles.js +113 -0
- package/dist/billing/shared/hooks/useActorProfiles.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,213 +1,247 @@
|
|
|
1
1
|
import { useBilling as e } from "../../../providers/BillingProvider.js";
|
|
2
2
|
import { AccessDenied as t } from "../../../shared/components/AccessDenied.js";
|
|
3
3
|
import { PageHeader as n } from "../../../shared/components/PageHeader.js";
|
|
4
|
+
import { ActorIdentity as r } from "../../../shared/components/ActorIdentity.js";
|
|
4
5
|
import "../../../shared/components/index.js";
|
|
5
|
-
import { useBillingAccountSelection as
|
|
6
|
-
import { directBillingGraphqlRequest as
|
|
6
|
+
import { useBillingAccountSelection as i } from "../../../hooks/useBillingAccountSelection.js";
|
|
7
|
+
import { directBillingGraphqlRequest as a } from "../../../shared/utils/directBillingGraphql.js";
|
|
7
8
|
import "../../../shared/utils/index.js";
|
|
8
|
-
import { useBillingPermissions as
|
|
9
|
-
import { useBillingAccounts as
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
9
|
+
import { useBillingPermissions as o } from "../../../hooks/useBillingPermissions.js";
|
|
10
|
+
import { useBillingAccounts as s } from "../../dashboard/hooks/useDashboard.js";
|
|
11
|
+
import { useActorProfiles as c } from "../../../shared/hooks/useActorProfiles.js";
|
|
12
|
+
import "../../../shared/hooks/index.js";
|
|
13
|
+
import { useCallback as l, useEffect as u, useMemo as d, useState as f } from "react";
|
|
14
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
12
15
|
//#region src/billing/modules/settings/pages/TeamPermissionsPage.tsx
|
|
13
|
-
var
|
|
14
|
-
let { apiGatewayUrl:
|
|
15
|
-
orgId:
|
|
16
|
-
billingAccounts:
|
|
16
|
+
var h = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, g = "\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n", _ = "\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n", v = "\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n", y = "\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n", b = () => {
|
|
17
|
+
let { apiGatewayUrl: b, authToken: x, orgId: S } = e(), { billingAccounts: C } = s(), { selectedAccountId: w } = i({
|
|
18
|
+
orgId: S,
|
|
19
|
+
billingAccounts: C,
|
|
17
20
|
syncFromUrl: !0,
|
|
18
21
|
urlParamName: "billingAccountId"
|
|
19
|
-
}),
|
|
20
|
-
apiGatewayUrl:
|
|
21
|
-
authToken:
|
|
22
|
-
orgId:
|
|
22
|
+
}), T = w ?? void 0, E = o({ billingAccountIdOverride: T }), [D, O] = f([]), [k, A] = f([]), [j, M] = f(!1), [N, P] = f(null), [F, I] = f(""), [L, R] = f(""), [z, B] = f(!1), V = l(async (e, t) => a({
|
|
23
|
+
apiGatewayUrl: b,
|
|
24
|
+
authToken: x,
|
|
25
|
+
orgId: S,
|
|
23
26
|
query: e,
|
|
24
27
|
variables: t
|
|
25
28
|
}), [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]),
|
|
30
|
-
if (
|
|
31
|
-
|
|
29
|
+
b,
|
|
30
|
+
x,
|
|
31
|
+
S
|
|
32
|
+
]), H = l(async () => {
|
|
33
|
+
if (T) {
|
|
34
|
+
M(!0), P(null);
|
|
32
35
|
try {
|
|
33
|
-
let [e, t] = await Promise.all([
|
|
34
|
-
|
|
36
|
+
let [e, t] = await Promise.all([V(g, { scopeId: T }), V(_, { scopeId: T })]);
|
|
37
|
+
O(e.roles ?? []), A(t.actors ?? []), R((e.roles ?? []).find((e) => /viewer/i.test(e.name))?.id ?? e.roles?.[0]?.id ?? "");
|
|
35
38
|
} catch (e) {
|
|
36
|
-
|
|
39
|
+
P(e instanceof Error ? e.message : "Failed to load team data");
|
|
37
40
|
} finally {
|
|
38
|
-
|
|
41
|
+
M(!1);
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
}, [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}, [
|
|
45
|
-
let
|
|
44
|
+
}, [T, V]);
|
|
45
|
+
u(() => {
|
|
46
|
+
H();
|
|
47
|
+
}, [H]);
|
|
48
|
+
let U = d(() => [...k].sort((e, t) => {
|
|
46
49
|
let n = Math.max(...e.roleAssignments.map((e) => e.role?.priority ?? 0), 0);
|
|
47
50
|
return Math.max(...t.roleAssignments.map((e) => e.role?.priority ?? 0), 0) - n;
|
|
48
|
-
}), [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
}), [k]), W = c(d(() => U.map((e) => ({
|
|
52
|
+
actorId: e.actorId,
|
|
53
|
+
actorType: e.actorType
|
|
54
|
+
})), [U])), G = d(() => {
|
|
55
|
+
if (T) return C.find((e) => e.id === T)?.name ?? void 0;
|
|
56
|
+
}, [T, C]), K = async (e) => {
|
|
57
|
+
if (e.preventDefault(), !T || !F.trim() || !L) return;
|
|
58
|
+
let t = F.trim();
|
|
59
|
+
if (!h.test(t)) {
|
|
60
|
+
P("User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.");
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
B(!0), P(null);
|
|
56
64
|
try {
|
|
57
|
-
await
|
|
65
|
+
await V(v, { input: {
|
|
58
66
|
actorId: t,
|
|
59
67
|
actorType: "USER",
|
|
60
|
-
roleId:
|
|
61
|
-
scopeId:
|
|
62
|
-
} }),
|
|
68
|
+
roleId: L,
|
|
69
|
+
scopeId: T
|
|
70
|
+
} }), I(""), await H();
|
|
63
71
|
} catch (e) {
|
|
64
|
-
|
|
72
|
+
P(e instanceof Error ? e.message : "Failed to assign role");
|
|
65
73
|
} finally {
|
|
66
|
-
|
|
74
|
+
B(!1);
|
|
67
75
|
}
|
|
68
|
-
},
|
|
76
|
+
}, q = async (e) => {
|
|
69
77
|
if (confirm("Remove this role assignment?")) {
|
|
70
|
-
|
|
78
|
+
P(null);
|
|
71
79
|
try {
|
|
72
|
-
await
|
|
80
|
+
await V(y, { assignmentId: e }), await H();
|
|
73
81
|
} catch (e) {
|
|
74
|
-
|
|
82
|
+
P(e instanceof Error ? e.message : "Failed to remove role");
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
};
|
|
78
|
-
return
|
|
86
|
+
return E.canManageBillingAccount ? T ? /* @__PURE__ */ m("div", {
|
|
79
87
|
className: "space-y-6",
|
|
80
88
|
children: [
|
|
81
|
-
/* @__PURE__ */
|
|
89
|
+
/* @__PURE__ */ p(n, {
|
|
82
90
|
title: "Team & Permissions",
|
|
83
91
|
description: "Control who can manage this billing account and what they can do."
|
|
84
92
|
}),
|
|
85
|
-
/* @__PURE__ */
|
|
86
|
-
className: "rounded-lg border border-border bg-card p-4 text-
|
|
93
|
+
/* @__PURE__ */ m("div", {
|
|
94
|
+
className: "rounded-lg border border-border bg-card p-4 text-sm",
|
|
87
95
|
children: [
|
|
88
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ p("span", {
|
|
89
97
|
className: "font-medium text-foreground",
|
|
90
98
|
children: "Billing account:"
|
|
91
99
|
}),
|
|
92
100
|
" ",
|
|
93
|
-
/* @__PURE__ */
|
|
94
|
-
className: "
|
|
95
|
-
children:
|
|
96
|
-
})
|
|
101
|
+
/* @__PURE__ */ p("span", {
|
|
102
|
+
className: "text-foreground",
|
|
103
|
+
children: G ?? "Unnamed account"
|
|
104
|
+
}),
|
|
105
|
+
T ? /* @__PURE__ */ m("details", {
|
|
106
|
+
className: "mt-2 text-xs text-muted-foreground",
|
|
107
|
+
children: [/* @__PURE__ */ p("summary", {
|
|
108
|
+
className: "cursor-pointer",
|
|
109
|
+
children: "Account ID"
|
|
110
|
+
}), /* @__PURE__ */ p("span", {
|
|
111
|
+
className: "mt-1 inline-block font-mono",
|
|
112
|
+
children: T
|
|
113
|
+
})]
|
|
114
|
+
}) : null
|
|
97
115
|
]
|
|
98
116
|
}),
|
|
99
|
-
|
|
117
|
+
N && /* @__PURE__ */ p("div", {
|
|
100
118
|
className: "rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive",
|
|
101
|
-
children:
|
|
119
|
+
children: N
|
|
102
120
|
}),
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
-
onSubmit: (e) => void
|
|
121
|
+
/* @__PURE__ */ m("form", {
|
|
122
|
+
onSubmit: (e) => void K(e),
|
|
105
123
|
className: "rounded-lg border border-border bg-card p-4 space-y-3",
|
|
106
124
|
children: [
|
|
107
|
-
/* @__PURE__ */
|
|
125
|
+
/* @__PURE__ */ p("h3", {
|
|
108
126
|
className: "text-sm font-semibold text-foreground",
|
|
109
127
|
children: "Add member"
|
|
110
128
|
}),
|
|
111
|
-
/* @__PURE__ */
|
|
129
|
+
/* @__PURE__ */ m("div", {
|
|
112
130
|
className: "grid grid-cols-1 gap-3 md:grid-cols-3",
|
|
113
|
-
children: [/* @__PURE__ */
|
|
131
|
+
children: [/* @__PURE__ */ m("div", {
|
|
114
132
|
className: "md:col-span-2",
|
|
115
|
-
children: [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ p("label", {
|
|
135
|
+
htmlFor: "newActorId",
|
|
136
|
+
className: "block text-xs font-medium text-muted-foreground",
|
|
137
|
+
children: "User ID"
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ p("input", {
|
|
140
|
+
id: "newActorId",
|
|
141
|
+
type: "text",
|
|
142
|
+
value: F,
|
|
143
|
+
onChange: (e) => I(e.target.value),
|
|
144
|
+
placeholder: "Paste the user ID from their profile page",
|
|
145
|
+
className: "mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm",
|
|
146
|
+
required: !0
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ p("p", {
|
|
149
|
+
className: "mt-1 text-[11px] text-muted-foreground",
|
|
150
|
+
children: "Find this on the user's profile page."
|
|
151
|
+
})
|
|
152
|
+
]
|
|
153
|
+
}), /* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("label", {
|
|
129
154
|
htmlFor: "newRoleId",
|
|
130
155
|
className: "block text-xs font-medium text-muted-foreground",
|
|
131
156
|
children: "Role"
|
|
132
|
-
}), /* @__PURE__ */
|
|
157
|
+
}), /* @__PURE__ */ p("select", {
|
|
133
158
|
id: "newRoleId",
|
|
134
|
-
value:
|
|
135
|
-
onChange: (e) =>
|
|
159
|
+
value: L,
|
|
160
|
+
onChange: (e) => R(e.target.value),
|
|
136
161
|
className: "mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm",
|
|
137
162
|
required: !0,
|
|
138
|
-
children:
|
|
163
|
+
children: D.map((e) => /* @__PURE__ */ p("option", {
|
|
139
164
|
value: e.id,
|
|
140
165
|
children: e.name
|
|
141
166
|
}, e.id))
|
|
142
167
|
})] })]
|
|
143
168
|
}),
|
|
144
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ p("button", {
|
|
145
170
|
type: "submit",
|
|
146
|
-
disabled:
|
|
171
|
+
disabled: z || !F.trim() || !L,
|
|
147
172
|
className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50",
|
|
148
|
-
children:
|
|
173
|
+
children: z ? "Adding…" : "Add member"
|
|
149
174
|
})
|
|
150
175
|
]
|
|
151
176
|
}),
|
|
152
|
-
/* @__PURE__ */
|
|
177
|
+
/* @__PURE__ */ m("div", {
|
|
153
178
|
className: "rounded-lg border border-border bg-card",
|
|
154
|
-
children: [/* @__PURE__ */
|
|
179
|
+
children: [/* @__PURE__ */ m("div", {
|
|
155
180
|
className: "border-b border-border px-4 py-3",
|
|
156
|
-
children: [/* @__PURE__ */
|
|
181
|
+
children: [/* @__PURE__ */ p("h3", {
|
|
157
182
|
className: "text-sm font-semibold text-foreground",
|
|
158
183
|
children: "Members"
|
|
159
|
-
}), /* @__PURE__ */
|
|
184
|
+
}), /* @__PURE__ */ p("p", {
|
|
160
185
|
className: "text-xs text-muted-foreground",
|
|
161
186
|
children: "Each member's role applies only to this billing account."
|
|
162
187
|
})]
|
|
163
|
-
}),
|
|
188
|
+
}), j ? /* @__PURE__ */ p("div", {
|
|
164
189
|
className: "p-6 text-sm text-muted-foreground",
|
|
165
190
|
children: "Loading…"
|
|
166
|
-
}) :
|
|
191
|
+
}) : U.length === 0 ? /* @__PURE__ */ p("div", {
|
|
167
192
|
className: "p-6 text-sm text-muted-foreground",
|
|
168
193
|
children: "No members assigned yet."
|
|
169
|
-
}) : /* @__PURE__ */
|
|
194
|
+
}) : /* @__PURE__ */ m("table", {
|
|
170
195
|
className: "w-full text-sm",
|
|
171
|
-
children: [/* @__PURE__ */
|
|
196
|
+
children: [/* @__PURE__ */ p("thead", { children: /* @__PURE__ */ m("tr", {
|
|
172
197
|
className: "border-b border-border text-left text-xs text-muted-foreground",
|
|
173
198
|
children: [
|
|
174
|
-
/* @__PURE__ */
|
|
199
|
+
/* @__PURE__ */ p("th", {
|
|
175
200
|
className: "px-4 py-2",
|
|
176
|
-
children: "
|
|
201
|
+
children: "Member"
|
|
177
202
|
}),
|
|
178
|
-
/* @__PURE__ */
|
|
203
|
+
/* @__PURE__ */ p("th", {
|
|
179
204
|
className: "px-4 py-2",
|
|
180
205
|
children: "Type"
|
|
181
206
|
}),
|
|
182
|
-
/* @__PURE__ */
|
|
207
|
+
/* @__PURE__ */ p("th", {
|
|
183
208
|
className: "px-4 py-2",
|
|
184
209
|
children: "Roles"
|
|
185
210
|
}),
|
|
186
|
-
/* @__PURE__ */
|
|
211
|
+
/* @__PURE__ */ p("th", {
|
|
187
212
|
className: "px-4 py-2 text-right",
|
|
188
213
|
children: "Actions"
|
|
189
214
|
})
|
|
190
215
|
]
|
|
191
|
-
}) }), /* @__PURE__ */
|
|
216
|
+
}) }), /* @__PURE__ */ p("tbody", { children: U.map((e) => /* @__PURE__ */ m("tr", {
|
|
192
217
|
className: "border-b border-border last:border-0",
|
|
193
218
|
children: [
|
|
194
|
-
/* @__PURE__ */
|
|
195
|
-
className: "px-4 py-3
|
|
196
|
-
children:
|
|
219
|
+
/* @__PURE__ */ p("td", {
|
|
220
|
+
className: "px-4 py-3",
|
|
221
|
+
children: /* @__PURE__ */ p(r, {
|
|
222
|
+
profile: W[e.actorId] ?? {
|
|
223
|
+
actorId: e.actorId,
|
|
224
|
+
actorType: e.actorType,
|
|
225
|
+
displayName: e.actorType.toLowerCase() === "user" ? `User ${e.actorId.slice(0, 8)}…` : e.actorType,
|
|
226
|
+
secondaryLabel: e.actorId.slice(0, 12),
|
|
227
|
+
resolved: !1
|
|
228
|
+
},
|
|
229
|
+
variant: "cell"
|
|
230
|
+
})
|
|
197
231
|
}),
|
|
198
|
-
/* @__PURE__ */
|
|
199
|
-
className: "px-4 py-3 text-xs",
|
|
200
|
-
children: e.actorType
|
|
232
|
+
/* @__PURE__ */ p("td", {
|
|
233
|
+
className: "px-4 py-3 text-xs capitalize text-muted-foreground",
|
|
234
|
+
children: e.actorType.toLowerCase()
|
|
201
235
|
}),
|
|
202
|
-
/* @__PURE__ */
|
|
236
|
+
/* @__PURE__ */ p("td", {
|
|
203
237
|
className: "px-4 py-3",
|
|
204
|
-
children: /* @__PURE__ */
|
|
238
|
+
children: /* @__PURE__ */ p("div", {
|
|
205
239
|
className: "flex flex-wrap gap-1",
|
|
206
|
-
children: e.roleAssignments.filter((e) => e.isActive).map((e) => /* @__PURE__ */
|
|
240
|
+
children: e.roleAssignments.filter((e) => e.isActive).map((e) => /* @__PURE__ */ m("span", {
|
|
207
241
|
className: "inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs",
|
|
208
|
-
children: [e.role?.name ?? e.roleId, /* @__PURE__ */
|
|
242
|
+
children: [e.role?.name ?? e.roleId, /* @__PURE__ */ p("button", {
|
|
209
243
|
type: "button",
|
|
210
|
-
onClick: () => void
|
|
244
|
+
onClick: () => void q(e.id),
|
|
211
245
|
className: "text-muted-foreground hover:text-destructive",
|
|
212
246
|
"aria-label": `Remove ${e.role?.name ?? "role"}`,
|
|
213
247
|
children: "×"
|
|
@@ -215,9 +249,9 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
215
249
|
}, e.id))
|
|
216
250
|
})
|
|
217
251
|
}),
|
|
218
|
-
/* @__PURE__ */
|
|
252
|
+
/* @__PURE__ */ p("td", {
|
|
219
253
|
className: "px-4 py-3 text-right",
|
|
220
|
-
children: /* @__PURE__ */
|
|
254
|
+
children: /* @__PURE__ */ m("span", {
|
|
221
255
|
className: "text-xs text-muted-foreground",
|
|
222
256
|
children: [e.effectivePermissionCount ?? "—", " permissions"]
|
|
223
257
|
})
|
|
@@ -226,31 +260,31 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
226
260
|
}, e.actorId)) })]
|
|
227
261
|
})]
|
|
228
262
|
}),
|
|
229
|
-
/* @__PURE__ */
|
|
263
|
+
/* @__PURE__ */ m("div", {
|
|
230
264
|
className: "rounded-lg border border-border bg-card p-4",
|
|
231
265
|
children: [
|
|
232
|
-
/* @__PURE__ */
|
|
266
|
+
/* @__PURE__ */ p("h3", {
|
|
233
267
|
className: "text-sm font-semibold text-foreground",
|
|
234
268
|
children: "Available roles"
|
|
235
269
|
}),
|
|
236
|
-
/* @__PURE__ */
|
|
270
|
+
/* @__PURE__ */ p("p", {
|
|
237
271
|
className: "mt-1 text-xs text-muted-foreground",
|
|
238
272
|
children: "Roles are cloned from the system templates when a billing account is created."
|
|
239
273
|
}),
|
|
240
|
-
/* @__PURE__ */
|
|
274
|
+
/* @__PURE__ */ p("ul", {
|
|
241
275
|
className: "mt-3 space-y-2",
|
|
242
|
-
children:
|
|
276
|
+
children: D.map((e) => /* @__PURE__ */ m("li", {
|
|
243
277
|
className: "flex items-start gap-3 rounded-md border border-border bg-background p-3",
|
|
244
|
-
children: [/* @__PURE__ */
|
|
278
|
+
children: [/* @__PURE__ */ m("div", {
|
|
245
279
|
className: "flex-1",
|
|
246
|
-
children: [/* @__PURE__ */
|
|
280
|
+
children: [/* @__PURE__ */ p("div", {
|
|
247
281
|
className: "text-sm font-medium text-foreground",
|
|
248
282
|
children: e.name
|
|
249
|
-
}), e.description && /* @__PURE__ */
|
|
283
|
+
}), e.description && /* @__PURE__ */ p("p", {
|
|
250
284
|
className: "mt-1 text-xs text-muted-foreground",
|
|
251
285
|
children: e.description
|
|
252
286
|
})]
|
|
253
|
-
}), /* @__PURE__ */
|
|
287
|
+
}), /* @__PURE__ */ m("span", {
|
|
254
288
|
className: "text-xs text-muted-foreground",
|
|
255
289
|
children: ["priority ", e.priority]
|
|
256
290
|
})]
|
|
@@ -259,18 +293,18 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
259
293
|
]
|
|
260
294
|
})
|
|
261
295
|
]
|
|
262
|
-
}) : /* @__PURE__ */
|
|
296
|
+
}) : /* @__PURE__ */ m("div", {
|
|
263
297
|
className: "space-y-6",
|
|
264
|
-
children: [/* @__PURE__ */
|
|
298
|
+
children: [/* @__PURE__ */ p(n, {
|
|
265
299
|
title: "Team & Permissions",
|
|
266
300
|
description: "Select a billing account to manage who can access it."
|
|
267
|
-
}), /* @__PURE__ */
|
|
301
|
+
}), /* @__PURE__ */ p("div", {
|
|
268
302
|
className: "rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
269
303
|
children: "No billing account selected. Pick one from the billing account selector to manage its members and roles."
|
|
270
304
|
})]
|
|
271
|
-
}) : /* @__PURE__ */
|
|
305
|
+
}) : /* @__PURE__ */ p(t, { message: "You don't have permission to manage team and permissions for this billing account." });
|
|
272
306
|
};
|
|
273
307
|
//#endregion
|
|
274
|
-
export {
|
|
308
|
+
export { b as TeamPermissionsPage };
|
|
275
309
|
|
|
276
310
|
//# sourceMappingURL=TeamPermissionsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n if (!confirm('Remove this role assignment?')) return;\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border bg-card p-4 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">Billing account:</span>{' '}\n <span className=\"font-mono\">{billingAccountId}</span>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border bg-card p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-muted-foreground\">\n User ID (actorId)\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"UUID of the user to grant access\"\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n />\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-muted-foreground\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border bg-card\">\n <div className=\"border-b border-border px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">Members</h3>\n <p className=\"text-xs text-muted-foreground\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-muted-foreground\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-muted-foreground\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-left text-xs text-muted-foreground\">\n <th className=\"px-4 py-2\">Actor ID</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border last:border-0\">\n <td className=\"px-4 py-3 font-mono text-xs\">{actor.actorId}</td>\n <td className=\"px-4 py-3 text-xs\">{actor.actorType}</td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => void handleRemoveAssignment(ra.id)}\n className=\"text-muted-foreground hover:text-destructive\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-muted-foreground\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border bg-card p-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">Available roles</h3>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border bg-background p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-foreground\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-muted-foreground\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, ActorIdentity, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport { useActorProfiles } from '../../../shared/hooks';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const profileInputs = useMemo(\n () =>\n sortedActors.map((a) => ({\n actorId: a.actorId,\n actorType: a.actorType,\n })),\n [sortedActors]\n );\n const profileMap = useActorProfiles(profileInputs);\n\n const billingAccountName = useMemo(() => {\n if (!billingAccountId) return undefined;\n return billingAccounts.find((acct) => acct.id === billingAccountId)?.name ?? undefined;\n }, [billingAccountId, billingAccounts]);\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n if (!confirm('Remove this role assignment?')) return;\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border bg-card p-4 text-sm\">\n <span className=\"font-medium text-foreground\">Billing account:</span>{' '}\n <span className=\"text-foreground\">{billingAccountName ?? 'Unnamed account'}</span>\n {billingAccountId ? (\n <details className=\"mt-2 text-xs text-muted-foreground\">\n <summary className=\"cursor-pointer\">Account ID</summary>\n <span className=\"mt-1 inline-block font-mono\">{billingAccountId}</span>\n </details>\n ) : null}\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border bg-card p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-muted-foreground\">\n User ID\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"Paste the user ID from their profile page\"\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n />\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n Find this on the user's profile page.\n </p>\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-muted-foreground\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border bg-card\">\n <div className=\"border-b border-border px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">Members</h3>\n <p className=\"text-xs text-muted-foreground\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-muted-foreground\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-muted-foreground\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-left text-xs text-muted-foreground\">\n <th className=\"px-4 py-2\">Member</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border last:border-0\">\n <td className=\"px-4 py-3\">\n <ActorIdentity\n profile={\n profileMap[actor.actorId] ?? {\n actorId: actor.actorId,\n actorType: actor.actorType,\n displayName:\n actor.actorType.toLowerCase() === 'user'\n ? `User ${actor.actorId.slice(0, 8)}…`\n : actor.actorType,\n secondaryLabel: actor.actorId.slice(0, 12),\n resolved: false,\n }\n }\n variant=\"cell\"\n />\n </td>\n <td className=\"px-4 py-3 text-xs capitalize text-muted-foreground\">\n {actor.actorType.toLowerCase()}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => void handleRemoveAssignment(ra.id)}\n className=\"text-muted-foreground hover:text-destructive\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-muted-foreground\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border bg-card p-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">Available roles</h3>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border bg-background p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-foreground\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-muted-foreground\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -49,12 +49,13 @@ var d = ({ subscription: d, onClose: f, onConfirm: p, isLoading: m = !1 }) => {
|
|
|
49
49
|
className: "flex items-center justify-between p-6 border-b border-border",
|
|
50
50
|
children: [/* @__PURE__ */ l("div", {
|
|
51
51
|
className: "flex items-center gap-2",
|
|
52
|
-
children: [/* @__PURE__ */ c(s, { className: "
|
|
52
|
+
children: [/* @__PURE__ */ c(s, { className: "size-5 text-primary" }), /* @__PURE__ */ c("h2", {
|
|
53
53
|
id: "manage-seats-title",
|
|
54
54
|
className: "text-lg font-semibold text-foreground",
|
|
55
55
|
children: g("billing.subscriptions.manageSeats", "Manage Seats")
|
|
56
56
|
})]
|
|
57
57
|
}), /* @__PURE__ */ c("button", {
|
|
58
|
+
type: "button",
|
|
58
59
|
onClick: f,
|
|
59
60
|
className: "text-muted-foreground hover:text-foreground transition-colors",
|
|
60
61
|
"aria-label": "Close manage seats dialog",
|
|
@@ -80,11 +81,12 @@ var d = ({ subscription: d, onClose: f, onConfirm: p, isLoading: m = !1 }) => {
|
|
|
80
81
|
className: "flex items-center gap-3",
|
|
81
82
|
children: [
|
|
82
83
|
/* @__PURE__ */ c("button", {
|
|
84
|
+
type: "button",
|
|
83
85
|
onClick: j,
|
|
84
86
|
disabled: C <= v || m,
|
|
85
|
-
className: "
|
|
87
|
+
className: "size-9 rounded-md border border-border flex items-center justify-center hover:bg-accent disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
|
|
86
88
|
"aria-label": "Decrease seat count",
|
|
87
|
-
children: /* @__PURE__ */ c(a, { className: "
|
|
89
|
+
children: /* @__PURE__ */ c(a, { className: "size-4" })
|
|
88
90
|
}),
|
|
89
91
|
/* @__PURE__ */ c("input", {
|
|
90
92
|
type: "number",
|
|
@@ -96,11 +98,12 @@ var d = ({ subscription: d, onClose: f, onConfirm: p, isLoading: m = !1 }) => {
|
|
|
96
98
|
className: "w-20 h-9 text-center border border-border rounded-md bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
|
|
97
99
|
}),
|
|
98
100
|
/* @__PURE__ */ c("button", {
|
|
101
|
+
type: "button",
|
|
99
102
|
onClick: M,
|
|
100
103
|
disabled: C >= y || m,
|
|
101
|
-
className: "
|
|
104
|
+
className: "size-9 rounded-md border border-border flex items-center justify-center hover:bg-accent disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
|
|
102
105
|
"aria-label": "Increase seat count",
|
|
103
|
-
children: /* @__PURE__ */ c(o, { className: "
|
|
106
|
+
children: /* @__PURE__ */ c(o, { className: "size-4" })
|
|
104
107
|
}),
|
|
105
108
|
/* @__PURE__ */ c("span", {
|
|
106
109
|
className: "text-sm text-muted-foreground",
|
|
@@ -135,26 +138,28 @@ var d = ({ subscription: d, onClose: f, onConfirm: p, isLoading: m = !1 }) => {
|
|
|
135
138
|
}),
|
|
136
139
|
A && D > 0 && /* @__PURE__ */ l("div", {
|
|
137
140
|
className: "flex gap-2 p-3 rounded-md bg-status-warning-bg-subtle border border-status-warning-border text-sm text-status-warning-text",
|
|
138
|
-
children: [/* @__PURE__ */ c(r, { className: "
|
|
141
|
+
children: [/* @__PURE__ */ c(r, { className: "size-4 mt-0.5 shrink-0" }), /* @__PURE__ */ c("span", { children: "Seat additions apply from the next billing cycle. A separate prorated charge will be raised for the remaining days in the current cycle." })]
|
|
139
142
|
}),
|
|
140
143
|
T && /* @__PURE__ */ l("div", {
|
|
141
144
|
className: "flex gap-2 p-3 rounded-md bg-destructive/10 border border-destructive/30 text-sm text-destructive",
|
|
142
|
-
children: [/* @__PURE__ */ c(r, { className: "
|
|
145
|
+
children: [/* @__PURE__ */ c(r, { className: "size-4 mt-0.5 shrink-0" }), /* @__PURE__ */ c("span", { children: T })]
|
|
143
146
|
})
|
|
144
147
|
]
|
|
145
148
|
}),
|
|
146
149
|
/* @__PURE__ */ l("div", {
|
|
147
150
|
className: "flex items-center justify-end gap-3 p-6 border-t border-border",
|
|
148
151
|
children: [/* @__PURE__ */ c("button", {
|
|
152
|
+
type: "button",
|
|
149
153
|
onClick: f,
|
|
150
154
|
disabled: m,
|
|
151
155
|
className: "px-4 py-2 text-sm rounded-md border border-border hover:bg-accent text-foreground transition-colors disabled:opacity-50",
|
|
152
156
|
children: "Cancel"
|
|
153
157
|
}), /* @__PURE__ */ l("button", {
|
|
158
|
+
type: "button",
|
|
154
159
|
onClick: P,
|
|
155
160
|
disabled: m || C === S || C < v || C > y,
|
|
156
161
|
className: "px-4 py-2 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2",
|
|
157
|
-
children: [m && /* @__PURE__ */ c(i, { className: "
|
|
162
|
+
children: [m && /* @__PURE__ */ c(i, { className: "size-4 animate-spin" }), C === S ? "No change" : D > 0 ? `Add ${D} seat${D === 1 ? "" : "s"}` : `Remove ${Math.abs(D)} seat${Math.abs(D) === 1 ? "" : "s"}`]
|
|
158
163
|
})]
|
|
159
164
|
})
|
|
160
165
|
]
|