@flowselections/admin-installatietechniek 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
  2. package/dist-lib/components/AdminForInstallatieTechniek.js +265 -21
  3. package/dist-lib/hooks/useModulePermission.d.ts +2 -1
  4. package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
  5. package/dist-lib/index.d.ts +5 -4
  6. package/dist-lib/index.d.ts.map +1 -1
  7. package/dist-lib/index.js +3 -3
  8. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
  9. package/dist-lib/integrations/supabase/auth-attacher.js +11 -1
  10. package/dist-lib/integrations/supabase/auth-middleware.d.ts +841 -155
  11. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/client.d.ts +841 -155
  13. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/client.server.d.ts +841 -155
  15. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  16. package/dist-lib/integrations/supabase/types.d.ts +870 -157
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/types.js +3 -0
  19. package/dist-lib/lib/admin.functions.d.ts +146927 -1852
  20. package/dist-lib/lib/admin.functions.d.ts.map +1 -1
  21. package/dist-lib/lib/admin.functions.js +446 -25
  22. package/dist-lib/lib/permission-types.d.ts +7 -0
  23. package/dist-lib/lib/permission-types.d.ts.map +1 -0
  24. package/dist-lib/lib/permission-types.js +1 -0
  25. package/dist-lib/lib/permissions.server.d.ts +1 -1
  26. package/dist-lib/lib/permissions.server.d.ts.map +1 -1
  27. package/package.json +3 -3
@@ -31,6 +31,7 @@ const roleIdInput = z.object({ roleId: z.string().uuid() });
31
31
  const assignRoleInput = z.object({
32
32
  userId: z.string().uuid(),
33
33
  roleId: z.string().uuid(),
34
+ profileId: z.string().uuid().optional(),
34
35
  });
35
36
  const rolePermissionsSchema = z
