feat(admin/groups): add groups entity with master-detail sub-routes

Creates the full groups section under /admin/groups/:
- +layout.server.ts: loads groups list via GET /api/groups
- GroupsListPanel.svelte: left list panel (name + permission count, active state)
- +layout.svelte: composes list panel + children slot
- +page.svelte: empty selection prompt
- [id]/+page.server.ts: update (PATCH) and delete actions
- [id]/+page.svelte: edit detail panel with Standard/Administrative permission sections
- new/+page.svelte and +page.server.ts: create group form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 01:26:45 +02:00
parent c8a834b91b
commit 8197db2c14
13 changed files with 577 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { page } from '$app/state';
import { m } from '$lib/paraglide/messages.js';
type Group = {
id: string;
name: string;
permissions: string[];
};
let { groups }: { groups: Group[] } = $props();
</script>
<div class="flex w-60 flex-shrink-0 flex-col overflow-hidden border-r border-line bg-surface">
<!-- Panel header -->
<div class="flex items-center justify-between border-b border-line px-3 py-2">
<span class="text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.admin_groups_list_title()}
</span>
<a
href="/admin/groups/new"
class="inline-flex items-center gap-1 rounded-sm px-2 py-1 text-xs font-medium text-ink-2 transition-colors hover:bg-muted hover:text-ink"
title={m.admin_btn_new_group()}
aria-label={m.admin_btn_new_group()}
>
<svg
class="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
{m.admin_btn_new_group()}
</a>
</div>
<!-- Scrollable group list -->
<div class="flex-1 overflow-y-auto">
{#if groups.length === 0}
<p class="px-4 py-6 text-center text-xs text-ink-3">
{m.admin_groups_empty()}
</p>
{:else}
{#each groups as group (group.id)}
{@const isActive = page.url.pathname.startsWith('/admin/groups/' + group.id)}
<a
href="/admin/groups/{group.id}"
aria-current={isActive ? 'page' : undefined}
class="block border-l-2 px-3 py-2.5 transition-colors {isActive
? 'border-primary bg-primary/10 dark:bg-primary/15'
: 'border-transparent hover:bg-muted'}"
>
<div class="text-sm font-bold text-ink">{group.name}</div>
<div class="mt-0.5 text-xs text-ink-3">
{m.admin_groups_permission_count({ count: group.permissions.length })}
</div>
</a>
{/each}
{/if}
</div>
</div>