@flowselections/admin-installatietechniek 1.0.6 → 1.0.8
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 +126 -28
- package/dist-lib/index.d.ts +5 -4
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +3 -3
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +11 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +511 -145
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +511 -145
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +511 -145
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +536 -161
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +11273 -3212
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +213 -49
- 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 +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"
|
|
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,11 +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 { Building2, ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, } from "lucide-react";
|
|
4
|
+
import { Building2, ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, ArrowLeft, } from "lucide-react";
|
|
5
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
8
|
const ACTIVE_PROFILE_STORAGE_KEY = "flowselections.admin.activeProfileId";
|
|
9
|
+
/** Sentinelwaarde in de profielkiezer voor "geen profiel actief". */
|
|
10
|
+
const ALL_PROFILES_VALUE = "__all__";
|
|
9
11
|
const USER_PERMISSION_LABELS = {
|
|
10
12
|
inherit: "Volg rol",
|
|
11
13
|
none: "Geen toegang",
|
|
@@ -13,8 +15,17 @@ const USER_PERMISSION_LABELS = {
|
|
|
13
15
|
edit: "Aanpassen",
|
|
14
16
|
};
|
|
15
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.";
|
|
16
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
|
+
}
|
|
17
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
|
+
}
|
|
18
29
|
if (/permission denied|row-level security|rls|forbidden|not authorized/i.test(error.message)) {
|
|
19
30
|
return "Geen rechten voor deze actie.";
|
|
20
31
|
}
|
|
@@ -40,6 +51,10 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
40
51
|
const [users, setUsers] = useState([]);
|
|
41
52
|
const [availableRoles, setAvailableRoles] = useState([]);
|
|
42
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);
|
|
43
58
|
const [submitting, setSubmitting] = useState(false);
|
|
44
59
|
const [renameTarget, setRenameTarget] = useState(null);
|
|
45
60
|
const [renameValue, setRenameValue] = useState("");
|
|
@@ -61,8 +76,11 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
61
76
|
const [deleting, setDeleting] = useState(false);
|
|
62
77
|
const [testOpen, setTestOpen] = useState(false);
|
|
63
78
|
const [testName, setTestName] = useState("");
|
|
79
|
+
const [testNameTouched, setTestNameTouched] = useState(false);
|
|
64
80
|
const [testRole, setTestRole] = useState("");
|
|
65
81
|
const [testCreating, setTestCreating] = useState(false);
|
|
82
|
+
const [profileLocked, setProfileLocked] = useState(false);
|
|
83
|
+
const [lockedProfile, setLockedProfile] = useState(null);
|
|
66
84
|
const [testCredentials, setTestCredentials] = useState(null);
|
|
67
85
|
const [searchQuery, setSearchQuery] = useState("");
|
|
68
86
|
const [roleFilter, setRoleFilter] = useState("all");
|
|
@@ -76,6 +94,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
76
94
|
return window.localStorage.getItem(ACTIVE_PROFILE_STORAGE_KEY) ?? "";
|
|
77
95
|
});
|
|
78
96
|
const [profileEditor, setProfileEditor] = useState(null);
|
|
97
|
+
const [allProfileRoles, setAllProfileRoles] = useState([]);
|
|
79
98
|
const [profileSaving, setProfileSaving] = useState(false);
|
|
80
99
|
const [profileDeleteTarget, setProfileDeleteTarget] = useState(null);
|
|
81
100
|
const [permUserId, setPermUserId] = useState("");
|
|
@@ -91,13 +110,21 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
91
110
|
const shellModulesKey = shellModules
|
|
92
111
|
.map((m) => `${m.id}:${m.name}:${(m.features ?? []).map((f) => `${f.id}=${f.name}`).join(",")}`)
|
|
93
112
|
.join("|");
|
|
94
|
-
const loadUsers = async () => {
|
|
113
|
+
const loadUsers = async (profileId = activeProfileId) => {
|
|
114
|
+
const requestId = ++usersRequestRef.current;
|
|
95
115
|
setLoading(true);
|
|
116
|
+
setUsersError(null);
|
|
96
117
|
try {
|
|
97
118
|
const result = await getUsersWithRoles({
|
|
98
|
-
data:
|
|
119
|
+
data: { profileId: profileId || undefined },
|
|
99
120
|
});
|
|
121
|
+
if (requestId !== usersRequestRef.current)
|
|
122
|
+
return;
|
|
100
123
|
setUsers(Array.isArray(result?.users) ? result.users : []);
|
|
124
|
+
setProfileLocked(Boolean(result?.profileLocked));
|
|
125
|
+
setLockedProfile(result?.profileLocked
|
|
126
|
+
? result?.activeProfile ?? null
|
|
127
|
+
: null);
|
|
101
128
|
const roleList = Array.isArray(result?.availableRoles) ? result.availableRoles : [];
|
|
102
129
|
setAvailableRoles(roleList);
|
|
103
130
|
const fallback = roleList[0]?.id ?? "";
|
|
@@ -105,27 +132,41 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
105
132
|
setTestRole((current) => current || fallback);
|
|
106
133
|
}
|
|
107
134
|
catch (error) {
|
|
108
|
-
|
|
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);
|
|
109
140
|
}
|
|
110
141
|
finally {
|
|
111
|
-
|
|
142
|
+
if (requestId === usersRequestRef.current)
|
|
143
|
+
setLoading(false);
|
|
112
144
|
}
|
|
113
145
|
};
|
|
114
|
-
const loadRoles = async () => {
|
|
146
|
+
const loadRoles = async (profileId = activeProfileId) => {
|
|
147
|
+
const requestId = ++rolesRequestRef.current;
|
|
115
148
|
setRolesLoading(true);
|
|
149
|
+
setRolesError(null);
|
|
116
150
|
try {
|
|
117
151
|
const result = await getRoles({
|
|
118
|
-
data:
|
|
152
|
+
data: { profileId: profileId || undefined },
|
|
119
153
|
});
|
|
154
|
+
if (requestId !== rolesRequestRef.current)
|
|
155
|
+
return;
|
|
120
156
|
setRoles(Array.isArray(result?.roles) ? result.roles : []);
|
|
121
157
|
setRoleModules(Array.isArray(result?.modules) ? result.modules : []);
|
|
122
158
|
setRolePermissions(Array.isArray(result?.permissions) ? result.permissions : []);
|
|
123
159
|
}
|
|
124
160
|
catch (error) {
|
|
125
|
-
|
|
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);
|
|
126
166
|
}
|
|
127
167
|
finally {
|
|
128
|
-
|
|
168
|
+
if (requestId === rolesRequestRef.current)
|
|
169
|
+
setRolesLoading(false);
|
|
129
170
|
}
|
|
130
171
|
};
|
|
131
172
|
const loadProfiles = async () => {
|
|
@@ -137,11 +178,8 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
137
178
|
const list = Array.isArray(result?.profiles) ? result.profiles : [];
|
|
138
179
|
setOrgProfiles(list);
|
|
139
180
|
setTenants(Array.isArray(result?.tenants) ? result.tenants : []);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return current;
|
|
143
|
-
return list.find((p) => p.isDefault)?.id ?? list[0]?.id ?? "";
|
|
144
|
-
});
|
|
181
|
+
// Nooit automatisch een profiel kiezen: leeg = overzicht (alle bedrijven).
|
|
182
|
+
setActiveProfileId((current) => current && list.some((p) => p.id === current) ? current : "");
|
|
145
183
|
}
|
|
146
184
|
catch (error) {
|
|
147
185
|
toast.error(getErrorMessage(error, "Fout bij het laden van bedrijfsprofielen"));
|
|
@@ -180,6 +218,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
180
218
|
setUserPermLoading(false);
|
|
181
219
|
}
|
|
182
220
|
};
|
|
221
|
+
// Registratie van shell-modules + profielen: los van de profielcontext.
|
|
183
222
|
useEffect(() => {
|
|
184
223
|
if (!canAccess)
|
|
185
224
|
return;
|
|
@@ -196,8 +235,6 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
196
235
|
if (cancelled)
|
|
197
236
|
return;
|
|
198
237
|
loadProfiles();
|
|
199
|
-
loadUsers();
|
|
200
|
-
loadRoles();
|
|
201
238
|
})();
|
|
202
239
|
return () => {
|
|
203
240
|
cancelled = true;
|
|
@@ -209,14 +246,27 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
209
246
|
return;
|
|
210
247
|
if (activeProfileId)
|
|
211
248
|
window.localStorage.setItem(ACTIVE_PROFILE_STORAGE_KEY, activeProfileId);
|
|
249
|
+
else
|
|
250
|
+
window.localStorage.removeItem(ACTIVE_PROFILE_STORAGE_KEY);
|
|
212
251
|
}, [activeProfileId]);
|
|
252
|
+
// Eén gecontroleerde laadactie per profielcontext (ook bij eerste render):
|
|
253
|
+
// profielgebonden state resetten en opnieuw laden binnen de nieuwe context.
|
|
213
254
|
useEffect(() => {
|
|
214
|
-
if (!canAccess
|
|
255
|
+
if (!canAccess)
|
|
215
256
|
return;
|
|
216
|
-
|
|
217
|
-
|
|
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);
|
|
218
268
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
-
}, [activeProfileId]);
|
|
269
|
+
}, [activeProfileId, canAccess]);
|
|
220
270
|
useEffect(() => {
|
|
221
271
|
if (!canAccess)
|
|
222
272
|
return;
|
|
@@ -254,7 +304,9 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
254
304
|
const handleAssign = async (userId, roleId) => {
|
|
255
305
|
setSubmitting(true);
|
|
256
306
|
try {
|
|
257
|
-
await assignRole({
|
|
307
|
+
await assignRole({
|
|
308
|
+
data: { userId, roleId, ...(activeProfileId ? { profileId: activeProfileId } : {}) },
|
|
309
|
+
});
|
|
258
310
|
await refreshOwnSessionIfSelf(userId, currentUserId);
|
|
259
311
|
toast.success("Rol toegekend");
|
|
260
312
|
await loadUsers();
|
|
@@ -464,10 +516,16 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
464
516
|
}
|
|
465
517
|
};
|
|
466
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([]));
|
|
467
523
|
setProfileEditor({
|
|
468
524
|
profileId: profile?.id ?? null,
|
|
469
525
|
name: profile?.name ?? "",
|
|
470
526
|
tenantId: profile?.tenantId ?? "",
|
|
527
|
+
tenantName: profile?.tenantName ?? profile?.name ?? "",
|
|
528
|
+
tenantNameTouched: Boolean(profile),
|
|
471
529
|
roleIds: profile?.roleIds ?? [],
|
|
472
530
|
moduleIds: profile?.moduleIds ?? [],
|
|
473
531
|
});
|
|
@@ -491,6 +549,8 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
491
549
|
const payload = {
|
|
492
550
|
name: profileEditor.name.trim(),
|
|
493
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()),
|
|
494
554
|
roleIds: profileEditor.roleIds,
|
|
495
555
|
moduleIds: profileEditor.moduleIds,
|
|
496
556
|
};
|
|
@@ -547,7 +607,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
547
607
|
toast.error(getErrorMessage(error, "Instellen mislukt"));
|
|
548
608
|
}
|
|
549
609
|
};
|
|
550
|
-
const allRolesForProfile =
|
|
610
|
+
const allRolesForProfile = allProfileRoles.length > 0 ? allProfileRoles : roles;
|
|
551
611
|
const filteredUsers = useMemo(() => {
|
|
552
612
|
const query = searchQuery.trim().toLowerCase();
|
|
553
613
|
const list = users.filter((u) => {
|
|
@@ -575,17 +635,26 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
575
635
|
}, [users, searchQuery, roleFilter, sortBy]);
|
|
576
636
|
const hasActiveFilters = searchQuery.trim() !== "" || roleFilter !== "all";
|
|
577
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
|
+
};
|
|
578
647
|
const canManageUser = (target) => isAdmin || !target.roles.some((r) => r.key === "admin");
|
|
579
648
|
if (!canAccess) {
|
|
580
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." }) }) }) }));
|
|
581
650
|
}
|
|
582
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [
|
|
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
|
|
583
652
|
? "Geen gebruikers gevonden met de huidige filters."
|
|
584
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) => {
|
|
585
654
|
const manageable = canManageUser(u);
|
|
586
655
|
const unassigned = assignableRoles.filter((r) => !u.roles.some((ur) => ur.id === r.id));
|
|
587
|
-
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));
|
|
588
|
-
}) })] }))] }), _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) => {
|
|
589
658
|
const count = rolePermissions.filter((p) => p.roleId === role.id && p.featureId === "" && (p.canView || p.canEdit)).length;
|
|
590
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));
|
|
591
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) => {
|
|
@@ -596,7 +665,27 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
596
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}`));
|
|
597
666
|
}
|
|
598
667
|
return rows;
|
|
599
|
-
}) })] }))] }), _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 }),
|
|
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) => {
|
|
600
689
|
const cell = editor?.cells[permCellKey(mod.id, "")] ?? {
|
|
601
690
|
canView: false,
|
|
602
691
|
canEdit: false,
|
|
@@ -611,7 +700,16 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
611
700
|
};
|
|
612
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));
|
|
613
702
|
})] }, mod.id));
|
|
614
|
-
}))] })] }), _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) =>
|
|
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) => {
|
|
615
713
|
e.preventDefault();
|
|
616
714
|
handleDeleteRole();
|
|
617
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) => {
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -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 '
|
|
6
|
-
export type {
|
|
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 {
|
|
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
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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.
|