import { getContext, setContext } from 'svelte' import { api, auth } from './pocketbase.svelte' import { getTeamContext } from './teams.svelte' import { getEventContext } from './events.svelte' import { dayOf, todayISO } from '$lib/time' import type { EventSeriesResponse } from '$lib/types' const KEY = Symbol('eventSeries') export type SeriesRule = 'woechentlich' | 'zweiwoechentlich' | 'monatlich' export const SERIES_RULE_LABEL: Record = { woechentlich: 'wöchentlich', zweiwoechentlich: 'alle zwei Wochen', monatlich: 'monatlich', } /** Wie viele Termine ein Erzeugen-Lauf höchstens anlegt. */ const BATCH = 12 /** * Nächste Termine einer Regel ab einem Startdatum. * * Gerechnet wird auf Kalendertagen, nicht auf Millisekunden: Über Sommer- und * Winterzeit hinweg verschiebt sich eine Woche sonst um eine Stunde und ein * Termin kippt auf den Vortag. */ export function occurrences( startDay: string, rule: SeriesRule, count: number, untilDay?: string, ): string[] { if (!startDay) return [] const [y, m, d] = startDay.split('-').map(Number) const out: string[] = [] let cursor = new Date(Date.UTC(y, m - 1, d)) for (let i = 0; i < count; i++) { const iso = cursor.toISOString().slice(0, 10) if (untilDay && iso > untilDay) break out.push(iso) if (rule === 'monatlich') { cursor = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, cursor.getUTCDate())) } else { const step = rule === 'zweiwoechentlich' ? 14 : 7 cursor = new Date(cursor.getTime() + step * 24 * 60 * 60 * 1000) } } return out } export class EventSeriesStore { records = $state([]) loading = $state(false) error = $state(null) private unsubscribe: (() => void) | null = null private teams = getTeamContext() private events = getEventContext() get scoped(): EventSeriesResponse[] { const teamId = this.teams.activeId if (!teamId) return [] return this.records.filter((r) => r.team === teamId) } getById(id: string) { return this.records.find((r) => r.id === id) } /** Termine, die aus dieser Serie entstanden sind. */ eventsOf(seriesId: string) { return this.events.scoped.filter((e) => e.series === seriesId) } async load() { this.loading = true this.error = null try { this.records = await api.collection('event_series').getFullList({ sort: '+name', requestKey: null, }) } catch (e: any) { this.error = e.message ?? 'Fehler beim Laden der Serien' console.error(e) } finally { this.loading = false } } subscribe() { if (this.unsubscribe) return api.collection('event_series').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_series subscribe failed:', e)) } async create(data: Partial) { const teamId = this.teams.activeId if (!teamId) throw new Error('Kein aktives Team') return await api.collection('event_series').create({ ...data, team: teamId, created_by: auth.user?.id, }) } async edit(id: string, data: Partial) { return await api.collection('event_series').update(id, data) } async remove(id: string) { await api.collection('event_series').delete(id) } /** * Legt die nächsten fälligen Termine an. Vorhandene bleiben unberührt — * erzeugt wird nur, was fehlt, damit ein zweiter Klick nichts verdoppelt. * Zurück kommt die Zahl der neuen Termine. */ async generate(seriesId: string): Promise { const series = this.getById(seriesId) if (!series) throw new Error('Serie nicht gefunden') if (!series.starts) throw new Error('Die Serie hat kein Startdatum.') const existing = new Set(this.eventsOf(seriesId).map((e) => dayOf(e.starts))) const today = todayISO() // Vergangene Termine nachträglich anzulegen hilft niemandem. const dates = occurrences( dayOf(series.starts), (series.rule || 'woechentlich') as SeriesRule, BATCH * 4, dayOf(series.until) || undefined, ).filter((d) => d >= today && !existing.has(d)) let created = 0 for (const day of dates.slice(0, BATCH)) { await api.collection('events').create({ name: series.name, description: series.description, location: series.location, starts: day, timing: !!series.timing, participation: series.participation || 'offen', series: series.id, team: series.team, created_by: auth.user?.id, }) created++ } return created } destroy() { if (this.unsubscribe) { this.unsubscribe() this.unsubscribe = null } } } export function setEventSeriesContext() { const store = new EventSeriesStore() setContext(KEY, store) return store } export function getEventSeriesContext(): EventSeriesStore { return getContext(KEY) }