feat: Stores für Flag-Typen und Trail-Marker
Zwei neue Store-Klassen für das Trail-Feature: - TrailFlagStore: Verwaltet die Meldungsarten (wie "Baum quer") mit seedDefaults() für das Anlegen der Standardmenge - TrailMarkerStore: Verwaltet verortete Meldungen auf Trails mit toggleResolved() zum Abhaken (nicht Löschen) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
247ac8ec6b
commit
6fd459e938
2 changed files with 220 additions and 0 deletions
121
frontend/src/lib/stores/trailFlags.svelte.ts
Normal file
121
frontend/src/lib/stores/trailFlags.svelte.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { getContext, setContext } from 'svelte'
|
||||
import { api } 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.
|
||||
*
|
||||
* 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)
|
||||
}
|
||||
|
||||
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(): Promise<number> {
|
||||
const teamId = this.teams.activeId
|
||||
if (!teamId) throw new Error('Kein aktives Team')
|
||||
|
||||
const existing = new Set(this.scoped.map((f) => f.label))
|
||||
let created = 0
|
||||
|
||||
for (const flag of DEFAULT_FLAGS) {
|
||||
if (existing.has(flag.label)) continue
|
||||
await api.collection('trail_flags').create({ ...flag, team: teamId })
|
||||
created++
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
99
frontend/src/lib/stores/trailMarkers.svelte.ts
Normal file
99
frontend/src/lib/stores/trailMarkers.svelte.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { getContext, setContext } from 'svelte'
|
||||
import { api, auth } from './pocketbase.svelte'
|
||||
import { getTeamContext } from './teams.svelte'
|
||||
import type { TrailMarkersResponse } from '$lib/types'
|
||||
|
||||
const KEY = Symbol('trailMarker')
|
||||
|
||||
export class TrailMarkerStore {
|
||||
records = $state<TrailMarkersResponse[]>([])
|
||||
loading = $state(false)
|
||||
error = $state<string | null>(null)
|
||||
|
||||
private unsubscribe: (() => void) | null = null
|
||||
private teams = getTeamContext()
|
||||
|
||||
/** Alle Marker eines Trails, jüngste zuerst */
|
||||
byTrail(trailId: string): TrailMarkersResponse[] {
|
||||
return this.records
|
||||
.filter((r) => r.trail === trailId)
|
||||
.sort((a, b) => (a.created < b.created ? 1 : -1))
|
||||
}
|
||||
|
||||
/** Nur die offenen — was auf der Karte auffallen soll */
|
||||
openByTrail(trailId: string): TrailMarkersResponse[] {
|
||||
return this.byTrail(trailId).filter((r) => !r.resolved)
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
this.records = await api.collection('trail_markers').getFullList({
|
||||
sort: '-created',
|
||||
requestKey: null,
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.error = e.message ?? 'Fehler beim Laden der Marker'
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
if (this.unsubscribe) return
|
||||
api.collection('trail_markers').subscribe('*', (e) => {
|
||||
const idx = this.records.findIndex((r) => r.id === e.record.id)
|
||||
if (e.action === 'create' && idx === -1) this.records = [e.record, ...this.records]
|
||||
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_markers subscribe failed:', e))
|
||||
}
|
||||
|
||||
async create(data: { trail: string; flag: string; lat: number; lng: number; note?: string }) {
|
||||
const teamId = this.teams.activeId
|
||||
if (!teamId) throw new Error('Kein aktives Team')
|
||||
|
||||
return await api.collection('trail_markers').create({
|
||||
...data,
|
||||
team: teamId,
|
||||
resolved: false,
|
||||
created_by: auth.user?.id,
|
||||
})
|
||||
}
|
||||
|
||||
async edit(id: string, data: Partial<TrailMarkersResponse>) {
|
||||
return await api.collection('trail_markers').update(id, data)
|
||||
}
|
||||
|
||||
/** Erledigt statt gelöscht — die Meldung bleibt als Historie erhalten. */
|
||||
async toggleResolved(id: string) {
|
||||
const marker = this.records.find((r) => r.id === id)
|
||||
if (!marker) throw new Error('Marker nicht gefunden')
|
||||
await api.collection('trail_markers').update(id, { resolved: !marker.resolved })
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await api.collection('trail_markers').delete(id)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe()
|
||||
this.unsubscribe = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setTrailMarkerContext() {
|
||||
const store = new TrailMarkerStore()
|
||||
setContext(KEY, store)
|
||||
return store
|
||||
}
|
||||
|
||||
export function getTrailMarkerContext(): TrailMarkerStore {
|
||||
return getContext<TrailMarkerStore>(KEY)
|
||||
}
|
||||
Loading…
Reference in a new issue