@flowselections/admin-installatietechniek 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
  2. package/dist-lib/components/AdminForInstallatieTechniek.js +265 -21
  3. package/dist-lib/hooks/useModulePermission.d.ts +2 -1
  4. package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
  5. package/dist-lib/index.d.ts +5 -4
  6. package/dist-lib/index.d.ts.map +1 -1
  7. package/dist-lib/index.js +3 -3
  8. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
  9. package/dist-lib/integrations/supabase/auth-attacher.js +11 -1
  10. package/dist-lib/integrations/supabase/auth-middleware.d.ts +841 -155
  11. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/client.d.ts +841 -155
  13. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/client.server.d.ts +841 -155
  15. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  16. package/dist-lib/integrations/supabase/types.d.ts +870 -157
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/types.js +3 -0
  19. package/dist-lib/lib/admin.functions.d.ts +146927 -1852
  20. package/dist-lib/lib/admin.functions.d.ts.map +1 -1
  21. package/dist-lib/lib/admin.functions.js +446 -25
  22. package/dist-lib/lib/permission-types.d.ts +7 -0
  23. package/dist-lib/lib/permission-types.d.ts.map +1 -0
  24. package/dist-lib/lib/permission-types.js +1 -0
  25. package/dist-lib/lib/permissions.server.d.ts +1 -1
  26. package/dist-lib/lib/permissions.server.d.ts.map +1 -1
  27. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"AAyFA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiED,wBAAgB,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAE,gCAAqC,+BAssCtG"}
