Die Rolle superadmin kommt als Feld an users, der erste ist stammtisch@dne.name. Vergeben wird sie nur in PocketBase: Die updateRule schliesst das Feld ueber @request.body.superadmin:isset = false aus, sonst koennte sich jeder selbst befoerdern - jeder darf schliesslich seinen eigenen Datensatz aendern. Besondere Rechte in der App haengen noch nicht daran. Flag-Typen bleiben ausdruecklich Sache der Teams. Ein neues Team bekommt beim Anlegen den hartkodierten Standardsatz aus der App und kann ihn danach frei aendern; eine appweite Verwaltung entfaellt damit. Ein Team ohne Flag-Typen koennte zwar Marker aufnehmen, aber nichts einordnen. Im Kopf stehen Avatar und Name links, Design-Umschalter und Logout rechts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
import { getContext, setContext } from 'svelte'
|
|
import { api, auth } from './pocketbase.svelte'
|
|
import { getTeamContext } from './teams.svelte'
|
|
import type { TrailFlagsResponse } from '$lib/types'
|
|
|
|
const KEY = Symbol('trailFlag')
|
|
|
|
/**
|
|
* Vorschlag für einen Standardsatz. Bewusst NICHT in der Migration: Seed-Daten
|
|
* dort kämen bei jedem Containerstart zurück, auch nachdem sie jemand gelöscht
|
|
* hat. Stattdessen legt sie die Flag-Verwaltung auf Knopfdruck an.
|
|
*
|
|
* Der Abgleich läuft über das Label. Wird ein Standard-Flag umbenannt, legt
|
|
* ein erneuter Aufruf es wieder neu an. Das ist hinnehmbar, weil die
|
|
* Flag-Verwaltung den Knopf nur zeigt, solange noch gar keine Flags existieren.
|
|
*
|
|
* icon: Name eines Lucide-Icons (lucide-svelte)
|
|
*/
|
|
export const DEFAULT_FLAGS = [
|
|
{ label: 'Baum quer', icon: 'TreePine', color: '#ca8a04', severity: 'warnung' },
|
|
{ label: 'Verblockt', icon: 'Blocks', color: '#ea580c', severity: 'warnung' },
|
|
{ label: 'Erosion', icon: 'Waves', color: '#a16207', severity: 'warnung' },
|
|
{ label: 'Sperrung', icon: 'Ban', color: '#dc2626', severity: 'kritisch' },
|
|
{ label: 'Bauarbeiten', icon: 'Construction', color: '#dc2626', severity: 'kritisch' },
|
|
{ label: 'Hinweis', icon: 'Info', color: '#2563eb', severity: 'info' },
|
|
]
|
|
|
|
export class TrailFlagStore {
|
|
records = $state<TrailFlagsResponse[]>([])
|
|
loading = $state(false)
|
|
error = $state<string | null>(null)
|
|
|
|
private unsubscribe: (() => void) | null = null
|
|
private teams = getTeamContext()
|
|
|
|
get scoped(): TrailFlagsResponse[] {
|
|
const teamId = this.teams.activeId
|
|
if (!teamId) return []
|
|
return this.records.filter((r) => r.team === teamId)
|
|
}
|
|
|
|
getById(id: string): TrailFlagsResponse | undefined {
|
|
return this.records.find((r) => r.id === id)
|
|
}
|
|
|
|
/**
|
|
* Flag-Typen anlegen, ändern und löschen darf nur, wer das Team führt —
|
|
* dieselbe Regel wie in createRule/updateRule/deleteRule der Collection.
|
|
* Die Prüfung hier blendet nur UI aus; durchgesetzt wird sie serverseitig.
|
|
*/
|
|
get canManage(): boolean {
|
|
const uid = auth.user?.id
|
|
if (!uid) return false
|
|
|
|
const team = this.teams.records.find((t) => t.id === this.teams.activeId)
|
|
if (!team) return false
|
|
|
|
return team.owner === uid || ((team.admins ?? []) as string[]).includes(uid)
|
|
}
|
|
|
|
async load() {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
this.records = await api.collection('trail_flags').getFullList({
|
|
sort: 'label',
|
|
requestKey: null,
|
|
})
|
|
} catch (e: any) {
|
|
this.error = e.message ?? 'Fehler beim Laden der Flags'
|
|
console.error(e)
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
}
|
|
|
|
subscribe() {
|
|
if (this.unsubscribe) return
|
|
api.collection('trail_flags').subscribe('*', (e) => {
|
|
const idx = this.records.findIndex((r) => r.id === e.record.id)
|
|
if (e.action === 'create' && idx === -1) this.records = [...this.records, e.record]
|
|
else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record
|
|
else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== e.record.id)
|
|
}).then((unsub) => {
|
|
this.unsubscribe = unsub
|
|
}).catch((e) => console.warn('trail_flags subscribe failed:', e))
|
|
}
|
|
|
|
async create(data: { label: string; icon: string; color: string; severity: string }) {
|
|
const teamId = this.teams.activeId
|
|
if (!teamId) throw new Error('Kein aktives Team')
|
|
return await api.collection('trail_flags').create({ ...data, team: teamId })
|
|
}
|
|
|
|
async edit(id: string, data: Partial<TrailFlagsResponse>) {
|
|
return await api.collection('trail_flags').update(id, data)
|
|
}
|
|
|
|
async remove(id: string) {
|
|
await api.collection('trail_flags').delete(id)
|
|
}
|
|
|
|
/**
|
|
* Legt die Standard-Flags an, die noch fehlen. Vorhandene bleiben
|
|
* unangetastet — der Knopf lässt sich also gefahrlos mehrfach drücken.
|
|
* Gibt die Anzahl neu angelegter Flags zurück.
|
|
*/
|
|
async seedDefaults(teamId = this.teams.activeId): Promise<number> {
|
|
if (!teamId) throw new Error('Kein aktives Team')
|
|
// Beim Anlegen eines Teams ist man dessen Owner, aber der Datensatz
|
|
// steckt womöglich noch nicht im Store — dann greift die Prüfung ins
|
|
// Leere. Für das eigene, gerade erzeugte Team wird sie übersprungen;
|
|
// die createRule der Collection setzt sie ohnehin serverseitig durch.
|
|
if (teamId === this.teams.activeId && !this.canManage) {
|
|
throw new Error('Dafür brauchst du Administratorrechte im Team.')
|
|
}
|
|
|
|
const existing = new Set(
|
|
this.records.filter((f) => f.team === teamId).map((f) => f.label),
|
|
)
|
|
let created = 0
|
|
|
|
for (const flag of DEFAULT_FLAGS) {
|
|
if (existing.has(flag.label)) continue
|
|
try {
|
|
await api.collection('trail_flags').create({ ...flag, team: teamId })
|
|
created++
|
|
} catch (e: any) {
|
|
// Bricht mitten in der Schleife etwas ab, sollen die bereits
|
|
// angelegten Flags nicht verschwiegen werden.
|
|
throw new Error(
|
|
`${created} von ${DEFAULT_FLAGS.length} Typen angelegt, dann: ` +
|
|
(e.message ?? 'unbekannter Fehler'),
|
|
)
|
|
}
|
|
}
|
|
|
|
return created
|
|
}
|
|
|
|
destroy() {
|
|
if (this.unsubscribe) {
|
|
this.unsubscribe()
|
|
this.unsubscribe = null
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setTrailFlagContext() {
|
|
const store = new TrailFlagStore()
|
|
setContext(KEY, store)
|
|
return store
|
|
}
|
|
|
|
export function getTrailFlagContext(): TrailFlagStore {
|
|
return getContext<TrailFlagStore>(KEY)
|
|
}
|