Migrate: PocketBase → SvelteKit + better-sqlite3 + JWT

Vollständige Migration weg von PocketBase. Neuer Stack:
- better-sqlite3 (WAL-Mode, direkte SQLite-Abfragen)
- jose (JWT HS256, 30 Tage Laufzeit)
- bcryptjs (Passwort-Hashing, cost 12)

Neue Dateien:
- src/lib/server/db.ts    → SQLite-Singleton + Schema + Helpers
- src/lib/server/auth.ts  → JWT sign/verify, bcrypt, Bearer-Token
- src/lib/user.ts         → Svelte-Store (ersetzt pb.authStore)
- src/lib/api.ts          → fetch()-Wrapper (ersetzt pb.collection())
- src/app.d.ts            → App.Locals TypeScript-Deklaration
- 30 neue API-Routes unter src/routes/api/

Entfernt:
- Abhängigkeit von pocketbase npm-Paket (bleibt im package.json bis
  alle Referenzen bereinigt sind)
- PocketBase-Container aus docker-compose.yml
- Migrations und Hooks aus Deploy-Pipeline

Docker: Ein einziger Container, SQLite-Volume unter /data/
Makefile: PocketBase-spezifische Targets entfernt
seed.js: Komplett neu für neue REST-API
This commit is contained in:
rene 2026-05-21 21:55:04 +02:00
parent 61c430f2e6
commit 39981c0d17
58 changed files with 2313 additions and 651 deletions

View file

