From 81f34905cfb5df6736751af011d40c7e752386d4 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 20 May 2026 20:33:09 +0200 Subject: [PATCH] Termine: Wochen- und Monatsansicht via @event-calendar/core, umschaltbar zur Listenansicht --- app/package-lock.json | 30 +++----- app/package.json | 1 + app/src/lib/event-calendar.d.ts | 13 ++++ app/src/routes/(app)/termine/+page.svelte | 83 ++++++++++++++++++++++- 4 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 app/src/lib/event-calendar.d.ts diff --git a/app/package-lock.json b/app/package-lock.json index dad8bd4..89829cd 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "vereinshaus", "version": "0.1.0", "dependencies": { + "@event-calendar/core": "^5.7.0", "ical-generator": "^10.2.0", "papaparse": "^5.5.3", "pocketbase": "^0.26.9", @@ -1622,6 +1623,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@event-calendar/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@event-calendar/core/-/core-5.7.0.tgz", + "integrity": "sha512-16S9TncV/az52qFjvmB691fMQA8qIo/Iz4kxrvOMLwD46oFzin07aWQQPBcgCFUN+58m9AbFYWnxkvO7OYAldg==", + "license": "MIT", + "dependencies": { + "svelte": "^5.55.4" + } + }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", @@ -1636,7 +1646,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1647,7 +1656,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1658,7 +1666,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1679,14 +1686,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2522,7 +2527,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -2644,7 +2648,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -2678,7 +2681,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT" }, "node_modules/@types/web-push": { @@ -2695,7 +2697,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2734,7 +2735,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2838,7 +2838,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -3066,7 +3065,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3283,7 +3281,6 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "dev": true, "license": "MIT" }, "node_modules/dunder-proto": { @@ -3483,14 +3480,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -4882,7 +4877,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -4913,7 +4907,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -5978,7 +5971,6 @@ "version": "5.55.8", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.8.tgz", "integrity": "sha512-4D6lyrMHmDaZalQOEBMCWCCidyZjSnec14/oPn0k627G6goxcck9xqMwz1tFLlQz+ZFvtTTHfFOlUayuAz0z6Q==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -6030,7 +6022,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -6870,7 +6861,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, "license": "MIT" } } diff --git a/app/package.json b/app/package.json index 3baa302..e84b59f 100644 --- a/app/package.json +++ b/app/package.json @@ -26,6 +26,7 @@ "workbox-precaching": "^7.4.1" }, "dependencies": { + "@event-calendar/core": "^5.7.0", "ical-generator": "^10.2.0", "papaparse": "^5.5.3", "pocketbase": "^0.26.9", diff --git a/app/src/lib/event-calendar.d.ts b/app/src/lib/event-calendar.d.ts new file mode 100644 index 0000000..703104d --- /dev/null +++ b/app/src/lib/event-calendar.d.ts @@ -0,0 +1,13 @@ +declare module '@event-calendar/core' { + import type { SvelteComponent } from 'svelte'; + + export class Calendar extends SvelteComponent<{ + plugins: any[]; + options: Record; + }> {} + + export const TimeGrid: any; + export const DayGrid: any; + export const List: any; + export const Interaction: any; +} diff --git a/app/src/routes/(app)/termine/+page.svelte b/app/src/routes/(app)/termine/+page.svelte index 121b488..01c2751 100644 --- a/app/src/routes/(app)/termine/+page.svelte +++ b/app/src/routes/(app)/termine/+page.svelte @@ -2,6 +2,8 @@ import { pb } from '$lib/pb'; 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'; @@ -14,6 +16,11 @@ let ausfaelle = $state([]); let loading = $state(true); + // Ansicht + type Ansicht = 'liste' | 'woche' | 'monat'; + let ansicht = $state('liste'); + const calPlugins = [TimeGrid, DayGrid]; + // Formular let showForm = $state(false); let editId = $state(null); @@ -232,6 +239,45 @@ function istMeinTermin(t: Termin): boolean { return t.durchfuehrender_id === userId(); } + + function verfuegbarkeitFarbe(t: Termin): string { + switch (t.verfuegbarkeit) { + case 'bestaetigt': return '#16a34a'; + case 'abgesagt': return '#dc2626'; + case 'vertretung_gesucht': return '#7c3aed'; + default: return '#1e40af'; + } + } + + const calEvents = $derived(termine.map(t => ({ + id: t.id, + title: t.titel, + start: t.beginn, + end: t.ende ?? new Date(new Date(t.beginn).getTime() + 60 * 60 * 1000).toISOString(), + backgroundColor: verfuegbarkeitFarbe(t), + extendedProps: { termin: t }, + }))); + + const calOptions = $derived({ + view: ansicht === 'monat' ? 'dayGridMonth' : 'timeGridWeek', + locale: 'de', + firstDay: 1, + height: ansicht === 'monat' ? 'auto' : '72dvh', + events: calEvents, + slotMinTime: '07:00:00', + slotMaxTime: '23:00:00', + allDaySlot: false, + headerToolbar: { + start: 'prev,next today', + center: 'title', + end: '', + }, + buttonText: { today: 'Heute' }, + eventClick: (info: any) => { + const t: Termin = info.event.extendedProps.termin; + if (isAdmin()) bearbeiten(t); + }, + }); Termine — vereins.haus @@ -241,6 +287,13 @@
{#if isAdmin()} Orte + {/if} +
+ + + +
+ {#if isAdmin()} {/if}
@@ -254,6 +307,10 @@ {#if loading}

Laden…

+{:else if ansicht !== 'liste'} +
+ +
{:else if termine.length === 0}

Noch keine Termine geplant.

{:else} @@ -497,13 +554,37 @@