1
+ {"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"AAiGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+GD,wBAAgB,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAE,gCAAqC,+BA8vDtG"}
@@ -1,10 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
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 { 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, setModuleEnforce, setUserModulePermission, syncModuleRegistry, updateRole, updateUserName, } from "../lib/admin.functions";
4
+ import { Building2, ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, ArrowLeft, } from "lucide-react";
5
+ import { assignRole, createRole, createProfile, createTestUser, deleteProfile, deleteRole, deleteUser, duplicateProfile, duplicateRole, getProfiles, getRoles, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setActiveProfile, setModuleEnforce, setUserModulePermission, syncModuleRegistry, updateProfile, updateRole, updateUserName, } from "../lib/admin.functions";
6
6
  import { useDiscoveredModules } from "../lib/moduleDiscovery";
7
7
  import { supabase } from "../integrations/supabase/client";
8
+ const ACTIVE_PROFILE_STORAGE_KEY = "flowselections.admin.activeProfileId";
9
+ /** Sentinelwaarde in de profielkiezer voor "geen profiel actief". */
10
+ const ALL_PROFILES_VALUE = "__all__";
8
11
  const USER_PERMISSION_LABELS = {
9
12
  inherit: "Volg rol",
10
13
  none: "Geen toegang",
@@ -12,8 +15,17 @@ const USER_PERMISSION_LABELS = {
12
15
  edit: "Aanpassen",
13
16
  };
14
17
  const permCellKey = (moduleId, featureId) => `${moduleId}::${featureId}`;
18
+ const SESSION_EXPIRED_MESSAGE = "Je sessie is verlopen of niet meer geldig. Log opnieuw in en probeer het nogmaals.";
15
19
  const getErrorMessage = (error, fallback) => {
20
+ // De auth-middleware gooit een ruwe Response bij een ongeldig/verlopen token.
21
+ // Die komt over de RPC-grens binnen als "[object Response]" — vertaal dat.
22
+ if (typeof Response !== "undefined" && error instanceof Response) {
23
+ return error.status === 401 ? SESSION_EXPIRED_MESSAGE : `Serverfout (${error.status}).`;
24
+ }
16
25
  if (error instanceof Error && error.message) {
26
+ if (/\[object Response\]|unauthorized|auth session missing/i.test(error.message)) {
27
+ return SESSION_EXPIRED_MESSAGE;
28
+ }
17
29
  if (/permission denied|row-level security|rls|forbidden|not authorized/i.test(error.message)) {
18
30
  return "Geen rechten voor deze actie.";
19
31
  }
@@ -39,6 +51,10 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
39
51
  const [users, setUsers] = useState([]);
40
52
  const [availableRoles, setAvailableRoles] = useState([]);
41
53
  const [loading, setLoading] = useState(true);
54
+ const [usersError, setUsersError] = useState(null);
55
+ const [rolesError, setRolesError] = useState(null);
56
+ const usersRequestRef = useRef(0);
57
+ const rolesRequestRef = useRef(0);
42
58
  const [submitting, setSubmitting] = useState(false);
43
59
  const [renameTarget, setRenameTarget] = useState(null);
44
60
  const [renameValue, setRenameValue] = useState("");
@@ -60,12 +76,27 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
60
76
  const [deleting, setDeleting] = useState(false);
61
77
  const [testOpen, setTestOpen] = useState(false);
62
78
  const [testName, setTestName] = useState("");
79
+ const [testNameTouched, setTestNameTouched] = useState(false);
63
80
  const [testRole, setTestRole] = useState("");
64
81
  const [testCreating, setTestCreating] = useState(false);
82
+ const [profileLocked, setProfileLocked] = useState(false);
83
+ const [lockedProfile, setLockedProfile] = useState(null);
65
84
  const [testCredentials, setTestCredentials] = useState(null);
66
85
  const [searchQuery, setSearchQuery] = useState("");
67
86
  const [roleFilter, setRoleFilter] = useState("all");
68
87
  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);
69
100
  const [permUserId, setPermUserId] = useState("");
70
101
  const [userPermModules, setUserPermModules] = useState([]);
71
102
  const [userPermMap, setUserPermMap] = useState({});
@@ -79,11 +110,21 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
79
110
  const shellModulesKey = shellModules
80
111
  .map((m) => `${m.id}:${m.name}:${(m.features ?? []).map((f) => `${f.id}=${f.name}`).join(",")}`)
81
112
  .join("|");
82
- const loadUsers = async () => {
113
+ const loadUsers = async (profileId = activeProfileId) => {
114
+ const requestId = ++usersRequestRef.current;
83
115
  setLoading(true);
116
+ setUsersError(null);
84
117
  try {
85
- const result = await getUsersWithRoles();
118
+ const result = await getUsersWithRoles({
119
+ data: { profileId: profileId || undefined },
120
+ });
121
+ if (requestId !== usersRequestRef.current)
122
+ return;
86
123
  setUsers(Array.isArray(result?.users) ? result.users : []);
124
+ setProfileLocked(Boolean(result?.profileLocked));
125
+ setLockedProfile(result?.profileLocked
126
+ ? result?.activeProfile ?? null
127
+ : null);
87
128
  const roleList = Array.isArray(result?.availableRoles) ? result.availableRoles : [];
88
129
  setAvailableRoles(roleList);
89
130
  const fallback = roleList[0]?.id ?? "";
@@ -91,25 +132,60 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
91
132
  setTestRole((current) => current || fallback);
92
133
  }
93
134
  catch (error) {
94
- toast.error(getErrorMessage(error, "Fout bij het laden van gebruikers en rollen"));
135
+ if (requestId !== usersRequestRef.current)
136
+ return;
137
+ const message = getErrorMessage(error, "Fout bij het laden van gebruikers en rollen");
138
+ setUsersError(message);
139
+ toast.error(message);
95
140
  }
96
141
  finally {
97
- setLoading(false);
142
+ if (requestId === usersRequestRef.current)
143
+ setLoading(false);
98
144
  }
99
145
  };
100
- const loadRoles = async () => {
146
+ const loadRoles = async (profileId = activeProfileId) => {
147
+ const requestId = ++rolesRequestRef.current;
101
148
  setRolesLoading(true);
149
+ setRolesError(null);
102
150
  try {
103
- const result = await getRoles();
151
+ const result = await getRoles({
152
+ data: { profileId: profileId || undefined },
153
+ });
154
+ if (requestId !== rolesRequestRef.current)
155
+ return;
104
156
  setRoles(Array.isArray(result?.roles) ? result.roles : []);
105
157
  setRoleModules(Array.isArray(result?.modules) ? result.modules : []);
106
158
  setRolePermissions(Array.isArray(result?.permissions) ? result.permissions : []);
107
159
  }
108
160
  catch (error) {
109
- toast.error(getErrorMessage(error, "Fout bij het laden van rollen"));
161
+ if (requestId !== rolesRequestRef.current)
162
+ return;
163
+ const message = getErrorMessage(error, "Fout bij het laden van rollen");
164
+ setRolesError(message);
165
+ toast.error(message);
166
+ }
167
+ finally {
168
+ if (requestId === rolesRequestRef.current)
169
+ setRolesLoading(false);
170
+ }
171
+ };
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"));
110
186
  }
111
187
  finally {
112
- setRolesLoading(false);
188
+ setProfilesLoading(false);
113
189
  }
114
190
  };
115
191
  const loadUserPermissions = async (userId) => {
@@ -142,6 +218,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
142
218
  setUserPermLoading(false);
143
219
  }
144
220
  };
221
+ // Registratie van shell-modules + profielen: los van de profielcontext.
145
222
  useEffect(() => {
146
223
  if (!canAccess)
147
224
  return;
@@ -157,14 +234,39 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
157
234
  }
158
235
  if (cancelled)
159
236
  return;
160
- loadUsers();
161
- loadRoles();
237
+ loadProfiles();
162
238
  })();
163
239
  return () => {
164
240
  cancelled = true;
165
241
  };
166
242
  // eslint-disable-next-line react-hooks/exhaustive-deps
167
243
  }, [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
+ useEffect(() => {
255
+ if (!canAccess)
256
+ return;
257
+ setUsers([]);
258
+ setRoles([]);
259
+ setAvailableRoles([]);
260
+ setRolePermissions([]);
261
+ setSearchQuery("");
262
+ setRoleFilter("all");
263
+ setPermUserId("");
264
+ setUserPermModules([]);
265
+ setUserPermMap({});
266
+ loadUsers(activeProfileId);
267
+ loadRoles(activeProfileId);
268
+ // eslint-disable-next-line react-hooks/exhaustive-deps
269
+ }, [activeProfileId, canAccess]);
168
270
  useEffect(() => {
169
271
  if (!canAccess)
170
272
  return;
@@ -202,7 +304,9 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
202
304
  const handleAssign = async (userId, roleId) => {
203
305
  setSubmitting(true);
204
306
  try {
205
- await assignRole({ data: { userId, roleId } });
307
+ await assignRole({
308
+ data: { userId, roleId, ...(activeProfileId ? { profileId: activeProfileId } : {}) },
309
+ });
206
310
  await refreshOwnSessionIfSelf(userId, currentUserId);
207
311
  toast.success("Rol toegekend");
208
312
  await loadUsers();
@@ -319,7 +423,12 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
319
423
  setInviting(true);
320
424
  try {
321
425
  await inviteUser({
322
- data: { email: inviteEmail.trim(), displayName: inviteName.trim(), roleId: inviteRole },
426
+ data: {
427
+ email: inviteEmail.trim(),
428
+ displayName: inviteName.trim(),
429
+ roleId: inviteRole,
430
+ ...(activeProfileId ? { profileId: activeProfileId } : {}),
431
+ },
323
432
  });
324
433
  toast.success(`Uitnodiging verstuurd naar ${inviteEmail.trim()}`);
325
434
  setInviteOpen(false);
@@ -378,7 +487,11 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
378
487
  setTestCreating(true);
379
488
  try {
380
489
  const result = await createTestUser({
381
- data: { displayName: testName.trim(), roleId: testRole },
490
+ data: {
491
+ displayName: testName.trim(),
492
+ roleId: testRole,
493
+ ...(activeProfileId ? { profileId: activeProfileId } : {}),
494
+ },
382
495
  });
383
496
  setTestCredentials({ email: result.email, password: result.password });
384
497
  setTestOpen(false);
@@ -402,6 +515,99 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
402
515
  toast.error("Kopiëren mislukt");
403
516
  }
404
517
  };
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;
405
611
  const filteredUsers = useMemo(() => {
406
612
  const query = searchQuery.trim().toLowerCase();
407
613
  const list = users.filter((u) => {
@@ -429,17 +635,26 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
429
635
  }, [users, searchQuery, roleFilter, sortBy]);
430
636
  const hasActiveFilters = searchQuery.trim() !== "" || roleFilter !== "all";
431
637
  const assignableRoles = availableRoles;
638
+ const suggestedTestName = (() => {
639
+ const roleName = assignableRoles.find((r) => r.id === testRole)?.name;
640
+ return roleName ? `Test ${roleName}` : "Test gebruiker";
641
+ })();
642
+ const openTestDialog = () => {
643
+ setTestNameTouched(false);
644
+ setTestName(suggestedTestName);
645
+ setTestOpen(true);
646
+ };
432
647
  const canManageUser = (target) => isAdmin || !target.roles.some((r) => r.key === "admin");
433
648
  if (!canAccess) {
434
649
  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." }) }) }) }));
435
650
  }
436
- 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" }), _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..." })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
651
+ return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [_jsxs(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 && orgProfiles.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Building2, { className: "h-4 w-4 text-muted-foreground" }), _jsxs(Select, { value: activeProfileId || ALL_PROFILES_VALUE, onValueChange: (value) => setActiveProfileId(value === ALL_PROFILES_VALUE ? "" : value), children: [_jsx(SelectTrigger, { className: "w-56", children: _jsx(SelectValue, { placeholder: "Bedrijfsprofiel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL_PROFILES_VALUE, children: "Alle bedrijven (overzicht)" }), orgProfiles.map((profile) => (_jsx(SelectItem, { value: profile.id, children: profile.name }, profile.id)))] })] })] })), isAdmin && activeTab === "profiles" && (_jsxs(Button, { size: "sm", onClick: () => openProfileEditor(), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Nieuw profiel"] })), 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"] })] }))] })] }), isAdmin && activeProfileId && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-md border bg-muted/40 px-3 py-2", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 px-2", onClick: () => setActiveProfileId(""), children: [_jsx(ArrowLeft, { className: "mr-1 h-4 w-4" }), "Alle bedrijven"] }), _jsxs("span", { className: "text-sm text-muted-foreground", children: ["Je bekijkt bedrijfsprofiel", " ", _jsx("span", { className: "font-medium text-foreground", children: orgProfiles.find((p) => p.id === activeProfileId)?.name ?? "onbekend" }), ". Alleen gebruikers en rollen van dit bedrijf worden getoond."] })] })), profileLocked && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2", children: [_jsx(Building2, { className: "h-4 w-4 text-muted-foreground" }), lockedProfile ? (_jsxs("span", { className: "text-sm text-muted-foreground", children: ["Je ziet uitsluitend bedrijfsprofiel", " ", _jsx("span", { className: "font-medium text-foreground", children: lockedProfile.name }), "."] })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "Er is nog geen bedrijfsprofiel aan je account gekoppeld. Vraag een beheerder om je aan een profiel te koppelen." }))] }))] }), _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" }), isAdmin && _jsx(TabsTrigger, { value: "profiles", children: "Profielen" })] }), _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(activeProfileId), children: "Opnieuw proberen" })] })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
437
652
  ? "Geen gebruikers gevonden met de huidige filters."
438
653
  : "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) => {
439
654
  const manageable = canManageUser(u);
440
655
  const unassigned = assignableRoles.filter((r) => !u.roles.some((ur) => ur.id === r.id));
441
- 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));
442
- }) })] }))] }), _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) => {
656
+ 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" })), activeProfileId && u.matchedVia === "role" && (_jsx(Badge, { variant: "outline", className: "text-xs", children: "Via rol" }))] }), _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));
657
+ }) })] }))] }), _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(activeProfileId), children: "Opnieuw proberen" })] })) : 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) => {
443
658
  const count = rolePermissions.filter((p) => p.roleId === role.id && p.featureId === "" && (p.canView || p.canEdit)).length;
444
659
  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));
