import { getContext, setContext } from 'svelte' import { api, auth } from './pocketbase.svelte' import { getEventContext } from './events.svelte' import { getRiderContext, riderName } from './riders.svelte' import type { EventParticipantsResponse, RidersResponse } from '$lib/types' const KEY = Symbol('eventParticipant') export type ParticipantStatus = 'angefragt' | 'zugesagt' | 'abgesagt' export const PARTICIPANT_STATUS_LABEL: Record = { angefragt: 'Angefragt', zugesagt: 'Dabei', abgesagt: 'Abgesagt', } /** * Teilnehmer eines Events. Fahrer gehören dem Team, nicht dem Event — wer bei * einem Termin dabei ist, steht hier. Die Startnummer hängt ebenfalls hier, * denn sie gilt je Event: Derselbe Fahrer kann in zwei Rennen zwei Nummern * haben, und beim Stammtisch gar keine. */ export class EventParticipantStore { records = $state([]) loading = $state(false) error = $state(null) private unsubscribe: (() => void) | null = null private events = getEventContext() private riders = getRiderContext() /** Nur Teilnehmer von Events des aktiven Teams. */ get scoped(): EventParticipantsResponse[] { const eventIds = new Set(this.events.scoped.map((e) => e.id)) return this.records.filter((r) => eventIds.has(r.event)) } /** Teilnehmer eines Events, nach Startnummer und dann nach Name. */ byEvent(eventId: string): EventParticipantsResponse[] { return this.records .filter((r) => r.event === eventId) .sort((a, b) => { const na = parseInt(a.number ?? '', 10) const nb = parseInt(b.number ?? '', 10) if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb if (Number.isFinite(na)) return -1 if (Number.isFinite(nb)) return 1 return this.nameOf(a.rider).localeCompare(this.nameOf(b.rider)) }) } /** Nur die Zugesagten — wer wirklich am Start steht. */ confirmedByEvent(eventId: string): EventParticipantsResponse[] { return this.byEvent(eventId).filter((r) => r.status === 'zugesagt') } byRider(riderId: string): EventParticipantsResponse[] { return this.scoped.filter((r) => r.rider === riderId) } get(eventId: string, riderId: string): EventParticipantsResponse | undefined { return this.records.find((r) => r.event === eventId && r.rider === riderId) } /** Startnummer eines Fahrers bei einem Event, falls vergeben. */ numberOf(eventId: string, riderId: string | undefined): string { if (!riderId) return '' return this.get(eventId, riderId)?.number ?? '' } /** * Startnummern sind je Event eindeutig — zwei gleiche Nummern in einem * Rennen wären an der Strecke nicht auseinanderzuhalten. */ isNumberTaken(eventId: string, number: string, excludeId?: string): boolean { const wanted = number.trim() if (!wanted) return false return this.byEvent(eventId).some( (r) => (r.number ?? '').trim() === wanted && r.id !== excludeId, ) } /** Teilnahme des eingeloggten Nutzers, sofern sein Konto an einem Fahrer hängt. */ own(eventId: string): EventParticipantsResponse | undefined { const uid = auth.user?.id if (!uid) return undefined const mine = new Set(this.riders.getByUser(uid).map((r) => r.id)) return this.byEvent(eventId).find((p) => mine.has(p.rider)) } /** „#12 Max Mustermann" — ohne Nummer nur der Name. */ riderLabel(eventId: string, riderId: string | undefined): string { if (!riderId) return 'Unbekannter Fahrer' const name = this.nameOf(riderId) || 'Unbekannter Fahrer' const number = this.numberOf(eventId, riderId) return number ? `#${number} ${name}` : name } private nameOf(riderId: string): string { const r = this.riders.getById(riderId) return r ? riderName(r) : '' } async load() { this.loading = true this.error = null try { this.records = await api.collection('event_participants').getFullList({ sort: '+created', requestKey: null, }) } catch (e: any) { this.error = e.message ?? 'Fehler beim Laden der Teilnehmer' console.error(e) } finally { this.loading = false } } subscribe() { if (this.unsubscribe) return api.collection('event_participants').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('event_participants subscribe failed:', e)) } async add(data: { event: string rider: string number?: string status?: ParticipantStatus }) { return await api.collection('event_participants').create({ status: 'zugesagt' as ParticipantStatus, ...data, created_by: auth.user?.id, }) } async edit(id: string, data: Partial) { return await api.collection('event_participants').update(id, data) } async setStatus(id: string, status: ParticipantStatus) { return await this.edit(id, { status } as Partial) } async remove(id: string) { await api.collection('event_participants').delete(id) } /** Fahrer des Teams, die bei diesem Event noch fehlen. */ available(eventId: string): RidersResponse[] { const taken = new Set(this.byEvent(eventId).map((r) => r.rider)) return this.riders.scoped.filter((r) => !taken.has(r.id)) } destroy() { if (this.unsubscribe) { this.unsubscribe() this.unsubscribe = null } } } export function setEventParticipantContext() { const store = new EventParticipantStore() setContext(KEY, store) return store } export function getEventParticipantContext(): EventParticipantStore { return getContext(KEY) }