From 209bc315a17d2b83e6175ed4ff3ef4b2f5150999 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 18 Sep 2025 21:36:46 +0200 Subject: [PATCH 01/94] feat: Add new dashboard modules and update architecture - Add ExpiringAppointments feature with card and admin views - Add UserStatistics feature with card and admin views - Create shared types for dashboard modules - Update App.vue to include new navigation - Enhance AutomaticGroups components - Add comprehensive architecture documentation - Improve component structure and styling This commit introduces new functionality for managing expiring appointments and user statistics, along with necessary architectural improvements. --- docs/architecture.md | 182 +++++++ src/App.vue | 54 +- src/components/AutomaticGroupsAdmin.vue | 5 + src/components/AutomaticGroupsCard.vue | 83 ++- src/components/ExpiringAppointmentsAdmin.vue | 513 +++++++++++++++++++ src/components/ExpiringAppointmentsCard.vue | 399 +++++++++++++++ src/components/Start.vue | 173 ++++--- src/components/UserStatisticsAdmin.vue | 144 ++++++ src/components/UserStatisticsCard.vue | 84 +++ src/types/modules.ts | 10 + 10 files changed, 1551 insertions(+), 96 deletions(-) create mode 100644 docs/architecture.md create mode 100644 src/components/ExpiringAppointmentsAdmin.vue create mode 100644 src/components/ExpiringAppointmentsCard.vue create mode 100644 src/components/UserStatisticsAdmin.vue create mode 100644 src/components/UserStatisticsCard.vue create mode 100644 src/types/modules.ts diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..580bdff --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,182 @@ +# ChurchTools Dashboard - Architektur-Dokumentation + +## Überblick +Das ChurchTools Dashboard ist eine moderne, modulare Webanwendung, die mit Vue 3 und TypeScript entwickelt wurde. Es bietet eine Benutzeroberfläche zur Überwachung und Verwaltung verschiedener Aspekte einer ChurchTools-Instanz. + +## Technologie-Stack + +### Kern-Technologien +- **Vue 3** - Progressives JavaScript-Framework +- **TypeScript** - Typisiertes JavaScript-Superset +- **Vite** - Schnelles Build-Tool und Entwicklungsserver +- **ChurchTools Client** - Offizielle Client-Bibliothek für die ChurchTools-API + +### Entwicklungswerkzeuge +- **Node.js** - JavaScript-Laufzeitumgebung +- **npm** - Paketmanager +- **ESLint** - Code-Linting +- **Prettier** - Code-Formatierung + +## Projektstruktur + +``` +src/ +├── components/ # Wiederverwendbare UI-Komponenten +│ ├── AutomaticGroupsAdmin.vue # Admin-Oberfläche für automatische Gruppen +│ ├── AutomaticGroupsCard.vue # Karte für die Übersicht der automatischen Gruppen +│ ├── ExpiringAppointmentsAdmin.vue # Admin-Oberfläche für auslaufende Serientermine +│ ├── ExpiringAppointmentsCard.vue # Karte für die Übersicht der auslaufenden Serientermine +│ ├── UserStatisticsAdmin.vue # Admin-Oberfläche für Benutzerstatistiken +│ ├── UserStatisticsCard.vue # Karte für Benutzerstatistiken +│ └── Start.vue # Haupt-Dashboard-Ansicht +├── types/ +│ └── modules.ts # TypeScript-Typdefinitionen für Dashboard-Module +├── App.vue # Hauptkomponente +├── main.ts # Einstiegspunkt der Anwendung +└── style.css # Globale Stile +``` + +## Wichtige Komponenten + +### 1. Hauptanwendung (App.vue) +- Verwaltet Routing und Layout +- Verwaltet den Anwendungszustand +- Rendert die Hauptnavigation und den Inhaltsbereich + +### 2. Start-Komponente (Start.vue) +- Hauptansicht des Dashboards +- Zeigt Feature-Karten in einem responsiven Grid an +- Verarbeitet die Navigation zu Admin-Ansichten +- Verwaltet das Layout und die Anpassungsfähigkeit der Karten + +### 3. Dashboard-Karten +- **AutomaticGroupsCard**: Zeigt Statistiken und Aktualisierungsstatus der automatischen Gruppen an +- **ExpiringAppointmentsCard**: Zeigt anstehende auslaufende Serientermine an +- **UserStatisticsCard**: Zeigt wichtige Benutzerkennzahlen und Statistiken an + +### 4. Admin-Komponenten +- **AutomaticGroupsAdmin**: Detaillierte Verwaltungsoberfläche für automatische Gruppen +- **ExpiringAppointmentsAdmin**: Verwaltungsoberfläche für auslaufende Serientermine +- **UserStatisticsAdmin**: Detaillierte Benutzerstatistiken und -analysen + +## Datenfluss + +1. **Initialisierung** + - Die App wird geladen und initialisiert den ChurchTools-Client + - Die Authentifizierung erfolgt im Entwicklungsmodus automatisch + - Dashboard-Module werden registriert und konfiguriert + +2. **Datenabruf** + - Komponenten rufen bei der Initialisierung Daten ab + - Daten werden bei Bedarf zwischengespeichert + - Automatische Aktualisierung kann manuell ausgelöst werden + - Umfassende Fehlerbehandlung mit Fallback auf Testdaten + +3. **Zustandsverwaltung** + - Lokaler Komponentenstatus mit `ref` und `reactive` + - Wiederverwendbare Funktionen für gemeinsame Logik + - Props und Events für die Kommunikation zwischen Komponenten + +## Authentifizierung + +- Nutzt die ChurchTools-Authentifizierung +- Entwicklungsmodus unterstützt automatische Anmeldung mit Umgebungsvariablen +- Sitzungsverwaltung wird vom ChurchTools-Client übernommen + +## Fehlerbehandlung + +- Globale Fehlerbehandlung für API-Aufrufe +- Benutzerfreundliche Fehlermeldungen +- Fallback auf Testdaten, wenn die API nicht verfügbar ist + +## Entwicklungsworkflow + +1. **Einrichtung** + ```bash + npm install + cp .env-example .env + # Konfigurieren Sie die Umgebungsvariablen in .env + ``` + +2. **Entwicklungsserver** + ```bash + npm run dev + ``` + +3. **Produktionsbuild** + ```bash + npm run build + ``` + +## Umgebungsvariablen + +Erforderliche Umgebungsvariablen (in `.env` gespeichert): +- `VITE_BASE_URL` - Basis-URL der ChurchTools-Instanz +- `VITE_USERNAME` - Benutzername für die automatische Anmeldung im Entwicklungsmodus +- `VITE_PASSWORD` - Passwort für die automatische Anmeldung im Entwicklungsmodus + +## Best Practices + +1. **Komponentendesign** + - Single-Responsibility-Prinzip + - Wiederverwendbare, kombinierbare Komponenten + - Klare Props- und Events-Schnittstellen + +2. **Zustandsverwaltung** + - Zustand möglichst lokal halten + - Composition API für komplexe Logik verwenden + - Direkte Mutationen vermeiden, Methoden verwenden + +3. **Styling** + - Komponentenbezogene Stile + - CSS-Variablen für das Theming + - Responsive Design-Patterns + +## Zukünftige Verbesserungen + +1. **Tests** + - Unit-Tests für Komponenten mit Vitest + - Integrationstests für den Datenfluss + - E2E-Tests für kritische Pfade mit Cypress + +2. **Funktionen** + - Weitere Dashboard-Widgets (z.B. Veranstaltungsstatistiken, Finanzübersicht) + - Anpassbares Dashboard-Layout mit Drag-and-Drop + - Echtzeitaktualisierungen mit WebSockets + - Exportfunktion für Berichte + - Benutzereinstellungen und Anpassung des Designs + +3. **Leistung** + - Code-Splitting für Admin-Ansichten + - Lazy Loading von Komponenten und Routen + - Optimiertes Laden und Caching von Assets + - Virtuelles Scrollen für große Datensätze + +4. **Barrierefreiheit** + - ARIA-Attribute und -Rollen + - Tastaturnavigation + - Hoher Kontrastmodus + +## Abhängigkeiten + +### Kern-Abhängigkeiten +- `@churchtools/churchtools-client` - Offizieller ChurchTools-API-Client +- `vue` - Kernframework (v3.x) +- `vite` - Build-Tool und Entwicklungsserver +- `typescript` - Typenprüfung und bessere Entwicklererfahrung + +### Entwicklungsabhängigkeiten +- `@vitejs/plugin-vue` - Vue 3-Unterstützung für Vite +- `@vue/compiler-sfc` - Single-File-Component-Compiler +- `eslint` - Code-Linting +- `prettier` - Code-Formatierung +- `sass` - CSS-Präprozessor (optional) + +## Browser-Unterstützung + +- Moderne Browser (Chrome, Firefox, Safari, Edge) +- IE11 wird nicht unterstützt (verwendet moderne JavaScript-Features) + +## Lizenz + +[Geben Sie hier Ihre Lizenz an] diff --git a/src/App.vue b/src/App.vue index 7f71216..9d15a15 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,5 @@ + + diff --git a/src/components/ExpiringAppointmentsCard.vue b/src/components/ExpiringAppointmentsCard.vue new file mode 100644 index 0000000..f564760 --- /dev/null +++ b/src/components/ExpiringAppointmentsCard.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/src/components/Start.vue b/src/components/Start.vue index f0efcb9..5ba8da3 100644 --- a/src/components/Start.vue +++ b/src/components/Start.vue @@ -3,99 +3,122 @@
-

