@flowselections/admin-installatietechniek 1.0.3 → 1.0.4
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-lib/components/AdminForInstallatieTechniek.d.ts +4 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +212 -197
- package/dist-lib/hooks/useModulePermission.d.ts +4 -3
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- package/dist-lib/hooks/useModulePermission.js +10 -6
- package/dist-lib/index.d.ts +1 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +0 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +407 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +407 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +407 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +411 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +48897 -1136
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +340 -181
- package/dist-lib/lib/moduleDiscovery.d.ts +7 -2
- package/dist-lib/lib/moduleDiscovery.d.ts.map +1 -1
- package/dist-lib/lib/moduleDiscovery.js +26 -11
- package/package.json +87 -87
- package/dist-lib/hooks/useModuleOrder.d.ts +0 -30
- package/dist-lib/hooks/useModuleOrder.d.ts.map +0 -1
- package/dist-lib/hooks/useModuleOrder.js +0 -99
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { useAuth, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger,
|
|
4
|
-
import {
|
|
5
|
-
import { assignRole, createTestUser, deleteUser,
|
|
6
|
-
import { notifyModuleOrderChanged } from "../hooks/useModuleOrder";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useAuth, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
4
|
+
import { ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, } from "lucide-react";
|
|
5
|
+
import { assignRole, createRole, createTestUser, deleteRole, deleteUser, duplicateRole, getRoles, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setUserModulePermission, syncModuleRegistry, updateRole, updateUserName, } from "../lib/admin.functions";
|
|
7
6
|
import { useDiscoveredModules } from "../lib/moduleDiscovery";
|
|
8
7
|
import { supabase } from "../integrations/supabase/client";
|
|
9
|
-
const PERMISSION_LABELS = {
|
|
10
|
-
none: "Geen toegang",
|
|
11
|
-
view: "Bekijken",
|
|
12
|
-
edit: "Aanpassen",
|
|
13
|
-
};
|
|
14
8
|
const USER_PERMISSION_LABELS = {
|
|
15
9
|
inherit: "Volg rol",
|
|
16
10
|
none: "Geen toegang",
|
|
17
11
|
view: "Bekijken",
|
|
18
12
|
edit: "Aanpassen",
|
|
19
13
|
};
|
|
14
|
+
const permCellKey = (moduleId, featureId) => `${moduleId}::${featureId}`;
|
|
20
15
|
const getErrorMessage = (error, fallback) => {
|
|
21
16
|
if (error instanceof Error && error.message) {
|
|
22
17
|
if (/permission denied|row-level security|rls|forbidden|not authorized/i.test(error.message)) {
|
|
@@ -28,7 +23,6 @@ const getErrorMessage = (error, fallback) => {
|
|
|
28
23
|
};
|
|
29
24
|
async function refreshOwnSessionIfSelf(targetUserId, currentUserId) {
|
|
30
25
|
if (currentUserId && targetUserId === currentUserId) {
|
|
31
|
-
// Nieuwe access-token ophalen zodat de user_roles JWT-claim direct actueel is.
|
|
32
26
|
await supabase.auth.refreshSession();
|
|
33
27
|
}
|
|
34
28
|
}
|
|
@@ -41,26 +35,32 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
41
35
|
const isAdministratie = authRoles.includes("administratie");
|
|
42
36
|
const canAccess = isAdmin || isAdministratie;
|
|
43
37
|
const currentUserId = user?.id;
|
|
38
|
+
const [activeTab, setActiveTab] = useState("users");
|
|
44
39
|
const [users, setUsers] = useState([]);
|
|
45
40
|
const [availableRoles, setAvailableRoles] = useState([]);
|
|
46
41
|
const [loading, setLoading] = useState(true);
|
|
47
|
-
const [selectedUser, setSelectedUser] = useState("");
|
|
48
|
-
const [selectedRole, setSelectedRole] = useState("planner");
|
|
49
42
|
const [submitting, setSubmitting] = useState(false);
|
|
50
|
-
const [
|
|
51
|
-
const [
|
|
52
|
-
const [
|
|
53
|
-
const [
|
|
43
|
+
const [renameTarget, setRenameTarget] = useState(null);
|
|
44
|
+
const [renameValue, setRenameValue] = useState("");
|
|
45
|
+
const [renaming, setRenaming] = useState(false);
|
|
46
|
+
const [roles, setRoles] = useState([]);
|
|
47
|
+
const [roleModules, setRoleModules] = useState([]);
|
|
48
|
+
const [rolePermissions, setRolePermissions] = useState([]);
|
|
49
|
+
const [rolesLoading, setRolesLoading] = useState(true);
|
|
50
|
+
const [editor, setEditor] = useState(null);
|
|
51
|
+
const [editorSaving, setEditorSaving] = useState(false);
|
|
52
|
+
const [expandedModules, setExpandedModules] = useState({});
|
|
53
|
+
const [roleDeleteTarget, setRoleDeleteTarget] = useState(null);
|
|
54
54
|
const [inviteOpen, setInviteOpen] = useState(false);
|
|
55
55
|
const [inviteName, setInviteName] = useState("");
|
|
56
56
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
57
|
-
const [inviteRole, setInviteRole] = useState("
|
|
57
|
+
const [inviteRole, setInviteRole] = useState("");
|
|
58
58
|
const [inviting, setInviting] = useState(false);
|
|
59
59
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
60
60
|
const [deleting, setDeleting] = useState(false);
|
|
61
61
|
const [testOpen, setTestOpen] = useState(false);
|
|
62
62
|
const [testName, setTestName] = useState("");
|
|
63
|
-
const [testRole, setTestRole] = useState("
|
|
63
|
+
const [testRole, setTestRole] = useState("");
|
|
64
64
|
const [testCreating, setTestCreating] = useState(false);
|
|
65
65
|
const [testCredentials, setTestCredentials] = useState(null);
|
|
66
66
|
const [searchQuery, setSearchQuery] = useState("");
|
|
@@ -70,25 +70,25 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
70
70
|
const [userPermModules, setUserPermModules] = useState([]);
|
|
71
71
|
const [userPermMap, setUserPermMap] = useState({});
|
|
72
72
|
const [userPermLoading, setUserPermLoading] = useState(false);
|
|
73
|
-
const [orderModules, setOrderModules] = useState([]);
|
|
74
|
-
const [orderLoading, setOrderLoading] = useState(false);
|
|
75
|
-
const [orderSaving, setOrderSaving] = useState(false);
|
|
76
|
-
const [orderSavedAt, setOrderSavedAt] = useState(null);
|
|
77
|
-
const orderSaveTimer = useRef(null);
|
|
78
|
-
const permKey = (role, moduleId) => `${role}::${moduleId}`;
|
|
79
73
|
const discoveredModules = useDiscoveredModules();
|
|
80
74
|
const shellModules = useMemo(() => {
|
|
81
75
|
const source = Array.isArray(modules) && modules.length > 0 ? modules : discoveredModules;
|
|
82
76
|
return source.filter((m) => Boolean(m?.id) && Boolean(m?.name));
|
|
83
77
|
}, [modules, discoveredModules]);
|
|
84
78
|
const effectiveShellId = shellId ?? (typeof window !== "undefined" ? window.location.host : "unknown-shell");
|
|
85
|
-
const shellModulesKey = shellModules
|
|
79
|
+
const shellModulesKey = shellModules
|
|
80
|
+
.map((m) => `${m.id}:${m.name}:${(m.features ?? []).map((f) => `${f.id}=${f.name}`).join(",")}`)
|
|
81
|
+
.join("|");
|
|
86
82
|
const loadUsers = async () => {
|
|
87
83
|
setLoading(true);
|
|
88
84
|
try {
|
|
89
85
|
const result = await getUsersWithRoles();
|
|
90
86
|
setUsers(Array.isArray(result?.users) ? result.users : []);
|
|
91
|
-
|
|
87
|
+
const roleList = Array.isArray(result?.availableRoles) ? result.availableRoles : [];
|
|
88
|
+
setAvailableRoles(roleList);
|
|
89
|
+
const fallback = roleList[0]?.id ?? "";
|
|
90
|
+
setInviteRole((current) => current || fallback);
|
|
91
|
+
setTestRole((current) => current || fallback);
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
94
|
toast.error(getErrorMessage(error, "Fout bij het laden van gebruikers en rollen"));
|
|
@@ -97,31 +97,19 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
97
97
|
setLoading(false);
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const loadRoles = async () => {
|
|
101
|
+
setRolesLoading(true);
|
|
102
102
|
try {
|
|
103
|
-
const result = await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
setPermRoles(roles);
|
|
108
|
-
setPermModules(mods);
|
|
109
|
-
const matrix = {};
|
|
110
|
-
for (const r of roles) {
|
|
111
|
-
for (const m of mods) {
|
|
112
|
-
matrix[permKey(r, m.id)] = "none";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
for (const p of perms) {
|
|
116
|
-
matrix[permKey(p.role, p.moduleId)] = p.permission;
|
|
117
|
-
}
|
|
118
|
-
setPermMatrix(matrix);
|
|
103
|
+
const result = await getRoles();
|
|
104
|
+
setRoles(Array.isArray(result?.roles) ? result.roles : []);
|
|
105
|
+
setRoleModules(Array.isArray(result?.modules) ? result.modules : []);
|
|
106
|
+
setRolePermissions(Array.isArray(result?.permissions) ? result.permissions : []);
|
|
119
107
|
}
|
|
120
108
|
catch (error) {
|
|
121
|
-
toast.error(getErrorMessage(error, "Fout bij het laden van
|
|
109
|
+
toast.error(getErrorMessage(error, "Fout bij het laden van rollen"));
|
|
122
110
|
}
|
|
123
111
|
finally {
|
|
124
|
-
|
|
112
|
+
setRolesLoading(false);
|
|
125
113
|
}
|
|
126
114
|
};
|
|
127
115
|
const loadUserPermissions = async (userId) => {
|
|
@@ -137,10 +125,14 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
137
125
|
const overrides = Array.isArray(result?.overrides) ? result.overrides : [];
|
|
138
126
|
setUserPermModules(mods);
|
|
139
127
|
const map = {};
|
|
140
|
-
for (const m of mods)
|
|
141
|
-
map[m.id] = "inherit";
|
|
142
|
-
|
|
143
|
-
|
|
128
|
+
for (const m of mods) {
|
|
129
|
+
map[permCellKey(m.id, "")] = "inherit";
|
|
130
|
+
for (const f of m.features ?? [])
|
|
131
|
+
map[permCellKey(m.id, f.id)] = "inherit";
|
|
132
|
+
}
|
|
133
|
+
for (const o of overrides) {
|
|
134
|
+
map[permCellKey(o.moduleId, o.featureId ?? "")] = o.permission;
|
|
135
|
+
}
|
|
144
136
|
setUserPermMap(map);
|
|
145
137
|
}
|
|
146
138
|
catch (error) {
|
|
@@ -150,95 +142,11 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
150
142
|
setUserPermLoading(false);
|
|
151
143
|
}
|
|
152
144
|
};
|
|
153
|
-
const loadModuleOrder = async () => {
|
|
154
|
-
setOrderLoading(true);
|
|
155
|
-
try {
|
|
156
|
-
const result = await getModuleOrder();
|
|
157
|
-
const saved = Array.isArray(result?.modules) ? result.modules : [];
|
|
158
|
-
const byId = new Map(saved.map((m) => [m.id, m]));
|
|
159
|
-
// Standaardvolgorde = de volgorde waarin de shell zijn modules levert.
|
|
160
|
-
// Opgeslagen posities krijgen voorrang; de rest houdt de bronvolgorde.
|
|
161
|
-
const base = [];
|
|
162
|
-
for (const m of shellModules) {
|
|
163
|
-
const match = byId.get(m.id);
|
|
164
|
-
base.push({ id: m.id, name: match?.name ?? m.name });
|
|
165
|
-
byId.delete(m.id);
|
|
166
|
-
}
|
|
167
|
-
for (const m of byId.values())
|
|
168
|
-
base.push({ id: m.id, name: m.name });
|
|
169
|
-
const positions = new Map();
|
|
170
|
-
for (const m of saved) {
|
|
171
|
-
if (typeof m.sortOrder === "number")
|
|
172
|
-
positions.set(m.id, m.sortOrder);
|
|
173
|
-
}
|
|
174
|
-
const ordered = base
|
|
175
|
-
.map((mod, index) => ({ mod, index }))
|
|
176
|
-
.sort((a, b) => {
|
|
177
|
-
const ao = positions.get(a.mod.id);
|
|
178
|
-
const bo = positions.get(b.mod.id);
|
|
179
|
-
if (ao !== undefined && bo !== undefined)
|
|
180
|
-
return ao - bo;
|
|
181
|
-
if (ao !== undefined)
|
|
182
|
-
return -1;
|
|
183
|
-
if (bo !== undefined)
|
|
184
|
-
return 1;
|
|
185
|
-
return a.index - b.index;
|
|
186
|
-
})
|
|
187
|
-
.map((e) => e.mod);
|
|
188
|
-
setOrderModules(ordered);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
toast.error(getErrorMessage(error, "Fout bij het laden van de zijbalk-volgorde"));
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
setOrderLoading(false);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
const persistOrder = useCallback(async (list, previous) => {
|
|
198
|
-
setOrderSaving(true);
|
|
199
|
-
try {
|
|
200
|
-
await setModuleOrder({ data: { moduleIds: list.map((m) => m.id) } });
|
|
201
|
-
setOrderSavedAt(Date.now());
|
|
202
|
-
// Zijbalk van de shell direct laten herordenen.
|
|
203
|
-
notifyModuleOrderChanged();
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
setOrderModules(previous);
|
|
207
|
-
toast.error(getErrorMessage(error, "Opslaan van de volgorde mislukt"));
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
setOrderSaving(false);
|
|
211
|
-
}
|
|
212
|
-
}, []);
|
|
213
|
-
const moveModule = (index, direction) => {
|
|
214
|
-
const previous = orderModules;
|
|
215
|
-
const next = [...previous];
|
|
216
|
-
const target = index + direction;
|
|
217
|
-
if (target < 0 || target >= next.length)
|
|
218
|
-
return;
|
|
219
|
-
const [item] = next.splice(index, 1);
|
|
220
|
-
next.splice(target, 0, item);
|
|
221
|
-
setOrderModules(next);
|
|
222
|
-
// Realtime opslaan: klikken worden kort gebundeld.
|
|
223
|
-
if (orderSaveTimer.current)
|
|
224
|
-
clearTimeout(orderSaveTimer.current);
|
|
225
|
-
orderSaveTimer.current = setTimeout(() => {
|
|
226
|
-
orderSaveTimer.current = null;
|
|
227
|
-
void persistOrder(next, previous);
|
|
228
|
-
}, 400);
|
|
229
|
-
};
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
return () => {
|
|
232
|
-
if (orderSaveTimer.current)
|
|
233
|
-
clearTimeout(orderSaveTimer.current);
|
|
234
|
-
};
|
|
235
|
-
}, []);
|
|
236
145
|
useEffect(() => {
|
|
237
146
|
if (!canAccess)
|
|
238
147
|
return;
|
|
239
148
|
let cancelled = false;
|
|
240
149
|
(async () => {
|
|
241
|
-
// Modules die deze shell heeft geladen automatisch registreren.
|
|
242
150
|
if (isAdmin && shellModules.length > 0) {
|
|
243
151
|
try {
|
|
244
152
|
await syncModuleRegistry({ data: { modules: shellModules, shellId: effectiveShellId } });
|
|
@@ -250,14 +158,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
250
158
|
if (cancelled)
|
|
251
159
|
return;
|
|
252
160
|
loadUsers();
|
|
253
|
-
|
|
254
|
-
if (isAdmin) {
|
|
255
|
-
loadPermissions();
|
|
256
|
-
loadModuleOrder();
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
setPermLoading(false);
|
|
260
|
-
}
|
|
161
|
+
loadRoles();
|
|
261
162
|
})();
|
|
262
163
|
return () => {
|
|
263
164
|
cancelled = true;
|
|
@@ -270,27 +171,28 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
270
171
|
loadUserPermissions(permUserId);
|
|
271
172
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
272
173
|
}, [permUserId, canAccess]);
|
|
273
|
-
const handleUserPermChange = async (moduleId, permission) => {
|
|
174
|
+
const handleUserPermChange = async (moduleId, featureId, permission) => {
|
|
274
175
|
if (!permUserId)
|
|
275
176
|
return;
|
|
276
|
-
const
|
|
277
|
-
|
|
177
|
+
const key = permCellKey(moduleId, featureId);
|
|
178
|
+
const prev = userPermMap[key] ?? "inherit";
|
|
179
|
+
setUserPermMap((m) => ({ ...m, [key]: permission }));
|
|
278
180
|
try {
|
|
279
|
-
await setUserModulePermission({
|
|
181
|
+
await setUserModulePermission({
|
|
182
|
+
data: { userId: permUserId, moduleId, featureId, permission },
|
|
183
|
+
});
|
|
280
184
|
toast.success("Persoonlijk recht opgeslagen");
|
|
281
185
|
}
|
|
282
186
|
catch (error) {
|
|
283
|
-
setUserPermMap((m) => ({ ...m, [
|
|
187
|
+
setUserPermMap((m) => ({ ...m, [key]: prev }));
|
|
284
188
|
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
285
189
|
}
|
|
286
190
|
};
|
|
287
|
-
const handleAssign = async () => {
|
|
288
|
-
if (!selectedUser || !selectedRole)
|
|
289
|
-
return;
|
|
191
|
+
const handleAssign = async (userId, roleId) => {
|
|
290
192
|
setSubmitting(true);
|
|
291
193
|
try {
|
|
292
|
-
await assignRole({ data: { userId
|
|
293
|
-
await refreshOwnSessionIfSelf(
|
|
194
|
+
await assignRole({ data: { userId, roleId } });
|
|
195
|
+
await refreshOwnSessionIfSelf(userId, currentUserId);
|
|
294
196
|
toast.success("Rol toegekend");
|
|
295
197
|
await loadUsers();
|
|
296
198
|
}
|
|
@@ -301,9 +203,9 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
301
203
|
setSubmitting(false);
|
|
302
204
|
}
|
|
303
205
|
};
|
|
304
|
-
const handleRemove = async (userId,
|
|
206
|
+
const handleRemove = async (userId, roleId) => {
|
|
305
207
|
try {
|
|
306
|
-
await removeRole({ data: { userId,
|
|
208
|
+
await removeRole({ data: { userId, roleId } });
|
|
307
209
|
await refreshOwnSessionIfSelf(userId, currentUserId);
|
|
308
210
|
toast.success("Rol verwijderd");
|
|
309
211
|
await loadUsers();
|
|
@@ -312,31 +214,106 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
312
214
|
toast.error(getErrorMessage(error, "Fout bij het verwijderen van rol"));
|
|
313
215
|
}
|
|
314
216
|
};
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
217
|
+
const openRoleEditor = (role) => {
|
|
218
|
+
const cells = {};
|
|
219
|
+
for (const m of roleModules) {
|
|
220
|
+
cells[permCellKey(m.id, "")] = { canView: false, canEdit: false };
|
|
221
|
+
for (const f of m.features ?? []) {
|
|
222
|
+
cells[permCellKey(m.id, f.id)] = { canView: false, canEdit: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (role) {
|
|
226
|
+
for (const p of rolePermissions.filter((p) => p.roleId === role.id)) {
|
|
227
|
+
cells[permCellKey(p.moduleId, p.featureId)] = { canView: p.canView, canEdit: p.canEdit };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
setEditor({ roleId: role?.id ?? null, name: role?.name ?? "", cells });
|
|
231
|
+
};
|
|
232
|
+
const setCell = (moduleId, featureId, field, value) => {
|
|
233
|
+
setEditor((state) => {
|
|
234
|
+
if (!state)
|
|
235
|
+
return state;
|
|
236
|
+
const cells = { ...state.cells };
|
|
237
|
+
const key = permCellKey(moduleId, featureId);
|
|
238
|
+
const current = cells[key] ?? { canView: false, canEdit: false };
|
|
239
|
+
const next = { ...current, [field]: value };
|
|
240
|
+
if (field === "canEdit" && value)
|
|
241
|
+
next.canView = true;
|
|
242
|
+
if (field === "canView" && !value)
|
|
243
|
+
next.canEdit = false;
|
|
244
|
+
cells[key] = next;
|
|
245
|
+
// Vinkje op moduleniveau zet alle onderdelen mee.
|
|
246
|
+
if (featureId === "") {
|
|
247
|
+
const mod = roleModules.find((m) => m.id === moduleId);
|
|
248
|
+
for (const f of mod?.features ?? []) {
|
|
249
|
+
cells[permCellKey(moduleId, f.id)] = { ...next };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { ...state, cells };
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
const handleSaveRole = async () => {
|
|
256
|
+
if (!editor || !editor.name.trim())
|
|
257
|
+
return;
|
|
258
|
+
setEditorSaving(true);
|
|
259
|
+
try {
|
|
260
|
+
const permissions = Object.entries(editor.cells).map(([key, value]) => {
|
|
261
|
+
const [moduleId, featureId] = key.split("::");
|
|
262
|
+
return { moduleId, featureId: featureId ?? "", canView: value.canView, canEdit: value.canEdit };
|
|
263
|
+
});
|
|
264
|
+
if (editor.roleId) {
|
|
265
|
+
await updateRole({ data: { roleId: editor.roleId, name: editor.name.trim(), permissions } });
|
|
266
|
+
toast.success("Rol bijgewerkt");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
await createRole({ data: { name: editor.name.trim(), permissions } });
|
|
270
|
+
toast.success("Rol aangemaakt");
|
|
271
|
+
}
|
|
272
|
+
setEditor(null);
|
|
273
|
+
await Promise.all([loadRoles(), loadUsers()]);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
setEditorSaving(false);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const handleDuplicateRole = async (role) => {
|
|
318
283
|
try {
|
|
319
|
-
await
|
|
320
|
-
toast.success("
|
|
284
|
+
await duplicateRole({ data: { roleId: role.id } });
|
|
285
|
+
toast.success("Rol gedupliceerd");
|
|
286
|
+
await Promise.all([loadRoles(), loadUsers()]);
|
|
321
287
|
}
|
|
322
288
|
catch (error) {
|
|
323
|
-
|
|
324
|
-
|
|
289
|
+
toast.error(getErrorMessage(error, "Dupliceren mislukt"));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const handleDeleteRole = async () => {
|
|
293
|
+
if (!roleDeleteTarget)
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
await deleteRole({ data: { roleId: roleDeleteTarget.id } });
|
|
297
|
+
toast.success("Rol verwijderd");
|
|
298
|
+
setRoleDeleteTarget(null);
|
|
299
|
+
await Promise.all([loadRoles(), loadUsers()]);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
toast.error(getErrorMessage(error, "Verwijderen mislukt"));
|
|
325
303
|
}
|
|
326
304
|
};
|
|
327
305
|
const handleInvite = async () => {
|
|
328
|
-
if (!inviteEmail.trim() || !inviteName.trim())
|
|
306
|
+
if (!inviteEmail.trim() || !inviteName.trim() || !inviteRole)
|
|
329
307
|
return;
|
|
330
308
|
setInviting(true);
|
|
331
309
|
try {
|
|
332
310
|
await inviteUser({
|
|
333
|
-
data: { email: inviteEmail.trim(), displayName: inviteName.trim(),
|
|
311
|
+
data: { email: inviteEmail.trim(), displayName: inviteName.trim(), roleId: inviteRole },
|
|
334
312
|
});
|
|
335
313
|
toast.success(`Uitnodiging verstuurd naar ${inviteEmail.trim()}`);
|
|
336
314
|
setInviteOpen(false);
|
|
337
315
|
setInviteName("");
|
|
338
316
|
setInviteEmail("");
|
|
339
|
-
setInviteRole("monteur");
|
|
340
317
|
await loadUsers();
|
|
341
318
|
}
|
|
342
319
|
catch (error) {
|
|
@@ -363,18 +340,38 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
363
340
|
setDeleting(false);
|
|
364
341
|
}
|
|
365
342
|
};
|
|
343
|
+
const openRename = (target) => {
|
|
344
|
+
setRenameTarget(target);
|
|
345
|
+
setRenameValue(target.displayName === "-" ? "" : target.displayName);
|
|
346
|
+
};
|
|
347
|
+
const handleRename = async () => {
|
|
348
|
+
if (!renameTarget || !renameValue.trim())
|
|
349
|
+
return;
|
|
350
|
+
setRenaming(true);
|
|
351
|
+
try {
|
|
352
|
+
await updateUserName({ data: { userId: renameTarget.id, displayName: renameValue.trim() } });
|
|
353
|
+
toast.success("Naam bijgewerkt");
|
|
354
|
+
setRenameTarget(null);
|
|
355
|
+
await loadUsers();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
toast.error(getErrorMessage(error, "Naam wijzigen mislukt"));
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
setRenaming(false);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
366
364
|
const handleCreateTestUser = async () => {
|
|
367
|
-
if (!testName.trim())
|
|
365
|
+
if (!testName.trim() || !testRole)
|
|
368
366
|
return;
|
|
369
367
|
setTestCreating(true);
|
|
370
368
|
try {
|
|
371
369
|
const result = await createTestUser({
|
|
372
|
-
data: { displayName: testName.trim(),
|
|
370
|
+
data: { displayName: testName.trim(), roleId: testRole },
|
|
373
371
|
});
|
|
374
372
|
setTestCredentials({ email: result.email, password: result.password });
|
|
375
373
|
setTestOpen(false);
|
|
376
374
|
setTestName("");
|
|
377
|
-
setTestRole("monteur");
|
|
378
375
|
toast.success("Testaccount aangemaakt");
|
|
379
376
|
await loadUsers();
|
|
380
377
|
}
|
|
@@ -394,55 +391,73 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
394
391
|
toast.error("Kopiëren mislukt");
|
|
395
392
|
}
|
|
396
393
|
};
|
|
397
|
-
const selectedUserHasRole = users.some((user) => user.id === selectedUser && user.roles.includes(selectedRole));
|
|
398
394
|
const filteredUsers = useMemo(() => {
|
|
399
395
|
const query = searchQuery.trim().toLowerCase();
|
|
400
|
-
|
|
401
|
-
const matchesQuery = !query ||
|
|
402
|
-
user.displayName.toLowerCase().includes(query) ||
|
|
403
|
-
user.email.toLowerCase().includes(query);
|
|
396
|
+
const list = users.filter((u) => {
|
|
397
|
+
const matchesQuery = !query || u.displayName.toLowerCase().includes(query) || u.email.toLowerCase().includes(query);
|
|
404
398
|
const matchesRole = roleFilter === "all" ||
|
|
405
|
-
(roleFilter === "none" ?
|
|
399
|
+
(roleFilter === "none" ? u.roles.length === 0 : u.roles.some((r) => r.id === roleFilter));
|
|
406
400
|
return matchesQuery && matchesRole;
|
|
407
401
|
});
|
|
408
|
-
|
|
402
|
+
return [...list].sort((a, b) => {
|
|
403
|
+
const roleA = a.roles[0]?.name ?? "";
|
|
404
|
+
const roleB = b.roles[0]?.name ?? "";
|
|
409
405
|
switch (sortBy) {
|
|
410
406
|
case "name-asc":
|
|
411
407
|
return a.displayName.localeCompare(b.displayName);
|
|
412
408
|
case "name-desc":
|
|
413
409
|
return b.displayName.localeCompare(a.displayName);
|
|
414
|
-
case "role-asc":
|
|
415
|
-
const roleA = a.roles[0] ?? "";
|
|
416
|
-
const roleB = b.roles[0] ?? "";
|
|
410
|
+
case "role-asc":
|
|
417
411
|
return roleA.localeCompare(roleB);
|
|
418
|
-
|
|
419
|
-
case "role-desc": {
|
|
420
|
-
const roleA = a.roles[0] ?? "";
|
|
421
|
-
const roleB = b.roles[0] ?? "";
|
|
412
|
+
case "role-desc":
|
|
422
413
|
return roleB.localeCompare(roleA);
|
|
423
|
-
}
|
|
424
414
|
default:
|
|
425
415
|
return 0;
|
|
426
416
|
}
|
|
427
417
|
});
|
|
428
|
-
return list;
|
|
429
418
|
}, [users, searchQuery, roleFilter, sortBy]);
|
|
430
419
|
const hasActiveFilters = searchQuery.trim() !== "" || roleFilter !== "all";
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const assignableRoles = isAdmin ? baseRoles : baseRoles.filter((r) => r !== "admin");
|
|
434
|
-
const canManageUser = (target) => isAdmin || !target.roles.includes("admin");
|
|
420
|
+
const assignableRoles = availableRoles;
|
|
421
|
+
const canManageUser = (target) => isAdmin || !target.roles.some((r) => r.key === "admin");
|
|
435
422
|
if (!canAccess) {
|
|
436
423
|
return (_jsx("main", { className: "p-6", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx("p", { className: "text-muted-foreground", children: "Je hebt geen toegang tot deze pagina." }) }) }) }));
|
|
437
424
|
}
|
|
438
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
425
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "space-y-4", children: _jsxs("div", { className: "flex flex-row items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Gebruikers & rollen" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Hier vind je alle gebruikers die toegang hebben en zie je welke rol zij hebben. Ook beheer je hier alle beschikbare rollen." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [isAdmin && activeTab === "roles" && (_jsxs(Button, { size: "sm", onClick: () => openRoleEditor(), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Nieuwe rol"] })), activeTab === "users" && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setTestOpen(true), children: [_jsx(FlaskConical, { className: "h-4 w-4 mr-2" }), "Testaccount"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setInviteOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-2" }), "Account aanmaken"] })] }))] })] }) }), _jsx(CardContent, { children: _jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "users", children: "Gebruikers" }), _jsx(TabsTrigger, { value: "roles", children: "Rollen" }), _jsx(TabsTrigger, { value: "user-permissions", children: "Rechten per gebruiker" })] }), _jsxs(TabsContent, { value: "users", className: "mt-4 space-y-6", children: [_jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-end", children: [_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Zoeken" }), _jsx(Input, { placeholder: "Zoek op naam of e-mail", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Filter op rol" }), _jsxs(Select, { value: roleFilter, onValueChange: setRoleFilter, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle rollen" }), availableRoles.map((role) => (_jsx(SelectItem, { value: role.id, children: role.name }, role.id))), _jsx(SelectItem, { value: "none", children: "Geen rollen" })] })] })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Sorteren" }), _jsxs(Select, { value: sortBy, onValueChange: (value) => setSortBy(value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "name-asc", children: "Naam A-Z" }), _jsx(SelectItem, { value: "name-desc", children: "Naam Z-A" }), _jsx(SelectItem, { value: "role-asc", children: "Rol A-Z" }), _jsx(SelectItem, { value: "role-desc", children: "Rol Z-A" })] })] })] })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
|
|
426
|
+
? "Geen gebruikers gevonden met de huidige filters."
|
|
427
|
+
: "Geen gebruikers gevonden." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Gebruiker" }), _jsx(TableHead, { children: "Rollen" }), _jsx(TableHead, { className: "w-16" })] }) }), _jsx(TableBody, { children: filteredUsers.map((u) => {
|
|
428
|
+
const manageable = canManageUser(u);
|
|
429
|
+
const unassigned = assignableRoles.filter((r) => !u.roles.some((ur) => ur.id === r.id));
|
|
430
|
+
return (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium", children: u.displayName }), !u.emailConfirmed && (_jsx(Badge, { variant: "outline", className: "text-xs", children: "Wacht op verificatie" })), u.isTestAccount && (_jsx(Badge, { variant: "secondary", className: "text-xs", children: "Test" }))] }), _jsx("div", { className: "text-xs text-muted-foreground", children: u.email })] }), _jsx(TableCell, { children: _jsx("div", { className: "flex flex-wrap gap-2", children: u.roles.length === 0 ? (_jsx("span", { className: "text-sm text-muted-foreground", children: "Geen rollen" })) : (u.roles.map((role) => (_jsx(Badge, { variant: "secondary", children: role.name }, `${u.id}-${role.id}`)))) }) }), _jsx(TableCell, { className: "text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", "aria-label": `Acties voor ${u.email}`, disabled: submitting, children: _jsx(MoreVertical, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-52", children: [_jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { disabled: !manageable || unassigned.length === 0, children: "Rol toekennen" }), _jsx(DropdownMenuSubContent, { children: unassigned.length === 0 ? (_jsx(DropdownMenuItem, { disabled: true, children: "Geen rollen beschikbaar" })) : (unassigned.map((role) => (_jsx(DropdownMenuItem, { onSelect: () => handleAssign(u.id, role.id), children: role.name }, `assign-${u.id}-${role.id}`)))) })] }), _jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { disabled: u.roles.length === 0, children: "Rol verwijderen" }), _jsx(DropdownMenuSubContent, { children: u.roles.map((role) => (_jsx(DropdownMenuItem, { disabled: role.key === "admin" && !isAdmin, onSelect: () => handleRemove(u.id, role.id), children: role.name }, `remove-${u.id}-${role.id}`))) })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: !manageable, onSelect: () => openRename(u), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), "Naam wijzigen"] }), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", disabled: u.id === currentUserId || !manageable, onSelect: () => setDeleteTarget(u), children: [_jsx(UserX, { className: "h-4 w-4 mr-2" }), "Account verwijderen"] })] })] }) })] }, u.id));
|
|
431
|
+
}) })] }))] }), _jsx(TabsContent, { value: "roles", className: "mt-4", children: rolesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : roles.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen rollen aangemaakt." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Rol" }), _jsx(TableHead, { children: "Toegang" }), _jsx(TableHead, { className: "w-16" })] }) }), _jsx(TableBody, { children: roles.map((role) => {
|
|
432
|
+
const count = rolePermissions.filter((p) => p.roleId === role.id && p.featureId === "" && (p.canView || p.canEdit)).length;
|
|
433
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsxs("div", { className: "flex items-center gap-2", children: [role.name, role.isProtected && (_jsx(Badge, { variant: "outline", className: "text-xs", children: "Beschermd" }))] }) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: count === 0 ? "Geen modules" : `${count} module(s)` }), _jsx(TableCell, { className: "text-right", children: isAdmin && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", "aria-label": `Acties voor ${role.name}`, children: _jsx(MoreVertical, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => openRoleEditor(role), children: "Bewerken" }), _jsx(DropdownMenuItem, { onClick: () => handleDuplicateRole(role), children: "Dupliceren" }), _jsx(DropdownMenuItem, { disabled: role.isProtected, className: "text-destructive", onClick: () => setRoleDeleteTarget(role), children: "Verwijderen" })] })] })) })] }, role.id));
|
|
434
|
+
}) })] })) }), _jsxs(TabsContent, { value: "user-permissions", className: "mt-4 space-y-6", children: [_jsxs("div", { className: "w-full sm:w-96 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Gebruiker" }), _jsxs(Select, { value: permUserId, onValueChange: setPermUserId, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer een gebruiker" }) }), _jsx(SelectContent, { children: users.map((u) => (_jsxs(SelectItem, { value: u.id, children: [u.displayName, " (", u.email, ")"] }, u.id))) })] })] }), !permUserId ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Kies een gebruiker om persoonlijke uitzonderingen in te stellen. \"Volg rol\" gebruikt het recht dat via de rollen van deze gebruiker geldt." })) : userPermLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : userPermModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd voor deze shell." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Module / onderdeel" }), _jsx(TableHead, { className: "w-56", children: "Recht" })] }) }), _jsx(TableBody, { children: userPermModules.flatMap((mod) => {
|
|
435
|
+
const rows = [
|
|
436
|
+
_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), _jsx(TableCell, { children: _jsxs(Select, { value: userPermMap[permCellKey(mod.id, "")] ?? "inherit", onValueChange: (v) => handleUserPermChange(mod.id, "", v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(USER_PERMISSION_LABELS).map((p) => (_jsx(SelectItem, { value: p, children: USER_PERMISSION_LABELS[p] }, p))) })] }) })] }, mod.id),
|
|
437
|
+
];
|
|
438
|
+
for (const f of mod.features ?? []) {
|
|
439
|
+
rows.push(_jsxs(TableRow, { children: [_jsx(TableCell, { className: "pl-8 text-sm text-muted-foreground", children: f.name }), _jsx(TableCell, { children: _jsxs(Select, { value: userPermMap[permCellKey(mod.id, f.id)] ?? "inherit", onValueChange: (v) => handleUserPermChange(mod.id, f.id, v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(USER_PERMISSION_LABELS).map((p) => (_jsx(SelectItem, { value: p, children: USER_PERMISSION_LABELS[p] }, p))) })] }) })] }, `${mod.id}-${f.id}`));
|
|
440
|
+
}
|
|
441
|
+
return rows;
|
|
442
|
+
}) })] }))] })] }) })] }), _jsx(Dialog, { open: editor !== null, onOpenChange: (open) => !open && !editorSaving && setEditor(null), children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: editor?.roleId ? "Rol bewerken" : "Rol toevoegen" }), _jsx(DialogDescription, { children: "Maak hier een rol aan en wijs de gewenste rechten toe. Vink je een onderdeel niet aan? Dan heeft de gebruiker geen toegang tot (een deel van) die pagina." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "role-name", children: "Naam rol" }), _jsx(Input, { id: "role-name", value: editor?.name ?? "", placeholder: "Vul een naam voor de rol in.", onChange: (e) => setEditor((state) => (state ? { ...state, name: e.target.value } : state)), disabled: editorSaving })] }), _jsxs("div", { className: "max-h-[45vh] overflow-y-auto rounded-md border", children: [_jsxs("div", { className: "flex items-center justify-end gap-8 border-b px-4 py-2 text-sm font-medium", children: [_jsx("span", { className: "w-16 text-center", children: "Lezen" }), _jsx("span", { className: "w-16 text-center", children: "Bewerken" })] }), roleModules.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd voor deze shell." })) : (roleModules.map((mod) => {
|
|
443
|
+
const cell = editor?.cells[permCellKey(mod.id, "")] ?? {
|
|
444
|
+
canView: false,
|
|
445
|
+
canEdit: false,
|
|
446
|
+
};
|
|
447
|
+
const expanded = Boolean(expandedModules[mod.id]);
|
|
448
|
+
const hasFeatures = (mod.features ?? []).length > 0;
|
|
449
|
+
return (_jsxs("div", { className: "border-b last:border-b-0", children: [_jsxs("div", { className: "flex items-center px-4 py-3", children: [_jsxs("button", { type: "button", className: "flex flex-1 items-center gap-2 text-left font-medium", onClick: () => setExpandedModules((s) => ({ ...s, [mod.id]: !s[mod.id] })), disabled: !hasFeatures, children: [hasFeatures ? (expanded ? (_jsx(ChevronDown, { className: "h-4 w-4" })) : (_jsx(ChevronRight, { className: "h-4 w-4" }))) : (_jsx("span", { className: "h-4 w-4" })), mod.name] }), _jsxs("div", { className: "flex items-center gap-8", children: [_jsx("span", { className: "flex w-16 justify-center", children: _jsx(Checkbox, { checked: cell.canView, onCheckedChange: (v) => setCell(mod.id, "", "canView", Boolean(v)), "aria-label": `Lezen ${mod.name}` }) }), _jsx("span", { className: "flex w-16 justify-center", children: _jsx(Checkbox, { checked: cell.canEdit, onCheckedChange: (v) => setCell(mod.id, "", "canEdit", Boolean(v)), "aria-label": `Bewerken ${mod.name}` }) })] })] }), expanded &&
|
|
450
|
+
(mod.features ?? []).map((f) => {
|
|
451
|
+
const fcell = editor?.cells[permCellKey(mod.id, f.id)] ?? {
|
|
452
|
+
canView: false,
|
|
453
|
+
canEdit: false,
|
|
454
|
+
};
|
|
455
|
+
return (_jsxs("div", { className: "flex items-center bg-muted/40 px-4 py-2 pl-10 text-sm", children: [_jsx("span", { className: "flex-1", children: f.name }), _jsxs("div", { className: "flex items-center gap-8", children: [_jsx("span", { className: "flex w-16 justify-center", children: _jsx(Checkbox, { checked: fcell.canView, onCheckedChange: (v) => setCell(mod.id, f.id, "canView", Boolean(v)), "aria-label": `Lezen ${mod.name} ${f.name}` }) }), _jsx("span", { className: "flex w-16 justify-center", children: _jsx(Checkbox, { checked: fcell.canEdit, onCheckedChange: (v) => setCell(mod.id, f.id, "canEdit", Boolean(v)), "aria-label": `Bewerken ${mod.name} ${f.name}` }) })] })] }, f.id));
|
|
456
|
+
})] }, mod.id));
|
|
457
|
+
}))] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setEditor(null), disabled: editorSaving, children: "Annuleren" }), _jsx(Button, { onClick: handleSaveRole, disabled: editorSaving || !editor?.name.trim(), children: editorSaving ? "Opslaan..." : "Opslaan" })] })] }) }), _jsx(Dialog, { open: inviteOpen, onOpenChange: setInviteOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Nieuw account aanmaken" }), _jsx(DialogDescription, { children: "De gebruiker ontvangt een uitnodigingsmail en kiest zelf een wachtwoord." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "invite-name", children: "Naam" }), _jsx(Input, { id: "invite-name", value: inviteName, onChange: (e) => setInviteName(e.target.value), placeholder: "Jan Jansen", disabled: inviting })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "invite-email", children: "E-mail" }), _jsx(Input, { id: "invite-email", type: "email", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), placeholder: "jan@voorbeeld.nl", disabled: inviting })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rol" }), _jsxs(Select, { value: inviteRole, onValueChange: setInviteRole, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies rol" }) }), _jsx(SelectContent, { children: assignableRoles.map((r) => (_jsx(SelectItem, { value: r.id, children: r.name }, r.id))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInviteOpen(false), disabled: inviting, children: "Annuleren" }), _jsx(Button, { onClick: handleInvite, disabled: inviting || !inviteEmail.trim() || !inviteName.trim() || !inviteRole, children: inviting ? "Versturen..." : "Uitnodiging versturen" })] })] }) }), _jsx(Dialog, { open: testOpen, onOpenChange: (open) => !testCreating && setTestOpen(open), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Testaccount aanmaken" }), _jsx(DialogDescription, { children: "Maakt direct een geverifieerd account met een automatisch wachtwoord \u2014 geen e-mail nodig." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "test-name", children: "Naam" }), _jsx(Input, { id: "test-name", value: testName, onChange: (e) => setTestName(e.target.value), placeholder: "Test Monteur", disabled: testCreating })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rol" }), _jsxs(Select, { value: testRole, onValueChange: setTestRole, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies rol" }) }), _jsx(SelectContent, { children: assignableRoles.map((r) => (_jsx(SelectItem, { value: r.id, children: r.name }, r.id))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setTestOpen(false), disabled: testCreating, children: "Annuleren" }), _jsx(Button, { onClick: handleCreateTestUser, disabled: testCreating || !testName.trim() || !testRole, children: testCreating ? "Aanmaken..." : "Aanmaken" })] })] }) }), _jsx(Dialog, { open: testCredentials !== null, onOpenChange: (open) => !open && setTestCredentials(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Testaccount aangemaakt" }), _jsx(DialogDescription, { children: "Kopieer de inloggegevens nu \u2014 het wachtwoord wordt maar \u00E9\u00E9n keer getoond." })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "E-mail" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { readOnly: true, value: testCredentials?.email ?? "" }), _jsx(Button, { variant: "outline", size: "icon", onClick: () => testCredentials && copyToClipboard(testCredentials.email, "E-mail"), "aria-label": "Kopieer e-mail", children: _jsx(Copy, { className: "h-4 w-4" }) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Wachtwoord" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { readOnly: true, value: testCredentials?.password ?? "", className: "font-mono" }), _jsx(Button, { variant: "outline", size: "icon", onClick: () => testCredentials && copyToClipboard(testCredentials.password, "Wachtwoord"), "aria-label": "Kopieer wachtwoord", children: _jsx(Copy, { className: "h-4 w-4" }) })] })] })] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => setTestCredentials(null), children: "Sluiten" }) })] }) }), _jsx(AlertDialog, { open: roleDeleteTarget !== null, onOpenChange: (open) => !open && setRoleDeleteTarget(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Rol verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je de rol ", _jsx("strong", { children: roleDeleteTarget?.name }), " wilt verwijderen? Gebruikers met deze rol verliezen de bijbehorende rechten."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: (e) => {
|
|
458
|
+
e.preventDefault();
|
|
459
|
+
handleDeleteRole();
|
|
460
|
+
}, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Verwijderen" })] })] }) }), _jsx(Dialog, { open: renameTarget !== null, onOpenChange: (open) => !open && setRenameTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Naam wijzigen" }), _jsxs(DialogDescription, { children: ["Pas de weergavenaam van ", renameTarget?.email, " aan."] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rename-input", children: "Naam" }), _jsx(Input, { id: "rename-input", value: renameValue, onChange: (e) => setRenameValue(e.target.value), placeholder: "Naam van de gebruiker" })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRenameTarget(null), disabled: renaming, children: "Annuleren" }), _jsx(Button, { onClick: handleRename, disabled: renaming || !renameValue.trim(), children: renaming ? "Opslaan..." : "Opslaan" })] })] }) }), _jsx(AlertDialog, { open: deleteTarget !== null, onOpenChange: (open) => !open && !deleting && setDeleteTarget(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Account verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: deleteTarget?.displayName || deleteTarget?.email }), " ", "wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: deleting, children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: (e) => {
|
|
446
461
|
e.preventDefault();
|
|
447
462
|
handleDelete();
|
|
448
463
|
}, disabled: deleting, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: deleting ? "Verwijderen..." : "Verwijderen" })] })] }) })] }));
|