@burdenoff/microfe-billing 2026.513.2 → 2026.514.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/modules/dashboard/hooks/useDashboard.js +32 -21
- package/dist/billing/modules/dashboard/hooks/useDashboard.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.d.ts.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +108 -102
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { GetBillingAccountsByOrgDocument as e, GetBillingAddressDocument as t, GetCreditTransactionsSummaryDocument as n, GetDefaultBillingAccountDashboardDocument as r, useAddPaymentMethodDashboardMutation as i, useCreateBillingAccountMutation as a, useDeletePaymentMethodDashboardMutation as o,
|
|
2
|
-
import { useCallback as
|
|
3
|
-
import { useApolloClient as
|
|
1
|
+
import { GetBillingAccountsByOrgDocument as e, GetBillingAddressDocument as t, GetCreditTransactionsSummaryDocument as n, GetDefaultBillingAccountDashboardDocument as r, useAddPaymentMethodDashboardMutation as i, useCreateBillingAccountMutation as a, useDeletePaymentMethodDashboardMutation as o, useGetBillingAccountsByOrgQuery as s, useGetDefaultBillingAccountDashboardQuery as c, useGetPaymentProviderConfigQuery as l, useInitiatePaymentMutation as u, useSetBillingAddressMutation as d, useSetDefaultBillingAccountMutation as f, useSpendCreditsMutation as p, useUpdateBillingAccountMutation as m, useUpdatePaymentMethodDashboardMutation as h, useVerifyPaymentMethodMutation as g } from "../../../../generated/global-operations.js";
|
|
2
|
+
import { useCallback as _ } from "react";
|
|
3
|
+
import { useApolloClient as v } from "@apollo/client/react";
|
|
4
4
|
//#region src/billing/modules/dashboard/hooks/useDashboard.ts
|
|
5
|
-
function
|
|
6
|
-
let { data: t, loading: n, error: r } =
|
|
5
|
+
function y(e = !1) {
|
|
6
|
+
let { data: t, loading: n, error: r } = l({
|
|
7
7
|
fetchPolicy: "cache-first",
|
|
8
8
|
skip: e
|
|
9
9
|
}), i = t?.getPaymentProviderConfig || null;
|
|
@@ -17,8 +17,19 @@ function v(e = !1) {
|
|
|
17
17
|
error: r
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
let { data: e, loading: t, error: n, refetch: r } = s({ fetchPolicy: "cache-and-network" }), i =
|
|
20
|
+
function b() {
|
|
21
|
+
let { data: e, loading: t, error: n, refetch: r } = s({ fetchPolicy: "cache-and-network" }), i = _(async () => {
|
|
22
|
+
await r();
|
|
23
|
+
}, [r]);
|
|
24
|
+
return {
|
|
25
|
+
billingAccounts: e?.getBillingAccountsByOrg || [],
|
|
26
|
+
isLoading: t,
|
|
27
|
+
error: n,
|
|
28
|
+
refetch: i
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function x() {
|
|
32
|
+
let { data: e, loading: t, error: n, refetch: r } = c({ fetchPolicy: "cache-and-network" }), i = _(async () => {
|
|
22
33
|
await r();
|
|
23
34
|
}, [r]), a = e?.getDefaultBillingAccount || null;
|
|
24
35
|
return {
|
|
@@ -30,37 +41,37 @@ function y() {
|
|
|
30
41
|
refetch: i
|
|
31
42
|
};
|
|
32
43
|
}
|
|
33
|
-
function
|
|
34
|
-
let s =
|
|
44
|
+
function S() {
|
|
45
|
+
let s = v(), [c, { loading: l }] = a(), [y, { loading: b }] = m(), [x, { loading: S }] = f(), [C, { loading: w }] = i(), [T, { loading: E }] = h(), [D, { loading: O }] = o(), [k, { loading: A }] = d(), [j, { loading: M }] = u(), [N, { loading: P }] = p(), [F, { loading: I }] = g();
|
|
35
46
|
return {
|
|
36
|
-
createBillingAccount:
|
|
47
|
+
createBillingAccount: _(async (t) => {
|
|
37
48
|
let { data: n, error: i } = await c({ variables: { input: t } });
|
|
38
49
|
if (i) throw Error(i.message ?? "Failed to create billing account");
|
|
39
50
|
if (!n?.createBillingAccount) throw Error("Failed to create billing account: no data returned");
|
|
40
51
|
return await s.refetchQueries({ include: [e, r] }), n.createBillingAccount;
|
|
41
52
|
}, [c, s]),
|
|
42
|
-
updateBillingAccount:
|
|
53
|
+
updateBillingAccount: _(async (e) => {
|
|
43
54
|
let { data: t, error: n } = await y({ variables: { input: e } });
|
|
44
55
|
if (n) throw Error(n.message ?? "Failed to update billing account");
|
|
45
56
|
if (!t?.updateBillingAccount) throw Error("Failed to update billing account: no data returned");
|
|
46
57
|
return await s.refetchQueries({ include: [r] }), t.updateBillingAccount;
|
|
47
58
|
}, [y, s]),
|
|
48
|
-
setDefaultBillingAccount:
|
|
59
|
+
setDefaultBillingAccount: _(async (t) => {
|
|
49
60
|
let { data: n, error: i } = await x({ variables: { id: t } });
|
|
50
61
|
if (i) throw Error(i.message ?? "Failed to set default billing account");
|
|
51
62
|
if (!n?.setDefaultBillingAccount) throw Error("Failed to set default billing account: no data returned");
|
|
52
63
|
return await s.refetchQueries({ include: [e, r] }), n.setDefaultBillingAccount;
|
|
53
64
|
}, [x, s]),
|
|
54
|
-
isCreatingAccount:
|
|
65
|
+
isCreatingAccount: l,
|
|
55
66
|
isUpdatingAccount: b,
|
|
56
67
|
isSettingDefaultAccount: S,
|
|
57
|
-
addPaymentMethod:
|
|
68
|
+
addPaymentMethod: _(async (e) => {
|
|
58
69
|
let { data: t, error: n } = await C({ variables: { input: e } });
|
|
59
70
|
if (n) throw Error(n.message ?? "Failed to add payment method");
|
|
60
71
|
if (!t?.addPaymentMethod) throw Error("Failed to add payment method: no data returned");
|
|
61
72
|
return t.addPaymentMethod;
|
|
62
73
|
}, [C]),
|
|
63
|
-
setDefaultPaymentMethod:
|
|
74
|
+
setDefaultPaymentMethod: _(async (e, t) => {
|
|
64
75
|
let { data: n, error: r } = await T({ variables: {
|
|
65
76
|
id: e,
|
|
66
77
|
isDefault: !0,
|
|
@@ -70,7 +81,7 @@ function b() {
|
|
|
70
81
|
if (!n?.updatePaymentMethod) throw Error("Failed to update payment method: no data returned");
|
|
71
82
|
return n.updatePaymentMethod;
|
|
72
83
|
}, [T]),
|
|
73
|
-
deletePaymentMethod:
|
|
84
|
+
deletePaymentMethod: _(async (e, t) => {
|
|
74
85
|
let { data: n, error: r } = await D({ variables: {
|
|
75
86
|
id: e,
|
|
76
87
|
billingAccountId: t
|
|
@@ -82,26 +93,26 @@ function b() {
|
|
|
82
93
|
isAddingPaymentMethod: w,
|
|
83
94
|
isUpdatingPaymentMethod: E,
|
|
84
95
|
isDeletingPaymentMethod: O,
|
|
85
|
-
setBillingAddress:
|
|
96
|
+
setBillingAddress: _(async (e) => {
|
|
86
97
|
let { data: n, error: i } = await k({ variables: { input: e } });
|
|
87
98
|
if (i) throw Error(i.message ?? "Failed to set billing address");
|
|
88
99
|
if (!n?.setBillingAddress) throw Error("Failed to set billing address: no data returned");
|
|
89
100
|
return await s.refetchQueries({ include: [t, r] }), n.setBillingAddress;
|
|
90
101
|
}, [k, s]),
|
|
91
102
|
isSettingAddress: A,
|
|
92
|
-
initiatePayment:
|
|
103
|
+
initiatePayment: _(async (e) => {
|
|
93
104
|
let { data: t, error: n } = await j({ variables: { input: e } });
|
|
94
105
|
if (n) throw Error(n.message ?? "Failed to initiate payment");
|
|
95
106
|
if (!t?.initiatePayment) throw Error("Failed to initiate payment: no data returned");
|
|
96
107
|
return t.initiatePayment;
|
|
97
108
|
}, [j]),
|
|
98
|
-
spendCredits:
|
|
109
|
+
spendCredits: _(async (e) => {
|
|
99
110
|
let { data: t, error: i } = await N({ variables: { input: e } });
|
|
100
111
|
if (i) throw Error(i.message ?? "Failed to spend credits");
|
|
101
112
|
if (!t?.spendCredits) throw Error("Failed to spend credits: no data returned");
|
|
102
113
|
return await s.refetchQueries({ include: [r, n] }), t.spendCredits;
|
|
103
114
|
}, [N, s]),
|
|
104
|
-
verifyPaymentMethod:
|
|
115
|
+
verifyPaymentMethod: _(async (e) => {
|
|
105
116
|
let { data: t, error: n } = await F({ variables: { input: e } });
|
|
106
117
|
if (n) throw Error(n.message ?? "Failed to verify payment method");
|
|
107
118
|
if (!t?.verifyBillingPaymentMethod) throw Error("Failed to verify payment method: no data returned");
|
|
@@ -113,6 +124,6 @@ function b() {
|
|
|
113
124
|
};
|
|
114
125
|
}
|
|
115
126
|
//#endregion
|
|
116
|
-
export { b as useDashboardMutations,
|
|
127
|
+
export { b as useBillingAccounts, S as useDashboardMutations, x as useDefaultDashboardAccount, y as usePaymentProviderConfig };
|
|
117
128
|
|
|
118
129
|
//# sourceMappingURL=useDashboard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDashboard.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/hooks/useDashboard.ts"],"sourcesContent":["/**\n * Dashboard Module - useDashboard Hook\n * Provides access to dashboard data and mutations for billing overview\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback, useMemo } from 'react';\nimport {\n useGetPaymentProviderConfigQuery,\n useGetBillingAccountsByOrgQuery,\n useGetDefaultBillingAccountDashboardQuery,\n useGetBillingAccountDashboardQuery,\n useGetActiveSubscriptionsQuery,\n useGetPaymentMethodsDashboardQuery,\n useGetBillingAddressQuery,\n useGetCreditTransactionsSummaryQuery,\n useGetRecommendedAddonsQuery,\n useGetAvailablePlansQuery,\n useCreateBillingAccountMutation,\n useUpdateBillingAccountMutation,\n useSetDefaultBillingAccountMutation,\n useAddPaymentMethodDashboardMutation,\n useUpdatePaymentMethodDashboardMutation,\n useDeletePaymentMethodDashboardMutation,\n useSetBillingAddressMutation,\n useInitiatePaymentMutation,\n useSpendCreditsMutation,\n useVerifyPaymentMethodMutation,\n GetBillingAccountsByOrgDocument,\n GetDefaultBillingAccountDashboardDocument,\n GetBillingAddressDocument,\n GetCreditTransactionsSummaryDocument,\n} from '../../../../generated/global-operations';\nimport type {\n BillingAccount,\n BillingSubscription,\n PaymentMethod,\n BillingAddress,\n CreditTransaction,\n Addon,\n Plan,\n CreateBillingAccountInput,\n UpdateBillingAccountInput,\n CreatePaymentMethodInput,\n SetBillingAddressInput,\n InitiatePaymentInput,\n InitiatePaymentResponse,\n SpendCreditsInput,\n VerifyPaymentMethodInput,\n VerifyPaymentMethodResponse,\n} from '../../../shared/types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseBillingAccountsResult {\n billingAccounts: BillingAccount[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseDashboardAccountResult {\n billingAccount: BillingAccount | null;\n paymentMethods: PaymentMethod[];\n billingAddresses: BillingAddress[];\n activeSubscriptions: BillingSubscription[];\n hasActiveSubscription: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseActiveSubscriptionsResult {\n activeSubscriptions: BillingSubscription[];\n hasActiveSubscription: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UsePaymentMethodsResult {\n paymentMethods: PaymentMethod[];\n defaultPaymentMethod: PaymentMethod | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseCreditTransactionsResult {\n creditTransactions: CreditTransaction[];\n totalCount: number;\n hasMore: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRecommendedAddonsResult {\n addons: Addon[];\n recommendedAddons: Addon[];\n isLoading: boolean;\n error: Error | undefined;\n}\n\ninterface UseAvailablePlansResult {\n plans: Plan[];\n activePlans: Plan[];\n isLoading: boolean;\n error: Error | undefined;\n}\n\ninterface UseDashboardMutationsResult {\n // Billing account\n createBillingAccount: (input: CreateBillingAccountInput) => Promise<BillingAccount>;\n updateBillingAccount: (input: UpdateBillingAccountInput) => Promise<BillingAccount>;\n setDefaultBillingAccount: (id: string) => Promise<BillingAccount>;\n isCreatingAccount: boolean;\n isUpdatingAccount: boolean;\n isSettingDefaultAccount: boolean;\n\n // Payment methods\n addPaymentMethod: (input: CreatePaymentMethodInput) => Promise<PaymentMethod>;\n setDefaultPaymentMethod: (id: string, billingAccountId: string) => Promise<PaymentMethod>;\n deletePaymentMethod: (id: string, billingAccountId: string) => Promise<boolean>;\n isAddingPaymentMethod: boolean;\n isUpdatingPaymentMethod: boolean;\n isDeletingPaymentMethod: boolean;\n\n // Billing address\n setBillingAddress: (input: SetBillingAddressInput) => Promise<BillingAddress>;\n isSettingAddress: boolean;\n\n // Payments\n initiatePayment: (input: InitiatePaymentInput) => Promise<InitiatePaymentResponse>;\n spendCredits: (input: SpendCreditsInput) => Promise<{ creditTransactionID: string }>;\n verifyPaymentMethod: (input: VerifyPaymentMethodInput) => Promise<VerifyPaymentMethodResponse>;\n isInitiatingPayment: boolean;\n isSpendingCredits: boolean;\n isVerifyingPaymentMethod: boolean;\n}\n\ninterface PaymentProviderConfig {\n stripeEnabled: boolean;\n stripePublishableKey: string | null;\n razorpayEnabled: boolean;\n razorpayKeyId: string | null;\n}\n\ninterface UsePaymentProviderConfigResult {\n config: PaymentProviderConfig | null;\n stripeEnabled: boolean;\n stripePublishableKey: string | null;\n razorpayEnabled: boolean;\n razorpayKeyId: string | null;\n isLoading: boolean;\n error: Error | undefined;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook for fetching payment provider configuration from backend\n * @param skip - Skip the query (useful for modals that only need data when open)\n */\nexport function usePaymentProviderConfig(skip = false): UsePaymentProviderConfigResult {\n const { data, loading, error } = useGetPaymentProviderConfigQuery({\n fetchPolicy: 'cache-first', // Cache the config as it rarely changes\n skip,\n });\n\n const config = data?.getPaymentProviderConfig || null;\n\n return {\n config,\n stripeEnabled: config?.stripeEnabled ?? false,\n stripePublishableKey: config?.stripePublishableKey ?? null,\n razorpayEnabled: config?.razorpayEnabled ?? false,\n razorpayKeyId: config?.razorpayKeyId ?? null,\n isLoading: skip ? false : loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for fetching all billing accounts for an org\n */\nexport function useBillingAccounts(): UseBillingAccountsResult {\n const { data, loading, error, refetch } = useGetBillingAccountsByOrgQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n billingAccounts: data?.getBillingAccountsByOrg || [],\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching active subscriptions for a billing account (optimized - only active status)\n */\nexport function useActiveSubscriptions(\n billingAccountId: string | undefined\n): UseActiveSubscriptionsResult {\n const { data, loading, error, refetch } = useGetActiveSubscriptionsQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const activeSubscriptions = data?.getActiveSubscriptions || [];\n\n return {\n activeSubscriptions,\n hasActiveSubscription: activeSubscriptions.length > 0,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching default billing account with dashboard details\n * Note: Subscriptions are NOT included - use useActiveSubscriptions separately\n */\nexport function useDefaultDashboardAccount(): Omit<\n UseDashboardAccountResult,\n 'activeSubscriptions' | 'hasActiveSubscription'\n> {\n const { data, loading, error, refetch } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const billingAccount = data?.getDefaultBillingAccount || null;\n const paymentMethods = (billingAccount?.paymentMethods || []).filter(Boolean) as PaymentMethod[];\n const billingAddresses = (billingAccount?.billingAddresses || []).filter(\n Boolean\n ) as BillingAddress[];\n\n return {\n billingAccount,\n paymentMethods,\n billingAddresses,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching billing account by ID with dashboard details\n * Note: Subscriptions are NOT included - use useActiveSubscriptions separately\n */\nexport function useDashboardAccount(\n id: string | undefined\n): Omit<UseDashboardAccountResult, 'activeSubscriptions' | 'hasActiveSubscription'> {\n const { data, loading, error, refetch } = useGetBillingAccountDashboardQuery({\n variables: { id: id! },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const billingAccount = data?.getBillingAccount || null;\n const paymentMethods = (billingAccount?.paymentMethods || []).filter(Boolean) as PaymentMethod[];\n const billingAddresses = (billingAccount?.billingAddresses || []).filter(\n Boolean\n ) as BillingAddress[];\n\n return {\n billingAccount,\n paymentMethods,\n billingAddresses,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching payment methods for a billing account\n */\nexport function usePaymentMethods(billingAccountId: string | undefined): UsePaymentMethodsResult {\n const { data, loading, error, refetch } = useGetPaymentMethodsDashboardQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const paymentMethods = useMemo<PaymentMethod[]>(\n () => data?.getPaymentMethods || [],\n [data?.getPaymentMethods]\n );\n const defaultPaymentMethod = useMemo(() => {\n return paymentMethods.find((pm: PaymentMethod) => pm.isDefault) || null;\n }, [paymentMethods]);\n\n return {\n paymentMethods,\n defaultPaymentMethod,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching billing address\n */\nexport function useBillingAddress(billingAccountId: string | undefined) {\n const { data, loading, error, refetch } = useGetBillingAddressQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n billingAddress: data?.getBillingAddress || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching credit transactions summary\n */\nexport function useCreditTransactionsSummary(\n billingAccountId: string | undefined,\n days = 30,\n page = 1,\n pageSize = 5\n): UseCreditTransactionsResult {\n const { data, loading, error, refetch } = useGetCreditTransactionsSummaryQuery({\n variables: { billingAccountID: billingAccountId!, days, page, pageSize },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getBillingAccountCreditTransactions || {\n creditTransactions: [],\n totalCount: 0,\n hasMore: false,\n };\n\n return {\n creditTransactions: result.creditTransactions,\n totalCount: result.totalCount,\n hasMore: result.hasMore,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching recommended addons (limited to 3 at backend level)\n */\nexport function useRecommendedAddons(): UseRecommendedAddonsResult {\n const { data, loading, error } = useGetRecommendedAddonsQuery({\n variables: { limit: 3 },\n fetchPolicy: 'cache-and-network',\n });\n\n const addons = useMemo<Addon[]>(() => data?.getAllAddOns || [], [data?.getAllAddOns]);\n\n // Filter to only active addons (limit already applied at backend)\n const recommendedAddons = useMemo(() => {\n return addons.filter((a: Addon) => a.isActive);\n }, [addons]);\n\n return {\n addons,\n recommendedAddons,\n isLoading: loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for fetching available plans\n */\nexport function useAvailablePlans(): UseAvailablePlansResult {\n const { data, loading, error } = useGetAvailablePlansQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const plans = useMemo<Plan[]>(() => data?.getAllPlans || [], [data?.getAllPlans]);\n\n const activePlans = useMemo(() => {\n return plans.filter((p: Plan) => p.isActive);\n }, [plans]);\n\n return {\n plans,\n activePlans,\n isLoading: loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for dashboard mutations\n */\nexport function useDashboardMutations(): UseDashboardMutationsResult {\n const client = useApolloClient();\n\n // Billing account mutations\n const [createBillingAccountMutation, { loading: isCreatingAccount }] =\n useCreateBillingAccountMutation();\n const [updateBillingAccountMutation, { loading: isUpdatingAccount }] =\n useUpdateBillingAccountMutation();\n const [setDefaultBillingAccountMutation, { loading: isSettingDefaultAccount }] =\n useSetDefaultBillingAccountMutation();\n\n // Payment method mutations\n const [addPaymentMethodMutation, { loading: isAddingPaymentMethod }] =\n useAddPaymentMethodDashboardMutation();\n const [updatePaymentMethodMutation, { loading: isUpdatingPaymentMethod }] =\n useUpdatePaymentMethodDashboardMutation();\n const [deletePaymentMethodMutation, { loading: isDeletingPaymentMethod }] =\n useDeletePaymentMethodDashboardMutation();\n\n // Billing address mutation\n const [setBillingAddressMutation, { loading: isSettingAddress }] = useSetBillingAddressMutation();\n\n // Payment mutations\n const [initiatePaymentMutation, { loading: isInitiatingPayment }] = useInitiatePaymentMutation();\n const [spendCreditsMutation, { loading: isSpendingCredits }] = useSpendCreditsMutation();\n const [verifyPaymentMethodMutation, { loading: isVerifyingPaymentMethod }] =\n useVerifyPaymentMethodMutation();\n\n // Billing account handlers\n const createBillingAccount = useCallback(\n async (input: CreateBillingAccountInput): Promise<BillingAccount> => {\n const { data, error } = await createBillingAccountMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to create billing account');\n }\n if (!data?.createBillingAccount) {\n throw new Error('Failed to create billing account: no data returned');\n }\n await client.refetchQueries({\n include: [GetBillingAccountsByOrgDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.createBillingAccount;\n },\n [createBillingAccountMutation, client]\n );\n\n const updateBillingAccount = useCallback(\n async (input: UpdateBillingAccountInput): Promise<BillingAccount> => {\n const { data, error } = await updateBillingAccountMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to update billing account');\n }\n if (!data?.updateBillingAccount) {\n throw new Error('Failed to update billing account: no data returned');\n }\n // Only refetch the default dashboard - component handles selected account refetch\n await client.refetchQueries({\n include: [GetDefaultBillingAccountDashboardDocument],\n });\n return data.updateBillingAccount;\n },\n [updateBillingAccountMutation, client]\n );\n\n const setDefaultBillingAccount = useCallback(\n async (id: string): Promise<BillingAccount> => {\n const { data, error } = await setDefaultBillingAccountMutation({\n variables: { id },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to set default billing account');\n }\n if (!data?.setDefaultBillingAccount) {\n throw new Error('Failed to set default billing account: no data returned');\n }\n // Refetch all billing accounts and default account to update isDefault flags\n await client.refetchQueries({\n include: [GetBillingAccountsByOrgDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.setDefaultBillingAccount;\n },\n [setDefaultBillingAccountMutation, client]\n );\n\n // Payment method handlers\n // Note: We don't use client.refetchQueries here because it tries to refetch ALL matching queries\n // including those that were skipped (e.g., GET_BILLING_ACCOUNT_DASHBOARD with no id).\n // The calling component should handle refetching the appropriate queries.\n const addPaymentMethod = useCallback(\n async (input: CreatePaymentMethodInput): Promise<PaymentMethod> => {\n const { data, error } = await addPaymentMethodMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to add payment method');\n }\n if (!data?.addPaymentMethod) {\n throw new Error('Failed to add payment method: no data returned');\n }\n return data.addPaymentMethod;\n },\n [addPaymentMethodMutation]\n );\n\n const setDefaultPaymentMethod = useCallback(\n async (id: string, billingAccountId: string): Promise<PaymentMethod> => {\n const { data, error } = await updatePaymentMethodMutation({\n variables: { id, isDefault: true, billingAccountId },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to update payment method');\n }\n if (!data?.updatePaymentMethod) {\n throw new Error('Failed to update payment method: no data returned');\n }\n return data.updatePaymentMethod;\n },\n [updatePaymentMethodMutation]\n );\n\n const deletePaymentMethod = useCallback(\n async (id: string, billingAccountId: string): Promise<boolean> => {\n const { data, error } = await deletePaymentMethodMutation({\n variables: { id, billingAccountId },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to delete payment method');\n }\n if (!data) {\n throw new Error('Failed to delete payment method: no data returned');\n }\n return data.deletePaymentMethod;\n },\n [deletePaymentMethodMutation]\n );\n\n // Billing address handler\n const setBillingAddress = useCallback(\n async (input: SetBillingAddressInput): Promise<BillingAddress> => {\n const { data, error } = await setBillingAddressMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to set billing address');\n }\n if (!data?.setBillingAddress) {\n throw new Error('Failed to set billing address: no data returned');\n }\n // Only refetch queries that don't have conditional required variables\n await client.refetchQueries({\n include: [GetBillingAddressDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.setBillingAddress;\n },\n [setBillingAddressMutation, client]\n );\n\n // Payment handlers\n const initiatePayment = useCallback(\n async (input: InitiatePaymentInput): Promise<InitiatePaymentResponse> => {\n const { data, error } = await initiatePaymentMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to initiate payment');\n }\n if (!data?.initiatePayment) {\n throw new Error('Failed to initiate payment: no data returned');\n }\n return data.initiatePayment;\n },\n [initiatePaymentMutation]\n );\n\n const spendCredits = useCallback(\n async (input: SpendCreditsInput): Promise<{ creditTransactionID: string }> => {\n const { data, error } = await spendCreditsMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to spend credits');\n }\n if (!data?.spendCredits) {\n throw new Error('Failed to spend credits: no data returned');\n }\n // Only refetch queries that don't have conditional required variables\n await client.refetchQueries({\n include: [GetDefaultBillingAccountDashboardDocument, GetCreditTransactionsSummaryDocument],\n });\n return data.spendCredits;\n },\n [spendCreditsMutation, client]\n );\n\n // Verify payment method handler\n const verifyPaymentMethod = useCallback(\n async (input: VerifyPaymentMethodInput): Promise<VerifyPaymentMethodResponse> => {\n const { data, error } = await verifyPaymentMethodMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to verify payment method');\n }\n if (!data?.verifyBillingPaymentMethod) {\n throw new Error('Failed to verify payment method: no data returned');\n }\n return data.verifyBillingPaymentMethod;\n },\n [verifyPaymentMethodMutation]\n );\n\n return {\n // Billing account\n createBillingAccount,\n updateBillingAccount,\n setDefaultBillingAccount,\n isCreatingAccount,\n isUpdatingAccount,\n isSettingDefaultAccount,\n\n // Payment methods\n addPaymentMethod,\n setDefaultPaymentMethod,\n deletePaymentMethod,\n isAddingPaymentMethod,\n isUpdatingPaymentMethod,\n isDeletingPaymentMethod,\n\n // Billing address\n setBillingAddress,\n isSettingAddress,\n\n // Payments\n initiatePayment,\n spendCredits,\n verifyPaymentMethod,\n isInitiatingPayment,\n isSpendingCredits,\n isVerifyingPaymentMethod,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDashboard.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/hooks/useDashboard.ts"],"sourcesContent":["/**\n * Dashboard Module - useDashboard Hook\n * Provides access to dashboard data and mutations for billing overview\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback, useMemo } from 'react';\nimport {\n useGetPaymentProviderConfigQuery,\n useGetBillingAccountsByOrgQuery,\n useGetDefaultBillingAccountDashboardQuery,\n useGetBillingAccountDashboardQuery,\n useGetActiveSubscriptionsQuery,\n useGetPaymentMethodsDashboardQuery,\n useGetBillingAddressQuery,\n useGetCreditTransactionsSummaryQuery,\n useGetRecommendedAddonsQuery,\n useGetAvailablePlansQuery,\n useCreateBillingAccountMutation,\n useUpdateBillingAccountMutation,\n useSetDefaultBillingAccountMutation,\n useAddPaymentMethodDashboardMutation,\n useUpdatePaymentMethodDashboardMutation,\n useDeletePaymentMethodDashboardMutation,\n useSetBillingAddressMutation,\n useInitiatePaymentMutation,\n useSpendCreditsMutation,\n useVerifyPaymentMethodMutation,\n GetBillingAccountsByOrgDocument,\n GetDefaultBillingAccountDashboardDocument,\n GetBillingAddressDocument,\n GetCreditTransactionsSummaryDocument,\n} from '../../../../generated/global-operations';\nimport type {\n BillingAccount,\n BillingSubscription,\n PaymentMethod,\n BillingAddress,\n CreditTransaction,\n Addon,\n Plan,\n CreateBillingAccountInput,\n UpdateBillingAccountInput,\n CreatePaymentMethodInput,\n SetBillingAddressInput,\n InitiatePaymentInput,\n InitiatePaymentResponse,\n SpendCreditsInput,\n VerifyPaymentMethodInput,\n VerifyPaymentMethodResponse,\n} from '../../../shared/types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseBillingAccountsResult {\n billingAccounts: BillingAccount[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseDashboardAccountResult {\n billingAccount: BillingAccount | null;\n paymentMethods: PaymentMethod[];\n billingAddresses: BillingAddress[];\n activeSubscriptions: BillingSubscription[];\n hasActiveSubscription: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseActiveSubscriptionsResult {\n activeSubscriptions: BillingSubscription[];\n hasActiveSubscription: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UsePaymentMethodsResult {\n paymentMethods: PaymentMethod[];\n defaultPaymentMethod: PaymentMethod | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseCreditTransactionsResult {\n creditTransactions: CreditTransaction[];\n totalCount: number;\n hasMore: boolean;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRecommendedAddonsResult {\n addons: Addon[];\n recommendedAddons: Addon[];\n isLoading: boolean;\n error: Error | undefined;\n}\n\ninterface UseAvailablePlansResult {\n plans: Plan[];\n activePlans: Plan[];\n isLoading: boolean;\n error: Error | undefined;\n}\n\ninterface UseDashboardMutationsResult {\n // Billing account\n createBillingAccount: (input: CreateBillingAccountInput) => Promise<BillingAccount>;\n updateBillingAccount: (input: UpdateBillingAccountInput) => Promise<BillingAccount>;\n setDefaultBillingAccount: (id: string) => Promise<BillingAccount>;\n isCreatingAccount: boolean;\n isUpdatingAccount: boolean;\n isSettingDefaultAccount: boolean;\n\n // Payment methods\n addPaymentMethod: (input: CreatePaymentMethodInput) => Promise<PaymentMethod>;\n setDefaultPaymentMethod: (id: string, billingAccountId: string) => Promise<PaymentMethod>;\n deletePaymentMethod: (id: string, billingAccountId: string) => Promise<boolean>;\n isAddingPaymentMethod: boolean;\n isUpdatingPaymentMethod: boolean;\n isDeletingPaymentMethod: boolean;\n\n // Billing address\n setBillingAddress: (input: SetBillingAddressInput) => Promise<BillingAddress>;\n isSettingAddress: boolean;\n\n // Payments\n initiatePayment: (input: InitiatePaymentInput) => Promise<InitiatePaymentResponse>;\n spendCredits: (input: SpendCreditsInput) => Promise<{ creditTransactionID: string }>;\n verifyPaymentMethod: (input: VerifyPaymentMethodInput) => Promise<VerifyPaymentMethodResponse>;\n isInitiatingPayment: boolean;\n isSpendingCredits: boolean;\n isVerifyingPaymentMethod: boolean;\n}\n\ninterface PaymentProviderConfig {\n stripeEnabled: boolean;\n stripePublishableKey: string | null;\n razorpayEnabled: boolean;\n razorpayKeyId: string | null;\n}\n\ninterface UsePaymentProviderConfigResult {\n config: PaymentProviderConfig | null;\n stripeEnabled: boolean;\n stripePublishableKey: string | null;\n razorpayEnabled: boolean;\n razorpayKeyId: string | null;\n isLoading: boolean;\n error: Error | undefined;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook for fetching payment provider configuration from backend\n * @param skip - Skip the query (useful for modals that only need data when open)\n */\nexport function usePaymentProviderConfig(skip = false): UsePaymentProviderConfigResult {\n const { data, loading, error } = useGetPaymentProviderConfigQuery({\n fetchPolicy: 'cache-first', // Cache the config as it rarely changes\n skip,\n });\n\n const config = data?.getPaymentProviderConfig || null;\n\n return {\n config,\n stripeEnabled: config?.stripeEnabled ?? false,\n stripePublishableKey: config?.stripePublishableKey ?? null,\n razorpayEnabled: config?.razorpayEnabled ?? false,\n razorpayKeyId: config?.razorpayKeyId ?? null,\n isLoading: skip ? false : loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for fetching all billing accounts for an org\n */\nexport function useBillingAccounts(): UseBillingAccountsResult {\n const { data, loading, error, refetch } = useGetBillingAccountsByOrgQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n billingAccounts: data?.getBillingAccountsByOrg || [],\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching active subscriptions for a billing account (optimized - only active status)\n */\nexport function useActiveSubscriptions(\n billingAccountId: string | undefined\n): UseActiveSubscriptionsResult {\n const { data, loading, error, refetch } = useGetActiveSubscriptionsQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const activeSubscriptions = data?.getActiveSubscriptions || [];\n\n return {\n activeSubscriptions,\n hasActiveSubscription: activeSubscriptions.length > 0,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching default billing account with dashboard details\n * Note: Subscriptions are NOT included - use useActiveSubscriptions separately\n */\nexport function useDefaultDashboardAccount(): Omit<\n UseDashboardAccountResult,\n 'activeSubscriptions' | 'hasActiveSubscription'\n> {\n const { data, loading, error, refetch } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const billingAccount = data?.getDefaultBillingAccount || null;\n const paymentMethods = (billingAccount?.paymentMethods || []).filter(Boolean) as PaymentMethod[];\n const billingAddresses = (billingAccount?.billingAddresses || []).filter(\n Boolean\n ) as BillingAddress[];\n\n return {\n billingAccount,\n paymentMethods,\n billingAddresses,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching billing account by ID with dashboard details\n * Note: Subscriptions are NOT included - use useActiveSubscriptions separately\n */\nexport function useDashboardAccount(\n id: string | undefined\n): Omit<UseDashboardAccountResult, 'activeSubscriptions' | 'hasActiveSubscription'> {\n const { data, loading, error, refetch } = useGetBillingAccountDashboardQuery({\n variables: { id: id! },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const billingAccount = data?.getBillingAccount || null;\n const paymentMethods = (billingAccount?.paymentMethods || []).filter(Boolean) as PaymentMethod[];\n const billingAddresses = (billingAccount?.billingAddresses || []).filter(\n Boolean\n ) as BillingAddress[];\n\n return {\n billingAccount,\n paymentMethods,\n billingAddresses,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching payment methods for a billing account\n */\nexport function usePaymentMethods(billingAccountId: string | undefined): UsePaymentMethodsResult {\n const { data, loading, error, refetch } = useGetPaymentMethodsDashboardQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const paymentMethods = useMemo<PaymentMethod[]>(\n () => data?.getPaymentMethods || [],\n [data?.getPaymentMethods]\n );\n const defaultPaymentMethod = useMemo(() => {\n return paymentMethods.find((pm: PaymentMethod) => pm.isDefault) || null;\n }, [paymentMethods]);\n\n return {\n paymentMethods,\n defaultPaymentMethod,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching billing address\n */\nexport function useBillingAddress(billingAccountId: string | undefined) {\n const { data, loading, error, refetch } = useGetBillingAddressQuery({\n variables: { billingAccountId: billingAccountId! },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n billingAddress: data?.getBillingAddress || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching credit transactions summary\n */\nexport function useCreditTransactionsSummary(\n billingAccountId: string | undefined,\n days = 30,\n page = 1,\n pageSize = 5\n): UseCreditTransactionsResult {\n const { data, loading, error, refetch } = useGetCreditTransactionsSummaryQuery({\n variables: { billingAccountID: billingAccountId!, days, page, pageSize },\n skip: !billingAccountId,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getBillingAccountCreditTransactions || {\n creditTransactions: [],\n totalCount: 0,\n hasMore: false,\n };\n\n return {\n creditTransactions: result.creditTransactions,\n totalCount: result.totalCount,\n hasMore: result.hasMore,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching recommended addons (limited to 3 at backend level)\n */\nexport function useRecommendedAddons(): UseRecommendedAddonsResult {\n const { data, loading, error } = useGetRecommendedAddonsQuery({\n variables: { limit: 3 },\n fetchPolicy: 'cache-and-network',\n });\n\n const addons = useMemo<Addon[]>(() => data?.getAllAddOns || [], [data?.getAllAddOns]);\n\n // Filter to only active addons (limit already applied at backend)\n const recommendedAddons = useMemo(() => {\n return addons.filter((a: Addon) => a.isActive);\n }, [addons]);\n\n return {\n addons,\n recommendedAddons,\n isLoading: loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for fetching available plans\n */\nexport function useAvailablePlans(): UseAvailablePlansResult {\n const { data, loading, error } = useGetAvailablePlansQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const plans = useMemo<Plan[]>(() => data?.getAllPlans || [], [data?.getAllPlans]);\n\n const activePlans = useMemo(() => {\n return plans.filter((p: Plan) => p.isActive);\n }, [plans]);\n\n return {\n plans,\n activePlans,\n isLoading: loading,\n error: error as Error | undefined,\n };\n}\n\n/**\n * Hook for dashboard mutations\n */\nexport function useDashboardMutations(): UseDashboardMutationsResult {\n const client = useApolloClient();\n\n // Billing account mutations\n const [createBillingAccountMutation, { loading: isCreatingAccount }] =\n useCreateBillingAccountMutation();\n const [updateBillingAccountMutation, { loading: isUpdatingAccount }] =\n useUpdateBillingAccountMutation();\n const [setDefaultBillingAccountMutation, { loading: isSettingDefaultAccount }] =\n useSetDefaultBillingAccountMutation();\n\n // Payment method mutations\n const [addPaymentMethodMutation, { loading: isAddingPaymentMethod }] =\n useAddPaymentMethodDashboardMutation();\n const [updatePaymentMethodMutation, { loading: isUpdatingPaymentMethod }] =\n useUpdatePaymentMethodDashboardMutation();\n const [deletePaymentMethodMutation, { loading: isDeletingPaymentMethod }] =\n useDeletePaymentMethodDashboardMutation();\n\n // Billing address mutation\n const [setBillingAddressMutation, { loading: isSettingAddress }] = useSetBillingAddressMutation();\n\n // Payment mutations\n const [initiatePaymentMutation, { loading: isInitiatingPayment }] = useInitiatePaymentMutation();\n const [spendCreditsMutation, { loading: isSpendingCredits }] = useSpendCreditsMutation();\n const [verifyPaymentMethodMutation, { loading: isVerifyingPaymentMethod }] =\n useVerifyPaymentMethodMutation();\n\n // Billing account handlers\n const createBillingAccount = useCallback(\n async (input: CreateBillingAccountInput): Promise<BillingAccount> => {\n const { data, error } = await createBillingAccountMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to create billing account');\n }\n if (!data?.createBillingAccount) {\n throw new Error('Failed to create billing account: no data returned');\n }\n await client.refetchQueries({\n include: [GetBillingAccountsByOrgDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.createBillingAccount;\n },\n [createBillingAccountMutation, client]\n );\n\n const updateBillingAccount = useCallback(\n async (input: UpdateBillingAccountInput): Promise<BillingAccount> => {\n const { data, error } = await updateBillingAccountMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to update billing account');\n }\n if (!data?.updateBillingAccount) {\n throw new Error('Failed to update billing account: no data returned');\n }\n // Only refetch the default dashboard - component handles selected account refetch\n await client.refetchQueries({\n include: [GetDefaultBillingAccountDashboardDocument],\n });\n return data.updateBillingAccount;\n },\n [updateBillingAccountMutation, client]\n );\n\n const setDefaultBillingAccount = useCallback(\n async (id: string): Promise<BillingAccount> => {\n const { data, error } = await setDefaultBillingAccountMutation({\n variables: { id },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to set default billing account');\n }\n if (!data?.setDefaultBillingAccount) {\n throw new Error('Failed to set default billing account: no data returned');\n }\n // Refetch all billing accounts and default account to update isDefault flags\n await client.refetchQueries({\n include: [GetBillingAccountsByOrgDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.setDefaultBillingAccount;\n },\n [setDefaultBillingAccountMutation, client]\n );\n\n // Payment method handlers\n // Note: We don't use client.refetchQueries here because it tries to refetch ALL matching queries\n // including those that were skipped (e.g., GET_BILLING_ACCOUNT_DASHBOARD with no id).\n // The calling component should handle refetching the appropriate queries.\n const addPaymentMethod = useCallback(\n async (input: CreatePaymentMethodInput): Promise<PaymentMethod> => {\n const { data, error } = await addPaymentMethodMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to add payment method');\n }\n if (!data?.addPaymentMethod) {\n throw new Error('Failed to add payment method: no data returned');\n }\n return data.addPaymentMethod;\n },\n [addPaymentMethodMutation]\n );\n\n const setDefaultPaymentMethod = useCallback(\n async (id: string, billingAccountId: string): Promise<PaymentMethod> => {\n const { data, error } = await updatePaymentMethodMutation({\n variables: { id, isDefault: true, billingAccountId },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to update payment method');\n }\n if (!data?.updatePaymentMethod) {\n throw new Error('Failed to update payment method: no data returned');\n }\n return data.updatePaymentMethod;\n },\n [updatePaymentMethodMutation]\n );\n\n const deletePaymentMethod = useCallback(\n async (id: string, billingAccountId: string): Promise<boolean> => {\n const { data, error } = await deletePaymentMethodMutation({\n variables: { id, billingAccountId },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to delete payment method');\n }\n if (!data) {\n throw new Error('Failed to delete payment method: no data returned');\n }\n return data.deletePaymentMethod;\n },\n [deletePaymentMethodMutation]\n );\n\n // Billing address handler\n const setBillingAddress = useCallback(\n async (input: SetBillingAddressInput): Promise<BillingAddress> => {\n const { data, error } = await setBillingAddressMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to set billing address');\n }\n if (!data?.setBillingAddress) {\n throw new Error('Failed to set billing address: no data returned');\n }\n // Only refetch queries that don't have conditional required variables\n await client.refetchQueries({\n include: [GetBillingAddressDocument, GetDefaultBillingAccountDashboardDocument],\n });\n return data.setBillingAddress;\n },\n [setBillingAddressMutation, client]\n );\n\n // Payment handlers\n const initiatePayment = useCallback(\n async (input: InitiatePaymentInput): Promise<InitiatePaymentResponse> => {\n const { data, error } = await initiatePaymentMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to initiate payment');\n }\n if (!data?.initiatePayment) {\n throw new Error('Failed to initiate payment: no data returned');\n }\n return data.initiatePayment;\n },\n [initiatePaymentMutation]\n );\n\n const spendCredits = useCallback(\n async (input: SpendCreditsInput): Promise<{ creditTransactionID: string }> => {\n const { data, error } = await spendCreditsMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to spend credits');\n }\n if (!data?.spendCredits) {\n throw new Error('Failed to spend credits: no data returned');\n }\n // Only refetch queries that don't have conditional required variables\n await client.refetchQueries({\n include: [GetDefaultBillingAccountDashboardDocument, GetCreditTransactionsSummaryDocument],\n });\n return data.spendCredits;\n },\n [spendCreditsMutation, client]\n );\n\n // Verify payment method handler\n const verifyPaymentMethod = useCallback(\n async (input: VerifyPaymentMethodInput): Promise<VerifyPaymentMethodResponse> => {\n const { data, error } = await verifyPaymentMethodMutation({\n variables: { input },\n });\n if (error) {\n throw new Error(error.message ?? 'Failed to verify payment method');\n }\n if (!data?.verifyBillingPaymentMethod) {\n throw new Error('Failed to verify payment method: no data returned');\n }\n return data.verifyBillingPaymentMethod;\n },\n [verifyPaymentMethodMutation]\n );\n\n return {\n // Billing account\n createBillingAccount,\n updateBillingAccount,\n setDefaultBillingAccount,\n isCreatingAccount,\n isUpdatingAccount,\n isSettingDefaultAccount,\n\n // Payment methods\n addPaymentMethod,\n setDefaultPaymentMethod,\n deletePaymentMethod,\n isAddingPaymentMethod,\n isUpdatingPaymentMethod,\n isDeletingPaymentMethod,\n\n // Billing address\n setBillingAddress,\n isSettingAddress,\n\n // Payments\n initiatePayment,\n spendCredits,\n verifyPaymentMethod,\n isInitiatingPayment,\n isSpendingCredits,\n isVerifyingPaymentMethod,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.d.ts","sourceRoot":"","sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAA6C,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.d.ts","sourceRoot":"","sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAA6C,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAuF3E,eAAO,MAAM,mBAAmB,EAAE,EAmSjC,CAAC"}
|
|
@@ -2,206 +2,212 @@ 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
4
|
import "../../../shared/components/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useBillingAccountSelection as r } from "../../../hooks/useBillingAccountSelection.js";
|
|
6
|
+
import { directBillingGraphqlRequest as i } from "../../../shared/utils/directBillingGraphql.js";
|
|
6
7
|
import "../../../shared/utils/index.js";
|
|
7
|
-
import { useBillingPermissions as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { jsx as
|
|
8
|
+
import { useBillingPermissions as a } from "../../../hooks/useBillingPermissions.js";
|
|
9
|
+
import { useBillingAccounts as o } from "../../dashboard/hooks/useDashboard.js";
|
|
10
|
+
import { useCallback as s, useEffect as c, useMemo as l, useState as u } from "react";
|
|
11
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
12
|
//#region src/billing/modules/settings/pages/TeamPermissionsPage.tsx
|
|
12
|
-
var
|
|
13
|
-
let { apiGatewayUrl:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n", h = "\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", g = "\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n", _ = "\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n", v = () => {
|
|
14
|
+
let { apiGatewayUrl: v, authToken: y, orgId: b } = e(), x = a(), { billingAccounts: S } = o(), { selectedAccountId: C } = r({
|
|
15
|
+
orgId: b,
|
|
16
|
+
billingAccounts: S,
|
|
17
|
+
syncFromUrl: !0,
|
|
18
|
+
urlParamName: "billingAccountId"
|
|
19
|
+
}), w = C ?? void 0, [T, E] = u([]), [D, O] = u([]), [k, A] = u(!1), [j, M] = u(null), [N, P] = u(""), [F, I] = u(""), [L, R] = u(!1), z = s(async (e, t) => i({
|
|
20
|
+
apiGatewayUrl: v,
|
|
21
|
+
authToken: y,
|
|
22
|
+
orgId: b,
|
|
17
23
|
query: e,
|
|
18
24
|
variables: t
|
|
19
25
|
}), [
|
|
20
|
-
_,
|
|
21
26
|
v,
|
|
22
|
-
y
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
y,
|
|
28
|
+
b
|
|
29
|
+
]), B = s(async () => {
|
|
30
|
+
if (w) {
|
|
31
|
+
A(!0), M(null);
|
|
26
32
|
try {
|
|
27
|
-
let [e, t] = await Promise.all([
|
|
28
|
-
|
|
33
|
+
let [e, t] = await Promise.all([z(m, { scopeId: w }), z(h, { scopeId: w })]);
|
|
34
|
+
E(e.roles ?? []), O(t.actors ?? []), I((e.roles ?? []).find((e) => /viewer/i.test(e.name))?.id ?? e.roles?.[0]?.id ?? "");
|
|
29
35
|
} catch (e) {
|
|
30
|
-
|
|
36
|
+
M(e instanceof Error ? e.message : "Failed to load team data");
|
|
31
37
|
} finally {
|
|
32
|
-
|
|
38
|
+
A(!1);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
|
-
}, [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}, [
|
|
39
|
-
let
|
|
41
|
+
}, [w, z]);
|
|
42
|
+
c(() => {
|
|
43
|
+
B();
|
|
44
|
+
}, [B]);
|
|
45
|
+
let V = l(() => [...D].sort((e, t) => {
|
|
40
46
|
let n = Math.max(...e.roleAssignments.map((e) => e.role?.priority ?? 0), 0);
|
|
41
47
|
return Math.max(...t.roleAssignments.map((e) => e.role?.priority ?? 0), 0) - n;
|
|
42
|
-
}), [
|
|
43
|
-
if (e.preventDefault(), !
|
|
44
|
-
let t =
|
|
45
|
-
if (!
|
|
46
|
-
|
|
48
|
+
}), [D]), H = async (e) => {
|
|
49
|
+
if (e.preventDefault(), !w || !N.trim() || !F) return;
|
|
50
|
+
let t = N.trim();
|
|
51
|
+
if (!p.test(t)) {
|
|
52
|
+
M("User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.");
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
|
-
|
|
55
|
+
R(!0), M(null);
|
|
50
56
|
try {
|
|
51
|
-
await
|
|
57
|
+
await z(g, { input: {
|
|
52
58
|
actorId: t,
|
|
53
59
|
actorType: "USER",
|
|
54
|
-
roleId:
|
|
55
|
-
scopeId:
|
|
56
|
-
} }),
|
|
60
|
+
roleId: F,
|
|
61
|
+
scopeId: w
|
|
62
|
+
} }), P(""), await B();
|
|
57
63
|
} catch (e) {
|
|
58
|
-
|
|
64
|
+
M(e instanceof Error ? e.message : "Failed to assign role");
|
|
59
65
|
} finally {
|
|
60
|
-
|
|
66
|
+
R(!1);
|
|
61
67
|
}
|
|
62
|
-
},
|
|
68
|
+
}, U = async (e) => {
|
|
63
69
|
if (confirm("Remove this role assignment?")) {
|
|
64
|
-
|
|
70
|
+
M(null);
|
|
65
71
|
try {
|
|
66
|
-
await
|
|
72
|
+
await z(_, { assignmentId: e }), await B();
|
|
67
73
|
} catch (e) {
|
|
68
|
-
|
|
74
|
+
M(e instanceof Error ? e.message : "Failed to remove role");
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
};
|
|
72
|
-
return
|
|
78
|
+
return x.canManageBillingAccount ? w ? /* @__PURE__ */ f("div", {
|
|
73
79
|
className: "space-y-6",
|
|
74
80
|
children: [
|
|
75
|
-
/* @__PURE__ */
|
|
81
|
+
/* @__PURE__ */ d(n, {
|
|
76
82
|
title: "Team & Permissions",
|
|
77
83
|
description: "Control who can manage this billing account and what they can do."
|
|
78
84
|
}),
|
|
79
|
-
/* @__PURE__ */
|
|
85
|
+
/* @__PURE__ */ f("div", {
|
|
80
86
|
className: "rounded-lg border border-border bg-card p-4 text-xs text-muted-foreground",
|
|
81
87
|
children: [
|
|
82
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ d("span", {
|
|
83
89
|
className: "font-medium text-foreground",
|
|
84
90
|
children: "Billing account:"
|
|
85
91
|
}),
|
|
86
92
|
" ",
|
|
87
|
-
/* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ d("span", {
|
|
88
94
|
className: "font-mono",
|
|
89
|
-
children:
|
|
95
|
+
children: w
|
|
90
96
|
})
|
|
91
97
|
]
|
|
92
98
|
}),
|
|
93
|
-
|
|
99
|
+
j && /* @__PURE__ */ d("div", {
|
|
94
100
|
className: "rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive",
|
|
95
|
-
children:
|
|
101
|
+
children: j
|
|
96
102
|
}),
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
-
onSubmit: (e) => void
|
|
103
|
+
/* @__PURE__ */ f("form", {
|
|
104
|
+
onSubmit: (e) => void H(e),
|
|
99
105
|
className: "rounded-lg border border-border bg-card p-4 space-y-3",
|
|
100
106
|
children: [
|
|
101
|
-
/* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ d("h3", {
|
|
102
108
|
className: "text-sm font-semibold text-foreground",
|
|
103
109
|
children: "Add member"
|
|
104
110
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
111
|
+
/* @__PURE__ */ f("div", {
|
|
106
112
|
className: "grid grid-cols-1 gap-3 md:grid-cols-3",
|
|
107
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ f("div", {
|
|
108
114
|
className: "md:col-span-2",
|
|
109
|
-
children: [/* @__PURE__ */
|
|
115
|
+
children: [/* @__PURE__ */ d("label", {
|
|
110
116
|
htmlFor: "newActorId",
|
|
111
117
|
className: "block text-xs font-medium text-muted-foreground",
|
|
112
118
|
children: "User ID (actorId)"
|
|
113
|
-
}), /* @__PURE__ */
|
|
119
|
+
}), /* @__PURE__ */ d("input", {
|
|
114
120
|
id: "newActorId",
|
|
115
121
|
type: "text",
|
|
116
|
-
value:
|
|
117
|
-
onChange: (e) =>
|
|
122
|
+
value: N,
|
|
123
|
+
onChange: (e) => P(e.target.value),
|
|
118
124
|
placeholder: "UUID of the user to grant access",
|
|
119
125
|
className: "mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm",
|
|
120
126
|
required: !0
|
|
121
127
|
})]
|
|
122
|
-
}), /* @__PURE__ */
|
|
128
|
+
}), /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("label", {
|
|
123
129
|
htmlFor: "newRoleId",
|
|
124
130
|
className: "block text-xs font-medium text-muted-foreground",
|
|
125
131
|
children: "Role"
|
|
126
|
-
}), /* @__PURE__ */
|
|
132
|
+
}), /* @__PURE__ */ d("select", {
|
|
127
133
|
id: "newRoleId",
|
|
128
|
-
value:
|
|
129
|
-
onChange: (e) =>
|
|
134
|
+
value: F,
|
|
135
|
+
onChange: (e) => I(e.target.value),
|
|
130
136
|
className: "mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm",
|
|
131
137
|
required: !0,
|
|
132
|
-
children:
|
|
138
|
+
children: T.map((e) => /* @__PURE__ */ d("option", {
|
|
133
139
|
value: e.id,
|
|
134
140
|
children: e.name
|
|
135
141
|
}, e.id))
|
|
136
142
|
})] })]
|
|
137
143
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ d("button", {
|
|
139
145
|
type: "submit",
|
|
140
|
-
disabled:
|
|
146
|
+
disabled: L || !N.trim() || !F,
|
|
141
147
|
className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50",
|
|
142
|
-
children:
|
|
148
|
+
children: L ? "Adding…" : "Add member"
|
|
143
149
|
})
|
|
144
150
|
]
|
|
145
151
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
152
|
+
/* @__PURE__ */ f("div", {
|
|
147
153
|
className: "rounded-lg border border-border bg-card",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
154
|
+
children: [/* @__PURE__ */ f("div", {
|
|
149
155
|
className: "border-b border-border px-4 py-3",
|
|
150
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ d("h3", {
|
|
151
157
|
className: "text-sm font-semibold text-foreground",
|
|
152
158
|
children: "Members"
|
|
153
|
-
}), /* @__PURE__ */
|
|
159
|
+
}), /* @__PURE__ */ d("p", {
|
|
154
160
|
className: "text-xs text-muted-foreground",
|
|
155
161
|
children: "Each member's role applies only to this billing account."
|
|
156
162
|
})]
|
|
157
|
-
}),
|
|
163
|
+
}), k ? /* @__PURE__ */ d("div", {
|
|
158
164
|
className: "p-6 text-sm text-muted-foreground",
|
|
159
165
|
children: "Loading…"
|
|
160
|
-
}) :
|
|
166
|
+
}) : V.length === 0 ? /* @__PURE__ */ d("div", {
|
|
161
167
|
className: "p-6 text-sm text-muted-foreground",
|
|
162
168
|
children: "No members assigned yet."
|
|
163
|
-
}) : /* @__PURE__ */
|
|
169
|
+
}) : /* @__PURE__ */ f("table", {
|
|
164
170
|
className: "w-full text-sm",
|
|
165
|
-
children: [/* @__PURE__ */
|
|
171
|
+
children: [/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
|
|
166
172
|
className: "border-b border-border text-left text-xs text-muted-foreground",
|
|
167
173
|
children: [
|
|
168
|
-
/* @__PURE__ */
|
|
174
|
+
/* @__PURE__ */ d("th", {
|
|
169
175
|
className: "px-4 py-2",
|
|
170
176
|
children: "Actor ID"
|
|
171
177
|
}),
|
|
172
|
-
/* @__PURE__ */
|
|
178
|
+
/* @__PURE__ */ d("th", {
|
|
173
179
|
className: "px-4 py-2",
|
|
174
180
|
children: "Type"
|
|
175
181
|
}),
|
|
176
|
-
/* @__PURE__ */
|
|
182
|
+
/* @__PURE__ */ d("th", {
|
|
177
183
|
className: "px-4 py-2",
|
|
178
184
|
children: "Roles"
|
|
179
185
|
}),
|
|
180
|
-
/* @__PURE__ */
|
|
186
|
+
/* @__PURE__ */ d("th", {
|
|
181
187
|
className: "px-4 py-2 text-right",
|
|
182
188
|
children: "Actions"
|
|
183
189
|
})
|
|
184
190
|
]
|
|
185
|
-
}) }), /* @__PURE__ */
|
|
191
|
+
}) }), /* @__PURE__ */ d("tbody", { children: V.map((e) => /* @__PURE__ */ f("tr", {
|
|
186
192
|
className: "border-b border-border last:border-0",
|
|
187
193
|
children: [
|
|
188
|
-
/* @__PURE__ */
|
|
194
|
+
/* @__PURE__ */ d("td", {
|
|
189
195
|
className: "px-4 py-3 font-mono text-xs",
|
|
190
196
|
children: e.actorId
|
|
191
197
|
}),
|
|
192
|
-
/* @__PURE__ */
|
|
198
|
+
/* @__PURE__ */ d("td", {
|
|
193
199
|
className: "px-4 py-3 text-xs",
|
|
194
200
|
children: e.actorType
|
|
195
201
|
}),
|
|
196
|
-
/* @__PURE__ */
|
|
202
|
+
/* @__PURE__ */ d("td", {
|
|
197
203
|
className: "px-4 py-3",
|
|
198
|
-
children: /* @__PURE__ */
|
|
204
|
+
children: /* @__PURE__ */ d("div", {
|
|
199
205
|
className: "flex flex-wrap gap-1",
|
|
200
|
-
children: e.roleAssignments.filter((e) => e.isActive).map((e) => /* @__PURE__ */
|
|
206
|
+
children: e.roleAssignments.filter((e) => e.isActive).map((e) => /* @__PURE__ */ f("span", {
|
|
201
207
|
className: "inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs",
|
|
202
|
-
children: [e.role?.name ?? e.roleId, /* @__PURE__ */
|
|
208
|
+
children: [e.role?.name ?? e.roleId, /* @__PURE__ */ d("button", {
|
|
203
209
|
type: "button",
|
|
204
|
-
onClick: () => void
|
|
210
|
+
onClick: () => void U(e.id),
|
|
205
211
|
className: "text-muted-foreground hover:text-destructive",
|
|
206
212
|
"aria-label": `Remove ${e.role?.name ?? "role"}`,
|
|
207
213
|
children: "×"
|
|
@@ -209,9 +215,9 @@ var f = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, p = "
|
|
|
209
215
|
}, e.id))
|
|
210
216
|
})
|
|
211
217
|
}),
|
|
212
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ d("td", {
|
|
213
219
|
className: "px-4 py-3 text-right",
|
|
214
|
-
children: /* @__PURE__ */
|
|
220
|
+
children: /* @__PURE__ */ f("span", {
|
|
215
221
|
className: "text-xs text-muted-foreground",
|
|
216
222
|
children: [e.effectivePermissionCount ?? "—", " permissions"]
|
|
217
223
|
})
|
|
@@ -220,31 +226,31 @@ var f = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, p = "
|
|
|
220
226
|
}, e.actorId)) })]
|
|
221
227
|
})]
|
|
222
228
|
}),
|
|
223
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ f("div", {
|
|
224
230
|
className: "rounded-lg border border-border bg-card p-4",
|
|
225
231
|
children: [
|
|
226
|
-
/* @__PURE__ */
|
|
232
|
+
/* @__PURE__ */ d("h3", {
|
|
227
233
|
className: "text-sm font-semibold text-foreground",
|
|
228
234
|
children: "Available roles"
|
|
229
235
|
}),
|
|
230
|
-
/* @__PURE__ */
|
|
236
|
+
/* @__PURE__ */ d("p", {
|
|
231
237
|
className: "mt-1 text-xs text-muted-foreground",
|
|
232
238
|
children: "Roles are cloned from the system templates when a billing account is created."
|
|
233
239
|
}),
|
|
234
|
-
/* @__PURE__ */
|
|
240
|
+
/* @__PURE__ */ d("ul", {
|
|
235
241
|
className: "mt-3 space-y-2",
|
|
236
|
-
children:
|
|
242
|
+
children: T.map((e) => /* @__PURE__ */ f("li", {
|
|
237
243
|
className: "flex items-start gap-3 rounded-md border border-border bg-background p-3",
|
|
238
|
-
children: [/* @__PURE__ */
|
|
244
|
+
children: [/* @__PURE__ */ f("div", {
|
|
239
245
|
className: "flex-1",
|
|
240
|
-
children: [/* @__PURE__ */
|
|
246
|
+
children: [/* @__PURE__ */ d("div", {
|
|
241
247
|
className: "text-sm font-medium text-foreground",
|
|
242
248
|
children: e.name
|
|
243
|
-
}), e.description && /* @__PURE__ */
|
|
249
|
+
}), e.description && /* @__PURE__ */ d("p", {
|
|
244
250
|
className: "mt-1 text-xs text-muted-foreground",
|
|
245
251
|
children: e.description
|
|
246
252
|
})]
|
|
247
|
-
}), /* @__PURE__ */
|
|
253
|
+
}), /* @__PURE__ */ f("span", {
|
|
248
254
|
className: "text-xs text-muted-foreground",
|
|
249
255
|
children: ["priority ", e.priority]
|
|
250
256
|
})]
|
|
@@ -253,18 +259,18 @@ var f = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, p = "
|
|
|
253
259
|
]
|
|
254
260
|
})
|
|
255
261
|
]
|
|
256
|
-
}) : /* @__PURE__ */
|
|
262
|
+
}) : /* @__PURE__ */ f("div", {
|
|
257
263
|
className: "space-y-6",
|
|
258
|
-
children: [/* @__PURE__ */
|
|
264
|
+
children: [/* @__PURE__ */ d(n, {
|
|
259
265
|
title: "Team & Permissions",
|
|
260
266
|
description: "Select a billing account to manage who can access it."
|
|
261
|
-
}), /* @__PURE__ */
|
|
267
|
+
}), /* @__PURE__ */ d("div", {
|
|
262
268
|
className: "rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
263
|
-
children: "No billing account selected.
|
|
269
|
+
children: "No billing account selected. Pick one from the billing account selector to manage its members and roles."
|
|
264
270
|
})]
|
|
265
|
-
}) : /* @__PURE__ */
|
|
271
|
+
}) : /* @__PURE__ */ d(t, { message: "You don't have permission to manage team and permissions for this billing account." });
|
|
266
272
|
};
|
|
267
273
|
//#endregion
|
|
268
|
-
export {
|
|
274
|
+
export { v as TeamPermissionsPage };
|
|
269
275
|
|
|
270
276
|
//# 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 { useSearchParams } from 'react-router-dom';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\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 const permissions = useBillingPermissions();\n\n // React Router subscribes to URL changes via useSearchParams, so the\n // page re-fetches when the user navigates between billing accounts\n // without a full reload. Reading window.location.search directly was\n // stale across in-app navigation.\n const [searchParams] = useSearchParams();\n const billingAccountId = searchParams.get('billingAccountId') ?? undefined;\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. Open this page from the billing settings of a specific\n account.\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, 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 const permissions = useBillingPermissions();\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 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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"}
|