stammtisch-hersbruck/frontend/src/routes/dashboard/settings/flags/+page.svelte
Daniel Michelberger 08af521d19 fix: Bestätigungsdialog und Farbvalidierung bei Flag-Verwaltung
- Ersetze native confirm() durch app.confirm.request() mit themingfähigem Dialog
- Erweitere Fehlermeldung beim Löschen mit Hinweis auf Marker-Abhängigkeiten
- Validiere Hex-Farben (#rrggbb) vor dem Speichern
- Konsistenz mit Projektmuster (events, runs, riders, times)
2026-08-06 19:11:38 +02:00

221 lines
8.3 KiB
Svelte

<script lang="ts">
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
import { app } from '$lib/stores/app.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
}
if (!/^#[0-9a-fA-F]{6}$/.test(form.color)) {
error = 'Bitte eine Farbe im Format #rrggbb 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.'
}
}
function remove(flag: TrailFlagsResponse) {
app.confirm.request({
title: 'Flag-Typ löschen?',
text: `„${flag.label}" wird entfernt. Marker, die ihn verwenden, verhindern das Löschen.`,
yes: async () => {
try {
await flags.remove(flag.id)
app.confirm.close()
} catch (e: any) {
app.confirm.close()
// Die deleteRule verhindert das Löschen, solange ein Marker
// den Typ verwendet — PocketBase meldet das nur technisch.
alert(
`„${flag.label}" konnte nicht gelöscht werden. ` +
'Vermutlich verwendet ihn noch ein Marker auf einem Trail. ' +
'Entferne zuerst die betroffenen Marker oder markiere sie als erledigt.',
)
}
},
no: () => app.confirm.close(),
})
}
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>