hazo_auth 3.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +228 -8
  2. package/SETUP_CHECKLIST.md +370 -0
  3. package/dist/app/api/hazo_auth/me/route.d.ts +3 -0
  4. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  5. package/dist/app/api/hazo_auth/me/route.js +9 -1
  6. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
  7. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
  8. package/dist/components/layouts/profile_stamp_test/index.d.ts +10 -0
  9. package/dist/components/layouts/profile_stamp_test/index.d.ts.map +1 -0
  10. package/dist/components/layouts/profile_stamp_test/index.js +51 -0
  11. package/dist/components/layouts/rbac_test/index.d.ts +15 -0
  12. package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
  13. package/dist/components/layouts/rbac_test/index.js +378 -0
  14. package/dist/components/layouts/shared/components/password_field.js +1 -1
  15. package/dist/components/layouts/shared/components/profile_stamp.d.ts +58 -0
  16. package/dist/components/layouts/shared/components/profile_stamp.d.ts.map +1 -0
  17. package/dist/components/layouts/shared/components/profile_stamp.js +72 -0
  18. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  19. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  20. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  21. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
  22. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
  23. package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
  24. package/dist/components/layouts/shared/index.d.ts +2 -0
  25. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  26. package/dist/components/layouts/shared/index.js +1 -0
  27. package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
  28. package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
  29. package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
  30. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
  31. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
  32. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
  33. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
  34. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
  35. package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
  36. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
  37. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
  38. package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
  39. package/dist/components/layouts/user_management/index.d.ts +9 -2
  40. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  41. package/dist/components/layouts/user_management/index.js +22 -6
  42. package/dist/components/ui/hover-card.d.ts +7 -0
  43. package/dist/components/ui/hover-card.d.ts.map +1 -0
  44. package/dist/components/ui/hover-card.js +29 -0
  45. package/dist/components/ui/index.d.ts +1 -0
  46. package/dist/components/ui/index.d.ts.map +1 -1
  47. package/dist/components/ui/index.js +1 -0
  48. package/dist/components/ui/select.d.ts +14 -0
  49. package/dist/components/ui/select.d.ts.map +1 -0
  50. package/dist/components/ui/select.js +59 -0
  51. package/dist/components/ui/tree-view.d.ts +108 -0
  52. package/dist/components/ui/tree-view.d.ts.map +1 -0
  53. package/dist/components/ui/tree-view.js +194 -0
  54. package/dist/lib/auth/auth_types.d.ts +45 -0
  55. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  56. package/dist/lib/auth/auth_types.js +13 -0
  57. package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
  58. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  59. package/dist/lib/auth/hazo_get_auth.server.js +107 -3
  60. package/dist/lib/auth/scope_cache.d.ts +92 -0
  61. package/dist/lib/auth/scope_cache.d.ts.map +1 -0
  62. package/dist/lib/auth/scope_cache.js +171 -0
  63. package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
  64. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
  65. package/dist/lib/scope_hierarchy_config.server.js +96 -0
  66. package/dist/lib/services/email_service.d.ts.map +1 -1
  67. package/dist/lib/services/email_service.js +7 -2
  68. package/dist/lib/services/profile_picture_service.d.ts +1 -7
  69. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  70. package/dist/lib/services/profile_picture_service.js +77 -32
  71. package/dist/lib/services/registration_service.js +1 -1
  72. package/dist/lib/services/scope_labels_service.d.ts +48 -0
  73. package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
  74. package/dist/lib/services/scope_labels_service.js +277 -0
  75. package/dist/lib/services/scope_service.d.ts +114 -0
  76. package/dist/lib/services/scope_service.d.ts.map +1 -0
  77. package/dist/lib/services/scope_service.js +582 -0
  78. package/dist/lib/services/user_scope_service.d.ts +74 -0
  79. package/dist/lib/services/user_scope_service.d.ts.map +1 -0
  80. package/dist/lib/services/user_scope_service.js +415 -0
  81. package/hazo_auth_config.example.ini +1 -1
  82. package/package.json +4 -1
