@burdenoff/microfe-billing 2026.513.1 → 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.
@@ -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 { directBillingGraphqlRequest as r } from "../../../shared/utils/directBillingGraphql.js";
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 i } from "../../../hooks/useBillingPermissions.js";
8
- import { useCallback as a, useEffect as o, useMemo as s, useState as c } from "react";
9
- import { useSearchParams as l } from "react-router-dom";
10
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
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 f = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, p = "\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n", m = "\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", h = "\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n", g = "\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n", _ = () => {
13
- let { apiGatewayUrl: _, authToken: v, orgId: y } = e(), b = i(), [x] = l(), S = x.get("billingAccountId") ?? void 0, [C, w] = c([]), [T, E] = c([]), [D, O] = c(!1), [k, A] = c(null), [j, M] = c(""), [N, P] = c(""), [F, I] = c(!1), L = a(async (e, t) => r({
14
- apiGatewayUrl: _,
15
- authToken: v,
16
- orgId: y,
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
- ]), R = a(async () => {
24
- if (S) {
25
- O(!0), A(null);
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([L(p, { scopeId: S }), L(m, { scopeId: S })]);
28
- w(e.roles ?? []), E(t.actors ?? []), P((e.roles ?? []).find((e) => /viewer/i.test(e.name))?.id ?? e.roles?.[0]?.id ?? "");
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
- A(e instanceof Error ? e.message : "Failed to load team data");
36
+ M(e instanceof Error ? e.message : "Failed to load team data");
31
37
  } finally {
32
- O(!1);
38
+ A(!1);
33
39
  }
34
40
  }
35
- }, [S, L]);
36
- o(() => {
37
- R();
38
- }, [R]);
39
- let z = s(() => [...T].sort((e, t) => {
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
- }), [T]), B = async (e) => {
43
- if (e.preventDefault(), !S || !j.trim() || !N) return;
44
- let t = j.trim();
45
- if (!f.test(t)) {
46
- A("User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.");
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
- I(!0), A(null);
55
+ R(!0), M(null);
50
56
  try {
51
- await L(h, { input: {
57
+ await z(g, { input: {
52
58
  actorId: t,
53
59
  actorType: "USER",
54
- roleId: N,
55
- scopeId: S
56
- } }), M(""), await R();
60
+ roleId: F,
61
+ scopeId: w
62
+ } }), P(""), await B();
57
63
  } catch (e) {
58
- A(e instanceof Error ? e.message : "Failed to assign role");
64
+ M(e instanceof Error ? e.message : "Failed to assign role");
59
65
  } finally {
60
- I(!1);
66
+ R(!1);
61
67
  }
62
- }, V = async (e) => {
68
+ }, U = async (e) => {
63
69
  if (confirm("Remove this role assignment?")) {
64
- A(null);
70
+ M(null);
65
71
  try {
66
- await L(g, { assignmentId: e }), await R();
72
+ await z(_, { assignmentId: e }), await B();
67
73
  } catch (e) {
68
- A(e instanceof Error ? e.message : "Failed to remove role");
74
+ M(e instanceof Error ? e.message : "Failed to remove role");
69
75
  }
70
76
  }
71
77
  };
72
- return b.canManageBillingAccount ? S ? /* @__PURE__ */ d("div", {
78
+ return x.canManageBillingAccount ? w ? /* @__PURE__ */ f("div", {
73
79
  className: "space-y-6",
74
80
  children: [
75
- /* @__PURE__ */ u(n, {
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__ */ d("div", {
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__ */ u("span", {
88
+ /* @__PURE__ */ d("span", {
83
89
  className: "font-medium text-foreground",
84
90
  children: "Billing account:"
85
91
  }),
86
92
  " ",
87
- /* @__PURE__ */ u("span", {
93
+ /* @__PURE__ */ d("span", {
88
94
  className: "font-mono",
89
- children: S
95
+ children: w
90
96
  })
91
97
  ]
92
98
  }),
93
- k && /* @__PURE__ */ u("div", {
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: k
101
+ children: j
96
102
  }),
97
- /* @__PURE__ */ d("form", {
98
- onSubmit: (e) => void B(e),
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__ */ u("h3", {
107
+ /* @__PURE__ */ d("h3", {
102
108
  className: "text-sm font-semibold text-foreground",
103
109
  children: "Add member"
104
110
  }),
105
- /* @__PURE__ */ d("div", {
111
+ /* @__PURE__ */ f("div", {
106
112
  className: "grid grid-cols-1 gap-3 md:grid-cols-3",
107
- children: [/* @__PURE__ */ d("div", {
113
+ children: [/* @__PURE__ */ f("div", {
108
114
  className: "md:col-span-2",
109
- children: [/* @__PURE__ */ u("label", {
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__ */ u("input", {
119
+ }), /* @__PURE__ */ d("input", {
114
120
  id: "newActorId",
115
121
  type: "text",
116
- value: j,
117
- onChange: (e) => M(e.target.value),
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__ */ d("div", { children: [/* @__PURE__ */ u("label", {
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__ */ u("select", {
132
+ }), /* @__PURE__ */ d("select", {
127
133
  id: "newRoleId",
128
- value: N,
129
- onChange: (e) => P(e.target.value),
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: C.map((e) => /* @__PURE__ */ u("option", {
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__ */ u("button", {
144
+ /* @__PURE__ */ d("button", {
139
145
  type: "submit",
140
- disabled: F || !j.trim() || !N,
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: F ? "Adding…" : "Add member"
148
+ children: L ? "Adding…" : "Add member"
143
149
  })
144
150
  ]
145
151
  }),
146
- /* @__PURE__ */ d("div", {
152
+ /* @__PURE__ */ f("div", {
147
153
  className: "rounded-lg border border-border bg-card",
148
- children: [/* @__PURE__ */ d("div", {
154
+ children: [/* @__PURE__ */ f("div", {
149
155
  className: "border-b border-border px-4 py-3",
150
- children: [/* @__PURE__ */ u("h3", {
156
+ children: [/* @__PURE__ */ d("h3", {
151
157
  className: "text-sm font-semibold text-foreground",
152
158
  children: "Members"
153
- }), /* @__PURE__ */ u("p", {
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
- }), D ? /* @__PURE__ */ u("div", {
163
+ }), k ? /* @__PURE__ */ d("div", {
158
164
  className: "p-6 text-sm text-muted-foreground",
159
165
  children: "Loading…"
160
- }) : z.length === 0 ? /* @__PURE__ */ u("div", {
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__ */ d("table", {
169
+ }) : /* @__PURE__ */ f("table", {
164
170
  className: "w-full text-sm",
165
- children: [/* @__PURE__ */ u("thead", { children: /* @__PURE__ */ d("tr", {
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__ */ u("th", {
174
+ /* @__PURE__ */ d("th", {
169
175
  className: "px-4 py-2",
170
176
  children: "Actor ID"
171
177
  }),
172
- /* @__PURE__ */ u("th", {
178
+ /* @__PURE__ */ d("th", {
173
179
  className: "px-4 py-2",
174
180
  children: "Type"
175
181
  }),
176
- /* @__PURE__ */ u("th", {
182
+ /* @__PURE__ */ d("th", {
177
183
  className: "px-4 py-2",
178
184
  children: "Roles"
179
185
  }),
180
- /* @__PURE__ */ u("th", {
186
+ /* @__PURE__ */ d("th", {
181
187
  className: "px-4 py-2 text-right",
182
188
  children: "Actions"
183
189
  })
184
190
  ]
185
- }) }), /* @__PURE__ */ u("tbody", { children: z.map((e) => /* @__PURE__ */ d("tr", {
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__ */ u("td", {
194
+ /* @__PURE__ */ d("td", {
189
195
  className: "px-4 py-3 font-mono text-xs",
190
196
  children: e.actorId
191
197
  }),
192
- /* @__PURE__ */ u("td", {
198
+ /* @__PURE__ */ d("td", {
193
199
  className: "px-4 py-3 text-xs",
194
200
  children: e.actorType
195
201
  }),
196
- /* @__PURE__ */ u("td", {
202
+ /* @__PURE__ */ d("td", {
197
203
  className: "px-4 py-3",
198
- children: /* @__PURE__ */ u("div", {
204
+ children: /* @__PURE__ */ d("div", {
199
205
  className: "flex flex-wrap gap-1",
200
- children: e.roleAssignments.filter((e) => e.isActive).map((e) => /* @__PURE__ */ d("span", {
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__ */ u("button", {
208
+ children: [e.role?.name ?? e.roleId, /* @__PURE__ */ d("button", {
203
209
  type: "button",
204
- onClick: () => void V(e.id),
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__ */ u("td", {
218
+ /* @__PURE__ */ d("td", {
213
219
  className: "px-4 py-3 text-right",
214
- children: /* @__PURE__ */ d("span", {
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__ */ d("div", {
229
+ /* @__PURE__ */ f("div", {
224
230
  className: "rounded-lg border border-border bg-card p-4",
225
231
  children: [
226
- /* @__PURE__ */ u("h3", {
232
+ /* @__PURE__ */ d("h3", {
227
233
  className: "text-sm font-semibold text-foreground",
228
234
  children: "Available roles"
229
235
  }),
230
- /* @__PURE__ */ u("p", {
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__ */ u("ul", {
240
+ /* @__PURE__ */ d("ul", {
235
241
  className: "mt-3 space-y-2",
236
- children: C.map((e) => /* @__PURE__ */ d("li", {
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__ */ d("div", {
244
+ children: [/* @__PURE__ */ f("div", {
239
245
  className: "flex-1",
240
- children: [/* @__PURE__ */ u("div", {
246
+ children: [/* @__PURE__ */ d("div", {
241
247
  className: "text-sm font-medium text-foreground",
242
248
  children: e.name
243
- }), e.description && /* @__PURE__ */ u("p", {
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__ */ d("span", {
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__ */ d("div", {
262
+ }) : /* @__PURE__ */ f("div", {
257
263
  className: "space-y-6",
258
- children: [/* @__PURE__ */ u(n, {
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__ */ u("div", {
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. Open this page from the billing settings of a specific account."
269
+ children: "No billing account selected. Pick one from the billing account selector to manage its members and roles."
264
270
  })]
265
- }) : /* @__PURE__ */ u(t, { message: "You don't have permission to manage team and permissions for this billing account." });
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 { _ as TeamPermissionsPage };
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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"}