restructure: flatten workspace nesting, move devcontainer to root
- backend/workspaces/backend/ → backend/ - backend/workspaces/frontend/ → frontend/ - backend/.devcontainer/ + .vscode/ → repo root (where VS Code expects them) - loose scripts/SQL files → scripts/ - replace nested git repo with single repo at project root - update docker-compose.yml build context and devcontainer.json path - add root .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
141
frontend/src/routes/admin/+page.server.ts
Normal file
141
frontend/src/routes/admin/+page.server.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export async function load({ fetch, locals }) {
|
||||
// 1. Check Permissions (Adapt logic to your user object)
|
||||
const user = locals.user;
|
||||
|
||||
|
||||
// Assuming user.group.permissions is an array of strings
|
||||
const hasAdmin = user?.groups.some(g => g.permissions.includes("ADMIN"));
|
||||
if (!hasAdmin) throw error(403, 'Zugriff verweigert');
|
||||
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
// 2. Load Data
|
||||
const [usersRes, groupsRes, tagsRes] = await Promise.all([
|
||||
fetch(baseUrl + '/api/users'),
|
||||
fetch(baseUrl + '/api/groups'),
|
||||
fetch(baseUrl + '/api/tags')
|
||||
]);
|
||||
return {
|
||||
users: await usersRes.json(),
|
||||
groups: await groupsRes.json(),
|
||||
tags: await tagsRes.json()
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
|
||||
createUser: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
// Extract array of group IDs
|
||||
// "groupIds" matches the name attribute in the <select>
|
||||
const groupIds = data.getAll('groupIds');
|
||||
|
||||
const payload = {
|
||||
username: data.get('username'),
|
||||
initialPassword: data.get('password'),
|
||||
groupIds: groupIds // Send array to backend
|
||||
};
|
||||
|
||||
console.log("Payload", JSON.stringify(payload))
|
||||
|
||||
const res = await fetch(baseUrl + '/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) return { success: false, message: 'Fehler beim Erstellen' };
|
||||
return { success: true, message: 'User angelegt' };
|
||||
},
|
||||
deleteUser: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id');
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
const res = await fetch(baseUrl + `/api/users/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, message: 'Fehler beim Löschen des Benutzers' };
|
||||
}
|
||||
return { success: true, message: 'Benutzer erfolgreich gelöscht' };
|
||||
},
|
||||
updateTag: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id');
|
||||
const name = data.get('name');
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
await fetch(baseUrl + `/api/tags/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
deleteTag: async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id');
|
||||
|
||||
await fetch(`/api/tags/${id}`, { method: 'DELETE' });
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
createGroup: async ({ request, fetch }) => {
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
const data = await request.formData();
|
||||
const payload = {
|
||||
name: data.get('name'),
|
||||
permissions: data.getAll('permissions') // Gets all checked checkboxes
|
||||
};
|
||||
|
||||
const res = await fetch(baseUrl + '/api/groups', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) return { success: false, message: 'Fehler beim Erstellen der Gruppe' };
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
updateGroup: async ({ request, fetch }) => {
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
const data = await request.formData();
|
||||
const id = data.get('id');
|
||||
const payload = {
|
||||
name: data.get('name'),
|
||||
permissions: data.getAll('permissions')
|
||||
};
|
||||
|
||||
const res = await fetch(baseUrl + `/api/groups/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) return { success: false, message: 'Fehler beim Aktualisieren' };
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
deleteGroup: async ({ request, fetch }) => {
|
||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
|
||||
const data = await request.formData();
|
||||
const id = data.get('id');
|
||||
const res = await fetch(baseUrl + `/api/groups/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (!res.ok) return { success: false, message: 'Gruppe kann nicht gelöscht werden (evtl. noch Benutzer zugeordnet?)' };
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
613
frontend/src/routes/admin/+page.svelte
Normal file
613
frontend/src/routes/admin/+page.svelte
Normal file
@@ -0,0 +1,613 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
console.log(data)
|
||||
|
||||
let activeTab = 'users';
|
||||
let editingTagId: string | null = null;
|
||||
let editingTagName = '';
|
||||
let editingUserId: string | null = null;
|
||||
|
||||
function startEditTag(tag: any) {
|
||||
editingTagId = tag.id;
|
||||
editingTagName = tag.name;
|
||||
}
|
||||
|
||||
function cancelEditTag() {
|
||||
editingTagId = null;
|
||||
editingTagName = '';
|
||||
}
|
||||
|
||||
function startEditUser(id: string) {
|
||||
editingUserId = id;
|
||||
}
|
||||
|
||||
function cancelEditUser() {
|
||||
editingUserId = null;
|
||||
}
|
||||
|
||||
let editingGroupId: string | null = null;
|
||||
const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION'];
|
||||
|
||||
function startEditGroup(id: string) {
|
||||
editingGroupId = id;
|
||||
}
|
||||
|
||||
function cancelEditGroup() {
|
||||
editingGroupId = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-serif text-brand-navy">Admin Dashboard</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200">
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'users'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
on:click={() => (activeTab = 'users')}>Benutzer</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'groups'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
on:click={() => (activeTab = 'groups')}>Gruppen</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'tags'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
on:click={() => (activeTab = 'tags')}>Schlagworte</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if form?.message}
|
||||
<div class="bg-brand-mint/20 text-brand-navy p-4 rounded mb-6 border border-brand-mint/50">
|
||||
{form.message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activeTab === 'users'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Benutzerverwaltung</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Login</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Gruppen</th
|
||||
>
|
||||
{#if editingUserId}
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Passwort</th
|
||||
>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each data.users as user}
|
||||
<tr class="group/row hover:bg-gray-50">
|
||||
{#if editingUserId === user.id}
|
||||
<!-- === EDIT MODE === -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.username}
|
||||
<!-- Hidden ID Input for the form -->
|
||||
<input
|
||||
type="hidden"
|
||||
name="username"
|
||||
value={user.username}
|
||||
form="edit-form-{user.id}"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<!-- Groups Select -->
|
||||
<select
|
||||
name="groupIds"
|
||||
multiple
|
||||
form="edit-form-{user.id}"
|
||||
class="block w-full rounded border-brand-mint text-xs p-1 min-h-[80px]"
|
||||
>
|
||||
{#each data.groups as group}
|
||||
<option
|
||||
value={group.id}
|
||||
selected={user.groups.some((g) => g.id === group.id)}
|
||||
>
|
||||
{group.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Auswahl</p>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right align-top">
|
||||
<!-- Password & Buttons -->
|
||||
<form
|
||||
id="edit-form-{user.id}"
|
||||
method="POST"
|
||||
action="?/createUser"
|
||||
use:enhance={() =>
|
||||
async ({ update }) => {
|
||||
await update();
|
||||
cancelEditUser();
|
||||
}}
|
||||
class="flex flex-col items-end gap-2"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Neues PW (optional)"
|
||||
class="w-32 py-1 px-2 text-xs border border-brand-mint rounded"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2 mt-1">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={cancelEditUser}
|
||||
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
{:else}
|
||||
<!-- === VIEW MODE === -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.username}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if user.groups && user.groups.length > 0}
|
||||
{#each user.groups as group}
|
||||
<span
|
||||
class="px-2 py-0.5 text-[10px] font-bold uppercase rounded-full bg-blue-50 text-blue-700 border border-blue-100"
|
||||
>
|
||||
{group.name}
|
||||
</span>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-xs text-gray-400 italic">Keine Gruppen</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<!-- Edit Button -->
|
||||
<button
|
||||
on:click={() => startEditUser(user.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteUser"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm(`Benutzer '${user.username}' wirklich löschen?`)) {
|
||||
cancel();
|
||||
}
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
class="flex items-center"
|
||||
>
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 transition-colors p-1"
|
||||
title="Benutzer löschen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Create User Form -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neuen Benutzer anlegen
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/createUser"
|
||||
use:enhance
|
||||
class="grid grid-cols-1 md:grid-cols-6 gap-4 items-start"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="Login"
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Passwort"
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
|
||||
<!-- Multi-Select for Groups -->
|
||||
<div class="md:col-span-3">
|
||||
<select
|
||||
name="groupIds"
|
||||
multiple
|
||||
class="rounded border-gray-300 text-sm w-full h-[42px] py-1"
|
||||
required
|
||||
title="Strg+Klick für mehrere"
|
||||
>
|
||||
{#each data.groups as group}
|
||||
<option value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Mehrfachauswahl</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full"
|
||||
>Anlegen</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{:else if activeTab === 'tags'}
|
||||
<!-- TAGS SECTION (unchanged logic, just ensuring style consistency) -->
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 bg-yellow-50/50">
|
||||
<h2 class="text-lg font-bold text-gray-700">Schlagworte</h2>
|
||||
<p class="text-xs text-yellow-800 mt-1">
|
||||
Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-gray-100 max-h-[600px] overflow-y-auto">
|
||||
{#each data.tags as tag}
|
||||
<li class="px-6 py-3 flex items-center justify-between hover:bg-gray-50 group">
|
||||
{#if editingTagId === tag.id}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateTag"
|
||||
use:enhance={() =>
|
||||
async ({ update }) => {
|
||||
await update();
|
||||
cancelEditTag();
|
||||
}}
|
||||
class="flex-1 flex gap-2 items-center"
|
||||
>
|
||||
<input type="hidden" name="id" value={tag.id} />
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
bind:value={editingTagName}
|
||||
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
|
||||
autofocus
|
||||
/>
|
||||
<button class="text-green-600 hover:text-green-800"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/></svg
|
||||
></button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={cancelEditTag}
|
||||
class="text-gray-400 hover:text-gray-600"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/></svg
|
||||
></button
|
||||
>
|
||||
</form>
|
||||
{:else}
|
||||
<span class="text-sm font-medium text-brand-navy bg-brand-sand/30 px-2 py-1 rounded">
|
||||
{tag.name}
|
||||
</span>
|
||||
<div
|
||||
class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<button
|
||||
on:click={() => startEditTag(tag)}
|
||||
class="p-1 text-gray-400 hover:text-brand-navy"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteTag"
|
||||
use:enhance={({ cancel }) => {
|
||||
// This runs BEFORE the request is sent
|
||||
if (
|
||||
!confirm(
|
||||
'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.'
|
||||
)
|
||||
) {
|
||||
cancel(); // Stop the request
|
||||
}
|
||||
|
||||
// This runs AFTER the server responds
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
class="inline"
|
||||
>
|
||||
<input type="hidden" name="id" value={tag.id} />
|
||||
<button class="p-1 text-gray-400 hover:text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else if activeTab === 'groups'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Gruppenverwaltung</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Name</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Berechtigungen</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Aktionen</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each data.groups as group}
|
||||
<tr class="group/row hover:bg-gray-50">
|
||||
{#if editingGroupId === group.id}
|
||||
<!-- EDIT MODE -->
|
||||
<td colspan="3" class="px-6 py-4">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateGroup"
|
||||
use:enhance={() =>
|
||||
async ({ update }) => {
|
||||
await update();
|
||||
cancelEditGroup();
|
||||
}}
|
||||
class="flex flex-col sm:flex-row items-start gap-4 w-full"
|
||||
>
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
|
||||
<!-- Name Input -->
|
||||
<div class="w-full sm:w-1/3">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={group.name}
|
||||
class="w-full text-sm border-brand-mint rounded"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Permissions Checkboxes -->
|
||||
<div class="flex-1 flex flex-wrap gap-4 items-center h-full pt-2">
|
||||
{#each availablePermissions as perm}
|
||||
<label
|
||||
class="inline-flex items-center text-xs font-bold text-gray-600 uppercase"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions"
|
||||
value={perm}
|
||||
checked={group.permissions.includes(perm)}
|
||||
class="mr-2 text-brand-navy focus:ring-brand-mint rounded border-gray-300"
|
||||
/>
|
||||
{perm.replace('_', ' ')}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 self-start sm:self-center">
|
||||
<button type="submit" class="text-green-600 hover:text-green-800 p-1">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={cancelEditGroup}
|
||||
class="text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
{:else}
|
||||
<!-- VIEW MODE -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-brand-navy">
|
||||
{group.name}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each group.permissions as perm}
|
||||
<span
|
||||
class="px-2 py-0.5 text-[10px] font-bold uppercase rounded-full
|
||||
{perm === 'ADMIN'
|
||||
? 'bg-red-50 text-red-700 border-red-100'
|
||||
: 'bg-gray-100 text-gray-600 border-gray-200'}"
|
||||
>
|
||||
{perm}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
on:click={() => startEditGroup(group.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteGroup"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm('Gruppe wirklich löschen?')) {
|
||||
cancel();
|
||||
}
|
||||
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 p-1 transition-colors"
|
||||
title="Löschen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- CREATE GROUP FORM -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neue Gruppe anlegen
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/createGroup"
|
||||
use:enhance
|
||||
class="flex flex-col md:flex-row gap-4 items-start md:items-center"
|
||||
>
|
||||
<div class="flex-1 w-full">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Gruppenname (z.B. Editoren)"
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
{#each availablePermissions as perm}
|
||||
<label class="inline-flex items-center text-xs font-bold text-gray-600 uppercase">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions"
|
||||
value={perm}
|
||||
class="mr-2 text-brand-navy focus:ring-brand-mint rounded border-gray-300"
|
||||
/>
|
||||
{perm.replace('_', ' ')}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto"
|
||||
>
|
||||
Anlegen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user