@flowselections/admin-installatietechniek 1.0.1
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/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts +12 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -0
- package/dist-lib/components/AdminForInstallatieTechniek.js +359 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
- package/dist-lib/components/TemplateModuleLogo.js +4 -0
- package/dist-lib/components/TemplateModulePage.d.ts +2 -0
- package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
- package/dist-lib/components/TemplateModulePage.js +44 -0
- package/dist-lib/components/TemplatePage.d.ts +2 -0
- package/dist-lib/components/TemplatePage.d.ts.map +1 -0
- package/dist-lib/components/TemplatePage.js +4 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
- package/dist-lib/hooks/useMijnModuleData.js +49 -0
- package/dist-lib/hooks/useModulePermission.d.ts +17 -0
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -0
- package/dist-lib/hooks/useModulePermission.js +42 -0
- package/dist-lib/index.d.ts +10 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +19 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +20806 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +49 -0
- package/dist-lib/integrations/supabase/client.d.ts +20802 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +20802 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +32 -0
- package/dist-lib/integrations/supabase/types.d.ts +21382 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +89 -0
- package/dist-lib/lib/admin.functions.d.ts +228936 -0
- package/dist-lib/lib/admin.functions.d.ts.map +1 -0
- package/dist-lib/lib/admin.functions.js +410 -0
- package/dist-lib/lib/admin.server.d.ts +29 -0
- package/dist-lib/lib/admin.server.d.ts.map +1 -0
- package/dist-lib/lib/admin.server.js +63 -0
- package/dist-lib/lib/moduleDiscovery.d.ts +10 -0
- package/dist-lib/lib/moduleDiscovery.d.ts.map +1 -0
- package/dist-lib/lib/moduleDiscovery.js +54 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +87 -0
- package/public/logo.png +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Forceert Tailwind classes die uit @flowselections/core komen maar niet
|
|
2
|
+
// voorkomen in de eigen broncode van deze module.
|
|
3
|
+
// Zonder dit bestand worden deze classes weggeoptimaliseerd op de
|
|
4
|
+
// Lovable productie build.
|
|
5
|
+
const _safelist = [
|
|
6
|
+
// Layout — Sidebar en shell structuur
|
|
7
|
+
'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
|
|
8
|
+
'items-start', 'items-center', 'justify-between', 'border-t',
|
|
9
|
+
'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
|
|
10
|
+
// Grid — InstellingenPage
|
|
11
|
+
'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
|
|
12
|
+
// Toggle/Switch — aan/uit zichtbaarheid
|
|
13
|
+
'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
|
|
14
|
+
];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AdminModuleInput {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AdminForInstallatieTechniekProps {
|
|
6
|
+
/** Modules die deze shell heeft geladen; worden automatisch geregistreerd. */
|
|
7
|
+
modules?: AdminModuleInput[];
|
|
8
|
+
/** Optionele naam van de shell, voor het opruimen van oude modules. */
|
|
9
|
+
shellId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function AdminForInstallatieTechniek({ modules, shellId }?: AdminForInstallatieTechniekProps): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=AdminForInstallatieTechniek.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"AAgEA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiDD,wBAAgB,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAE,gCAAqC,+BAy0BtG"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useAuth, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Card, CardContent, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Badge, toast, } from "@flowselections/core";
|
|
4
|
+
import { Copy, FlaskConical, Shield, Trash2, UserPlus, UserX } from "lucide-react";
|
|
5
|
+
import { assignRole, createTestUser, deleteUser, getModulePermissions, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setModulePermission, setUserModulePermission, syncModuleRegistry, } from "../lib/admin.functions";
|
|
6
|
+
import { useDiscoveredModules } from "../lib/moduleDiscovery";
|
|
7
|
+
import { supabase } from "../integrations/supabase/client";
|
|
8
|
+
const PERMISSION_LABELS = {
|
|
9
|
+
none: "Geen toegang",
|
|
10
|
+
view: "Bekijken",
|
|
11
|
+
edit: "Aanpassen",
|
|
12
|
+
};
|
|
13
|
+
const USER_PERMISSION_LABELS = {
|
|
14
|
+
inherit: "Volg rol",
|
|
15
|
+
none: "Geen toegang",
|
|
16
|
+
view: "Bekijken",
|
|
17
|
+
edit: "Aanpassen",
|
|
18
|
+
};
|
|
19
|
+
const getErrorMessage = (error, fallback) => {
|
|
20
|
+
if (error instanceof Error && error.message) {
|
|
21
|
+
if (/permission denied|row-level security|rls|forbidden|not authorized/i.test(error.message)) {
|
|
22
|
+
return "Geen rechten voor deze actie.";
|
|
23
|
+
}
|
|
24
|
+
return error.message;
|
|
25
|
+
}
|
|
26
|
+
return fallback;
|
|
27
|
+
};
|
|
28
|
+
async function refreshOwnSessionIfSelf(targetUserId, currentUserId) {
|
|
29
|
+
if (currentUserId && targetUserId === currentUserId) {
|
|
30
|
+
// Nieuwe access-token ophalen zodat de user_roles JWT-claim direct actueel is.
|
|
31
|
+
await supabase.auth.refreshSession();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
35
|
+
const auth = useAuth();
|
|
36
|
+
const { isAdmin, user } = auth;
|
|
37
|
+
const authRoles = Array.isArray(auth.roles)
|
|
38
|
+
? auth.roles
|
|
39
|
+
: [];
|
|
40
|
+
const isAdministratie = authRoles.includes("administratie");
|
|
41
|
+
const canAccess = isAdmin || isAdministratie;
|
|
42
|
+
const currentUserId = user?.id;
|
|
43
|
+
const [users, setUsers] = useState([]);
|
|
44
|
+
const [availableRoles, setAvailableRoles] = useState([]);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
const [selectedUser, setSelectedUser] = useState("");
|
|
47
|
+
const [selectedRole, setSelectedRole] = useState("planner");
|
|
48
|
+
const [submitting, setSubmitting] = useState(false);
|
|
49
|
+
const [permRoles, setPermRoles] = useState([]);
|
|
50
|
+
const [permModules, setPermModules] = useState([]);
|
|
51
|
+
const [permMatrix, setPermMatrix] = useState({});
|
|
52
|
+
const [permLoading, setPermLoading] = useState(true);
|
|
53
|
+
const [inviteOpen, setInviteOpen] = useState(false);
|
|
54
|
+
const [inviteName, setInviteName] = useState("");
|
|
55
|
+
const [inviteEmail, setInviteEmail] = useState("");
|
|
56
|
+
const [inviteRole, setInviteRole] = useState("monteur");
|
|
57
|
+
const [inviting, setInviting] = useState(false);
|
|
58
|
+
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
59
|
+
const [deleting, setDeleting] = useState(false);
|
|
60
|
+
const [testOpen, setTestOpen] = useState(false);
|
|
61
|
+
const [testName, setTestName] = useState("");
|
|
62
|
+
const [testRole, setTestRole] = useState("monteur");
|
|
63
|
+
const [testCreating, setTestCreating] = useState(false);
|
|
64
|
+
const [testCredentials, setTestCredentials] = useState(null);
|
|
65
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
66
|
+
const [roleFilter, setRoleFilter] = useState("all");
|
|
67
|
+
const [sortBy, setSortBy] = useState("name-asc");
|
|
68
|
+
const [permUserId, setPermUserId] = useState("");
|
|
69
|
+
const [userPermModules, setUserPermModules] = useState([]);
|
|
70
|
+
const [userPermMap, setUserPermMap] = useState({});
|
|
71
|
+
const [userPermLoading, setUserPermLoading] = useState(false);
|
|
72
|
+
const permKey = (role, moduleId) => `${role}::${moduleId}`;
|
|
73
|
+
const discoveredModules = useDiscoveredModules();
|
|
74
|
+
const shellModules = useMemo(() => {
|
|
75
|
+
const source = Array.isArray(modules) && modules.length > 0 ? modules : discoveredModules;
|
|
76
|
+
return source.filter((m) => Boolean(m?.id) && Boolean(m?.name));
|
|
77
|
+
}, [modules, discoveredModules]);
|
|
78
|
+
const effectiveShellId = shellId ?? (typeof window !== "undefined" ? window.location.host : "unknown-shell");
|
|
79
|
+
const shellModulesKey = shellModules.map((m) => `${m.id}:${m.name}`).join("|");
|
|
80
|
+
const loadUsers = async () => {
|
|
81
|
+
setLoading(true);
|
|
82
|
+
try {
|
|
83
|
+
const result = await getUsersWithRoles();
|
|
84
|
+
setUsers(Array.isArray(result?.users) ? result.users : []);
|
|
85
|
+
setAvailableRoles(Array.isArray(result?.availableRoles) ? result.availableRoles : []);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
toast.error(getErrorMessage(error, "Fout bij het laden van gebruikers en rollen"));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const loadPermissions = async () => {
|
|
95
|
+
setPermLoading(true);
|
|
96
|
+
try {
|
|
97
|
+
const result = await getModulePermissions();
|
|
98
|
+
const roles = Array.isArray(result?.roles) ? result.roles : [];
|
|
99
|
+
const mods = Array.isArray(result?.modules) ? result.modules : [];
|
|
100
|
+
const perms = Array.isArray(result?.permissions) ? result.permissions : [];
|
|
101
|
+
setPermRoles(roles);
|
|
102
|
+
setPermModules(mods);
|
|
103
|
+
const matrix = {};
|
|
104
|
+
for (const r of roles) {
|
|
105
|
+
for (const m of mods) {
|
|
106
|
+
matrix[permKey(r, m.id)] = "none";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const p of perms) {
|
|
110
|
+
matrix[permKey(p.role, p.moduleId)] = p.permission;
|
|
111
|
+
}
|
|
112
|
+
setPermMatrix(matrix);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
toast.error(getErrorMessage(error, "Fout bij het laden van module-machtigingen"));
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setPermLoading(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const loadUserPermissions = async (userId) => {
|
|
122
|
+
if (!userId) {
|
|
123
|
+
setUserPermModules([]);
|
|
124
|
+
setUserPermMap({});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setUserPermLoading(true);
|
|
128
|
+
try {
|
|
129
|
+
const result = await getUserModulePermissions({ data: { userId } });
|
|
130
|
+
const mods = Array.isArray(result?.modules) ? result.modules : [];
|
|
131
|
+
const overrides = Array.isArray(result?.overrides) ? result.overrides : [];
|
|
132
|
+
setUserPermModules(mods);
|
|
133
|
+
const map = {};
|
|
134
|
+
for (const m of mods)
|
|
135
|
+
map[m.id] = "inherit";
|
|
136
|
+
for (const o of overrides)
|
|
137
|
+
map[o.moduleId] = o.permission;
|
|
138
|
+
setUserPermMap(map);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
toast.error(getErrorMessage(error, "Fout bij het laden van persoonlijke rechten"));
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
setUserPermLoading(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!canAccess)
|
|
149
|
+
return;
|
|
150
|
+
let cancelled = false;
|
|
151
|
+
(async () => {
|
|
152
|
+
// Modules die deze shell heeft geladen automatisch registreren.
|
|
153
|
+
if (isAdmin && shellModules.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
await syncModuleRegistry({ data: { modules: shellModules, shellId: effectiveShellId } });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("Module-registratie mislukt", error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (cancelled)
|
|
162
|
+
return;
|
|
163
|
+
loadUsers();
|
|
164
|
+
// Module-machtigingen zijn strikt admin-only.
|
|
165
|
+
if (isAdmin) {
|
|
166
|
+
loadPermissions();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
setPermLoading(false);
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
172
|
+
return () => {
|
|
173
|
+
cancelled = true;
|
|
174
|
+
};
|
|
175
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
176
|
+
}, [canAccess, isAdmin, shellModulesKey, effectiveShellId]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!canAccess)
|
|
179
|
+
return;
|
|
180
|
+
loadUserPermissions(permUserId);
|
|
181
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
+
}, [permUserId, canAccess]);
|
|
183
|
+
const handleUserPermChange = async (moduleId, permission) => {
|
|
184
|
+
if (!permUserId)
|
|
185
|
+
return;
|
|
186
|
+
const prev = userPermMap[moduleId] ?? "inherit";
|
|
187
|
+
setUserPermMap((m) => ({ ...m, [moduleId]: permission }));
|
|
188
|
+
try {
|
|
189
|
+
await setUserModulePermission({ data: { userId: permUserId, moduleId, permission } });
|
|
190
|
+
toast.success("Persoonlijk recht opgeslagen");
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
setUserPermMap((m) => ({ ...m, [moduleId]: prev }));
|
|
194
|
+
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const handleAssign = async () => {
|
|
198
|
+
if (!selectedUser || !selectedRole)
|
|
199
|
+
return;
|
|
200
|
+
setSubmitting(true);
|
|
201
|
+
try {
|
|
202
|
+
await assignRole({ data: { userId: selectedUser, role: selectedRole } });
|
|
203
|
+
await refreshOwnSessionIfSelf(selectedUser, currentUserId);
|
|
204
|
+
toast.success("Rol toegekend");
|
|
205
|
+
await loadUsers();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
toast.error(getErrorMessage(error, "Fout bij het toekennen van rol"));
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
setSubmitting(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const handleRemove = async (userId, role) => {
|
|
215
|
+
try {
|
|
216
|
+
await removeRole({ data: { userId, role } });
|
|
217
|
+
await refreshOwnSessionIfSelf(userId, currentUserId);
|
|
218
|
+
toast.success("Rol verwijderd");
|
|
219
|
+
await loadUsers();
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
toast.error(getErrorMessage(error, "Fout bij het verwijderen van rol"));
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const handlePermChange = async (role, moduleId, permission) => {
|
|
226
|
+
const prev = permMatrix[permKey(role, moduleId)];
|
|
227
|
+
setPermMatrix((m) => ({ ...m, [permKey(role, moduleId)]: permission }));
|
|
228
|
+
try {
|
|
229
|
+
await setModulePermission({ data: { role, moduleId, permission } });
|
|
230
|
+
toast.success("Machtiging opgeslagen");
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
setPermMatrix((m) => ({ ...m, [permKey(role, moduleId)]: prev ?? "none" }));
|
|
234
|
+
toast.error(getErrorMessage(error, "Fout bij het opslaan van machtiging"));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const handleInvite = async () => {
|
|
238
|
+
if (!inviteEmail.trim() || !inviteName.trim())
|
|
239
|
+
return;
|
|
240
|
+
setInviting(true);
|
|
241
|
+
try {
|
|
242
|
+
await inviteUser({
|
|
243
|
+
data: { email: inviteEmail.trim(), displayName: inviteName.trim(), role: inviteRole },
|
|
244
|
+
});
|
|
245
|
+
toast.success(`Uitnodiging verstuurd naar ${inviteEmail.trim()}`);
|
|
246
|
+
setInviteOpen(false);
|
|
247
|
+
setInviteName("");
|
|
248
|
+
setInviteEmail("");
|
|
249
|
+
setInviteRole("monteur");
|
|
250
|
+
await loadUsers();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
toast.error(getErrorMessage(error, "Uitnodigen mislukt"));
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
setInviting(false);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const handleDelete = async () => {
|
|
260
|
+
if (!deleteTarget)
|
|
261
|
+
return;
|
|
262
|
+
setDeleting(true);
|
|
263
|
+
try {
|
|
264
|
+
await deleteUser({ data: { userId: deleteTarget.id } });
|
|
265
|
+
toast.success(`Account ${deleteTarget.email} verwijderd`);
|
|
266
|
+
setDeleteTarget(null);
|
|
267
|
+
await loadUsers();
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
toast.error(getErrorMessage(error, "Verwijderen mislukt"));
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
setDeleting(false);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const handleCreateTestUser = async () => {
|
|
277
|
+
if (!testName.trim())
|
|
278
|
+
return;
|
|
279
|
+
setTestCreating(true);
|
|
280
|
+
try {
|
|
281
|
+
const result = await createTestUser({
|
|
282
|
+
data: { displayName: testName.trim(), role: testRole },
|
|
283
|
+
});
|
|
284
|
+
setTestCredentials({ email: result.email, password: result.password });
|
|
285
|
+
setTestOpen(false);
|
|
286
|
+
setTestName("");
|
|
287
|
+
setTestRole("monteur");
|
|
288
|
+
toast.success("Testaccount aangemaakt");
|
|
289
|
+
await loadUsers();
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
toast.error(getErrorMessage(error, "Aanmaken mislukt"));
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
setTestCreating(false);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const copyToClipboard = async (value, label) => {
|
|
299
|
+
try {
|
|
300
|
+
await navigator.clipboard.writeText(value);
|
|
301
|
+
toast.success(`${label} gekopieerd`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
toast.error("Kopiëren mislukt");
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const selectedUserHasRole = users.some((user) => user.id === selectedUser && user.roles.includes(selectedRole));
|
|
308
|
+
const filteredUsers = useMemo(() => {
|
|
309
|
+
const query = searchQuery.trim().toLowerCase();
|
|
310
|
+
let list = users.filter((user) => {
|
|
311
|
+
const matchesQuery = !query ||
|
|
312
|
+
user.displayName.toLowerCase().includes(query) ||
|
|
313
|
+
user.email.toLowerCase().includes(query);
|
|
314
|
+
const matchesRole = roleFilter === "all" ||
|
|
315
|
+
(roleFilter === "none" ? user.roles.length === 0 : user.roles.includes(roleFilter));
|
|
316
|
+
return matchesQuery && matchesRole;
|
|
317
|
+
});
|
|
318
|
+
list = [...list].sort((a, b) => {
|
|
319
|
+
switch (sortBy) {
|
|
320
|
+
case "name-asc":
|
|
321
|
+
return a.displayName.localeCompare(b.displayName);
|
|
322
|
+
case "name-desc":
|
|
323
|
+
return b.displayName.localeCompare(a.displayName);
|
|
324
|
+
case "role-asc": {
|
|
325
|
+
const roleA = a.roles[0] ?? "";
|
|
326
|
+
const roleB = b.roles[0] ?? "";
|
|
327
|
+
return roleA.localeCompare(roleB);
|
|
328
|
+
}
|
|
329
|
+
case "role-desc": {
|
|
330
|
+
const roleA = a.roles[0] ?? "";
|
|
331
|
+
const roleB = b.roles[0] ?? "";
|
|
332
|
+
return roleB.localeCompare(roleA);
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return list;
|
|
339
|
+
}, [users, searchQuery, roleFilter, sortBy]);
|
|
340
|
+
const hasActiveFilters = searchQuery.trim() !== "" || roleFilter !== "all";
|
|
341
|
+
// Administratie mag de adminrol niet toekennen of verwijderen.
|
|
342
|
+
const baseRoles = availableRoles.length > 0 ? availableRoles : ["admin", "planner", "monteur", "administratie"];
|
|
343
|
+
const assignableRoles = isAdmin ? baseRoles : baseRoles.filter((r) => r !== "admin");
|
|
344
|
+
const canManageUser = (target) => isAdmin || !target.roles.includes("admin");
|
|
345
|
+
if (!canAccess) {
|
|
346
|
+
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." }) }) }) }));
|
|
347
|
+
}
|
|
348
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Tabs, { defaultValue: "users", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "users", children: "Gebruikers & rollen" }), isAdmin && _jsx(TabsTrigger, { value: "permissions", children: "Module-machtigingen" }), _jsx(TabsTrigger, { value: "user-permissions", children: "Rechten per gebruiker" })] }), _jsx(TabsContent, { value: "users", className: "mt-4", children: _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Gebruikersrollen beheren" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => setTestOpen(true), size: "sm", children: [_jsx(FlaskConical, { className: "h-4 w-4 mr-2" }), "Testaccount aanmaken"] }), _jsxs(Button, { onClick: () => setInviteOpen(true), size: "sm", children: [_jsx(UserPlus, { className: "h-4 w-4 mr-2" }), "Account aanmaken"] })] })] }), _jsxs(CardContent, { className: "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: "Gebruiker" }), _jsxs(Select, { value: selectedUser, onValueChange: setSelectedUser, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Selecteer een gebruiker" }) }), _jsx(SelectContent, { children: users.map((user) => (_jsxs(SelectItem, { value: user.id, children: [user.displayName, " (", user.email, ")"] }, user.id))) })] })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Rol" }), _jsxs(Select, { value: selectedRole, onValueChange: (value) => setSelectedRole(value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: assignableRoles.map((role) => (_jsx(SelectItem, { value: role, children: role }, role))) })] })] }), _jsx(Button, { onClick: handleAssign, disabled: !selectedUser || submitting || loading || selectedUserHasRole, children: submitting ? "Toekennen..." : selectedUserHasRole ? "Rol bestaat al" : "Rol toekennen" })] }), _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: (value) => setRoleFilter(value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle rollen" }), availableRoles.map((role) => (_jsx(SelectItem, { value: role, children: role }, role))), _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 ? "Geen gebruikers gevonden met de huidige filters." : "Geen gebruikers gevonden." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Gebruiker" }), _jsx(TableHead, { children: "E-mail" }), _jsx(TableHead, { children: "Rollen" }), _jsx(TableHead, { className: "w-24" })] }) }), _jsx(TableBody, { children: filteredUsers.map((user) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { children: user.displayName }), !user.emailConfirmed && (_jsx(Badge, { variant: "outline", className: "text-xs", children: "Wacht op verificatie" })), user.isTestAccount && (_jsx(Badge, { variant: "secondary", className: "text-xs", children: "Test" }))] }) }), _jsx(TableCell, { children: user.email }), _jsx(TableCell, { children: _jsx("div", { className: "flex flex-wrap gap-2", children: user.roles.length === 0 ? (_jsx("span", { className: "text-sm text-muted-foreground", children: "Geen rollen" })) : (user.roles.map((role) => (_jsxs(Badge, { variant: "secondary", className: "gap-1", children: [role, !(role === "admin" && !isAdmin) && (_jsx("button", { onClick: () => handleRemove(user.id, role), className: "ml-1 rounded-sm hover:text-destructive", "aria-label": `Verwijder rol ${role}`, children: _jsx(Trash2, { className: "h-3 w-3" }) }))] }, `${user.id}-${role}`)))) }) }), _jsx(TableCell, { className: "text-right", children: _jsx(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive hover:bg-destructive/10", disabled: user.id === currentUserId || !canManageUser(user), title: user.id === currentUserId
|
|
349
|
+
? "Je kunt je eigen account niet verwijderen."
|
|
350
|
+
: !canManageUser(user)
|
|
351
|
+
? "Alleen een admin kan een adminaccount verwijderen."
|
|
352
|
+
: "Verwijder account", onClick: () => setDeleteTarget(user), "aria-label": `Verwijder account ${user.email}`, children: _jsx(UserX, { className: "h-4 w-4" }) }) })] }, user.id))) })] }))] })] }) }), isAdmin && (_jsx(TabsContent, { value: "permissions", className: "mt-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Module-machtigingen per rol" })] }) }), _jsx(CardContent, { children: permLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Module" }), permRoles.map((role) => (_jsx(TableHead, { className: "capitalize", children: role }, role)))] }) }), _jsx(TableBody, { children: permModules.map((mod) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), permRoles.map((role) => {
|
|
353
|
+
const value = permMatrix[permKey(role, mod.id)] ?? "none";
|
|
354
|
+
return (_jsx(TableCell, { children: _jsxs(Select, { value: value, onValueChange: (v) => handlePermChange(role, mod.id, v), children: [_jsx(SelectTrigger, { className: "w-40", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(PERMISSION_LABELS).map((p) => (_jsx(SelectItem, { value: p, children: PERMISSION_LABELS[p] }, p))) })] }) }, role));
|
|
355
|
+
})] }, mod.id))) })] })) })] }) })), _jsx(TabsContent, { value: "user-permissions", className: "mt-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Rechten per gebruiker" })] }) }), _jsxs(CardContent, { className: "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" }), _jsx(TableHead, { className: "w-56", children: "Recht" })] }) }), _jsx(TableBody, { children: userPermModules.map((mod) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), _jsx(TableCell, { children: _jsxs(Select, { value: userPermMap[mod.id] ?? "inherit", onValueChange: (v) => handleUserPermChange(mod.id, v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(USER_PERMISSION_LABELS).map((p) => (_jsx(SelectItem, { value: p, children: USER_PERMISSION_LABELS[p] }, p))) })] }) })] }, mod.id))) })] }))] })] }) })] }), _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: (v) => setInviteRole(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: assignableRoles.map((r) => (_jsx(SelectItem, { value: r, children: r }, r))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInviteOpen(false), disabled: inviting, children: "Annuleren" }), _jsx(Button, { onClick: handleInvite, disabled: inviting || !inviteEmail.trim() || !inviteName.trim(), 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: (v) => setTestRole(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: assignableRoles.map((r) => (_jsx(SelectItem, { value: r, children: r }, r))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setTestOpen(false), disabled: testCreating, children: "Annuleren" }), _jsx(Button, { onClick: handleCreateTestUser, disabled: testCreating || !testName.trim(), 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: 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) => {
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
handleDelete();
|
|
358
|
+
}, disabled: deleting, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: deleting ? "Verwijderen..." : "Verwijderen" })] })] }) })] }));
|
|
359
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModuleLogo.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModuleLogo.tsx"],"names":[],"mappings":"AAQA,UAAU,uBAAuB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,+BAwBxE"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplateModuleLogo({ className }) {
|
|
3
|
+
return (_jsxs("svg", { viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className, "aria-hidden": "true", children: [_jsx("rect", { width: "40", height: "40", rx: "8", fill: "currentColor", opacity: "0.1" }), _jsx("text", { x: "50%", y: "50%", dominantBaseline: "middle", textAnchor: "middle", fontSize: "18", fontWeight: "600", fill: "currentColor", children: "M" })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModulePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModulePage.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,kBAAkB,gCAmFjC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/TemplateModulePage.tsx — EXAMPLE PAGE COMPONENT
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Pagina-componenten horen in src/components/, NIET alleen in src/routes/.
|
|
6
|
+
// src/routes/ wordt NIET naar npm gepubliceerd (zie tsconfig.build.json exclude).
|
|
7
|
+
//
|
|
8
|
+
// GEEN <Header /> hier — die zit in _authenticated.tsx via staticData.title
|
|
9
|
+
// ============================================================================
|
|
10
|
+
import { useState, useEffect } from "react";
|
|
11
|
+
import { useAuth, supabase, Button, Card, CardContent, CardHeader, CardTitle, toast, } from "@flowselections/core";
|
|
12
|
+
import { TemplateModuleLogo } from './TemplateModuleLogo';
|
|
13
|
+
export function TemplateModulePage() {
|
|
14
|
+
// Auth is guaranteed — this user is already logged in
|
|
15
|
+
const { user, isAdmin } = useAuth();
|
|
16
|
+
// Example state — replace with your own
|
|
17
|
+
const [items, setItems] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
loadData();
|
|
21
|
+
}, []);
|
|
22
|
+
const loadData = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
// Example query — replace "products" with your table
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from("products")
|
|
28
|
+
.select("*")
|
|
29
|
+
.limit(10);
|
|
30
|
+
if (error)
|
|
31
|
+
throw error;
|
|
32
|
+
// ✅ Altijd Array.isArray bewaken op server-function data
|
|
33
|
+
// const safeItems = Array.isArray(data?.items) ? data.items : [];
|
|
34
|
+
setItems(Array.isArray(data) ? data : []);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
toast.error("Fout bij het laden van gegevens");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [isAdmin && (_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Ingelogd als admin: ", user?.email] })), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Overzicht" }) }), _jsxs(CardContent, { children: [loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen gegevens gevonden." })) : (_jsxs("p", { className: "text-sm", children: [items.length, " items geladen."] })), _jsx(Button, { onClick: loadData, className: "mt-4", disabled: loading, children: "Vernieuwen" })] })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(TemplateModuleLogo, { className: "w-6 h-6 text-primary" }), _jsx("span", { children: "SVG-component (voorkeur voor vector-logo's)" })] })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplatePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplatePage.tsx"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,gCAS3B"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function TemplatePage() {
|
|
3
|
+
return (_jsxs("div", { className: "p-6", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Module pagina" }), _jsx("p", { className: "text-muted-foreground mt-2", children: "Vervang dit component met de pagina's van jouw module." })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MijnModuleSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/MijnModuleSettingsCard.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,sBAAsB,gCA+CrC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/settings/MijnModuleSettingsCard.tsx — EXAMPLE SETTINGS CARD
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// This is an example of a settings card for your module.
|
|
6
|
+
// It appears on the main settings page when registered in src/index.ts.
|
|
7
|
+
//
|
|
8
|
+
// RULES:
|
|
9
|
+
// - Must return a <Card> as its root element
|
|
10
|
+
// - Handles its own data fetching — no props are passed by the core
|
|
11
|
+
// - Use the same Card primitives from @flowselections/core
|
|
12
|
+
//
|
|
13
|
+
// WHAT TO DO:
|
|
14
|
+
// - Rename this file to match your card's purpose
|
|
15
|
+
// - Add your real settings UI
|
|
16
|
+
// - Register it in src/index.ts under settingsCards
|
|
17
|
+
// - Delete this file entirely if your module has no settings
|
|
18
|
+
// ============================================================================
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Input, Label, toast, } from "@flowselections/core";
|
|
21
|
+
import { Settings2 } from "lucide-react";
|
|
22
|
+
export function MijnModuleSettingsCard() {
|
|
23
|
+
// Replace with your real settings state
|
|
24
|
+
const [setting, setSetting] = useState("");
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const handleSave = async () => {
|
|
27
|
+
setSaving(true);
|
|
28
|
+
try {
|
|
29
|
+
// Example: save to a table — replace with your real query
|
|
30
|
+
// const { error } = await supabase.from("my_settings").upsert({ ... });
|
|
31
|
+
// if (error) throw error;
|
|
32
|
+
toast.success("Instellingen opgeslagen");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
toast.error("Fout bij het opslaan");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setSaving(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Module Instellingen" }), " "] }), _jsx(CardDescription, { children: "Beheer de instellingen voor deze module " })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "setting", children: "Instelling" }), _jsx(Input, { id: "setting", value: setting, onChange: (e) => setSetting(e.target.value), placeholder: "Waarde..." })] }), _jsx(Button, { onClick: handleSave, disabled: saving, children: saving ? "Opslaan..." : "Opslaan" })] })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface MijnItem {
|
|
2
|
+
id: string;
|
|
3
|
+
created_at: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare function useMijnModuleData(): {
|
|
7
|
+
items: MijnItem[];
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
reload: () => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=useMijnModuleData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMijnModuleData.d.ts","sourceRoot":"","sources":["../../src/hooks/useMijnModuleData.ts"],"names":[],"mappings":"AAkBA,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,iBAAiB;;;;;EAkChC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/hooks/useMijnModuleData.ts — EXAMPLE DATA HOOK
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Put your data fetching logic in hooks like this, not directly in components.
|
|
5
|
+
// This keeps your pages clean and makes it easy to reuse data across pages.
|
|
6
|
+
//
|
|
7
|
+
// WHAT TO DO:
|
|
8
|
+
// - Rename to match your data (e.g. useProducts, useOrders, useSuppliers)
|
|
9
|
+
// - Replace the example query with your real Supabase query
|
|
10
|
+
// - Replace MijnItem with your actual table's row type
|
|
11
|
+
// - Import and use in your page components
|
|
12
|
+
// - Delete this file if you prefer a different pattern
|
|
13
|
+
// ============================================================================
|
|
14
|
+
import { useState, useEffect, useCallback } from "react";
|
|
15
|
+
import { supabase, toast } from "@flowselections/core";
|
|
16
|
+
export function useMijnModuleData() {
|
|
17
|
+
const [items, setItems] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const load = useCallback(async () => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
const { data, error } = await supabase
|
|
25
|
+
.from("products") // ← CHANGE THIS to your table
|
|
26
|
+
.select("*")
|
|
27
|
+
.order("created_at", { ascending: false });
|
|
28
|
+
if (error)
|
|
29
|
+
throw error;
|
|
30
|
+
setItems(data ?? []);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
setError("Fout bij het laden van gegevens");
|
|
34
|
+
toast.error("Fout bij het laden van gegevens");
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
load();
|
|
42
|
+
}, [load]);
|
|
43
|
+
return {
|
|
44
|
+
items,
|
|
45
|
+
loading,
|
|
46
|
+
error,
|
|
47
|
+
reload: load,
|
|
48
|
+
};
|
|
49
|
+
}
|