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