Churchtools Dashboard

+

ChurchTools Dashboard

-

Dieses Modul hilft, das ChurchTools System zu überwachen

+

Zentrale Übersicht für ChurchTools Module

- +
-
-
-

{{ feature.title }}

-
-
-
{{ feature.icon }}
-

{{ feature.description }}

-
{{ feature.value }}
-
-
- - - -
- - -
-
-

System Test

-
-
-

Teste die Dashboard-Funktionalität:

- -
- ✅ Test erfolgreich ausgeführt {{ testCounter }} Mal -
+
+
diff --git a/src/components/UserStatisticsCard.vue b/src/components/UserStatisticsCard.vue new file mode 100644 index 0000000..04fe1d0 --- /dev/null +++ b/src/components/UserStatisticsCard.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/types/modules.ts b/src/types/modules.ts new file mode 100644 index 0000000..517b001 --- /dev/null +++ b/src/types/modules.ts @@ -0,0 +1,10 @@ +import type { Component } from 'vue'; + +export interface DashboardModule { + id: string; + title: string; + icon: string; + description: string; + cardComponent: Component; + adminComponent: Component; +} From 5c433496fcbe469ab62d4d01dac53dacfb49784c Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Fri, 19 Sep 2025 09:37:47 +0200 Subject: [PATCH 02/94] wip --- package-lock.json | 73 +- package.json | 3 +- src/components/ExpiringAppointmentsAdmin.vue | 1053 ++++++++++++++---- src/components/ExpiringAppointmentsCard.vue | 1023 ++++++++++++----- src/services/churchtools.ts | 144 +++ vite.config.ts | 6 + 6 files changed, 1845 insertions(+), 457 deletions(-) create mode 100644 src/services/churchtools.ts diff --git a/package-lock.json b/package-lock.json index c2179f1..4a71e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "@vitejs/plugin-vue": "^6.0.1", - "vue": "^3.5.21" + "vue": "^3.5.21", + "vue-i18n": "^9.14.5" }, "devDependencies": { "@churchtools/churchtools-client": "^1.4.0", @@ -494,6 +495,50 @@ "node": ">=18" } }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -861,6 +906,12 @@ "@vue/shared": "3.5.21" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/reactivity": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", @@ -1659,6 +1710,26 @@ "optional": true } } + }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } } } } diff --git a/package.json b/package.json index f3e6d67..6fbbcb3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@vitejs/plugin-vue": "^6.0.1", - "vue": "^3.5.21" + "vue": "^3.5.21", + "vue-i18n": "^9.14.5" } } diff --git a/src/components/ExpiringAppointmentsAdmin.vue b/src/components/ExpiringAppointmentsAdmin.vue index 7b8eea2..61ea1c1 100644 --- a/src/components/ExpiringAppointmentsAdmin.vue +++ b/src/components/ExpiringAppointmentsAdmin.vue @@ -1,254 +1,909 @@ diff --git a/src/services/churchtools.ts b/src/services/churchtools.ts new file mode 100644 index 0000000..018611a --- /dev/null +++ b/src/services/churchtools.ts @@ -0,0 +1,144 @@ +import { churchtoolsClient } from '@churchtools/churchtools-client'; + +export interface Calendar { + id: number; + name: string; + nameTranslated: string; + sortKey: number; + color: string; + isPublic: boolean; + isPrivate: boolean; + randomUrl: string; + icalUrl: string; + meta: { + createdBy: string; + createdAt: string; + modifiedBy: string; + modifiedAt: string; + }; +} + +export interface Appointment { + id: number; + title: string; + startDate: string; + endDate: string; + allDay: boolean; + note: string; + appointmentType: { + id: number; + name: string; + nameTranslated: string; + }; + baseAppointmentId: number | null; + series: { + id: number; + repeatId: number; + repeatFrequency: string; + repeatUntil: string | null; + repeatOption: any; + } | null; + calendar: { + id: number; + name: string; + nameTranslated: string; + }; +} + +/** + * Fetches all calendars from ChurchTools + */ +export async function fetchCalendars(): Promise { + const response = await churchtoolsClient.get('/calendars'); + return response.data || []; +} + +/** + * Fetches appointments for a specific calendar within a date range + */ +export async function fetchAppointments( + calendarIds: number[], + startDate: Date, + endDate: Date +): Promise { + const start = startDate.toISOString().split('T')[0]; + const end = endDate.toISOString().split('T')[0]; + + const response = await churchtoolsClient.get('/calendars/appointments', { + params: { + from: start, + to: end, + calendar_ids: calendarIds.join(',') + } + }); + + return response.data || []; +} + +/** + * Identifies church and group calendars + */ +export async function identifyCalendars(): Promise<{ + churchCalendars: Calendar[]; + groupCalendars: Calendar[]; +}> { + const calendars = await fetchCalendars(); + + // Filter out private calendars and sort by name + const publicCalendars = calendars + .filter(cal => !cal.isPrivate) + .sort((a, b) => a.name.localeCompare(b.name)); + + // This is a simple heuristic - adjust based on your ChurchTools setup + const churchCalendars = publicCalendars.filter(cal => + !cal.name.toLowerCase().includes('gruppe') && + !cal.name.toLowerCase().includes('team') + ); + + const groupCalendars = publicCalendars.filter(cal => + cal.name.toLowerCase().includes('gruppe') || + cal.name.toLowerCase().includes('team') + ); + + return { churchCalendars, groupCalendars }; +} + +/** + * Finds all recurring appointment series that are about to end + */ +export async function findExpiringSeries(daysInAdvance: number = 60): Promise { + const now = new Date(); + const endDate = new Date(); + endDate.setDate(now.getDate() + daysInAdvance); + + // Get all relevant calendars + const { churchCalendars, groupCalendars } = await identifyCalendars(); + const allCalendarIds = [ + ...churchCalendars.map(c => c.id), + ...groupCalendars.map(c => c.id) + ]; + + // Fetch appointments + const appointments = await fetchAppointments(allCalendarIds, now, endDate); + + // Find recurring appointments that are ending soon + const expiringSeries = appointments.filter(appointment => { + // Only consider recurring appointments + if (!appointment.series) return false; + + // Check if the series has an end date that's within our time frame + if (appointment.series.repeatUntil) { + const repeatUntil = new Date(appointment.series.repeatUntil); + return repeatUntil >= now && repeatUntil <= endDate; + } + + return false; + }); + + // Remove duplicates (multiple instances of the same series) + const uniqueSeries = Array.from(new Map( + expiringSeries.map(item => [item.series?.id, item]) + ).values()); + + return uniqueSeries; +} diff --git a/vite.config.ts b/vite.config.ts index 7117409..3259a2b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig, loadEnv } from 'vite'; import vue from '@vitejs/plugin-vue'; +import path from 'path'; // https://vitejs.dev/config/ export default ({ mode }) => { @@ -7,6 +8,11 @@ export default ({ mode }) => { return defineConfig({ plugins: [vue()], base: mode === 'development' ? '/' : `/ccm/${process.env.VITE_KEY}/`, + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, server: { host: '0.0.0.0', port: 5173, From e3f42f6621dc4c800676481633886e8ceb39e1a1 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Fri, 19 Sep 2025 10:20:30 +0200 Subject: [PATCH 03/94] wip --- src/components/ExpiringAppointmentsAdmin.vue | 9 +- src/components/ExpiringAppointmentsCard.vue | 96 ++++++++++++--- src/services/churchtools.ts | 120 ++++++++----------- 3 files changed, 129 insertions(+), 96 deletions(-) diff --git a/src/components/ExpiringAppointmentsAdmin.vue b/src/components/ExpiringAppointmentsAdmin.vue index 61ea1c1..298f34b 100644 --- a/src/components/ExpiringAppointmentsAdmin.vue +++ b/src/components/ExpiringAppointmentsAdmin.vue @@ -143,21 +143,21 @@
{{ appointment.title }}
- {{ formatTime(appointment.startDate) }} - {{ formatTime(appointment.endDate) }} + {{ formatTime(appointment.base.startDate) }} - {{ formatTime(appointment.endDate) }}
-
+
{{ truncateText(appointment.note, 40) }}
- + {{ appointment.calendar.name }}
{{ formatDate(appointment.startDate) }} - {{ appointment.series?.repeatUntil ? formatDate(appointment.series.repeatUntil) : $t('noEndDate') }} + {{ appointment.series?.repeatUntil ? formatDate(appointment.base.repeatUntil) : $t('noEndDate') }} {{ getStatusText(getAppointmentStatus(appointment)) }} @@ -288,6 +288,7 @@ const fetchData = async () => { // Fetch expiring series const expiringSeries = await findExpiringSeries(daysInAdvance.value); + appointments.value = expiringSeries; } catch (err) { diff --git a/src/components/ExpiringAppointmentsCard.vue b/src/components/ExpiringAppointmentsCard.vue index 3b6d5cc..04f9363 100644 --- a/src/components/ExpiringAppointmentsCard.vue +++ b/src/components/ExpiringAppointmentsCard.vue @@ -81,30 +81,27 @@

{{ appointment.title }}

- - {{ appointment.calendar.name }} + + {{ getCalendarName(appointment) }}
- {{ formatDate(appointment.startDate) }} - {{ formatTime(appointment.startDate) }} - {{ formatTime(appointment.endDate) }} -
+ {{ formatDate(appointment.base.startDate) }} + {{ appointment.base.title}} +
-
+
- Endet am {{ formatDate(appointment.series.repeatUntil) }} - - - {{ getDaysLeftText(appointment) }} - + Endet am {{ formatDate(appointment.base.repeatUntil) }} +
-
+
- {{ truncateText(appointment.note, 60) }} + {{ truncateText(appointment.base.title, 60) }}
@@ -172,6 +169,26 @@ const visibleAppointments = computed(() => { return showAll.value ? appointments.value : appointments.value.slice(0, 3); }); +// Safe calendar accessors +const getCalendarName = (appointment: Appointment) => { + if ('base' in appointment) { + return appointment.base.calendar?.name || 'Unbekannter Kalender'; + } + return appointment.calendar?.name || 'Unbekannter Kalender'; +}; + +const getCalendarColor = (appointment: Appointment) => { + let color = '#cccccc'; // Default gray color + + if ('base' in appointment) { + color = appointment.base.calendar?.color || color; + } else { + color = appointment.calendar?.color || color; + } + + return color; +}; + const hasMore = computed(() => { return appointments.value.length > 3; }); @@ -186,9 +203,9 @@ const expiredCount = computed(() => { // Get status of an appointment const getAppointmentStatus = (appointment: Appointment): string => { - if (!appointment.series?.repeatUntil) return 'active'; + if (!appointment.base?.repeatUntil) return 'active'; - const endDate = new Date(appointment.series.repeatUntil); + const endDate = new Date(appointment.base.repeatUntil); const today = new Date(); const daysUntilEnd = Math.ceil((endDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); @@ -199,9 +216,9 @@ const getAppointmentStatus = (appointment: Appointment): string => { // Get days left until the appointment series ends const getDaysLeft = (appointment: Appointment): number => { - if (!appointment.series?.repeatUntil) return 999; + if (!appointment.base?.repeatUntil) return 999; - const endDate = new Date(appointment.series.repeatUntil); + const endDate = new Date(appointment.base.repeatUntil); const today = new Date(); today.setHours(0, 0, 0, 0); @@ -216,7 +233,15 @@ const fetchData = async () => { try { const expiringSeries = await findExpiringSeries(daysInAdvance.value); + + // Log the first few appointments to debug date issues + console.log('Fetched appointments:', expiringSeries); + if (expiringSeries.length > 0) { + console.log('First appointment structure:', JSON.parse(JSON.stringify(expiringSeries[0]))); + } + appointments.value = expiringSeries; + } catch (err) { console.error('Error fetching appointments:', err); error.value = 'Fehler beim Laden der Termine. Bitte versuchen Sie es erneut.'; @@ -226,8 +251,31 @@ const fetchData = async () => { }; // Format date for display -const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString(); +const formatDate = (dateString: string | undefined | null): string => { + if (!dateString) { + console.warn('formatDate received undefined or null dateString'); + return 'Ungültiges Datum'; + } + const date = new Date(dateString); + if (isNaN(date.getTime())) { + console.warn('Invalid date string in formatDate:', dateString); + return 'Ungültiges Datum'; + } + return date.toLocaleDateString(); +}; + +// Format time for display +const formatTime = (dateString: string | undefined | null): string => { + if (!dateString) { + console.warn('formatTime received undefined or null dateString'); + return '--:--'; + } + const date = new Date(dateString); + if (isNaN(date.getTime())) { + console.warn('Invalid date string in formatTime:', dateString); + return '--:--'; + } + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; // Get status class @@ -260,6 +308,13 @@ const refreshData = () => { fetchData(); }; +// Text truncation utility +const truncateText = (text: string, maxLength: number): string => { + if (!text) return ''; + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; +}; + // Initialize component onMounted(() => { fetchData(); @@ -597,6 +652,9 @@ onMounted(() => { font-size: 0.7rem; font-weight: 600; color: white; + background-color: #cccccc; /* Fallback color */ + padding: 0.2rem 0.6rem; + border-radius: 12px; padding: 0.2rem 0.6rem; border-radius: 12px; white-space: nowrap; diff --git a/src/services/churchtools.ts b/src/services/churchtools.ts index 018611a..15857b3 100644 --- a/src/services/churchtools.ts +++ b/src/services/churchtools.ts @@ -18,39 +18,16 @@ export interface Calendar { }; } -export interface Appointment { - id: number; - title: string; - startDate: string; - endDate: string; - allDay: boolean; - note: string; - appointmentType: { - id: number; - name: string; - nameTranslated: string; - }; - baseAppointmentId: number | null; - series: { - id: number; - repeatId: number; - repeatFrequency: string; - repeatUntil: string | null; - repeatOption: any; - } | null; - calendar: { - id: number; - name: string; - nameTranslated: string; - }; -} +import type { AppointmentBase, AppointmentCalculated } from '../ct-types'; + +type Appointment = AppointmentBase | AppointmentCalculated; /** * Fetches all calendars from ChurchTools */ export async function fetchCalendars(): Promise { - const response = await churchtoolsClient.get('/calendars'); - return response.data || []; + const response = await churchtoolsClient.get('/calendars'); + return response || []; } /** @@ -63,44 +40,31 @@ export async function fetchAppointments( ): Promise { const start = startDate.toISOString().split('T')[0]; const end = endDate.toISOString().split('T')[0]; - - const response = await churchtoolsClient.get('/calendars/appointments', { - params: { - from: start, - to: end, - calendar_ids: calendarIds.join(',') - } - }); - - return response.data || []; + + const response = await churchtoolsClient.get('/calendars/appointments', + { + from: start, + to: end, + 'calendar_ids[]': calendarIds + }); + + // Ensure we always return an array of Appointment objects + return Array.isArray(response) ? response : []; } /** * Identifies church and group calendars */ export async function identifyCalendars(): Promise<{ - churchCalendars: Calendar[]; - groupCalendars: Calendar[]; + publicCalendars: Calendar[] }> { - const calendars = await fetchCalendars(); - + const calendars = await fetchCalendars(); // Filter out private calendars and sort by name const publicCalendars = calendars .filter(cal => !cal.isPrivate) .sort((a, b) => a.name.localeCompare(b.name)); - - // This is a simple heuristic - adjust based on your ChurchTools setup - const churchCalendars = publicCalendars.filter(cal => - !cal.name.toLowerCase().includes('gruppe') && - !cal.name.toLowerCase().includes('team') - ); - - const groupCalendars = publicCalendars.filter(cal => - cal.name.toLowerCase().includes('gruppe') || - cal.name.toLowerCase().includes('team') - ); - - return { churchCalendars, groupCalendars }; + + return { publicCalendars}; } /** @@ -110,35 +74,45 @@ export async function findExpiringSeries(daysInAdvance: number = 60): Promise c.id), - ...groupCalendars.map(c => c.id) - ]; - + ...publicCalendars.map(c => c.id), + ].filter((v, i, a) => a.indexOf(v) === i); // Remove duplicates + + console.log('Fetching appointments for calendar IDs:', allCalendarIds); // Fetch appointments const appointments = await fetchAppointments(allCalendarIds, now, endDate); - + // Find recurring appointments that are ending soon const expiringSeries = appointments.filter(appointment => { - // Only consider recurring appointments - if (!appointment.series) return false; + // Handle both AppointmentBase and AppointmentCalculated types + const base = 'base' in appointment ? appointment.base : appointment; + // Only consider recurring appointments with a repeatUntil date + if (!base.repeatUntil) return false; + // Check if the series has an end date that's within our time frame - if (appointment.series.repeatUntil) { - const repeatUntil = new Date(appointment.series.repeatUntil); - return repeatUntil >= now && repeatUntil <= endDate; - } - - return false; + const endDateObj = new Date(base.repeatUntil); + return endDateObj >= now ; }); - + // Remove duplicates (multiple instances of the same series) const uniqueSeries = Array.from(new Map( - expiringSeries.map(item => [item.series?.id, item]) + expiringSeries.map(item => { + const base = 'base' in item ? item.base : item; + return [base.id, item]; // Using base.id as the unique identifier + }) ).values()); - +debugger; return uniqueSeries; } From 0c0c24dd349d62176d6930012c3345d906a6bf2a Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Fri, 19 Sep 2025 15:02:31 +0200 Subject: [PATCH 04/94] wip --- package-lock.json | 46 ++ package.json | 3 + src/components/ExpiringAppointmentsAdmin.vue | 581 +++++++++++++++++-- src/main.ts | 49 ++ 4 files changed, 620 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a71e65..3f68f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "churchtools-dashboard", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.0.1", + "@fortawesome/free-solid-svg-icons": "^7.0.1", + "@fortawesome/vue-fontawesome": "^3.1.2", "@vitejs/plugin-vue": "^6.0.1", "vue": "^3.5.21", "vue-i18n": "^9.14.5" @@ -495,6 +498,49 @@ "node": ">=18" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", + "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.1.tgz", + "integrity": "sha512-x0cR55ILVqFpUioSMf6ebpRCMXMcheGN743P05W2RB5uCNpJUqWIqW66Lap8PfL/lngvjTbZj0BNSUweIr/fHQ==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.1.tgz", + "integrity": "sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.1.2.tgz", + "integrity": "sha512-mhYnBIuuW8OIMHf31kOjaBmyE7BMrwBorhrOHVud6vTTu+7IPQNWB+DWaHoE75v10dRF5s/dFtcrgE7vKSEWwQ==", + "license": "MIT", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "vue": ">= 3.0.0 < 4" + } + }, "node_modules/@intlify/core-base": { "version": "9.14.5", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", diff --git a/package.json b/package.json index 6fbbcb3..811ab83 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "vite": "^7.1.2" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.0.1", + "@fortawesome/free-solid-svg-icons": "^7.0.1", + "@fortawesome/vue-fontawesome": "^3.1.2", "@vitejs/plugin-vue": "^6.0.1", "vue": "^3.5.21", "vue-i18n": "^9.14.5" diff --git a/src/components/ExpiringAppointmentsAdmin.vue b/src/components/ExpiringAppointmentsAdmin.vue index 298f34b..ad80aed 100644 --- a/src/components/ExpiringAppointmentsAdmin.vue +++ b/src/components/ExpiringAppointmentsAdmin.vue @@ -2,17 +2,17 @@
-

{{ $t('expiringAppointments') }}

-

- {{ $t('manageExpiringAppointments') }} + Verwalten Sie auslaufende Terminserien

- + - {{ $t('days') }} + Tagen enden
-

{{ $t('loadingAppointments') }}

+

Lade Termine...

{{ error }}

@@ -44,15 +44,15 @@
{{ filteredAppointments.length }}
-
{{ $t('totalAppointments') }}
+
Gesamte Termine
{{ getCountByStatus('expiring') }}
-
{{ $t('expiringSoon') }}
+
Laufen bald ab
{{ getCountByStatus('expired') }}
-
{{ $t('expired') }}
+
Abgelaufen
@@ -62,7 +62,7 @@ @@ -70,7 +70,7 @@
- +