feat: Verwaltung der Flag-Typen
Die Flag-Verwaltungsseite ermöglicht Team-Admins und -Besitzern, die Marker-Typen für Trails zu erstellen, bearbeiten und zu löschen. Einfache Mitglieder sehen die Flags nur. Hinweistexte informieren über Berechtigungen und ermöglichen Nicht-Admins, die Benutzeroberfläche zu verstehen.
This commit is contained in:
parent
8e256d0184
commit
c44ec4377f
1 changed files with 202 additions and 0 deletions
202
frontend/src/routes/dashboard/settings/flags/+page.svelte
Normal file
202
frontend/src/routes/dashboard/settings/flags/+page.svelte
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<script lang="ts">
|
||||
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import * as Dialog from '@/components/ui/dialog'
|
||||
import { Plus, Pencil, Trash2, Sparkles } from 'lucide-svelte'
|
||||
import type { TrailFlagsResponse } from '$lib/types'
|
||||
|
||||
const flags = getTrailFlagContext()
|
||||
|
||||
let dialog = $state(false)
|
||||
let editing = $state<TrailFlagsResponse | null>(null)
|
||||
let form = $state<{ label: string; icon: string; color: string; severity: string }>({
|
||||
label: '',
|
||||
icon: 'Info',
|
||||
color: '#dc2626',
|
||||
severity: 'warnung',
|
||||
})
|
||||
let error = $state<string | null>(null)
|
||||
let seeding = $state(false)
|
||||
|
||||
const SEVERITIES = [
|
||||
{ value: 'info', label: 'Info' },
|
||||
{ value: 'warnung', label: 'Warnung' },
|
||||
{ value: 'kritisch', label: 'Kritisch' },
|
||||
] as const
|
||||
|
||||
function openCreate() {
|
||||
editing = null
|
||||
form = { label: '', icon: 'Info', color: '#dc2626', severity: 'warnung' }
|
||||
error = null
|
||||
dialog = true
|
||||
}
|
||||
|
||||
function openEdit(flag: TrailFlagsResponse) {
|
||||
editing = flag
|
||||
form = {
|
||||
label: flag.label,
|
||||
icon: flag.icon ?? 'Info',
|
||||
color: flag.color ?? '#dc2626',
|
||||
severity: flag.severity ?? 'warnung',
|
||||
}
|
||||
error = null
|
||||
dialog = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.label.trim()) {
|
||||
error = 'Bitte eine Bezeichnung angeben.'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (editing) await flags.edit(editing.id, { ...form } as Parameters<typeof flags.edit>[1])
|
||||
else await flags.create({ ...form })
|
||||
dialog = false
|
||||
} catch (e: any) {
|
||||
error = e.message ?? 'Der Flag-Typ konnte nicht gespeichert werden.'
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(flag: TrailFlagsResponse) {
|
||||
if (!confirm(`„${flag.label}" wirklich löschen?`)) return
|
||||
try {
|
||||
await flags.remove(flag.id)
|
||||
} catch (e: any) {
|
||||
// Marker verweisen auf den Flag-Typ — PocketBase lehnt das Löschen ab
|
||||
alert(e.message ?? 'Der Flag-Typ konnte nicht gelöscht werden. Vermutlich ist er noch in Verwendung.')
|
||||
}
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
seeding = true
|
||||
try {
|
||||
const n = await flags.seedDefaults()
|
||||
if (n === 0) alert('Alle Standard-Typen sind bereits vorhanden.')
|
||||
} catch (e: any) {
|
||||
alert(e.message ?? 'Die Standard-Typen konnten nicht angelegt werden.')
|
||||
} finally {
|
||||
seeding = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Flag-Typen</h1>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Wofür sich Marker auf einem Trail setzen lassen.
|
||||
</p>
|
||||
{#if !flags.canManage}
|
||||
<p class="text-sm text-muted-foreground mt-3">
|
||||
Flag-Typen verwaltet, wer das Team führt. Du kannst sie beim Setzen
|
||||
von Markern verwenden.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if flags.canManage}
|
||||
<div class="flex gap-2">
|
||||
{#if flags.scoped.length === 0}
|
||||
<Button variant="outline" onclick={seed} disabled={seeding}>
|
||||
<Sparkles class="size-4 mr-2" />
|
||||
{seeding ? 'Wird angelegt …' : 'Standardsatz anlegen'}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button onclick={openCreate}>
|
||||
<Plus class="size-4 mr-2" />
|
||||
Neuer Typ
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if flags.scoped.length === 0}
|
||||
<Card>
|
||||
<CardContent class="py-10 text-center text-muted-foreground">
|
||||
{#if flags.canManage}
|
||||
Noch keine Flag-Typen. Leg einen an oder starte mit dem Standardsatz.
|
||||
{:else}
|
||||
Das Team hat noch keine Flag-Typen angelegt.
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each flags.scoped as flag (flag.id)}
|
||||
<Card>
|
||||
<CardContent class="py-3 flex items-center gap-3">
|
||||
<span class="size-4 rounded-full shrink-0" style:background={flag.color}></span>
|
||||
<span class="font-medium flex-1">{flag.label}</span>
|
||||
<Badge variant={flag.severity === 'kritisch' ? 'destructive' : 'secondary'}>
|
||||
{flag.severity}
|
||||
</Badge>
|
||||
{#if flags.canManage}
|
||||
<Button variant="ghost" size="sm" onclick={() => openEdit(flag)}>
|
||||
<Pencil class="size-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onclick={() => remove(flag)}>
|
||||
<Trash2 class="size-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Root bind:open={dialog}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{editing ? 'Flag-Typ bearbeiten' : 'Neuer Flag-Typ'}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="flag-label">Bezeichnung</Label>
|
||||
<Input id="flag-label" bind:value={form.label} placeholder="z. B. Baum quer" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="flag-color">Farbe</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<input id="flag-color" type="color" bind:value={form.color} class="h-9 w-16 rounded border" />
|
||||
<Input bind:value={form.color} class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Schweregrad</Label>
|
||||
<div class="flex gap-2">
|
||||
{#each SEVERITIES as s (s.value)}
|
||||
<Button
|
||||
variant={form.severity === s.value ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onclick={() => (form.severity = s.value)}
|
||||
>
|
||||
{s.label}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="flag-icon">Icon</Label>
|
||||
<Input id="flag-icon" bind:value={form.icon} placeholder="Lucide-Name, z. B. TreePine" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Name eines Icons von lucide.dev, in Schreibweise wie „TreePine".
|
||||
</p>
|
||||
</div>
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (dialog = false)}>Abbrechen</Button>
|
||||
<Button onclick={save}>Speichern</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Loading…
Reference in a new issue