@ -2,39 +2,27 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { pb } from '$lib/pb';
import { user } from '$lib/user';
import Icon from '$lib/components/Icon.svelte';
import type { IconName } from '$lib/icons';
let { children } = $props();
onMount(() => {
if (!pb.authStore.isValid) {
goto('/login');
return;
}
if (!pb.authStore.record?.verein_id) {
goto('/onboarding');
return;
}
if (!$user) { goto('/login'); return; }
if (!$user.verein_id) { goto('/onboarding'); return; }
registerPush();
});
async function registerPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
try {
const reg = await navigator.serviceWorker.ready;
// VAPID public key vom Server holen
const keyRes = await fetch('/api/push/key');
const { publicKey } = await keyRes.json();
if (!publicKey) return;
// Bestehende oder neue Subscription
let sub = await reg.pushManager.getSubscription();
if (!sub) {
sub = await reg.pushManager.subscribe({
@ -42,18 +30,10 @@
applicationServerKey: urlBase64ToUint8Array(publicKey) as BufferSource,
});
}
// In PocketBase speichern
await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: pb.authStore.token,
},
body: JSON.stringify({
subscription: sub.toJSON(),
userId: pb.authStore.record?.id,
}),
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$user?.token}` },
body: JSON.stringify({ subscription: sub.toJSON(), userId: $user?.id }),
});
} catch (e) {
console.warn('[push] Registrierung fehlgeschlagen:', e);
@ -67,7 +47,7 @@
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
}
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
const isAdmin = () => !$user?.rolle || $user?.rolle === 'admin';
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
{ href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { onMount } from 'svelte';
import type { Verein, Termin } from '$lib/types';
@ -8,15 +8,12 @@
let loading = $state(true);
onMount(async () => {
const vid = pb.authStore.record?.verein_id as string;
const now = new Date().toISOString();
[verein, termine] = await Promise.all([
pb.collection('vereine').getOne<Verein>(vid),
pb.collection('termine').getList<Termin>(1, 3, {
filter: `beginn >= "${now}"`,
sort: 'beginn',
}).then(r => r.items),
api.get<Verein>('/vereine'),
api.get<Termin[]>('/termine', { sort: 'beginn', page: '1', perPage: '3' }),
]);
termine = (termine as any)?.items ?? termine;
loading = false;
});

View file

@ -1,5 +1,7 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { user } from '$lib/user';
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { generatePain008, downloadXml, minEinzugsdatum, type SepaPosition } from '$lib/sepa';
@ -41,11 +43,10 @@
};
onMount(async () => {
if (pb.authStore.record?.rolle === 'trainer') { goto('/'); return; }
const vid = pb.authStore.record?.verein_id as string;
if (get(user)?.rolle === 'trainer') { goto('/'); return; }
[beitraege, verein] = await Promise.all([
pb.collection('beitraege').getFullList<Beitrag>({ sort: 'name' }),
pb.collection('vereine').getOne<Verein>(vid).catch(() => null),
api.get<Beitrag[]>('/beitraege', { sort: 'name' }),
api.get<Verein>('/vereine').catch(() => null),
]);
loading = false;
});
@ -71,13 +72,12 @@
}
saving = true;
try {
const vid = pb.authStore.record?.verein_id as string;
const data = { verein_id: vid, name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
const data = { name: fName.trim(), betrag, rhythmus: fRhythmus, beschreibung: fBeschr.trim() };
if (editId) {
await pb.collection('beitraege').update(editId, data);
await api.put('/beitraege/' + editId, data);
beitraege = beitraege.map(b => b.id === editId ? { ...b, ...data } as Beitrag : b);
} else {
const neu = await pb.collection('beitraege').create<Beitrag>(data);
const neu = await api.post<Beitrag>('/beitraege', data);
beitraege = [...beitraege, neu].sort((a, b) => a.name.localeCompare(b.name));
}
showForm = false;
@ -90,7 +90,7 @@
async function loeschen(b: Beitrag) {
if (!confirm(`"${b.name}" wirklich löschen?`)) return;
await pb.collection('beitraege').delete(b.id);
await api.del('/beitraege/' + b.id);
beitraege = beitraege.filter(x => x.id !== b.id);
}
@ -102,9 +102,7 @@
einzugsdatum = minEinzugsdatum();
sepaLoading = true;
try {
const alle = await pb.collection('mitglieder').getFullList<Mitglied>({
filter: 'status = "aktiv"', sort: 'nachname,vorname',
});
const alle = await api.get<Mitglied[]>('/mitglieder', { status: 'aktiv', sort: 'nachname,vorname' });
const mit = alle.filter(m => m.iban?.trim());
const ohne = alle.length - mit.length;
sepaPreview = { mitglieder: mit, ohne };
@ -157,10 +155,9 @@
downloadXml(xml, `sepa-einzug-${einzugsdatum}.xml`);
// Einzüge als "ausstehend" anlegen
const vid = pb.authStore.record?.verein_id as string;
await Promise.all(
sepaPreview.mitglieder.map((m) =>
pb.collection('einzuege').create({
api.post('/einzuege', {
mitglied_id: m.id,
beitrag_id: sepaFor!.id,
betrag: sepaFor!.betrag,

View file

@ -1,10 +1,12 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { user } from '$lib/user';
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import type { Verein, Gruppe } from '$lib/types';
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
let loading = $state(true);
let saving = $state(false);
@ -60,19 +62,19 @@
];
onMount(async () => {
vereinId = pb.authStore.record?.verein_id as string;
vereinId = get(user)?.verein_id ?? '';
const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
pb.collection('vereine').getOne<Verein>(vereinId),
api.get<Verein>('/vereine'),
isAdmin()
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
? api.get<any[]>('/users')
: Promise.resolve([]),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
pb.collection('mitglieder').getList(1, 1, { filter: `verein_id = "${vereinId}"` }).then(r => r.totalItems),
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
api.get<{ total: number }>('/mitglieder/count').catch(() => ({ total: 0 })),
]);
trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
gruppen = alleGruppen;
plan = v.plan ?? 'free';
mitgliederAnz = mitgliederCount;
mitgliederAnz = (mitgliederCount as any).total ?? 0;
name = v.name ?? '';
adresse = v.adresse ?? '';
plz = v.plz ?? '';
@ -91,7 +93,7 @@
if (!name.trim()) { error = 'Vereinsname ist Pflichtfeld.'; return; }
error = ''; success = ''; saving = true;
try {
await pb.collection('vereine').update(vereinId, {
await api.patch('/vereine', {
name: name.trim(),
adresse: adresse.trim() || null,
plz: plz.trim() || null,
@ -113,17 +115,14 @@
}
async function trainerEinladen() {
const token = crypto.randomUUID().replace(/-/g, '');
await pb.collection('einladungen').create({
verein_id: vereinId, rolle: 'trainer', token, genutzt: false,
});
einladungUrl = `${window.location.origin}/invite/${token}`;
const inv = await api.post<{ token: string }>('/einladungen', { rolle: 'trainer' });
einladungUrl = `${window.location.origin}/invite/${inv.token}`;
einladungKopiert = false;
}
async function trainerEntfernen(uid: string) {
if (!confirm('Trainer wirklich entfernen?')) return;
await pb.collection('users').update(uid, { rolle: null, verein_id: null });
await api.del('/users/' + uid);
trainer = trainer.filter(t => t.id !== uid);
}
@ -135,7 +134,7 @@
}
function abmelden() {
pb.authStore.clear();
user.clear();
goto('/login');
}
</script>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { onMount } from 'svelte';
import Papa from 'papaparse';
import type { Mitglied } from '$lib/types';
@ -39,10 +39,9 @@
];
onMount(async () => {
const vid = pb.authStore.record?.verein_id as string;
const [m, v] = await Promise.all([
pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' }),
pb.collection('vereine').getOne<any>(vid),
api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' }),
api.get<any>('/vereine'),
]);
mitglieder = m;
vereinName = v.name ?? '';
@ -93,14 +92,13 @@
}
async function exportiereBackup() {
const vid = pb.authStore.record?.verein_id as string;
const [verein, mitgl, gruppen, termine, beitraege, nachrichten] = await Promise.all([
pb.collection('vereine').getOne<any>(vid),
pb.collection('mitglieder').getFullList(),
pb.collection('gruppen').getFullList(),
pb.collection('termine').getFullList(),
pb.collection('beitraege').getFullList(),
pb.collection('nachrichten').getFullList(),
api.get<any>('/vereine'),
api.get<any[]>('/mitglieder'),
api.get<any[]>('/gruppen'),
api.get<any[]>('/termine'),
api.get<any[]>('/beitraege'),
api.get<any[]>('/nachrichten'),
]);
const backup = {
exportiert_am: new Date().toISOString(),
@ -177,12 +175,11 @@
async function importStarten() {
importLaeuft = true;
const vid = pb.authStore.record?.verein_id as string;
let ok = 0;
const fehler: string[] = [];
for (const row of csvRows) {
const record: Record<string, any> = { verein_id: vid, status: 'aktiv' };
const record: Record<string, any> = { status: 'aktiv' };
for (const [csvSpalte, ziel] of Object.entries(feldMapping)) {
if (!ziel) continue;
const wert = row[csvSpalte]?.trim() ?? '';
@ -200,7 +197,7 @@
continue;
}
try {
await pb.collection('mitglieder').create(record);
await api.post('/mitglieder', record);
ok++;
} catch (e: unknown) {
fehler.push(`${record.vorname} ${record.nachname}: ${e instanceof Error ? e.message : 'Fehler'}`);
@ -208,7 +205,7 @@
}
// Mitgliederliste aktualisieren
mitglieder = await pb.collection('mitglieder').getFullList<Mitglied>({ sort: 'nachname,vorname' });
mitglieder = await api.get<Mitglied[]>('/mitglieder', { sort: 'nachname,vorname' });
importResult = { ok, fehler };
importPhase = 'done';
importLaeuft = false;

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { onMount } from 'svelte';
let mitglieder = $state<any[]>([]);
@ -9,8 +9,8 @@
onMount(async () => {
[mitglieder, gruppen] = await Promise.all([
pb.collection('mitglieder').getFullList({ sort: 'nachname,vorname' }),
pb.collection('gruppen').getFullList({ sort: 'name' })
api.get<any[]>('/mitglieder', { sort: 'nachname,vorname' }),
api.get<any[]>('/gruppen', { sort: 'name' })
]);
loading = false;
});

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
@ -61,8 +61,8 @@
onMount(async () => {
const [m, g] = await Promise.all([
pb.collection('mitglieder').getOne<Mitglied>(id),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
api.get<Mitglied>('/mitglieder/' + id),
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
]);
loadRecord(m);
gruppen = g;
@ -78,7 +78,7 @@
async function speichern() {
error = ''; saving = true;
try {
await pb.collection('mitglieder').update(id, {
await api.put('/mitglieder/' + id, {
vorname: vorname.trim(),
nachname: nachname.trim(),
email: email.trim() || null,
@ -107,7 +107,7 @@
async function loeschen() {
try {
await pb.collection('mitglieder').delete(id);
await api.del('/mitglieder/' + id);
goto('/mitglieder');
} catch (e: unknown) {
error = e instanceof Error ? e.message : 'Fehler beim Löschen.';

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
@ -31,7 +31,7 @@
let loading = $state(false);
onMount(async () => {
gruppen = await pb.collection('gruppen').getFullList({ sort: 'name' });
gruppen = await api.get<any[]>('/gruppen', { sort: 'name' });
});
function toggleGruppe(id: string) {
@ -43,9 +43,7 @@
async function speichern() {
error = ''; loading = true;
try {
const verein_id = pb.authStore.record?.verein_id as string;
await pb.collection('mitglieder').create({
verein_id,
await api.post('/mitglieder', {
vorname: vorname.trim(),
nachname: nachname.trim(),
email: email.trim() || null,

View file

@ -1,5 +1,7 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { user } from '$lib/user';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
import type { Nachricht, Gruppe } from '$lib/types';
@ -18,8 +20,8 @@
onMount(async () => {
[nachrichten, gruppen] = await Promise.all([
pb.collection('nachrichten').getFullList<Nachricht>({ sort: '-gesendet_am' }),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
api.get<Nachricht[]>('/nachrichten', { sort: '-gesendet_am' }),
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
]);
loading = false;
});
@ -43,12 +45,7 @@
sendError = ''; sending = true;
try {
const verein_id = pb.authStore.record?.verein_id as string;
const autor_id = pb.authStore.record?.id as string;
const record = await pb.collection('nachrichten').create<Nachricht>({
verein_id,
autor_id,
const record = await api.post<Nachricht>('/nachrichten', {
betreff: fBetreff.trim(),
text: fText.trim(),
gruppe_ids: fGruppeIds,
@ -63,7 +60,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: pb.authStore.token,
Authorization: `Bearer ${get(user)?.token ?? ''}`,
},
body: JSON.stringify({
titel: fBetreff.trim(),

View file

@ -1,10 +1,12 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { user } from '$lib/user';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
import type { Neuigkeit, Gruppe, Termin } from '$lib/types';
const canPost = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle !== null;
const userId = () => pb.authStore.record?.id as string;
const canPost = () => true; // alle eingeloggten User dürfen posten
const userId = () => get(user)?.id as string;
let neuigkeiten = $state<Neuigkeit[]>([]);
let gruppen = $state<Gruppe[]>([]);
@ -28,31 +30,25 @@
let ladeError = $state('');
onMount(async () => {
const vid = pb.authStore.record?.verein_id as string;
try {
// Queries einzeln damit ein Fehler sichtbar wird
const [nList, gList] = await Promise.all([
pb.collection('neuigkeiten').getFullList<Neuigkeit>({
sort: '-created',
}),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
api.get<Neuigkeit[]>('/neuigkeiten', { sort: '-created' }),
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
]);
neuigkeiten = nList;
gruppen = gList;
// Termine der letzten 30 Tage + zukünftige
try {
const von = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().replace('T', ' ');
termine = await pb.collection('termine').getFullList<Termin>({
filter: `beginn >= '${von}'`, sort: '-beginn',
});
termine = await api.get<Termin[]>('/termine', { sort: '-beginn' });
} catch { termine = []; }
// Reaktionen separat damit Fehler nicht alles blockiert
try {
const [rList, meineList] = await Promise.all([
pb.collection('reaktionen').getFullList({ filter: `beitrag_id.verein_id = "${vid}"` }),
pb.collection('reaktionen').getFullList({ filter: `user_id = "${userId()}"` }),
api.get<any[]>('/reaktionen'),
api.get<any[]>('/reaktionen', { meine: 'true' }),
]);
const counts: Record<string, number> = {};
for (const r of rList) counts[r.beitrag_id] = (counts[r.beitrag_id] ?? 0) + 1;
@ -96,11 +92,9 @@
}
formError = ''; saving = true;
try {
const vid = pb.authStore.record?.verein_id as string;
const u = get(user);
const form = new FormData();
form.append('verein_id', vid);
form.append('autor_id', userId());
form.append('autor_name', pb.authStore.record?.name ?? '');
form.append('autor_name', u?.name ?? '');
if (fText.trim()) form.append('text', fText.trim());
if (fTerminId) form.append('termin_id', fTerminId);
for (const id of fGruppeIds) form.append('gruppe_ids', id);
@ -108,7 +102,7 @@
for (const file of Array.from(fDateien)) form.append('medien', file);
}
const neu = await pb.collection('neuigkeiten').create<Neuigkeit>(form);
const neu = await api.postForm<Neuigkeit>('/neuigkeiten', form);
neuigkeiten = [neu, ...neuigkeiten];
showForm = false;
fText = ''; fGruppeIds = []; fTerminId = ''; fDateien = null; previews = [];
@ -121,26 +115,24 @@
async function loeschen(n: Neuigkeit) {
if (!confirm('Beitrag löschen?')) return;
await pb.collection('neuigkeiten').delete(n.id);
await api.del('/neuigkeiten/' + n.id);
neuigkeiten = neuigkeiten.filter(x => x.id !== n.id);
}
async function toggleReaktion(n: Neuigkeit) {
if (meineReaktion[n.id]) {
await pb.collection('reaktionen').delete(meineReaktion[n.id]);
await api.del('/reaktionen/' + meineReaktion[n.id]);
meineReaktion = { ...meineReaktion, [n.id]: '' };
reaktionen = { ...reaktionen, [n.id]: Math.max(0, (reaktionen[n.id] ?? 1) - 1) };
} else {
const r = await pb.collection('reaktionen').create({
beitrag_id: n.id, user_id: userId(),
});
const r = await api.post<{ id: string }>('/reaktionen', { beitrag_id: n.id });
meineReaktion = { ...meineReaktion, [n.id]: r.id };
reaktionen = { ...reaktionen, [n.id]: (reaktionen[n.id] ?? 0) + 1 };
}
}
function mediaUrl(n: Neuigkeit, datei: string, thumb = false): string {
return pb.files.getURL(n as any, datei, thumb ? { thumb: '400x300' } : {});
return api.fileUrl(n.verein_id, n.id, datei, thumb);
}
function isVideo(datei: string): boolean {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { onMount } from 'svelte';
import type { Veranstaltungsort, OrtAusfall } from '$lib/types';
@ -31,10 +31,9 @@
};
onMount(async () => {
const vid = pb.authStore.record?.verein_id as string;
[orte, ausfaelle] = await Promise.all([
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}"` }),
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name' }),
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
]);
loading = false;
});
@ -54,13 +53,12 @@
if (!fName.trim()) { ortError = 'Name ist Pflichtfeld.'; return; }
ortError = ''; ortSaving = true;
try {
const vid = pb.authStore.record?.verein_id as string;
const data = { verein_id: vid, name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
const data = { name: fName.trim(), adresse: fAdresse.trim() || null, typ: fTyp, aktiv: fAktiv };
if (editOrtId) {
const u = await pb.collection('veranstaltungsorte').update<Veranstaltungsort>(editOrtId, data);
const u = await api.put<Veranstaltungsort>('/veranstaltungsorte/' + editOrtId, data);
orte = orte.map(o => o.id === editOrtId ? u : o);
} else {
const n = await pb.collection('veranstaltungsorte').create<Veranstaltungsort>(data);
const n = await api.post<Veranstaltungsort>('/veranstaltungsorte', data);
orte = [...orte, n].sort((a, b) => a.name.localeCompare(b.name));
}
showOrtForm = false;
@ -73,7 +71,7 @@
async function ortLoeschen(id: string) {
if (!confirm('Ort wirklich löschen? Alle verknüpften Termine verlieren die Ortzuordnung.')) return;
await pb.collection('veranstaltungsorte').delete(id);
await api.del('/veranstaltungsorte/' + id);
orte = orte.filter(o => o.id !== id);
ausfaelle = ausfaelle.filter(a => a.ort_id !== id);
}
@ -88,7 +86,7 @@
if (aVon > aBis) { ausfallError = 'Bis muss nach Von liegen.'; return; }
ausfallError = ''; ausfallSaving = true;
try {
const n = await pb.collection('ort_ausfaelle').create<OrtAusfall>({
const n = await api.post<OrtAusfall>('/ort_ausfaelle', {
ort_id: aOrtId, von: aVon, bis: aBis, grund: aGrund.trim() || null,
});
ausfaelle = [...ausfaelle, n].sort((a, b) => a.von.localeCompare(b.von));
@ -101,7 +99,7 @@
}
async function ausfallLoeschen(id: string) {
await pb.collection('ort_ausfaelle').delete(id);
await api.del('/ort_ausfaelle/' + id);
ausfaelle = ausfaelle.filter(a => a.id !== id);
}

View file

@ -1,13 +1,15 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { api } from '$lib/api';
import { user } from '$lib/user';
import { get } from 'svelte/store';
import { onMount } from 'svelte';
import { RRule } from 'rrule';
import { Calendar, TimeGrid, DayGrid } from '@event-calendar/core';
import '@event-calendar/core/index.css';
import type { Termin, Gruppe, Verfuegbarkeit, Veranstaltungsort, OrtAusfall } from '$lib/types';
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
const userId = () => pb.authStore.record?.id as string;
const isAdmin = () => { const u = get(user); return !u?.rolle || u.rolle === 'admin'; };
const userId = () => get(user)?.id as string;
let termine = $state<Termin[]>([]);
let gruppen = $state<Gruppe[]>([]);
@ -59,15 +61,14 @@
);
onMount(async () => {
const vid = pb.authStore.record?.verein_id as string;
[termine, gruppen, alleUser, orte, ausfaelle] = await Promise.all([
pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
api.get<Termin[]>('/termine', { sort: 'beginn' }),
api.get<Gruppe[]>('/gruppen', { sort: 'name' }),
isAdmin()
? pb.collection('users').getFullList({ filter: `verein_id = "${vid}" && rolle = "trainer"` })
? api.get<any[]>('/users', { rolle: 'trainer' })
: Promise.resolve([]),
pb.collection('veranstaltungsorte').getFullList<Veranstaltungsort>({ sort: 'name', filter: `verein_id = "${vid}" && aktiv = true` }),
pb.collection('ort_ausfaelle').getFullList<OrtAusfall>({ sort: 'von' }),
api.get<Veranstaltungsort[]>('/veranstaltungsorte', { sort: 'name', aktiv: 'true' }),
api.get<OrtAusfall[]>('/ort_ausfaelle', { sort: 'von' }),
]);
loading = false;
});
@ -110,9 +111,7 @@
}
function generiereTerminDaten(beginn: Date, rruleStr: string | null) {
const vid = pb.authStore.record?.verein_id as string;
return {
verein_id: vid,
titel: fTitel.trim(),
beschreibung: fBeschr.trim() || null,
ort: fOrtId ? null : (fOrt.trim() || null),
@ -146,7 +145,7 @@
const neu = await Promise.all(
dates.map(d =>
pb.collection('termine').create<Termin>({
api.post<Termin>('/termine', {
...generiereTerminDaten(d, rruleStr),
beginn: d.toISOString(),
ende: fEnde ? new Date(d.getTime() + dauer).toISOString() : null,
@ -162,10 +161,10 @@
ende: fEnde ? fromLocal(fEnde) : null,
};
if (editId) {
const updated = await pb.collection('termine').update<Termin>(editId, data);
const updated = await api.put<Termin>('/termine/' + editId, data);
termine = termine.map(t => t.id === editId ? updated : t);
} else {
const neu = await pb.collection('termine').create<Termin>(data);
const neu = await api.post<Termin>('/termine', data);
termine = [...termine, neu];
}
}
@ -180,10 +179,10 @@
async function loeschen(t: Termin, ganzeSerieLoeschen: boolean) {
if (ganzeSerieLoeschen && t.serie_id) {
const serie = termine.filter(x => x.serie_id === t.serie_id);
await Promise.all(serie.map(s => pb.collection('termine').delete(s.id)));
await Promise.all(serie.map(s => api.del('/termine/' + s.id)));
termine = termine.filter(x => x.serie_id !== t.serie_id);
} else {
await pb.collection('termine').delete(t.id);
await api.del('/termine/' + t.id);
termine = termine.filter(x => x.id !== t.id);
}
showDelete = null;
@ -202,7 +201,7 @@
});
async function setVerfuegbarkeit(t: Termin, v: Verfuegbarkeit) {
const updated = await pb.collection('termine').update<Termin>(t.id, { verfuegbarkeit: v });
const updated = await api.put<Termin>('/termine/' + t.id, { verfuegbarkeit: v });
termine = termine.map(x => x.id === t.id ? updated : x);
}