445
660
  }) })] })) }), _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) => {
@@ -450,7 +665,27 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
450
665
  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}`));
451
666
  }
452
667
  return rows;
453
- }) })] }))] }), _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 }), _jsx(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" })] }) })] }, 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: [_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) => {
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))) })] }))] }), isAdmin && (_jsxs(TabsContent, { value: "profiles", className: "mt-4 space-y-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Een bedrijfsprofiel bundelt de rollen en modules van \u00E9\u00E9n organisatie. Het gekozen profiel bepaalt welke medewerkers en rollen je hierboven ziet." }), profilesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : orgProfiles.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen bedrijfsprofielen." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Profiel" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { className: "w-24", children: "Rollen" }), _jsx(TableHead, { className: "w-24", children: "Modules" }), _jsx(TableHead, { className: "w-28", children: "Gebruikers" }), _jsx(TableHead, { className: "w-16 text-right", children: "Acties" })] }) }), _jsx(TableBody, { children: orgProfiles.map((profile) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsxs("div", { className: "flex items-center gap-2", children: [profile.name, profile.isDefault && _jsx(Badge, { variant: "secondary", children: "Standaard" })] }) }), _jsx(TableCell, { className: "text-muted-foreground", children: profile.tenantName ?? "-" }), _jsx(TableCell, { children: profile.roleIds.length }), _jsx(TableCell, { children: profile.moduleIds.length }), _jsx(TableCell, { children: profile.userCount }), _jsx(TableCell, { className: "text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", children: _jsx(MoreVertical, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => openProfileEditor(profile), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), "Bewerken"] }), _jsxs(DropdownMenuItem, { onClick: () => handleDuplicateProfile(profile), children: [_jsx(Copy, { className: "h-4 w-4 mr-2" }), "Dupliceren"] }), !profile.isDefault && (_jsxs(DropdownMenuItem, { onClick: () => handleSetActiveProfile(profile), children: [_jsx(Shield, { className: "h-4 w-4 mr-2" }), "Als standaard instellen"] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive", disabled: profile.isDefault, onClick: () => setProfileDeleteTarget(profile), children: [_jsx(UserX, { className: "h-4 w-4 mr-2" }), "Verwijderen"] })] })] }) })] }, profile.id))) })] }))] }))] }) })] }), _jsx(Dialog, { open: profileEditor !== null, onOpenChange: (open) => !open && !profileSaving && setProfileEditor(null), children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: profileEditor?.profileId ? "Bedrijfsprofiel bewerken" : "Bedrijfsprofiel toevoegen" }), _jsx(DialogDescription, { children: "Koppel het profiel aan een bedrijf en kies welke rollen en modules erbij horen." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "profile-name", children: "Naam profiel" }), _jsx(Input, { id: "profile-name", value: profileEditor?.name ?? "", placeholder: "Bijvoorbeeld: Installatie Techniek BV", onChange: (e) => setProfileEditor((state) => state
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) => {
454
689
  const cell = editor?.cells[permCellKey(mod.id, "")] ?? {
455
690
  canView: false,
456
691
  canEdit: false,
@@ -465,7 +700,16 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
465
700
  };
466
701
  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));
467
702
  })] }, mod.id));
468
- }))] })] }), _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) => {
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))) })] })] })] }), _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) => {
704
+ setTestNameTouched(true);
705
+ setTestName(e.target.value);
706
+ }, placeholder: suggestedTestName, disabled: testCreating })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rol" }), _jsxs(Select, { value: testRole, onValueChange: (value) => {
707
+ setTestRole(value);
708
+ if (!testNameTouched) {
709
+ const roleName = assignableRoles.find((r) => r.id === value)?.name;
710
+ setTestName(roleName ? `Test ${roleName}` : "");
711
+ }
712
+ }, 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) => {
469
713
  e.preventDefault();
470
714
  handleDeleteRole();
471
715
  }, 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) => {
@@ -1,4 +1,5 @@
1
- export type ModulePermission = "none" | "view" | "edit";
1
+ import type { ModulePermission } from "../lib/permission-types";
2
+ export type { ModulePermission };
2
3
  /**
3
4
  * Haalt de effectieve rechten van de ingelogde gebruiker op voor alle
4
5
  * geregistreerde modules en onderdelen. Persoonlijke uitzonderingen winnen
@@ -1 +1 @@
1
- {"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/hooks/useModulePermission.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAMxD;;;;GAIG;AACH,wBAAgB,sBAAsB;;;EAmBrC;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAAK;;;;;EAgBnE;AAED,kDAAkD;AAClD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE;IACvD,iBAAiB,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,iBAEA"}
1
+ {"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/hooks/useModulePermission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAMjC;;;;GAIG;AACH,wBAAgB,sBAAsB;;;EAmBrC;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAAK;;;;;EAgBnE;AAED,kDAAkD;AAClD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE;IACvD,iBAAiB,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,iBAEA"}
@@ -2,11 +2,12 @@ import type { FlowModule } from "@flowselections/core";
2
2
  export * from './_core-safelist';
3
3
  export { AdminForInstallatieTechniek } from './components/AdminForInstallatieTechniek';
4
4
  export type { AdminForInstallatieTechniekProps, AdminModuleInput } from './components/AdminForInstallatieTechniek';
5
- export { useModulePermission, useMyModulePermissions } from './hooks/useModulePermission';
6
- export type { ModulePermission } from './hooks/useModulePermission';
5
+ export { useModulePermission, useMyModulePermissions, useModulePermissionTransform, ModuleAccessGuard, } from '@flowselections/core';
6
+ export type { PermissionLevel, ModuleAccessGuardProps } from '@flowselections/core';
7
+ /** @deprecated gebruik PermissionLevel uit @flowselections/core */
8
+ export type ModulePermission = import('@flowselections/core').PermissionLevel;
7
9
  export { useDiscoveredModules } from './lib/moduleDiscovery';
8
10
  export type { DiscoveredModule, DiscoveredFeature } from './lib/moduleDiscovery';
9
- export { ModuleAccessGuard } from './components/ModuleAccessGuard';
10
- export { requireModulePermission } from './lib/permissions.server';
11
+ export { requireModulePermission } from '@flowselections/core/server';
11
12
  export declare const adminInstallatietechniekModule: FlowModule;
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1F,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,eAAO,MAAM,8BAA8B,EAAE,UAY5C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAEnH,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACpF,mEAAmE;AACnE,MAAM,MAAM,gBAAgB,GAAG,OAAO,sBAAsB,EAAE,eAAe,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAItE,eAAO,MAAM,8BAA8B,EAAE,UAY5C,CAAC"}
package/dist-lib/index.js CHANGED
@@ -4,10 +4,10 @@
4
4
  import { Shield } from "lucide-react";
5
5
  export * from './_core-safelist';
6
6
  export { AdminForInstallatieTechniek } from './components/AdminForInstallatieTechniek';
7
- export { useModulePermission, useMyModulePermissions } from './hooks/useModulePermission';
7
+ // Check-laag komt uit core; deze module blijft alleen het beheer leveren.
8
+ export { useModulePermission, useMyModulePermissions, useModulePermissionTransform, ModuleAccessGuard, } from '@flowselections/core';
8
9
  export { useDiscoveredModules } from './lib/moduleDiscovery';
9
- export { ModuleAccessGuard } from './components/ModuleAccessGuard';
10
- export { requireModulePermission } from './lib/permissions.server';
10
+ export { requireModulePermission } from '@flowselections/core/server';
11
11
  export const adminInstallatietechniekModule = {
12
12
  id: "admin-installatietechniek",
13
13
  name: "Admin Installatietechniek",
@@ -1 +1 @@
1
- {"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,mHAY9B,CAAA"}
1
+ {"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,mHAwB9B,CAAA"}
@@ -5,7 +5,17 @@ import { supabase } from './client';
5
5
  // the browser never attaches the bearer token to serverFn RPCs.
6
6
  export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(async ({ next }) => {
7
7
  const { data } = await supabase.auth.getSession();
8
- const token = data.session?.access_token;
8
+ let session = data.session;
9
+ // Een (bijna) verlopen access-token wordt door de server geweigerd met
10
+ // "Auth session missing!". Ververs proactief zodat serverFn-aanroepen niet
11
+ // willekeurig met 401 falen.
12
+ const expiresAt = session?.expires_at ?? 0;
13
+ const nowSeconds = Math.floor(Date.now() / 1000);
14
+ if (session && expiresAt && expiresAt - nowSeconds < 60) {
15
+ const refreshed = await supabase.auth.refreshSession();
16
+ session = refreshed.data.session ?? session;
17
+ }
18
+ const token = session?.access_token;
9
19
  return next({
10
20
  // `x-sb-auth` is een fallback: sommige preview-proxies overschrijven de
11
21
  // Authorization-header, waardoor de server een ongeldig token zag.