@flowselections/admin-installatietechniek 1.0.7 → 1.0.9
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.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +117 -215
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +19312 -10201
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/admin.functions.d.ts +168505 -127289
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +132 -445
- package/dist-lib/lib/admin.server.d.ts +20 -3
- package/dist-lib/lib/admin.server.d.ts.map +1 -1
- package/dist-lib/lib/admin.server.js +101 -3
- package/dist-lib/lib/serverErrors.d.ts +7 -0
- package/dist-lib/lib/serverErrors.d.ts.map +1 -0
- package/dist-lib/lib/serverErrors.js +29 -0
- package/package.json +4 -4
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
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, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
4
|
-
import {
|
|
5
|
-
import { assignRole, createRole,
|
|
4
|
+
import { ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, AlertTriangle, } from "lucide-react";
|
|
5
|
+
import { assignRole, createRole, createTestUser, deleteRole, deleteUser, duplicateRole, getRoles, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setModuleEnforce, setUserModulePermission, syncModuleRegistry, updateRole, updateUserName, updateUserContact, } from "../lib/admin.functions";
|
|
6
6
|
import { useDiscoveredModules } from "../lib/moduleDiscovery";
|
|
7
7
|
import { supabase } from "../integrations/supabase/client";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const ALL_PROFILES_VALUE = "__all__";
|
|
8
|
+
const CONTACT_ROLE_KEYS = ["monteur", "planner", "administratie"];
|
|
9
|
+
const PHONE_RE = /^[0-9+()\-\s]{8,25}$/;
|
|
11
10
|
const USER_PERMISSION_LABELS = {
|
|
12
11
|
inherit: "Volg rol",
|
|
13
12
|
none: "Geen toegang",
|
|
@@ -40,10 +39,12 @@ async function refreshOwnSessionIfSelf(targetUserId, currentUserId) {
|
|
|
40
39
|
}
|
|
41
40
|
export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
42
41
|
const auth = useAuth();
|
|
43
|
-
const {
|
|
42
|
+
const { user } = auth;
|
|
44
43
|
const authRoles = Array.isArray(auth.roles)
|
|
45
44
|
? auth.roles
|
|
46
45
|
: [];
|
|
46
|
+
// Superadmin mag alles wat admin mag.
|
|
47
|
+
const isAdmin = Boolean(auth.isAdmin) || authRoles.includes("superadmin");
|
|
47
48
|
const isAdministratie = authRoles.includes("administratie");
|
|
48
49
|
const canAccess = isAdmin || isAdministratie;
|
|
49
50
|
const currentUserId = user?.id;
|
|
@@ -56,9 +57,12 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
56
57
|
const usersRequestRef = useRef(0);
|
|
57
58
|
const rolesRequestRef = useRef(0);
|
|
58
59
|
const [submitting, setSubmitting] = useState(false);
|
|
59
|
-
const [
|
|
60
|
+
const [editTarget, setEditTarget] = useState(null);
|
|
61
|
+
const [detailTarget, setDetailTarget] = useState(null);
|
|
62
|
+
const [editSaving, setEditSaving] = useState(false);
|
|
60
63
|
const [renameValue, setRenameValue] = useState("");
|
|
61
|
-
const [
|
|
64
|
+
const [contactAddress, setContactAddress] = useState("");
|
|
65
|
+
const [contactPhone, setContactPhone] = useState("");
|
|
62
66
|
const [roles, setRoles] = useState([]);
|
|
63
67
|
const [roleModules, setRoleModules] = useState([]);
|
|
64
68
|
const [rolePermissions, setRolePermissions] = useState([]);
|
|
@@ -71,6 +75,8 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
71
75
|
const [inviteName, setInviteName] = useState("");
|
|
72
76
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
73
77
|
const [inviteRole, setInviteRole] = useState("");
|
|
78
|
+
const [inviteAddress, setInviteAddress] = useState("");
|
|
79
|
+
const [invitePhone, setInvitePhone] = useState("");
|
|
74
80
|
const [inviting, setInviting] = useState(false);
|
|
75
81
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
76
82
|
const [deleting, setDeleting] = useState(false);
|
|
@@ -79,24 +85,10 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
79
85
|
const [testNameTouched, setTestNameTouched] = useState(false);
|
|
80
86
|
const [testRole, setTestRole] = useState("");
|
|
81
87
|
const [testCreating, setTestCreating] = useState(false);
|
|
82
|
-
const [profileLocked, setProfileLocked] = useState(false);
|
|
83
|
-
const [lockedProfile, setLockedProfile] = useState(null);
|
|
84
88
|
const [testCredentials, setTestCredentials] = useState(null);
|
|
85
89
|
const [searchQuery, setSearchQuery] = useState("");
|
|
86
90
|
const [roleFilter, setRoleFilter] = useState("all");
|
|
87
91
|
const [sortBy, setSortBy] = useState("name-asc");
|
|
88
|
-
const [orgProfiles, setOrgProfiles] = useState([]);
|
|
89
|
-
const [tenants, setTenants] = useState([]);
|
|
90
|
-
const [profilesLoading, setProfilesLoading] = useState(false);
|
|
91
|
-
const [activeProfileId, setActiveProfileId] = useState(() => {
|
|
92
|
-
if (typeof window === "undefined")
|
|
93
|
-
return "";
|
|
94
|
-
return window.localStorage.getItem(ACTIVE_PROFILE_STORAGE_KEY) ?? "";
|
|
95
|
-
});
|
|
96
|
-
const [profileEditor, setProfileEditor] = useState(null);
|
|
97
|
-
const [allProfileRoles, setAllProfileRoles] = useState([]);
|
|
98
|
-
const [profileSaving, setProfileSaving] = useState(false);
|
|
99
|
-
const [profileDeleteTarget, setProfileDeleteTarget] = useState(null);
|
|
100
92
|
const [permUserId, setPermUserId] = useState("");
|
|
101
93
|
const [userPermModules, setUserPermModules] = useState([]);
|
|
102
94
|
const [userPermMap, setUserPermMap] = useState({});
|
|
@@ -110,21 +102,15 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
110
102
|
const shellModulesKey = shellModules
|
|
111
103
|
.map((m) => `${m.id}:${m.name}:${(m.features ?? []).map((f) => `${f.id}=${f.name}`).join(",")}`)
|
|
112
104
|
.join("|");
|
|
113
|
-
const loadUsers = async (
|
|
105
|
+
const loadUsers = async () => {
|
|
114
106
|
const requestId = ++usersRequestRef.current;
|
|
115
107
|
setLoading(true);
|
|
116
108
|
setUsersError(null);
|
|
117
109
|
try {
|
|
118
|
-
const result = await getUsersWithRoles(
|
|
119
|
-
data: { profileId: profileId || undefined },
|
|
120
|
-
});
|
|
110
|
+
const result = await getUsersWithRoles();
|
|
121
111
|
if (requestId !== usersRequestRef.current)
|
|
122
112
|
return;
|
|
123
113
|
setUsers(Array.isArray(result?.users) ? result.users : []);
|
|
124
|
-
setProfileLocked(Boolean(result?.profileLocked));
|
|
125
|
-
setLockedProfile(result?.profileLocked
|
|
126
|
-
? result?.activeProfile ?? null
|
|
127
|
-
: null);
|
|
128
114
|
const roleList = Array.isArray(result?.availableRoles) ? result.availableRoles : [];
|
|
129
115
|
setAvailableRoles(roleList);
|
|
130
116
|
const fallback = roleList[0]?.id ?? "";
|
|
@@ -143,14 +129,12 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
143
129
|
setLoading(false);
|
|
144
130
|
}
|
|
145
131
|
};
|
|
146
|
-
const loadRoles = async (
|
|
132
|
+
const loadRoles = async () => {
|
|
147
133
|
const requestId = ++rolesRequestRef.current;
|
|
148
134
|
setRolesLoading(true);
|
|
149
135
|
setRolesError(null);
|
|
150
136
|
try {
|
|
151
|
-
const result = await getRoles(
|
|
152
|
-
data: { profileId: profileId || undefined },
|
|
153
|
-
});
|
|
137
|
+
const result = await getRoles();
|
|
154
138
|
if (requestId !== rolesRequestRef.current)
|
|
155
139
|
return;
|
|
156
140
|
setRoles(Array.isArray(result?.roles) ? result.roles : []);
|
|
@@ -169,25 +153,6 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
169
153
|
setRolesLoading(false);
|
|
170
154
|
}
|
|
171
155
|
};
|
|
172
|
-
const loadProfiles = async () => {
|
|
173
|
-
if (!isAdmin)
|
|
174
|
-
return;
|
|
175
|
-
setProfilesLoading(true);
|
|
176
|
-
try {
|
|
177
|
-
const result = await getProfiles();
|
|
178
|
-
const list = Array.isArray(result?.profiles) ? result.profiles : [];
|
|
179
|
-
setOrgProfiles(list);
|
|
180
|
-
setTenants(Array.isArray(result?.tenants) ? result.tenants : []);
|
|
181
|
-
// Nooit automatisch een profiel kiezen: leeg = overzicht (alle bedrijven).
|
|
182
|
-
setActiveProfileId((current) => current && list.some((p) => p.id === current) ? current : "");
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
toast.error(getErrorMessage(error, "Fout bij het laden van bedrijfsprofielen"));
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
setProfilesLoading(false);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
156
|
const loadUserPermissions = async (userId) => {
|
|
192
157
|
if (!userId) {
|
|
193
158
|
setUserPermModules([]);
|
|
@@ -218,7 +183,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
218
183
|
setUserPermLoading(false);
|
|
219
184
|
}
|
|
220
185
|
};
|
|
221
|
-
// Registratie van shell-modules
|
|
186
|
+
// Registratie van shell-modules.
|
|
222
187
|
useEffect(() => {
|
|
223
188
|
if (!canAccess)
|
|
224
189
|
return;
|
|
@@ -232,41 +197,19 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
232
197
|
console.error("Module-registratie mislukt", error);
|
|
233
198
|
}
|
|
234
199
|
}
|
|
235
|
-
if (cancelled)
|
|
236
|
-
return;
|
|
237
|
-
loadProfiles();
|
|
238
200
|
})();
|
|
239
201
|
return () => {
|
|
240
202
|
cancelled = true;
|
|
241
203
|
};
|
|
242
204
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
243
205
|
}, [canAccess, isAdmin, shellModulesKey, effectiveShellId]);
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
if (typeof window === "undefined")
|
|
246
|
-
return;
|
|
247
|
-
if (activeProfileId)
|
|
248
|
-
window.localStorage.setItem(ACTIVE_PROFILE_STORAGE_KEY, activeProfileId);
|
|
249
|
-
else
|
|
250
|
-
window.localStorage.removeItem(ACTIVE_PROFILE_STORAGE_KEY);
|
|
251
|
-
}, [activeProfileId]);
|
|
252
|
-
// Eén gecontroleerde laadactie per profielcontext (ook bij eerste render):
|
|
253
|
-
// profielgebonden state resetten en opnieuw laden binnen de nieuwe context.
|
|
254
206
|
useEffect(() => {
|
|
255
207
|
if (!canAccess)
|
|
256
208
|
return;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
setAvailableRoles([]);
|
|
260
|
-
setRolePermissions([]);
|
|
261
|
-
setSearchQuery("");
|
|
262
|
-
setRoleFilter("all");
|
|
263
|
-
setPermUserId("");
|
|
264
|
-
setUserPermModules([]);
|
|
265
|
-
setUserPermMap({});
|
|
266
|
-
loadUsers(activeProfileId);
|
|
267
|
-
loadRoles(activeProfileId);
|
|
209
|
+
loadUsers();
|
|
210
|
+
loadRoles();
|
|
268
211
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
269
|
-
}, [
|
|
212
|
+
}, [canAccess]);
|
|
270
213
|
useEffect(() => {
|
|
271
214
|
if (!canAccess)
|
|
272
215
|
return;
|
|
@@ -305,7 +248,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
305
248
|
setSubmitting(true);
|
|
306
249
|
try {
|
|
307
250
|
await assignRole({
|
|
308
|
-
data: { userId, roleId
|
|
251
|
+
data: { userId, roleId },
|
|
309
252
|
});
|
|
310
253
|
await refreshOwnSessionIfSelf(userId, currentUserId);
|
|
311
254
|
toast.success("Rol toegekend");
|
|
@@ -367,6 +310,28 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
367
310
|
return { ...state, cells };
|
|
368
311
|
});
|
|
369
312
|
};
|
|
313
|
+
// Zet één kolom (Lezen of Bewerken) in één keer aan/uit voor alle modules
|
|
314
|
+
// en onderdelen. Handig bij lange modulelijsten.
|
|
315
|
+
const setAllCells = (field, value) => {
|
|
316
|
+
setEditor((state) => {
|
|
317
|
+
if (!state)
|
|
318
|
+
return state;
|
|
319
|
+
const cells = {};
|
|
320
|
+
for (const [key, current] of Object.entries(state.cells)) {
|
|
321
|
+
// Alleen de aangeklikte kolom aanpassen: geen cascade tussen
|
|
322
|
+
// Lezen en Bewerken, zodat "Niets" bij Lezen de Bewerken-vinkjes
|
|
323
|
+
// ongemoeid laat (en andersom).
|
|
324
|
+
cells[key] = { ...current, [field]: value };
|
|
325
|
+
}
|
|
326
|
+
return { ...state, cells };
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
const allCellsOn = (field) => {
|
|
330
|
+
if (!editor)
|
|
331
|
+
return false;
|
|
332
|
+
const values = Object.values(editor.cells);
|
|
333
|
+
return values.length > 0 && values.every((cell) => cell[field]);
|
|
334
|
+
};
|
|
370
335
|
const handleSaveRole = async () => {
|
|
371
336
|
if (!editor || !editor.name.trim())
|
|
372
337
|
return;
|
|
@@ -417,9 +382,20 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
417
382
|
toast.error(getErrorMessage(error, "Verwijderen mislukt"));
|
|
418
383
|
}
|
|
419
384
|
};
|
|
385
|
+
const inviteRoleKey = roles.find((r) => r.id === inviteRole)?.key ?? "";
|
|
386
|
+
const inviteContactRequired = CONTACT_ROLE_KEYS.includes(inviteRoleKey);
|
|
387
|
+
const inviteContactOk = !inviteContactRequired || inviteAddress.trim().length >= 5;
|
|
420
388
|
const handleInvite = async () => {
|
|
421
389
|
if (!inviteEmail.trim() || !inviteName.trim() || !inviteRole)
|
|
422
390
|
return;
|
|
391
|
+
if (!inviteContactOk) {
|
|
392
|
+
toast.error("Vul een adres in (minimaal 5 tekens).");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (invitePhone.trim() && !PHONE_RE.test(invitePhone.trim())) {
|
|
396
|
+
toast.error("Ongeldig telefoonnummer.");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
423
399
|
setInviting(true);
|
|
424
400
|
try {
|
|
425
401
|
await inviteUser({
|
|
@@ -427,13 +403,16 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
427
403
|
email: inviteEmail.trim(),
|
|
428
404
|
displayName: inviteName.trim(),
|
|
429
405
|
roleId: inviteRole,
|
|
430
|
-
|
|
406
|
+
address: inviteAddress.trim(),
|
|
407
|
+
phone: invitePhone.trim(),
|
|
431
408
|
},
|
|
432
409
|
});
|
|
433
410
|
toast.success(`Uitnodiging verstuurd naar ${inviteEmail.trim()}`);
|
|
434
411
|
setInviteOpen(false);
|
|
435
412
|
setInviteName("");
|
|
436
413
|
setInviteEmail("");
|
|
414
|
+
setInviteAddress("");
|
|
415
|
+
setInvitePhone("");
|
|
437
416
|
await loadUsers();
|
|
438
417
|
}
|
|
439
418
|
catch (error) {
|
|
@@ -443,6 +422,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
443
422
|
setInviting(false);
|
|
444
423
|
}
|
|
445
424
|
};
|
|
425
|
+
const needsContact = (u) => u.roles.some((r) => CONTACT_ROLE_KEYS.includes(r.key));
|
|
446
426
|
const handleDelete = async () => {
|
|
447
427
|
if (!deleteTarget)
|
|
448
428
|
return;
|
|
@@ -460,25 +440,47 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
460
440
|
setDeleting(false);
|
|
461
441
|
}
|
|
462
442
|
};
|
|
463
|
-
const
|
|
464
|
-
|
|
443
|
+
const openEdit = (target) => {
|
|
444
|
+
setEditTarget(target);
|
|
465
445
|
setRenameValue(target.displayName === "-" ? "" : target.displayName);
|
|
446
|
+
setContactAddress(target.address ?? "");
|
|
447
|
+
setContactPhone(target.phone ?? "");
|
|
466
448
|
};
|
|
467
|
-
const
|
|
468
|
-
if (!
|
|
449
|
+
const handleSaveEdit = async () => {
|
|
450
|
+
if (!editTarget)
|
|
451
|
+
return;
|
|
452
|
+
const name = renameValue.trim();
|
|
453
|
+
if (!name) {
|
|
454
|
+
toast.error("Vul een naam in");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (needsContact(editTarget) && contactAddress.trim().length < 5) {
|
|
458
|
+
toast.error("Vul een adres in (minimaal 5 tekens).");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (contactPhone.trim() && !PHONE_RE.test(contactPhone.trim())) {
|
|
462
|
+
toast.error("Ongeldig telefoonnummer.");
|
|
469
463
|
return;
|
|
470
|
-
|
|
464
|
+
}
|
|
465
|
+
setEditSaving(true);
|
|
471
466
|
try {
|
|
472
|
-
await updateUserName({ data: { userId:
|
|
473
|
-
|
|
474
|
-
|
|
467
|
+
await updateUserName({ data: { userId: editTarget.id, displayName: name } });
|
|
468
|
+
await updateUserContact({
|
|
469
|
+
data: {
|
|
470
|
+
userId: editTarget.id,
|
|
471
|
+
address: contactAddress.trim(),
|
|
472
|
+
phone: contactPhone.trim(),
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
toast.success("Gegevens bijgewerkt");
|
|
476
|
+
setEditTarget(null);
|
|
475
477
|
await loadUsers();
|
|
476
478
|
}
|
|
477
479
|
catch (error) {
|
|
478
|
-
toast.error(getErrorMessage(error, "
|
|
480
|
+
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
479
481
|
}
|
|
480
482
|
finally {
|
|
481
|
-
|
|
483
|
+
setEditSaving(false);
|
|
482
484
|
}
|
|
483
485
|
};
|
|
484
486
|
const handleCreateTestUser = async () => {
|
|
@@ -490,7 +492,6 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
490
492
|
data: {
|
|
491
493
|
displayName: testName.trim(),
|
|
492
494
|
roleId: testRole,
|
|
493
|
-
...(activeProfileId ? { profileId: activeProfileId } : {}),
|
|
494
495
|
},
|
|
495
496
|
});
|
|
496
497
|
setTestCredentials({ email: result.email, password: result.password });
|
|
@@ -515,99 +516,6 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
515
516
|
toast.error("Kopiëren mislukt");
|
|
516
517
|
}
|
|
517
518
|
};
|
|
518
|
-
const openProfileEditor = (profile) => {
|
|
519
|
-
// Profielen mogen rollen buiten de huidige scope bevatten: haal de volledige lijst op.
|
|
520
|
-
getRoles({ data: {} })
|
|
521
|
-
.then((result) => setAllProfileRoles(Array.isArray(result?.roles) ? result.roles : []))
|
|
522
|
-
.catch(() => setAllProfileRoles([]));
|
|
523
|
-
setProfileEditor({
|
|
524
|
-
profileId: profile?.id ?? null,
|
|
525
|
-
name: profile?.name ?? "",
|
|
526
|
-
tenantId: profile?.tenantId ?? "",
|
|
527
|
-
tenantName: profile?.tenantName ?? profile?.name ?? "",
|
|
528
|
-
tenantNameTouched: Boolean(profile),
|
|
529
|
-
roleIds: profile?.roleIds ?? [],
|
|
530
|
-
moduleIds: profile?.moduleIds ?? [],
|
|
531
|
-
});
|
|
532
|
-
};
|
|
533
|
-
const toggleProfileValue = (field, value) => {
|
|
534
|
-
setProfileEditor((state) => {
|
|
535
|
-
if (!state)
|
|
536
|
-
return state;
|
|
537
|
-
const list = state[field];
|
|
538
|
-
return {
|
|
539
|
-
...state,
|
|
540
|
-
[field]: list.includes(value) ? list.filter((v) => v !== value) : [...list, value],
|
|
541
|
-
};
|
|
542
|
-
});
|
|
543
|
-
};
|
|
544
|
-
const handleSaveProfile = async () => {
|
|
545
|
-
if (!profileEditor || !profileEditor.name.trim())
|
|
546
|
-
return;
|
|
547
|
-
setProfileSaving(true);
|
|
548
|
-
try {
|
|
549
|
-
const payload = {
|
|
550
|
-
name: profileEditor.name.trim(),
|
|
551
|
-
tenantId: profileEditor.tenantId ? profileEditor.tenantId : null,
|
|
552
|
-
// Leeg gelaten? Dan gebruiken we de profielnaam als bedrijfsnaam.
|
|
553
|
-
tenantName: (profileEditor.tenantName.trim() || profileEditor.name.trim()),
|
|
554
|
-
roleIds: profileEditor.roleIds,
|
|
555
|
-
moduleIds: profileEditor.moduleIds,
|
|
556
|
-
};
|
|
557
|
-
if (profileEditor.profileId) {
|
|
558
|
-
await updateProfile({ data: { profileId: profileEditor.profileId, ...payload } });
|
|
559
|
-
toast.success("Bedrijfsprofiel bijgewerkt");
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
await createProfile({ data: payload });
|
|
563
|
-
toast.success("Bedrijfsprofiel aangemaakt");
|
|
564
|
-
}
|
|
565
|
-
setProfileEditor(null);
|
|
566
|
-
await loadProfiles();
|
|
567
|
-
await Promise.all([loadUsers(), loadRoles()]);
|
|
568
|
-
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
571
|
-
}
|
|
572
|
-
finally {
|
|
573
|
-
setProfileSaving(false);
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
const handleDuplicateProfile = async (profile) => {
|
|
577
|
-
try {
|
|
578
|
-
await duplicateProfile({ data: { profileId: profile.id } });
|
|
579
|
-
toast.success("Bedrijfsprofiel gedupliceerd");
|
|
580
|
-
await loadProfiles();
|
|
581
|
-
}
|
|
582
|
-
catch (error) {
|
|
583
|
-
toast.error(getErrorMessage(error, "Dupliceren mislukt"));
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
const handleDeleteProfile = async () => {
|
|
587
|
-
if (!profileDeleteTarget)
|
|
588
|
-
return;
|
|
589
|
-
try {
|
|
590
|
-
await deleteProfile({ data: { profileId: profileDeleteTarget.id } });
|
|
591
|
-
toast.success("Bedrijfsprofiel verwijderd");
|
|
592
|
-
setProfileDeleteTarget(null);
|
|
593
|
-
await loadProfiles();
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
toast.error(getErrorMessage(error, "Verwijderen mislukt"));
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
const handleSetActiveProfile = async (profile) => {
|
|
600
|
-
try {
|
|
601
|
-
await setActiveProfile({ data: { profileId: profile.id } });
|
|
602
|
-
setActiveProfileId(profile.id);
|
|
603
|
-
toast.success(`${profile.name} is nu het standaardprofiel`);
|
|
604
|
-
await loadProfiles();
|
|
605
|
-
}
|
|
606
|
-
catch (error) {
|
|
607
|
-
toast.error(getErrorMessage(error, "Instellen mislukt"));
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
const allRolesForProfile = allProfileRoles.length > 0 ? allProfileRoles : roles;
|
|
611
519
|
const filteredUsers = useMemo(() => {
|
|
612
520
|
const query = searchQuery.trim().toLowerCase();
|
|
613
521
|
const list = users.filter((u) => {
|
|
@@ -644,20 +552,23 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
644
552
|
setTestName(suggestedTestName);
|
|
645
553
|
setTestOpen(true);
|
|
646
554
|
};
|
|
647
|
-
const canManageUser = (target) => isAdmin || !target.roles.some((r) => r.key === "admin");
|
|
555
|
+
const canManageUser = (target) => isAdmin || !target.roles.some((r) => r.key === "admin" || r.key === "superadmin");
|
|
648
556
|
if (!canAccess) {
|
|
649
557
|
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." }) }) }) }));
|
|
650
558
|
}
|
|
651
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [
|
|
559
|
+
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: openTestDialog, 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" }), _jsx(TabsTrigger, { value: "modules", children: "Modules" })] }), _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..." })) : usersError ? (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4", children: [_jsx("p", { className: "text-sm text-destructive", children: usersError }), _jsx(Button, { variant: "outline", size: "sm", className: "mt-3", onClick: () => loadUsers(), children: "Opnieuw proberen" })] })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
|
|
652
560
|
? "Geen gebruikers gevonden met de huidige filters."
|
|
653
561
|
: "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) => {
|
|
654
562
|
const manageable = canManageUser(u);
|
|
655
563
|
const unassigned = assignableRoles.filter((r) => !u.roles.some((ur) => ur.id === r.id));
|
|
656
|
-
return (_jsxs(TableRow, {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
564
|
+
return (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setDetailTarget(u), children: [_jsxs(TableCell, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", className: "font-medium text-left hover:underline", onClick: (e) => {
|
|
565
|
+
e.stopPropagation();
|
|
566
|
+
setDetailTarget(u);
|
|
567
|
+
}, 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", onClick: (e) => e.stopPropagation(), 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", onClick: (e) => e.stopPropagation(), children: [_jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { disabled: !manageable || unassigned.length === 0, children: "Rol toekennen" }), _jsx(DropdownMenuSubContent, { onClick: (e) => e.stopPropagation(), 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, { onClick: (e) => e.stopPropagation(), children: u.roles.map((role) => (_jsx(DropdownMenuItem, { disabled: (role.key === "admin" || role.key === "superadmin") && !isAdmin, onSelect: () => handleRemove(u.id, role.id), children: role.name }, `remove-${u.id}-${role.id}`))) })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: !manageable, onSelect: () => openEdit(u), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), "Gegevens 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));
|
|
568
|
+
}) })] }))] }), _jsx(TabsContent, { value: "roles", className: "mt-4", children: rolesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : rolesError ? (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4", children: [_jsx("p", { className: "text-sm text-destructive", children: rolesError }), _jsx(Button, { variant: "outline", size: "sm", className: "mt-3", onClick: () => loadRoles(), children: "Opnieuw proberen" })] })) : roles.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen rollen aangemaakt." })) : (_jsxs(_Fragment, { children: [roles.some((r) => r.unmapped) && (_jsxs("div", { className: "mb-4 flex gap-3 rounded-md border border-destructive/40 bg-destructive/5 p-4", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0 text-destructive" }), _jsxs("div", { className: "text-sm", children: [_jsx("p", { className: "font-medium text-destructive", children: "Een of meer rollen werken niet" }), _jsxs("p", { className: "text-muted-foreground", children: ["De sleutel van deze rollen is onbekend in het systeem, waardoor een toekenning stil wordt overgeslagen: ", " ", roles.filter((r) => r.unmapped).map((r) => r.name).join(", "), ". Gebruikers met zo'n rol krijgen er geen enkele toegang door."] })] })] })), _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) => {
|
|
569
|
+
const count = rolePermissions.filter((p) => p.roleId === role.id && p.featureId === "" && (p.canView || p.canEdit)).length;
|
|
570
|
+
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" })), role.unmapped && (_jsxs(Badge, { variant: "destructive", className: "gap-1 text-xs", title: "De sleutel van deze rol is onbekend in het systeem; toekenningen worden stil overgeslagen.", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), "Werkt niet"] }))] }) }), _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));
|
|
571
|
+
}) })] })] })) }), _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) => {
|
|
661
572
|
const rows = [
|
|
662
573
|
_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),
|
|
663
574
|
];
|
|
@@ -665,27 +576,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
665
576
|
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}`));
|
|
666
577
|
}
|
|
667
578
|
return rows;
|
|
668
|
-
}) })] }))] }), _jsxs(TabsContent, { value: "modules", className: "mt-4 space-y-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Schakel \"Toegang afdwingen\" in voor een module om de rollen en gebruikersrechten actief te laten controleren. Modules zonder deze schakelaar blijven voor iedereen toegankelijk (default-allow)." }), roleModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Module" }), _jsx(TableHead, { className: "w-48", children: "Toegang afdwingen" })] }) }), _jsx(TableBody, { children: roleModules.map((mod) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), _jsxs(TableCell, { children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: `enforce-${mod.id}`, checked: mod.enforce ?? false, onCheckedChange: (checked) => handleEnforceChange(mod.id, checked), disabled: submitting || !isAdmin }), _jsx(Label, { htmlFor: `enforce-${mod.id}`, className: "text-sm font-normal", children: (mod.enforce ?? false) ? "Ingeschakeld" : "Uitgeschakeld" })] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Beheerders houden altijd toegang, ook als afdwingen aanstaat." })] })] }, mod.id))) })] }))] })
|
|
669
|
-
? {
|
|
670
|
-
...state,
|
|
671
|
-
name: e.target.value,
|
|
672
|
-
// Zolang de bedrijfsnaam niet handmatig is aangepast, volgt hij de profielnaam.
|
|
673
|
-
tenantName: state.tenantNameTouched ? state.tenantName : e.target.value,
|
|
674
|
-
}
|
|
675
|
-
: state), disabled: profileSaving })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bedrijf" }), _jsxs(Select, { value: profileEditor?.tenantId ? profileEditor.tenantId : "none", onValueChange: (value) => setProfileEditor((state) => {
|
|
676
|
-
if (!state)
|
|
677
|
-
return state;
|
|
678
|
-
const tenantId = value === "none" ? "" : value;
|
|
679
|
-
const picked = tenants.find((t) => t.id === tenantId);
|
|
680
|
-
return {
|
|
681
|
-
...state,
|
|
682
|
-
tenantId,
|
|
683
|
-
tenantName: picked ? picked.name : state.name,
|
|
684
|
-
tenantNameTouched: Boolean(picked),
|
|
685
|
-
};
|
|
686
|
-
}), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een bedrijf" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "none", children: "Nieuw bedrijf aanmaken" }), tenants.map((tenant) => (_jsx(SelectItem, { value: tenant.id, children: tenant.name }, tenant.id)))] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "profile-tenant-name", children: "Bedrijfsnaam" }), _jsx(Input, { id: "profile-tenant-name", value: profileEditor?.tenantName ?? "", placeholder: "Zelfde als profielnaam", onChange: (e) => setProfileEditor((state) => state
|
|
687
|
-
? { ...state, tenantName: e.target.value, tenantNameTouched: true }
|
|
688
|
-
: state), disabled: profileSaving }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wordt gebruikt als weergavenaam van het bedrijf. Leeg laten neemt de profielnaam over." })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rollen in dit profiel" }), _jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto rounded-md border p-3", children: allRolesForProfile.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen rollen beschikbaar." })) : (allRolesForProfile.map((role) => (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: profileEditor?.roleIds.includes(role.id) ?? false, onCheckedChange: () => toggleProfileValue("roleIds", role.id), disabled: profileSaving }), role.name] }, role.id)))) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Modules in dit profiel" }), _jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto rounded-md border p-3", children: roleModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen modules geregistreerd." })) : (roleModules.map((mod) => (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: profileEditor?.moduleIds.includes(mod.id) ?? false, onCheckedChange: () => toggleProfileValue("moduleIds", mod.id), disabled: profileSaving }), mod.name] }, mod.id)))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setProfileEditor(null), disabled: profileSaving, children: "Annuleren" }), _jsx(Button, { onClick: handleSaveProfile, disabled: profileSaving || !profileEditor?.name.trim(), children: profileSaving ? "Opslaan..." : "Opslaan" })] })] }) }), _jsx(AlertDialog, { open: profileDeleteTarget !== null, onOpenChange: (open) => !open && setProfileDeleteTarget(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Bedrijfsprofiel verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je \"", profileDeleteTarget?.name, "\" wilt verwijderen? Rollen en gebruikers blijven bestaan, alleen de bundeling verdwijnt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteProfile, children: "Verwijderen" })] })] }) }), _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) => {
|
|
579
|
+
}) })] }))] }), _jsxs(TabsContent, { value: "modules", className: "mt-4 space-y-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Schakel \"Toegang afdwingen\" in voor een module om de rollen en gebruikersrechten actief te laten controleren. Modules zonder deze schakelaar blijven voor iedereen toegankelijk (default-allow)." }), roleModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Module" }), _jsx(TableHead, { className: "w-48", children: "Toegang afdwingen" })] }) }), _jsx(TableBody, { children: roleModules.map((mod) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), _jsxs(TableCell, { children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: `enforce-${mod.id}`, checked: mod.enforce ?? false, onCheckedChange: (checked) => handleEnforceChange(mod.id, checked), disabled: submitting || !isAdmin }), _jsx(Label, { htmlFor: `enforce-${mod.id}`, className: "text-sm font-normal", children: (mod.enforce ?? false) ? "Ingeschakeld" : "Uitgeschakeld" })] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Beheerders houden altijd toegang, ook als afdwingen aanstaat." })] })] }, mod.id))) })] }))] })] }) })] }), _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: [_jsxs("div", { className: "flex w-16 flex-col items-center gap-1", children: [_jsx("span", { children: "Lezen" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "h-6 px-2 text-xs", disabled: editorSaving, onClick: () => setAllCells("canView", !allCellsOn("canView")), children: allCellsOn("canView") ? "Niets" : "Alles" })] }), _jsxs("div", { className: "flex w-16 flex-col items-center gap-1", children: [_jsx("span", { children: "Bewerken" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "h-6 px-2 text-xs", disabled: editorSaving, onClick: () => setAllCells("canEdit", !allCellsOn("canEdit")), children: allCellsOn("canEdit") ? "Niets" : "Alles" })] })] }), 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) => {
|
|
689
580
|
const cell = editor?.cells[permCellKey(mod.id, "")] ?? {
|
|
690
581
|
canView: false,
|
|
691
582
|
canEdit: false,
|
|
@@ -700,7 +591,11 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
700
591
|
};
|
|
701
592
|
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));
|
|
702
593
|
})] }, mod.id));
|
|
703
|
-
}))] })] }), _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))) })] })] })
|
|
594
|
+
}))] })] }), _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("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "invite-address", children: ["Adres ", inviteContactRequired && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { id: "invite-address", value: inviteAddress, onChange: (e) => setInviteAddress(e.target.value), placeholder: "Dorpsstraat 1, 1234 AB Amsterdam", disabled: inviting })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "invite-phone", children: "Telefoonnummer (optioneel)" }), _jsx(Input, { id: "invite-phone", value: invitePhone, onChange: (e) => setInvitePhone(e.target.value), placeholder: "+31 6 12345678", disabled: inviting })] }), inviteContactRequired && !inviteContactOk && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Voor deze rol is een adres verplicht. Het adres wordt gecontroleerd via Google Maps." }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInviteOpen(false), disabled: inviting, children: "Annuleren" }), _jsx(Button, { onClick: handleInvite, disabled: inviting ||
|
|
595
|
+
!inviteEmail.trim() ||
|
|
596
|
+
!inviteName.trim() ||
|
|
597
|
+
!inviteRole ||
|
|
598
|
+
!inviteContactOk, 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) => {
|
|
704
599
|
setTestNameTouched(true);
|
|
705
600
|
setTestName(e.target.value);
|
|
706
601
|
}, placeholder: suggestedTestName, disabled: testCreating })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rol" }), _jsxs(Select, { value: testRole, onValueChange: (value) => {
|
|
@@ -712,7 +607,14 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
712
607
|
}, 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) => {
|
|
713
608
|
e.preventDefault();
|
|
714
609
|
handleDeleteRole();
|
|
715
|
-
}, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Verwijderen" })] })] }) }), _jsx(Dialog, { open:
|
|
610
|
+
}, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Verwijderen" })] })] }) }), _jsx(Dialog, { open: detailTarget !== null, onOpenChange: (open) => !open && setDetailTarget(null), children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Gegevens van ", detailTarget?.displayName] }), _jsx(DialogDescription, { children: detailTarget?.email })] }), detailTarget && (() => {
|
|
611
|
+
return (_jsxs("div", { className: "space-y-5 max-h-[60vh] overflow-y-auto pr-1", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Rollen" }), _jsx("div", { className: "flex flex-wrap gap-2", children: detailTarget.roles.length === 0 ? (_jsx("span", { className: "text-sm text-muted-foreground", children: "Geen rollen" })) : (detailTarget.roles.map((role) => (_jsx(Badge, { variant: "secondary", children: role.name }, `detail-${detailTarget.id}-${role.id}`)))) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Contactgegevens" }), _jsxs("p", { className: "text-sm", children: ["Adres:", " ", detailTarget.address?.trim() ? (detailTarget.address) : (_jsx("span", { className: "text-destructive", children: "Ontbreekt" }))] }), _jsxs("p", { className: "text-sm", children: ["Telefoon:", " ", detailTarget.phone?.trim() ? (detailTarget.phone) : (_jsx("span", { className: "text-muted-foreground", children: "Niet ingevuld" }))] })] })] }));
|
|
612
|
+
})(), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setDetailTarget(null), children: "Sluiten" }), _jsx(Button, { disabled: !detailTarget || !canManageUser(detailTarget), onClick: () => {
|
|
613
|
+
if (!detailTarget)
|
|
614
|
+
return;
|
|
615
|
+
openEdit(detailTarget);
|
|
616
|
+
setDetailTarget(null);
|
|
617
|
+
}, children: "Wijzigen" })] })] }) }), _jsx(Dialog, { open: editTarget !== null, onOpenChange: (open) => !open && !editSaving && setEditTarget(null), children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Gegevens wijzigen" }), _jsxs(DialogDescription, { children: ["Pas naam en contactgegevens van ", editTarget?.email, " in \u00E9\u00E9n keer aan."] })] }), _jsxs("div", { className: "space-y-5 max-h-[60vh] overflow-y-auto pr-1", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "edit-name", children: "Naam" }), _jsx(Input, { id: "edit-name", value: renameValue, onChange: (e) => setRenameValue(e.target.value), placeholder: "Naam van de gebruiker", disabled: editSaving })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "edit-address", children: "Adres" }), _jsx(Input, { id: "edit-address", value: contactAddress, onChange: (e) => setContactAddress(e.target.value), placeholder: "Dorpsstraat 1, 1234 AB Amsterdam", disabled: editSaving })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "edit-phone", children: "Telefoonnummer (optioneel)" }), _jsx(Input, { id: "edit-phone", value: contactPhone, onChange: (e) => setContactPhone(e.target.value), placeholder: "+31 6 12345678", disabled: editSaving }), editTarget && needsContact(editTarget) && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Voor deze rol is een adres verplicht. Het adres wordt gecontroleerd via Google Maps." }))] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setEditTarget(null), disabled: editSaving, children: "Annuleren" }), _jsx(Button, { onClick: handleSaveEdit, disabled: editSaving || !renameValue.trim(), children: editSaving ? "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) => {
|
|
716
618
|
e.preventDefault();
|
|
717
619
|
handleDelete();
|
|
718
620
|
}, disabled: deleting, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: deleting ? "Verwijderen..." : "Verwijderen" })] })] }) })] }));
|