@@ -0,0 +1,291 @@
1
+ // file_description: Scope Hierarchy tab component for managing HRBAC scopes (L1-L7) using tree view
2
+ // section: client_directive
3
+ "use client";
4
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
5
+ // section: imports
6
+ import { useState, useEffect, useCallback, useMemo } from "react";
7
+ import { TreeView } from "../../../ui/tree-view";
8
+ import { Button } from "../../../ui/button";
9
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../ui/dialog";
10
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../../../ui/alert-dialog";
11
+ import { Input } from "../../../ui/input";
12
+ import { Label } from "../../../ui/label";
13
+ import { Loader2, Plus, Edit, Trash2, CircleCheck, CircleX, Building2, FolderTree, RefreshCw, } from "lucide-react";
14
+ import { toast } from "sonner";
15
+ import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider";
16
+ const SCOPE_LEVEL_LABELS = {
17
+ hazo_scopes_l1: "Level 1",
18
+ hazo_scopes_l2: "Level 2",
19
+ hazo_scopes_l3: "Level 3",
20
+ hazo_scopes_l4: "Level 4",
21
+ hazo_scopes_l5: "Level 5",
22
+ hazo_scopes_l6: "Level 6",
23
+ hazo_scopes_l7: "Level 7",
24
+ };
25
+ // section: helpers
26
+ function getLevelNumber(level) {
27
+ return parseInt(level.replace("hazo_scopes_l", ""));
28
+ }
29
+ function getChildLevel(level) {
30
+ const num = getLevelNumber(level);
31
+ if (num >= 7)
32
+ return null;
33
+ return `hazo_scopes_l${num + 1}`;
34
+ }
35
+ // Convert ScopeTreeNode to TreeDataItem format
36
+ function convertToTreeData(nodes, onEdit, onDelete, onAddChild) {
37
+ return nodes.map((node) => {
38
+ const levelNum = getLevelNumber(node.level);
39
+ const hasChildren = node.children && node.children.length > 0;
40
+ const canHaveChildren = levelNum < 7;
41
+ const item = {
42
+ id: node.id,
43
+ name: `${node.name} (${node.seq})`,
44
+ icon: Building2,
45
+ scopeData: node,
46
+ actions: (_jsxs("div", { className: "flex items-center gap-1", children: [canHaveChildren && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
47
+ e.stopPropagation();
48
+ onAddChild(node);
49
+ }, title: "Add child scope", children: _jsx(Plus, { className: "h-3 w-3" }) })), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
50
+ e.stopPropagation();
51
+ onEdit(node);
52
+ }, title: "Edit scope", children: _jsx(Edit, { className: "h-3 w-3" }) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0 text-destructive hover:text-destructive", onClick: (e) => {
53
+ e.stopPropagation();
54
+ onDelete(node);
55
+ }, title: "Delete scope", children: _jsx(Trash2, { className: "h-3 w-3" }) })] })),
56
+ };
57
+ if (hasChildren) {
58
+ item.children = convertToTreeData(node.children, onEdit, onDelete, onAddChild);
59
+ }
60
+ return item;
61
+ });
62
+ }
63
+ // section: component
64
+ /**
65
+ * Scope Hierarchy tab component for managing HRBAC scopes
66
+ * Displays scopes in a tree view for intuitive hierarchy configuration
67
+ * @param props - Component props
68
+ * @returns Scope Hierarchy tab component
69
+ */
70
+ export function ScopeHierarchyTab({ className, defaultOrg = "", }) {
71
+ const { apiBasePath } = useHazoAuthConfig();
72
+ // State
73
+ const [tree, setTree] = useState([]);
74
+ const [loading, setLoading] = useState(true);
75
+ const [actionLoading, setActionLoading] = useState(false);
76
+ const [org, setOrg] = useState(defaultOrg);
77
+ const [selectedItem, setSelectedItem] = useState();
78
+ // Dialog state
79
+ const [addDialogOpen, setAddDialogOpen] = useState(false);
80
+ const [editDialogOpen, setEditDialogOpen] = useState(false);
81
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
82
+ const [selectedScope, setSelectedScope] = useState(null);
83
+ const [addParentScope, setAddParentScope] = useState(null);
84
+ // Form state
85
+ const [newName, setNewName] = useState("");
86
+ const [newOrg, setNewOrg] = useState(defaultOrg);
87
+ const [editName, setEditName] = useState("");
88
+ // Load tree data
89
+ const loadTree = useCallback(async () => {
90
+ if (!org) {
91
+ setTree([]);
92
+ setLoading(false);
93
+ return;
94
+ }
95
+ setLoading(true);
96
+ try {
97
+ const params = new URLSearchParams({ action: "tree", org });
98
+ const response = await fetch(`${apiBasePath}/scope_management/scopes?${params}`);
99
+ const data = await response.json();
100
+ if (data.success) {
101
+ setTree(data.tree || []);
102
+ }
103
+ else {
104
+ toast.error(data.error || "Failed to load scope hierarchy");
105
+ setTree([]);
106
+ }
107
+ }
108
+ catch (error) {
109
+ toast.error("Failed to load scope hierarchy");
110
+ setTree([]);
111
+ }
112
+ finally {
113
+ setLoading(false);
114
+ }
115
+ }, [apiBasePath, org]);
116
+ // Load data when org changes
117
+ useEffect(() => {
118
+ void loadTree();
119
+ }, [loadTree]);
120
+ // Handle add scope (root level)
121
+ const handleAddRootScope = () => {
122
+ setAddParentScope(null);
123
+ setNewOrg(org || defaultOrg);
124
+ setNewName("");
125
+ setAddDialogOpen(true);
126
+ };
127
+ // Handle add child scope
128
+ const handleAddChildScope = (parent) => {
129
+ setAddParentScope(parent);
130
+ setNewOrg(parent.org);
131
+ setNewName("");
132
+ setAddDialogOpen(true);
133
+ };
134
+ // Handle edit scope
135
+ const openEditDialog = (scope) => {
136
+ setSelectedScope(scope);
137
+ setEditName(scope.name);
138
+ setEditDialogOpen(true);
139
+ };
140
+ // Handle delete scope
141
+ const openDeleteDialog = (scope) => {
142
+ setSelectedScope(scope);
143
+ setDeleteDialogOpen(true);
144
+ };
145
+ // Create scope
146
+ const handleCreateScope = async () => {
147
+ if (!newName.trim()) {
148
+ toast.error("Name is required");
149
+ return;
150
+ }
151
+ if (!newOrg.trim()) {
152
+ toast.error("Organization is required");
153
+ return;
154
+ }
155
+ setActionLoading(true);
156
+ try {
157
+ const level = addParentScope
158
+ ? getChildLevel(addParentScope.level)
159
+ : "hazo_scopes_l1";
160
+ if (!level) {
161
+ toast.error("Cannot add children to Level 7 scopes");
162
+ return;
163
+ }
164
+ const body = {
165
+ level,
166
+ org: newOrg.trim(),
167
+ name: newName.trim(),
168
+ };
169
+ if (addParentScope) {
170
+ body.parent_scope_id = addParentScope.id;
171
+ }
172
+ const response = await fetch(`${apiBasePath}/scope_management/scopes`, {
173
+ method: "POST",
174
+ headers: { "Content-Type": "application/json" },
175
+ body: JSON.stringify(body),
176
+ });
177
+ const data = await response.json();
178
+ if (data.success) {
179
+ toast.success("Scope created successfully");
180
+ setAddDialogOpen(false);
181
+ setNewName("");
182
+ setAddParentScope(null);
183
+ await loadTree();
184
+ }
185
+ else {
186
+ toast.error(data.error || "Failed to create scope");
187
+ }
188
+ }
189
+ catch (error) {
190
+ toast.error("Failed to create scope");
191
+ }
192
+ finally {
193
+ setActionLoading(false);
194
+ }
195
+ };
196
+ // Update scope
197
+ const handleUpdateScope = async () => {
198
+ if (!selectedScope)
199
+ return;
200
+ if (!editName.trim()) {
201
+ toast.error("Name is required");
202
+ return;
203
+ }
204
+ setActionLoading(true);
205
+ try {
206
+ const body = {
207
+ level: selectedScope.level,
208
+ scope_id: selectedScope.id,
209
+ name: editName.trim(),
210
+ };
211
+ const response = await fetch(`${apiBasePath}/scope_management/scopes`, {
212
+ method: "PATCH",
213
+ headers: { "Content-Type": "application/json" },
214
+ body: JSON.stringify(body),
215
+ });
216
+ const data = await response.json();
217
+ if (data.success) {
218
+ toast.success("Scope updated successfully");
219
+ setEditDialogOpen(false);
220
+ setSelectedScope(null);
221
+ setEditName("");
222
+ await loadTree();
223
+ }
224
+ else {
225
+ toast.error(data.error || "Failed to update scope");
226
+ }
227
+ }
228
+ catch (error) {
229
+ toast.error("Failed to update scope");
230
+ }
231
+ finally {
232
+ setActionLoading(false);
233
+ }
234
+ };
235
+ // Delete scope
236
+ const handleDeleteScope = async () => {
237
+ if (!selectedScope)
238
+ return;
239
+ setActionLoading(true);
240
+ try {
241
+ const params = new URLSearchParams({
242
+ level: selectedScope.level,
243
+ scope_id: selectedScope.id,
244
+ });
245
+ const response = await fetch(`${apiBasePath}/scope_management/scopes?${params}`, {
246
+ method: "DELETE",
247
+ });
248
+ const data = await response.json();
249
+ if (data.success) {
250
+ toast.success("Scope deleted successfully");
251
+ setDeleteDialogOpen(false);
252
+ setSelectedScope(null);
253
+ await loadTree();
254
+ }
255
+ else {
256
+ toast.error(data.error || "Failed to delete scope");
257
+ }
258
+ }
259
+ catch (error) {
260
+ toast.error("Failed to delete scope");
261
+ }
262
+ finally {
263
+ setActionLoading(false);
264
+ }
265
+ };
266
+ // Convert tree to TreeDataItem format
267
+ const treeData = useMemo(() => {
268
+ return convertToTreeData(tree, openEditDialog, openDeleteDialog, handleAddChildScope);
269
+ }, [tree]);
270
+ // Handle tree item selection
271
+ const handleSelectChange = (item) => {
272
+ setSelectedItem(item);
273
+ };
274
+ // Get level label for dialog
275
+ const getAddDialogLevelLabel = () => {
276
+ if (!addParentScope)
277
+ return "Level 1";
278
+ const childLevel = getChildLevel(addParentScope.level);
279
+ return childLevel ? SCOPE_LEVEL_LABELS[childLevel] : "Unknown";
280
+ };
281
+ return (_jsxs("div", { className: `cls_scope_hierarchy_tab flex flex-col gap-4 w-full ${className || ""}`, children: [_jsxs("div", { className: "cls_scope_hierarchy_header flex items-center justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "cls_scope_hierarchy_header_left flex items-center gap-4", children: [_jsxs("div", { className: "cls_scope_hierarchy_org_filter flex items-center gap-2", children: [_jsx(Label, { htmlFor: "scope_org", className: "text-sm font-medium", children: "Organization:" }), _jsx(Input, { id: "scope_org", value: org, onChange: (e) => setOrg(e.target.value), placeholder: "Enter organization", className: "w-[200px]" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void loadTree(), disabled: loading || !org, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}` }), "Refresh"] })] }), _jsx("div", { className: "cls_scope_hierarchy_header_right", children: _jsxs(Button, { onClick: handleAddRootScope, variant: "default", size: "sm", disabled: !org, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Add Root Scope"] }) })] }), loading ? (_jsx("div", { className: "cls_scope_hierarchy_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : !org ? (_jsxs("div", { className: "cls_scope_hierarchy_empty flex flex-col items-center justify-center p-8 border rounded-lg border-dashed", children: [_jsx(Building2, { className: "h-12 w-12 text-muted-foreground mb-4" }), _jsx("p", { className: "text-muted-foreground text-center", children: "Enter an organization name to view scope hierarchy" })] })) : tree.length === 0 ? (_jsxs("div", { className: "cls_scope_hierarchy_empty flex flex-col items-center justify-center p-8 border rounded-lg border-dashed", children: [_jsx(FolderTree, { className: "h-12 w-12 text-muted-foreground mb-4" }), _jsxs("p", { className: "text-muted-foreground text-center mb-4", children: ["No scopes found for organization \"", org, "\""] }), _jsxs(Button, { onClick: handleAddRootScope, variant: "outline", size: "sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Create First Scope"] })] })) : (_jsx("div", { className: "cls_scope_hierarchy_tree_container border rounded-lg overflow-auto w-full min-h-[300px]", children: _jsx(TreeView, { data: treeData, expandAll: true, defaultNodeIcon: Building2, defaultLeafIcon: Building2, onSelectChange: handleSelectChange, className: "w-full" }) })), (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.scopeData) && (_jsxs("div", { className: "cls_scope_hierarchy_selected_info p-4 border rounded-lg bg-muted/50", children: [_jsx("h4", { className: "font-medium mb-2", children: "Selected Scope" }), _jsxs("div", { className: "grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Name:" }), " ", selectedItem.scopeData.name] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Seq:" }), " ", selectedItem.scopeData.seq] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Level:" }), " ", SCOPE_LEVEL_LABELS[selectedItem.scopeData.level]] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Org:" }), " ", selectedItem.scopeData.org] })] })] })), _jsx(Dialog, { open: addDialogOpen, onOpenChange: setAddDialogOpen, children: _jsxs(DialogContent, { className: "cls_scope_hierarchy_add_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: addParentScope
282
+ ? `Add Child Scope to "${addParentScope.name}"`
283
+ : "Add Root Scope" }), _jsxs(DialogDescription, { children: ["Create a new scope at ", getAddDialogLevelLabel(), ".", addParentScope &&
284
+ ` This will be a child of "${addParentScope.name}".`] })] }), _jsxs("div", { className: "flex flex-col gap-4 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_scope_name", children: "Name *" }), _jsx(Input, { id: "new_scope_name", value: newName, onChange: (e) => setNewName(e.target.value), placeholder: "Enter scope name" })] }), !addParentScope && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_scope_org", children: "Organization *" }), _jsx(Input, { id: "new_scope_org", value: newOrg, onChange: (e) => setNewOrg(e.target.value), placeholder: "Enter organization" })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleCreateScope, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Creating..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Create"] })) }), _jsxs(Button, { onClick: () => setAddDialogOpen(false), variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(Dialog, { open: editDialogOpen, onOpenChange: setEditDialogOpen, children: _jsxs(DialogContent, { className: "cls_scope_hierarchy_edit_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Edit Scope" }), _jsxs(DialogDescription, { children: ["Update scope: ", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.name, " (", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.seq, ")"] })] }), _jsx("div", { className: "flex flex-col gap-4 py-4", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "edit_scope_name", children: "Name *" }), _jsx(Input, { id: "edit_scope_name", value: editName, onChange: (e) => setEditName(e.target.value), placeholder: "Enter scope name" })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleUpdateScope, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Save"] })) }), _jsxs(Button, { onClick: () => {
285
+ setEditDialogOpen(false);
286
+ setSelectedScope(null);
287
+ }, variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(AlertDialog, { open: deleteDialogOpen, onOpenChange: setDeleteDialogOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Delete Scope" }), _jsxs(AlertDialogDescription, { children: ["Are you sure you want to delete \"", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.name, "\" (", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.seq, ")? This action cannot be undone and will also delete all child scopes."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogAction, { onClick: handleDeleteScope, disabled: actionLoading, children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Deleting..."] })) : ("Delete") }), _jsx(AlertDialogCancel, { onClick: () => {
288
+ setDeleteDialogOpen(false);
289
+ setSelectedScope(null);
290
+ }, children: "Cancel" })] })] }) })] }));
291
+ }
@@ -0,0 +1,13 @@
1
+ export type ScopeLabelsTabProps = {
2
+ className?: string;
3
+ defaultOrg?: string;
4
+ };
5
+ /**
6
+ * Scope Labels tab component for configuring friendly names for scope levels
7
+ * Shows all 7 scope levels with their current labels from database
8
+ * Empty inputs for levels without labels - no placeholders
9
+ * @param props - Component props
10
+ * @returns Scope Labels tab component
11
+ */
12
+ export declare function ScopeLabelsTab({ className, defaultOrg }: ScopeLabelsTabProps): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=scope_labels_tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_labels_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/user_management/components/scope_labels_tab.tsx"],"names":[],"mappings":"AAsBA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAeF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,UAAe,EAAE,EAAE,mBAAmB,2CA4NjF"}
@@ -0,0 +1,158 @@
1
+ // file_description: Scope Labels tab component for configuring friendly names for scope levels
2
+ // section: client_directive
3
+ "use client";
4
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
5
+ // section: imports
6
+ import { useState, useEffect, useCallback } from "react";
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../../ui/table";
8
+ import { Button } from "../../../ui/button";
9
+ import { Input } from "../../../ui/input";
10
+ import { Label } from "../../../ui/label";
11
+ import { Loader2, Save } from "lucide-react";
12
+ import { toast } from "sonner";
13
+ import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider";
14
+ const SCOPE_LEVELS = [
15
+ "hazo_scopes_l1",
16
+ "hazo_scopes_l2",
17
+ "hazo_scopes_l3",
18
+ "hazo_scopes_l4",
19
+ "hazo_scopes_l5",
20
+ "hazo_scopes_l6",
21
+ "hazo_scopes_l7",
22
+ ];
23
+ // section: component
24
+ /**
25
+ * Scope Labels tab component for configuring friendly names for scope levels
26
+ * Shows all 7 scope levels with their current labels from database
27
+ * Empty inputs for levels without labels - no placeholders
28
+ * @param props - Component props
29
+ * @returns Scope Labels tab component
30
+ */
31
+ export function ScopeLabelsTab({ className, defaultOrg = "" }) {
32
+ const { apiBasePath } = useHazoAuthConfig();
33
+ // State - simple record of scope_type to label string (empty string if not set)
34
+ const [labels, setLabels] = useState(() => {
35
+ const initial = {};
36
+ for (const level of SCOPE_LEVELS) {
37
+ initial[level] = "";
38
+ }
39
+ return initial;
40
+ });
41
+ const [originalLabels, setOriginalLabels] = useState(null);
42
+ const [loading, setLoading] = useState(true);
43
+ const [saving, setSaving] = useState(false);
44
+ const [org, setOrg] = useState(defaultOrg);
45
+ // Load labels from database (only real DB records, not synthetic defaults)
46
+ const loadLabels = useCallback(async () => {
47
+ if (!org.trim()) {
48
+ // Reset to empty if no org
49
+ const empty = {};
50
+ for (const level of SCOPE_LEVELS) {
51
+ empty[level] = "";
52
+ }
53
+ setLabels(empty);
54
+ setOriginalLabels(empty);
55
+ setLoading(false);
56
+ return;
57
+ }
58
+ setLoading(true);
59
+ try {
60
+ // Fetch WITHOUT defaults - only get actual DB records
61
+ const params = new URLSearchParams({ org: org.trim(), include_defaults: "false" });
62
+ const response = await fetch(`${apiBasePath}/scope_management/labels?${params}`);
63
+ const data = await response.json();
64
+ if (data.success) {
65
+ // Start with empty labels
66
+ const newLabels = {};
67
+ for (const level of SCOPE_LEVELS) {
68
+ newLabels[level] = "";
69
+ }
70
+ // Fill in labels from database
71
+ const dbLabels = data.labels || [];
72
+ for (const dbLabel of dbLabels) {
73
+ if (dbLabel.scope_type && dbLabel.label) {
74
+ newLabels[dbLabel.scope_type] = dbLabel.label;
75
+ }
76
+ }
77
+ setLabels(newLabels);
78
+ setOriginalLabels(Object.assign({}, newLabels));
79
+ }
80
+ else {
81
+ toast.error(data.error || "Failed to load labels");
82
+ }
83
+ }
84
+ catch (_a) {
85
+ toast.error("Failed to load labels");
86
+ }
87
+ finally {
88
+ setLoading(false);
89
+ }
90
+ }, [apiBasePath, org]);
91
+ // Load labels when org changes
92
+ useEffect(() => {
93
+ void loadLabels();
94
+ }, [loadLabels]);
95
+ // Handle label change
96
+ const handleLabelChange = (level, value) => {
97
+ setLabels((prev) => (Object.assign(Object.assign({}, prev), { [level]: value })));
98
+ };
99
+ // Check if there are unsaved changes
100
+ const hasChanges = () => {
101
+ if (!originalLabels)
102
+ return false;
103
+ for (const level of SCOPE_LEVELS) {
104
+ if (labels[level] !== originalLabels[level]) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ };
110
+ // Save all labels - send all non-empty labels to be upserted
111
+ const handleSave = async () => {
112
+ if (!org.trim()) {
113
+ toast.error("Organization is required");
114
+ return;
115
+ }
116
+ // Collect all non-empty labels
117
+ const labelsToSave = [];
118
+ for (const level of SCOPE_LEVELS) {
119
+ const label = labels[level].trim();
120
+ if (label) {
121
+ labelsToSave.push({
122
+ scope_type: level,
123
+ label: label,
124
+ });
125
+ }
126
+ }
127
+ setSaving(true);
128
+ try {
129
+ const response = await fetch(`${apiBasePath}/scope_management/labels`, {
130
+ method: "PUT",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({
133
+ org: org.trim(),
134
+ labels: labelsToSave,
135
+ }),
136
+ });
137
+ const data = await response.json();
138
+ if (data.success) {
139
+ toast.success("Labels saved successfully");
140
+ // Reload to get fresh state
141
+ await loadLabels();
142
+ }
143
+ else {
144
+ toast.error(data.error || "Failed to save labels");
145
+ }
146
+ }
147
+ catch (_a) {
148
+ toast.error("Failed to save labels");
149
+ }
150
+ finally {
151
+ setSaving(false);
152
+ }
153
+ };
154
+ return (_jsxs("div", { className: `cls_scope_labels_tab flex flex-col gap-4 w-full ${className || ""}`, children: [_jsxs("div", { className: "cls_scope_labels_header flex items-center justify-between gap-4 flex-wrap", children: [_jsx("div", { className: "cls_scope_labels_header_left flex items-center gap-4", children: _jsxs("div", { className: "cls_scope_labels_org_input flex items-center gap-2", children: [_jsx(Label, { htmlFor: "labels_org", className: "text-sm font-medium", children: "Organization:" }), _jsx(Input, { id: "labels_org", value: org, onChange: (e) => setOrg(e.target.value), placeholder: "Enter organization name", className: "w-[200px]" })] }) }), _jsx("div", { className: "cls_scope_labels_header_right", children: _jsx(Button, { onClick: handleSave, disabled: saving || !hasChanges() || !org.trim(), variant: "default", size: "sm", children: saving ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save Changes"] })) }) })] }), loading ? (_jsx("div", { className: "cls_scope_labels_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "cls_scope_labels_table_container border rounded-lg overflow-auto w-full", children: _jsxs(Table, { className: "cls_scope_labels_table w-full", children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[180px]", children: "Scope Level" }), _jsx(TableHead, { children: "Label" })] }) }), _jsx(TableBody, { children: SCOPE_LEVELS.map((level) => {
155
+ const label = labels[level];
156
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-sm", children: level }), _jsx(TableCell, { children: _jsx(Input, { value: label, onChange: (e) => handleLabelChange(level, e.target.value), className: "max-w-[400px]", disabled: !org.trim() }) })] }, level));
157
+ }) })] }) })), !org.trim() && (_jsx("div", { className: "cls_scope_labels_info text-sm text-muted-foreground text-center p-4 bg-muted/50 rounded-lg", children: "Enter an organization name to customize scope labels." }))] }));
158
+ }
@@ -0,0 +1,11 @@
1
+ export type UserScopesTabProps = {
2
+ className?: string;
3
+ };
4
+ /**
5
+ * User Scopes tab component for assigning scopes to users
6
+ * Two-panel layout: Users list | Scope assignments
7
+ * @param props - Component props
8
+ * @returns User Scopes tab component
9
+ */
10
+ export declare function UserScopesTab({ className }: UserScopesTabProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=user_scopes_tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user_scopes_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/user_management/components/user_scopes_tab.tsx"],"names":[],"mappings":"AAoDA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA6EF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE,kBAAkB,2CA8f9D"}