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:
parent
61c430f2e6
commit
39981c0d17
58 changed files with 2313 additions and 651 deletions
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue