stammtisch-hersbruck/frontend/src/lib/stores/notificationPrefs.svelte.ts
Daniel Michelberger 279796111c wip: Der abgebrochene Kurbeler-Umbau, wie er hier liegengeblieben ist
Kein fertiger Stand, sondern ein Zwischenstand: Hier wurde im September 2026
angefangen, die App auf „Kurbeler" umzubenennen — Wortmarke, Icons, brand.ts,
Benachrichtigungen, das Benutzerverzeichnis. Weitergegangen ist es dann im
eigenen Repo `~/Dokumente/Projekte/kurbeler`, das die vollständige Historie
dieses Repos mitgenommen hat.

Committet wird das hier nicht, weil es gebraucht würde, sondern damit es nicht
als Haufen unversionierter Dateien im Arbeitsverzeichnis verrottet. Wer in
zwei Jahren nachsieht, findet so eine Geschichte statt eines Rätsels — und das
Stammtisch-Wappen, das nur hier je existiert hat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTw6xVYgjMy9AQtfs1Gdyu
2026-09-11 15:42:33 +02:00

182 lines
6.1 KiB
TypeScript

import { api, auth } from './pocketbase.svelte'
import type { NotificationPrefsResponse } from '$lib/types'
/**
* Was jemand per Mail erfahren will.
*
* Kein Context-Store wie die Collection-Stores: Diese Einstellungen gehören
* dem Konto und nicht dem aktiven Team, es gibt sie genau einmal je
* Anmeldung. Ein Modul-Singleton ist dafür die ehrlichere Form.
*
* Ein Datensatz entsteht erst beim ersten Speichern. Bis dahin gilt
* `DEFAULTS` — dieselbe Linie wie serverseitig in
* backend/pb_hooks/notifications_lib.js: An heißt „betrifft mich", aus heißt
* „ist viel". Beide Listen müssen zusammenpassen; stünde hier etwas anderes,
* zeigte die Oberfläche einen Schalter an, der nichts mit dem zu tun hat, was
* tatsächlich verschickt wird.
*/
export type NotificationType =
| 'event_created'
| 'event_participation'
| 'event_changed'
| 'trail_comment'
| 'trail_marker'
| 'trail_status'
| 'team_member'
export type NotificationPrefs = Record<NotificationType, boolean> & { enabled: boolean }
export const DEFAULTS: NotificationPrefs = {
enabled: true,
event_created: true,
event_participation: true,
event_changed: true,
trail_comment: false,
trail_marker: false,
trail_status: true,
team_member: false,
}
/** Reihenfolge, Beschriftung und Begründung — was die Oberfläche anzeigt. */
export const NOTIFICATION_GROUPS: {
title: string
items: { key: NotificationType; label: string; hint: string }[]
}[] = [
{
title: 'Events',
items: [
{
key: 'event_created',
label: 'Neues Event im Team',
hint: 'Sobald jemand einen Termin anlegt.',
},
{
key: 'event_participation',
label: 'Du stehst auf der Teilnehmerliste',
hint: 'Wenn dich jemand zu einem Event einträgt.',
},
{
key: 'event_changed',
label: 'Termin oder Ort verschoben',
hint: 'Nur bei Datum und Ort — nicht bei jeder Textänderung.',
},
],
},
{
title: 'Trails',
items: [
{
key: 'trail_status',
label: 'Ein Trail wird gesperrt oder wieder frei',
hint: 'Danach richtet sich, ob die Ausfahrt am Sonntag stattfindet.',
},
{
key: 'trail_marker',
label: 'Neue Meldung an der Strecke',
hint: 'Umgestürzter Baum, kaputte Anlieger, jede neue Markierung.',
},
{
key: 'trail_comment',
label: 'Neuer Kommentar',
hint: 'Jeder Kommentar an jedem Trail des Teams. Kann viel werden.',
},
],
},
{
title: 'Team',
items: [
{
key: 'team_member',
label: 'Jemand ist neu im Kader',
hint: 'Bei jedem neu angelegten Fahrer.',
},
],
},
]
const COLLECTION = 'notification_prefs'
export class NotificationPrefsStore {
/** Der Datensatz, sofern es schon einen gibt. */
record = $state<NotificationPrefsResponse | null>(null)
values = $state<NotificationPrefs>({ ...DEFAULTS })
loading = $state(false)
saving = $state(false)
error = $state<string | null>(null)
async load() {
const uid = auth.user?.id
if (!uid) return
this.loading = true
this.error = null
try {
const list = await api.collection(COLLECTION).getFullList({
filter: `user = "${uid}"`,
requestKey: null,
})
const found = (list[0] as NotificationPrefsResponse) ?? null
this.record = found
// Ohne Datensatz bleiben die Voreinstellungen stehen. Sie sind
// keine Behelfsanzeige, sondern genau das, was der Server tut.
if (found) {
const next = { ...DEFAULTS }
for (const key of Object.keys(DEFAULTS) as (keyof NotificationPrefs)[]) {
next[key] = !!found[key as keyof NotificationPrefsResponse]
}
this.values = next
}
} catch (e: any) {
this.error = e.message ?? 'Die Einstellungen konnten nicht geladen werden.'
} finally {
this.loading = false
}
}
/**
* Einen Schalter umlegen und sofort speichern.
*
* Kein Speichern-Knopf: Es sind Schalter, sie gehören nur dem Kontoinhaber
* und jeder für sich ergibt sofort Sinn — genau wie Name und Bild im
* Profil. Ein Knopf, der fast immer nichts zu tun hat, ist eine Frage an
* jemanden, der längst geantwortet hat.
*/
async set(key: keyof NotificationPrefs, value: boolean) {
const uid = auth.user?.id
if (!uid || this.saving) return
const previous = this.values[key]
this.values = { ...this.values, [key]: value }
this.saving = true
this.error = null
try {
if (this.record) {
this.record = (await api
.collection(COLLECTION)
.update(this.record.id, { [key]: value })) as NotificationPrefsResponse
} else {
// Der erste Griff an die Schalter legt den Datensatz an — mit
// allen Werten, nicht nur dem geänderten. Sonst stünden die
// übrigen auf `false` statt auf ihrer Voreinstellung, und
// wer einen Schalter ausmacht, hätte nebenbei alle anderen
// mit ausgemacht.
this.record = (await api.collection(COLLECTION).create({
...this.values,
user: uid,
})) as NotificationPrefsResponse
}
} catch (e: any) {
this.values = { ...this.values, [key]: previous }
this.error = e.message ?? 'Das konnte nicht gespeichert werden.'
} finally {
this.saving = false
}
}
}
export const notificationPrefs = new NotificationPrefsStore()