36
37
  .array(z.object({
@@ -53,6 +54,7 @@ const inviteUserInput = z.object({
53
54
  email: z.string().trim().email().max(255),
54
55
  displayName: z.string().trim().min(1).max(100),
55
56
  roleId: z.string().uuid(),
57
+ profileId: z.string().uuid().optional(),
56
58
  });
57
59
  const deleteUserInput = z.object({
58
60
  userId: z.string().uuid(),
@@ -65,6 +67,7 @@ const createTestUserInput = z.object({
65
67
  displayName: z.string().trim().min(1).max(100),
66
68
  roleId: z.string().uuid(),
67
69
  email: z.string().trim().email().max(255).optional(),
70
+ profileId: z.string().uuid().optional(),
68
71
  });
69
72
  function slugify(value) {
70
73
  return (value
@@ -94,13 +97,93 @@ async function assertMayUseRole(supabase, roleId, isAdmin) {
94
97
  }
95
98
  return role;
96
99
  }
97
- export const getUsersWithRoles = createServerFn({ method: "GET" })
100
+ const profileScopeInput = z
101
+ .object({ profileId: z.string().uuid().optional() })
102
+ .optional();
103
+ /**
104
+ * Tenant + rol-/module-scope van een bedrijfsprofiel.
105
+ * Zonder `profileId` is er geen scope (= overzicht: alles zichtbaar).
106
+ */
107
+ async function loadProfileScope(supabase, profileId) {
108
+ if (!profileId)
109
+ return null;
110
+ const { data, error } = await supabase
111
+ .from("profiles_org")
112
+ .select("id, name, tenant_id, is_default")
113
+ .eq("id", profileId)
114
+ .maybeSingle();
115
+ if (error)
116
+ throw error;
117
+ if (!data)
118
+ return null;
119
+ const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
120
+ supabase.from("profile_roles").select("role_id").eq("profile_id", data.id),
121
+ supabase.from("profile_modules").select("module_id").eq("profile_id", data.id),
122
+ ]);
123
+ return {
124
+ id: data.id,
125
+ name: data.name,
126
+ tenantId: (data.tenant_id ?? null),
127
+ roleIds: (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id),
128
+ moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
129
+ };
130
+ }
131
+ /**
132
+ * Bepaalt op welk bedrijfsprofiel deze aanvraag draait.
133
+ * - Admin: vrije keuze (leeg = overzicht met alles).
134
+ * - Niet-admin (administratie): altijd vastgezet op het eigen profiel,
135
+ * afgeleid uit de tenant van de gebruiker of anders uit de profielen
136
+ * waaraan een van zijn rollen gekoppeld is.
137
+ */
138
+ async function resolveEffectiveProfileId(admin, userId, requestedProfileId, isAdmin) {
139
+ if (isAdmin)
140
+ return { profileId: requestedProfileId, locked: false };
141
+ const { data: me } = await admin
142
+ .from("profiles")
143
+ .select("tenant_id")
144
+ .eq("id", userId)
145
+ .maybeSingle();
146
+ if (me?.tenant_id) {
147
+ const { data: byTenant } = await admin
148
+ .from("profiles_org")
149
+ .select("id")
150
+ .eq("tenant_id", me.tenant_id)
151
+ .limit(1);
152
+ const first = Array.isArray(byTenant) ? byTenant[0] : null;
153
+ if (first?.id)
154
+ return { profileId: first.id, locked: true };
155
+ }
156
+ const { data: myRoles } = await admin
157
+ .from("role_assignments")
158
+ .select("role_id")
159
+ .eq("user_id", userId);
160
+ const roleIds = (Array.isArray(myRoles) ? myRoles : []).map((r) => r.role_id);
161
+ if (roleIds.length > 0) {
162
+ const { data: links } = await admin
163
+ .from("profile_roles")
164
+ .select("profile_id")
165
+ .in("role_id", roleIds)
166
+ .limit(1);
167
+ const link = Array.isArray(links) ? links[0] : null;
168
+ if (link?.profile_id)
169
+ return { profileId: link.profile_id, locked: true };
170
+ }
171
+ return { profileId: undefined, locked: true };
172
+ }
173
+ export const getUsersWithRoles = createServerFn({ method: "POST" })
98
174
  .middleware([requireSupabaseAuth])
99
- .handler(async ({ context }) => {
175
+ .inputValidator((data) => profileScopeInput.parse(data))
176
+ .handler(async ({ context, data }) => {
100
177
  const { isAdmin } = await requireUserAdmin(context);
101
178
  const { supabaseAdmin } = await import("../integrations/supabase/client.server");
179
+ const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
180
+ const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
181
+ const profilesQuery = supabaseAdmin
182
+ .from("profiles")
183
+ .select("id, email, display_name, tenant_id")
184
+ .order("email", { ascending: true });
102
185
  const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
103
- supabaseAdmin.from("profiles").select("id, email, display_name").order("email", { ascending: true }),
186
+ profilesQuery,
104
187
  supabaseAdmin.from("role_assignments").select("user_id, role_id"),
105
188
  supabaseAdmin.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
106
189
  supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
@@ -115,9 +198,12 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
115
198
  throw authError;
116
199
  const safeProfiles = Array.isArray(profiles) ? profiles : [];
117
200
  const safeAssignments = Array.isArray(assignments) ? assignments : [];
118
- const safeRoles = Array.isArray(roles) ? roles : [];
201
+ const allRoles = Array.isArray(roles) ? roles : [];
202
+ const safeRoles = scope && scope.roleIds.length > 0
203
+ ? allRoles.filter((r) => scope.roleIds.includes(r.id))
204
+ : allRoles;
119
205
  const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
120
- const roleById = new Map(safeRoles.map((r) => [r.id, r]));
206
+ const roleById = new Map(allRoles.map((r) => [r.id, r]));
121
207
  const confirmedById = new Map();
122
208
  const isTestById = new Map();
123
209
  for (const u of safeAuthUsers) {
@@ -136,19 +222,52 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
136
222
  }
137
223
  rolesByUser.set(a.user_id, list);
138
224
  }
225
+ // Binnen een profiel horen gebruikers erbij via de tenant OF via een rol
226
+ // die aan het profiel gekoppeld is. Zonder profiel: iedereen.
227
+ const scopedRoleIds = new Set(scope?.roleIds ?? []);
228
+ const usersViaRole = new Set();
229
+ if (scope && scopedRoleIds.size > 0) {
230
+ for (const a of safeAssignments) {
231
+ if (scopedRoleIds.has(a.role_id))
232
+ usersViaRole.add(a.user_id);
233
+ }
234
+ }
235
+ // Vastgezette gebruiker zonder gekoppeld profiel: niets tonen.
236
+ const noProfileLock = effective.locked && !scope;
237
+ const scopedProfiles = noProfileLock
238
+ ? []
239
+ : !scope
240
+ ? safeProfiles
241
+ : safeProfiles.filter((p) => {
242
+ const viaTenant = Boolean(scope.tenantId) && p.tenant_id === scope.tenantId;
243
+ return viaTenant || usersViaRole.has(p.id);
244
+ });
139
245
  return {
140
- users: safeProfiles.map((profile) => ({
141
- id: profile.id,
142
- email: profile.email ?? "-",
143
- displayName: profile.display_name ?? "-",
144
- roles: rolesByUser.get(profile.id) ?? [],
145
- emailConfirmed: confirmedById.get(profile.id) ?? false,
146
- isTestAccount: isTestById.get(profile.id) ?? false,
147
- })),
148
- availableRoles: safeRoles
246
+ users: scopedProfiles.map((profile) => {
247
+ const viaTenant = Boolean(scope?.tenantId) && profile.tenant_id === scope?.tenantId;
248
+ const viaRole = usersViaRole.has(profile.id);
249
+ return {
250
+ id: profile.id,
251
+ email: profile.email ?? "-",
252
+ displayName: profile.display_name ?? "-",
253
+ roles: rolesByUser.get(profile.id) ?? [],
254
+ emailConfirmed: confirmedById.get(profile.id) ?? false,
255
+ isTestAccount: isTestById.get(profile.id) ?? false,
256
+ matchedVia: !scope
257
+ ? null
258
+ : viaTenant && viaRole
259
+ ? "both"
260
+ : viaTenant
261
+ ? "tenant"
262
+ : "role",
263
+ };
264
+ }),
265
+ availableRoles: (noProfileLock ? [] : safeRoles)
149
266
  .filter((r) => isAdmin || r.key !== "admin")
150
267
  .map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
151
268
  isAdmin,
269
+ profileLocked: effective.locked,
270
+ activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
152
271
  };
153
272
  });
154
273
  export const assignRole = createServerFn({ method: "POST" })
@@ -172,6 +291,19 @@ export const assignRole = createServerFn({ method: "POST" })
172
291
  .insert({ user_id: data.userId, role_id: data.roleId });
173
292
  if (error)
174
293
  throw error;
294
+ // Binnen een actief bedrijfsprofiel: koppel de gebruiker aan de tenant van
295
+ // dat profiel als die nog leeg is, zodat de koppeling vanzelf dichtgroeit.
296
+ if (data.profileId) {
297
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
298
+ const scope = await loadProfileScope(supabaseAdmin, data.profileId);
299
+ if (scope?.tenantId) {
300
+ await supabaseAdmin
301
+ .from("profiles")
302
+ .update({ tenant_id: scope.tenantId })
303
+ .eq("id", data.userId)
304
+ .is("tenant_id", null);
305
+ }
306
+ }
175
307
  return { ok: true };
176
308
  });
177
309
  export const removeRole = createServerFn({ method: "POST" })
@@ -193,10 +325,14 @@ export const removeRole = createServerFn({ method: "POST" })
193
325
  return { ok: true };
194
326
  });
195
327
  /** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
196
- export const getRoles = createServerFn({ method: "GET" })
328
+ export const getRoles = createServerFn({ method: "POST" })
197
329
  .middleware([requireSupabaseAuth])
198
- .handler(async ({ context }) => {
199
- await requireUserAdmin(context);
330
+ .inputValidator((data) => profileScopeInput.parse(data))
331
+ .handler(async ({ context, data }) => {
332
+ const { isAdmin } = await requireUserAdmin(context);
333
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
334
+ const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
335
+ const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
200
336
  const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
201
337
  context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
202
338
  context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
@@ -211,9 +347,16 @@ export const getRoles = createServerFn({ method: "GET" })
211
347
  throw registryError;
212
348
  if (featuresError)
213
349
  throw featuresError;
214
- const safeRoles = Array.isArray(roles) ? roles : [];
350
+ const noProfileLock = effective.locked && !scope;
351
+ const allRoles = noProfileLock ? [] : Array.isArray(roles) ? roles : [];
352
+ const safeRoles = scope && scope.roleIds.length > 0
353
+ ? allRoles.filter((r) => scope.roleIds.includes(r.id))
354
+ : allRoles;
215
355
  const safePerms = Array.isArray(perms) ? perms : [];
216
- const safeRegistry = Array.isArray(registry) ? registry : [];
356
+ const allRegistry = noProfileLock ? [] : Array.isArray(registry) ? registry : [];
357
+ const safeRegistry = scope && scope.moduleIds.length > 0
358
+ ? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
359
+ : allRegistry;
217
360
  const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
218
361
  return {
219
362
  roles: safeRoles.map((r) => ({
@@ -239,6 +382,8 @@ export const getRoles = createServerFn({ method: "GET" })
239
382
  canView: Boolean(p.can_view),
240
383
  canEdit: Boolean(p.can_edit),
241
384
  })),
385
+ profileLocked: effective.locked,
386
+ activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
242
387
  };
243
388
  });
244
389
  async function writeRolePermissions(supabase, roleId, permissions) {
@@ -368,12 +513,22 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
368
513
  if (modules.length === 0)
369
514
  return { ok: true, count: 0 };
370
515
  const now = new Date().toISOString();
516
+ const { data: existingRows, error: existingError } = await context.supabase
517
+ .from("module_registry")
518
+ .select("module_id")
519
+ .in("module_id", modules.map((m) => m.id));
520
+ if (existingError)
521
+ throw existingError;
522
+ const knownIds = new Set((Array.isArray(existingRows) ? existingRows : []).map((r) => r.module_id));
371
523
  const { error } = await context.supabase.from("module_registry").upsert(modules.map((m) => ({
372
524
  module_id: m.id,
373
525
  name: m.name,
374
526
  shell_id: data.shellId ?? null,
375
527
  is_active: true,
376
528
  last_seen_at: now,
529
+ // Nieuwe modules komen standaard met afdwingen aan binnen; bestaande
530
+ // modules behouden hun eigen instelling.
531
+ ...(knownIds.has(m.id) ? {} : { enforce: true }),
377
532
  })), { onConflict: "module_id" });
378
533
  if (error)
379
534
  throw error;
@@ -525,9 +680,13 @@ export const inviteUser = createServerFn({ method: "POST" })
525
680
  throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
526
681
  }
527
682
  const userId = invited.user.id;
528
- const { error: profileError } = await supabaseAdmin
529
- .from("profiles")
530
- .upsert({ id: userId, email: data.email, display_name: data.displayName }, { onConflict: "id" });
683
+ const scope = await loadProfileScope(supabaseAdmin, data.profileId);
684
+ const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
685
+ id: userId,
686
+ email: data.email,
687
+ display_name: data.displayName,
688
+ ...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
689
+ }, { onConflict: "id" });
531
690
  if (profileError)
532
691
  throw profileError;
533
692
  const { error: roleError } = await supabaseAdmin
@@ -623,9 +782,13 @@ export const createTestUser = createServerFn({ method: "POST" })
623
782
  throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
624
783
  }
625
784
  const userId = created.user.id;
626
- const { error: profileError } = await supabaseAdmin
627
- .from("profiles")
628
- .upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
785
+ const scope = await loadProfileScope(supabaseAdmin, data.profileId);
786
+ const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
787
+ id: userId,
788
+ email,
789
+ display_name: data.displayName,
790
+ ...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
791
+ }, { onConflict: "id" });
629
792
  if (profileError)
630
793
  throw profileError;
631
794
  const { error: roleError } = await supabaseAdmin
@@ -635,3 +798,261 @@ export const createTestUser = createServerFn({ method: "POST" })
635
798
  throw roleError;
636
799
  return { ok: true, userId, email, password };
637
800
  });
801
+ // ============================================================================
802
+ // Bedrijfsprofielen: bundelen rollen + modules per organisatie (tenant)
803
+ // ============================================================================
804
+ const profileIdInput = z.object({ profileId: z.string().uuid() });
805
+ const profileBodySchema = {
806
+ name: z.string().trim().min(1).max(120),
807
+ tenantId: z.string().uuid().nullable().optional(),
808
+ /** Naam van het bedrijf; maakt een nieuw bedrijf aan of hernoemt het gekozen bedrijf. */
809
+ tenantName: z.string().trim().max(120).optional(),
810
+ roleIds: z.array(z.string().uuid()).max(200).default([]),
811
+ moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
812
+ };
813
+ const createProfileInput = z.object(profileBodySchema);
814
+ const updateProfileInput = z.object({ profileId: z.string().uuid(), ...profileBodySchema });
815
+ /**
816
+ * Bepaalt het tenant-id voor een profiel:
817
+ * - bestaand bedrijf + gewijzigde naam -> hernoemen
818
+ * - geen bedrijf + naam -> nieuw bedrijf aanmaken
819
+ */
820
+ async function resolveTenant(supabase, tenantId, tenantName) {
821
+ const name = (tenantName ?? "").trim();
822
+ if (tenantId) {
823
+ if (name) {
824
+ const { data: current, error: readError } = await supabase
825
+ .from("tenants")
826
+ .select("name")
827
+ .eq("id", tenantId)
828
+ .maybeSingle();
829
+ if (readError)
830
+ throw readError;
831
+ if (current && current.name !== name) {
832
+ const { error } = await supabase
833
+ .from("tenants")
834
+ .update({ name, updated_at: new Date().toISOString() })
835
+ .eq("id", tenantId);
836
+ if (error)
837
+ throw error;
838
+ }
839
+ }
840
+ return tenantId;
841
+ }
842
+ if (!name)
843
+ return null;
844
+ const { data: created, error } = await supabase
845
+ .from("tenants")
846
+ .insert({ name })
847
+ .select("id")
848
+ .single();
849
+ if (error)
850
+ throw error;
851
+ return created.id;
852
+ }
853
+ async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
854
+ const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
855
+ if (delRoles)
856
+ throw delRoles;
857
+ const { error: delModules } = await supabase.from("profile_modules").delete().eq("profile_id", profileId);
858
+ if (delModules)
859
+ throw delModules;
860
+ const uniqueRoles = Array.from(new Set(roleIds));
861
+ if (uniqueRoles.length > 0) {
862
+ const { error } = await supabase
863
+ .from("profile_roles")
864
+ .insert(uniqueRoles.map((roleId) => ({ profile_id: profileId, role_id: roleId })));
865
+ if (error)
866
+ throw error;
867
+ }
868
+ const uniqueModules = Array.from(new Set(moduleIds));
869
+ if (uniqueModules.length > 0) {
870
+ const { error } = await supabase
871
+ .from("profile_modules")
872
+ .insert(uniqueModules.map((moduleId) => ({ profile_id: profileId, module_id: moduleId })));
873
+ if (error)
874
+ throw error;
875
+ }
876
+ }
877
+ /** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
878
+ export const getProfiles = createServerFn({ method: "GET" })
879
+ .middleware([requireSupabaseAuth])
880
+ .handler(async ({ context }) => {
881
+ const { isAdmin } = await requireUserAdmin(context);
882
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
883
+ const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, undefined, isAdmin);
884
+ const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
885
+ context.supabase
886
+ .from("profiles_org")
887
+ .select("id, name, tenant_id, is_default")
888
+ .order("name", { ascending: true }),
889
+ context.supabase.from("profile_roles").select("profile_id, role_id"),
890
+ context.supabase.from("profile_modules").select("profile_id, module_id"),
891
+ // `tenants` is via RLS beperkt tot de eigen tenant; namen tonen mag hier
892
+ // omdat de aanvrager al is geverifieerd als admin/administratie.
893
+ supabaseAdmin.from("tenants").select("id, name").order("name", { ascending: true }),
894
+ ]);
895
+ if (profilesError)
896
+ throw profilesError;
897
+ if (profileRolesError)
898
+ throw profileRolesError;
899
+ if (profileModulesError)
900
+ throw profileModulesError;
901
+ if (tenantsError)
902
+ throw tenantsError;
903
+ const { data: userRows, error: userError } = await supabaseAdmin
904
+ .from("profiles")
905
+ .select("id, tenant_id");
906
+ if (userError)
907
+ throw userError;
908
+ const allProfilesOrg = Array.isArray(profilesOrg) ? profilesOrg : [];
909
+ // Niet-admin: alleen het eigen (vastgezette) profiel.
910
+ const safeProfiles = effective.locked
911
+ ? allProfilesOrg.filter((p) => p.id === effective.profileId)
912
+ : allProfilesOrg;
913
+ const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
914
+ const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
915
+ const safeTenants = Array.isArray(tenants) ? tenants : [];
916
+ const safeUsers = Array.isArray(userRows) ? userRows : [];
917
+ const usersByTenant = new Map();
918
+ for (const u of safeUsers) {
919
+ if (!u.tenant_id)
920
+ continue;
921
+ usersByTenant.set(u.tenant_id, (usersByTenant.get(u.tenant_id) ?? 0) + 1);
922
+ }
923
+ const tenantName = new Map(safeTenants.map((t) => [t.id, t.name]));
924
+ return {
925
+ profiles: safeProfiles.map((p) => ({
926
+ id: p.id,
927
+ name: p.name,
928
+ tenantId: (p.tenant_id ?? null),
929
+ tenantName: p.tenant_id ? tenantName.get(p.tenant_id) ?? null : null,
930
+ isDefault: Boolean(p.is_default),
931
+ roleIds: safeProfileRoles.filter((r) => r.profile_id === p.id).map((r) => r.role_id),
932
+ moduleIds: safeProfileModules
933
+ .filter((m) => m.profile_id === p.id)
934
+ .map((m) => m.module_id),
935
+ userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
936
+ })),
937
+ tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
938
+ profileLocked: effective.locked,
939
+ lockedProfileId: effective.locked ? effective.profileId ?? null : null,
940
+ };
941
+ });
942
+ export const createProfile = createServerFn({ method: "POST" })
943
+ .middleware([requireSupabaseAuth])
944
+ .inputValidator((data) => createProfileInput.parse(data))
945
+ .handler(async ({ context, data }) => {
946
+ await requireAdmin(context);
947
+ // `tenants` heeft geen insert-policy voor admins; na de adminguard mag dit
948
+ // veilig via de serverclient.
949
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
950
+ const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
951
+ const { data: created, error } = await context.supabase
952
+ .from("profiles_org")
953
+ .insert({ name: data.name, tenant_id: tenantId })
954
+ .select("id")
955
+ .single();
956
+ if (error)
957
+ throw error;
958
+ await writeProfileLinks(context.supabase, created.id, data.roleIds, data.moduleIds);
959
+ return { ok: true, profileId: created.id };
960
+ });
961
+ export const updateProfile = createServerFn({ method: "POST" })
962
+ .middleware([requireSupabaseAuth])
963
+ .inputValidator((data) => updateProfileInput.parse(data))
964
+ .handler(async ({ context, data }) => {
965
+ await requireAdmin(context);
966
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
967
+ const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
968
+ const { error } = await context.supabase
969
+ .from("profiles_org")
970
+ .update({ name: data.name, tenant_id: tenantId, updated_at: new Date().toISOString() })
971
+ .eq("id", data.profileId);
972
+ if (error)
973
+ throw error;
974
+ await writeProfileLinks(context.supabase, data.profileId, data.roleIds, data.moduleIds);
975
+ return { ok: true };
976
+ });
977
+ export const duplicateProfile = createServerFn({ method: "POST" })
978
+ .middleware([requireSupabaseAuth])
979
+ .inputValidator((data) => profileIdInput.parse(data))
980
+ .handler(async ({ context, data }) => {
981
+ await requireAdmin(context);
982
+ const { data: source, error: sourceError } = await context.supabase
983
+ .from("profiles_org")
984
+ .select("name, tenant_id")
985
+ .eq("id", data.profileId)
986
+ .maybeSingle();
987
+ if (sourceError)
988
+ throw sourceError;
989
+ if (!source)
990
+ throw new Error("Profiel bestaat niet.");
991
+ const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
992
+ context.supabase.from("profile_roles").select("role_id").eq("profile_id", data.profileId),
993
+ context.supabase.from("profile_modules").select("module_id").eq("profile_id", data.profileId),
994
+ ]);
995
+ const { data: created, error } = await context.supabase
996
+ .from("profiles_org")
997
+ .insert({ name: `${source.name} (kopie)`, tenant_id: source.tenant_id })
998
+ .select("id")
999
+ .single();
1000
+ if (error)
1001
+ throw error;
1002
+ await writeProfileLinks(context.supabase, created.id, (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id), (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id));
1003
+ return { ok: true, profileId: created.id };
1004
+ });
1005
+ export const deleteProfile = createServerFn({ method: "POST" })
1006
+ .middleware([requireSupabaseAuth])
1007
+ .inputValidator((data) => profileIdInput.parse(data))
1008
+ .handler(async ({ context, data }) => {
1009
+ await requireAdmin(context);
1010
+ const { data: profile, error: profileError } = await context.supabase
1011
+ .from("profiles_org")
1012
+ .select("id, tenant_id, is_default")
1013
+ .eq("id", data.profileId)
1014
+ .maybeSingle();
1015
+ if (profileError)
1016
+ throw profileError;
1017
+ if (!profile)
1018
+ throw new Error("Profiel bestaat niet.");
1019
+ if (profile.is_default)
1020
+ throw new Error("Het actieve profiel kan niet verwijderd worden.");
1021
+ if (profile.tenant_id) {
1022
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
1023
+ const { count, error: countError } = await supabaseAdmin
1024
+ .from("profiles")
1025
+ .select("id", { count: "exact", head: true })
1026
+ .eq("tenant_id", profile.tenant_id);
1027
+ if (countError)
1028
+ throw countError;
1029
+ if ((count ?? 0) > 0) {
1030
+ throw new Error("Er zijn nog gebruikers aan dit bedrijf gekoppeld.");
1031
+ }
1032
+ }
1033
+ const { error } = await context.supabase.from("profiles_org").delete().eq("id", data.profileId);
1034
+ if (error)
1035
+ throw error;
1036
+ return { ok: true };
1037
+ });
1038
+ /** Zet precies één profiel als actief (is_default). */
1039
+ export const setActiveProfile = createServerFn({ method: "POST" })
1040
+ .middleware([requireSupabaseAuth])
1041
+ .inputValidator((data) => profileIdInput.parse(data))
1042
+ .handler(async ({ context, data }) => {
1043
+ await requireAdmin(context);
1044
+ const { error: clearError } = await context.supabase
1045
+ .from("profiles_org")
1046
+ .update({ is_default: false })
1047
+ .eq("is_default", true)
1048
+ .neq("id", data.profileId);
1049
+ if (clearError)
1050
+ throw clearError;
1051
+ const { error } = await context.supabase
1052
+ .from("profiles_org")
1053
+ .update({ is_default: true, updated_at: new Date().toISOString() })
1054
+ .eq("id", data.profileId);
1055
+ if (error)
1056
+ throw error;
1057
+ return { ok: true };
1058
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Losse types voor de check-laag (lezen/afdwingen van moduletoegang).
3
+ * Bewust zonder imports uit de rest van deze module, zodat de check-laag
4
+ * later 1-op-1 naar @flowselections/core kan verhuizen.
5
+ */
6
+ export type ModulePermission = "none" | "view" | "edit";
7
+ //# sourceMappingURL=permission-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-types.d.ts","sourceRoot":"","sources":["../../src/lib/permission-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ModulePermission } from "../hooks/useModulePermission";
1
+ import type { ModulePermission } from "../lib/permission-types";
2
2
  interface PermissionContext {
3
3
  supabase: any;
4
4
  userId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/lib/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,UAAU,iBAAiB;IACzB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAK,EACd,KAAK,GAAE,MAAM,GAAG,MAAe,6BA0BhC"}
1
+ {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/lib/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,UAAU,iBAAiB;IACzB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAK,EACd,KAAK,GAAE,MAAM,GAAG,MAAe,6BA0BhC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/admin-installatietechniek",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "flowselections": {
5
5
  "moduleId": "admin-installatietechniek",
6
6
  "pages": [
@@ -40,7 +40,7 @@
40
40
  "lint": "eslint ."
41
41
  },
42
42
  "peerDependencies": {
43
- "@flowselections/core": "*",
43
+ "@flowselections/core": ">=1.0.18",
44
44
  "@tanstack/react-query": "^5.0.0",
45
45
  "@tanstack/react-router": "^1.0.0",
46
46
  "@tanstack/react-start": "^1.0.0",
@@ -55,7 +55,7 @@
55
55
  "devDependencies": {
56
56
  "@cloudflare/vite-plugin": "^1.25.5",
57
57
  "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.16",
58
+ "@flowselections/core": "1.0.18",
59
59
  "@lovable.dev/vite-tanstack-config": "2.13.1",
60
60
  "@rsbuild/core": "^1.0.0",
61
61
  "@tailwindcss/vite": "^4.2.1",