From c053b44640d183d3c11fe3749d0e25583964f89d Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 09:18:38 +0200 Subject: [PATCH 01/21] =?UTF-8?q?docs:=20Anforderungsdokument=20f=C3=BCr?= =?UTF-8?q?=20Raumbuchungsanfragen=20Management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- docs/ANFORDERUNG_Raumbuchungsanfragen.md | 204 +++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 docs/ANFORDERUNG_Raumbuchungsanfragen.md diff --git a/docs/ANFORDERUNG_Raumbuchungsanfragen.md b/docs/ANFORDERUNG_Raumbuchungsanfragen.md new file mode 100644 index 0000000..242898e --- /dev/null +++ b/docs/ANFORDERUNG_Raumbuchungsanfragen.md @@ -0,0 +1,204 @@ +# Anforderungsdokument: Raumbuchungsanfragen Management + +## 📋 Übersicht + +Neue Dashboard-Sektion zur Verwaltung von Raumbuchungsanfragen mit Konflikt-Erkennung und Genehmigungsworkflow. + +## 🎯 Funktionale Anforderungen + +### 1. Raumbuchungsanfragen Übersicht (Dashboard Card) + +- **Anzeige**: Alle offenen Raumbuchungsanfragen +- **Spalten**: + - Raum / Ressource + - Datum / Uhrzeit + - Ersteller / Person (im Auftrag von) + - Status (offen / wartend) + - Konflikte (falls vorhanden) + - Aktionen + +- **Filterung**: + - Nach Status + - Nach Raum/Ressource + - Nach Datum + - Nach Konflikten (nur mit Konflikten / ohne Konflikte / alle) + - Freitext-Suche (durchsucht: Raum, Ersteller, Person, Bemerkungen) + +- **Sortierung**: + - Spalten sind sortierbar (Klick auf Spalten-Header) + - Standardsortierung: Datum aufsteigend + +### 2. Konflikterkennung + +- **Konflikt-Definition**: Überschneidende Buchungen zum gleichen Raum/gleicher Ressource im gleichen Zeitfenster +- **Visuelles Highlighting**: + - Zeile mit Konflikt-Indikator (z.B. rotes Icon/Badge) + - Liste der konfliktierenden Buchungen anzeigen + - Verantwortliche Person aus konfligierenden Buchungen identifizieren + +- **Konflikt-Details**: + - Welche andere Buchung(en) konfligieren + - Wer ist die verantwortliche Person (Ersteller/im Auftrag von) der Konflikt-Buchung(en) + +### 3. Admin-Panel (Verwaltungsbereich) + +- **Detailansicht** fĂŒr jede Anfrage mit: + - VollstĂ€ndige Buchungsdetails + - Konflikt-Informationen (falls vorhanden) + - Bemerkungen-Feld + +- **Zeilen-Auswahl**: + - Checkbox in jeder Zeile zur Markierung + - "Select All" Checkbox im Header (mit Status-Anzeige: "3 von 10 ausgewĂ€hlt") + - Toggle Select All auch fĂŒr gefilterte Liste + +- **Individuelle Aktionen** (pro Zeile): + - ✅ **BestĂ€tigen**: Anfrage akzeptieren, Status → genehmigt + - ❌ **Ablehnen**: + - Bemerkungsfeld (erforderlich) + - BestĂ€tigungs-Dialog + - E-Mail-Versand triggern + +- **Bulk-Operationen** (fĂŒr markierte Zeilen): + - ✅ **Mehrere Anfragen bestĂ€tigen**: Alle markierten genehmigen + - ❌ **Mehrere Anfragen ablehnen**: Dialog mit gemeinsamer Bemerkung oder individuelle Bemerkungen + - đŸ—‘ïž **Löschen**: Markierte Anfragen löschen (nur in bestimmtem Status) + - Bulk-Aktion wird deaktiviert, wenn keine Zeile markiert ist + - Erfolgs-Feedback nach Bulk-Operation (z.B. "3 Anfragen genehmigt") + +### 4. E-Mail-Benachrichtigungen (bei Ablehnung) + +#### Standard-Ablehnung: +- **EmpfĂ€nger**: + - Ersteller der Anfrage + - Oder: Person "im Auftrag von" (falls angegeben) + +- **Inhalt**: + - Raum/Ressource, Datum, Uhrzeit + - BegrĂŒndung/Bemerkung vom Admin + - Optional: Alternativ-Zeiten vorschlagen + +#### Ablehnung mit Konflikt: +- **EmpfĂ€nger**: + - Ersteller/Person der abgelehnten Anfrage + - **PLUS**: Verantwortliche Person(en) aus konfligierenden Buchung(en) + +- **Inhalt**: + - BegrĂŒndung: "Konflikt mit Buchung von [Name]" + - Details der konfliktierenden Buchung(en) + - Bemerkung vom Admin + +## đŸ—ïž Technische Struktur + +Folgt dem Muster bestehender Module: + +``` +src/components/room-bookings/ +├── RoomBookingsCard.vue # Dashboard Card +├── RoomBookingsAdmin.vue # Admin Panel +└── useRoomBookings.ts # Composable mit API-Logik +``` + +## 📊 Datenmodelle + +### RoomBooking +```typescript +{ + id: string + room: string + date: string // ISO-Date + startTime: string + endTime: string + createdBy: string // Person-ID + createdByName: string + onBehalfOf?: string // Person-ID (optional) + onBehalfOfName?: string + status: 'open' | 'approved' | 'rejected' + remarks?: string + conflicts?: ConflictInfo[] +} +``` + +### ConflictInfo +```typescript +{ + conflictingBookingId: string + room: string + date: string + startTime: string + endTime: string + conflictingPerson: string // Ersteller oder "im Auftrag von" + conflictingPersonId: string + conflictingPersonEmail: string +} +``` + +## 🔌 API-Integration (ChurchTools) + +- [TBD] Endpoint fĂŒr Raumbuchungsanfragen abrufen +- [TBD] Endpoint fĂŒr Raumbuchung akzeptieren +- [TBD] Endpoint fĂŒr Raumbuchung ablehnen +- [TBD] E-Mail-Service Integration (bestehend oder neu) + +## 🎹 UI/UX + +- Nutze **BaseCard** fĂŒr Dashboard-Ansicht +- Nutze **AdminTable** fĂŒr Admin-Panel (nach Muster Tags/AutomaticGroups) +- ChurchTools Design Classes (ct-btn, ct-card, ct-select, ct-modal) +- Konflikt-Highlighting: Rot/Orange Badge oder Icon + +## ✅ Akzeptanzkriterien + +### GrundfunktionalitĂ€t +- [ ] Dashboard zeigt alle offenen Raumbuchungsanfragen +- [ ] Konflikte werden erkannt und angezeigt + +### Filterung & Sortierung +- [ ] Filterung nach Status funktioniert +- [ ] Filterung nach Raum/Ressource funktioniert +- [ ] Filterung nach Datum funktioniert +- [ ] Filterung nach Konflikten (mit/ohne) funktioniert +- [ ] Freitext-Suche durchsucht alle relevanten Felder +- [ ] Spalten sind sortierbar +- [ ] Sortierindikatoren sichtbar (Pfeil im Header) + +### Zeilen-Auswahl & Bulk-Operationen +- [ ] Checkboxes in jeder Zeile funktionieren +- [ ] Select All Checkbox wĂ€hlt/deselektiert alle Zeilen +- [ ] Select All Checkbox arbeitet mit gefilterten Daten +- [ ] Anzeige: "X von Y ausgewĂ€hlt" ist korrekt +- [ ] Bulk-Buttons nur aktiv, wenn Zeilen markiert sind +- [ ] Bulk BestĂ€tigung funktioniert +- [ ] Bulk Ablehnung funktioniert +- [ ] Erfolgs-Feedback nach Bulk-Operation angezeigt + +### Individuelle Aktionen +- [ ] Admin kann einzelne Anfrage bestĂ€tigen +- [ ] Admin kann einzelne Anfrage mit Bemerkung ablehnen +- [ ] BestĂ€tigungs-Dialog vor kritischen Aktionen + +### E-Mail-Versand +- [ ] E-Mail wird an korrekten EmpfĂ€nger versandt +- [ ] E-Mail enthĂ€lt vollstĂ€ndige Informationen +- [ ] Konflikt-Info ist in E-Mail enthalten (falls zutreffend) + +### Code-QualitĂ€t +- [ ] Komponenten folgen bestehendem Design-Pattern (BaseCard, AdminTable) +- [ ] TypeScript korrekt typisiert +- [ ] Code lĂ€sst sich mit `npm run lint` prĂŒfen +- [ ] Keine Warnings/Errors beim Build + +## 📝 NĂ€chste Schritte + +1. [x] Anforderungsdokument erstellen +2. [ ] Verfeinern & Clarification mit Stakeholder +3. [ ] ChurchTools API-Endpoints identifizieren +4. [ ] E-Mail-Template definieren +5. [ ] Module implementieren +6. [ ] Testen + +--- + +**Status**: Entwurf +**Erstellt**: 2026-04-16 +**Letzte Änderung**: 2026-04-16 From df276953f8f7afef5ca4483b12ca08b079432287 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 09:24:02 +0200 Subject: [PATCH 02/21] =?UTF-8?q?docs:=20Motivation=20und=20Problembeschre?= =?UTF-8?q?ibung=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- docs/ANFORDERUNG_Raumbuchungsanfragen.md | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/ANFORDERUNG_Raumbuchungsanfragen.md b/docs/ANFORDERUNG_Raumbuchungsanfragen.md index 242898e..2629c30 100644 --- a/docs/ANFORDERUNG_Raumbuchungsanfragen.md +++ b/docs/ANFORDERUNG_Raumbuchungsanfragen.md @@ -4,6 +4,44 @@ Neue Dashboard-Sektion zur Verwaltung von Raumbuchungsanfragen mit Konflikt-Erkennung und Genehmigungsworkflow. +## 🎯 Motivation & Probleme im bisherigen Prozess + +### Problem 1: Unzureichende Kommunikation bei Ablehnung +**Aktuell**: Bemerkungen bei Ablehnung einer Ressourcenbuchung werden nicht in der Ablehnungsmail ĂŒbermittelt +**Folge**: Admin muss zusĂ€tzlich noch manuell kommunizieren, warum eine Buchung abgelehnt wurde +**Lösung**: +- Bemerkungen direkt in die Ablehnungsmail ĂŒbernehmen +- Hinweis in Mail: "Weitere Details zur Ablehnung finden Sie in der Ressourcen-Buchung" +- Direktlink zur Buchung in der Mail (wenn technisch möglich) + +### Problem 2: Manuelle Konflikt-Erkennung ist fehleranfĂ€llig +**Aktuell**: Admin sieht auf der Startseite eine Liste mit 100+ offenen EintrĂ€gen und muss jeden einzeln öffnen, um Konflikte zu erkennen +**Folge**: Zeitaufwendig, fehleranfĂ€llig, Konflikte werden leicht ĂŒbersehen +**Lösung**: Dediziertes Dashboard mit Konflikt-Highlighting auf Listenlevel + +### Problem 3: Filter sind zu breit +**Aktuell**: +- Filter "offene BestĂ€tigungen" zeigt ALLE RĂ€ume, nicht nur die mit offenen Anfragen +- Konflikte sind in der Filteransicht nicht sichtbar +**Folge**: Admin muss trotzdem weitere Filterung und Klicks machen, um relevante Buchungen zu finden +**Lösung**: +- Nur RĂ€ume/Ressourcen anzeigen, fĂŒr die offene Anfragen existieren +- Konflikte direkt in der Listenansicht sichtbar +- Filterung nach Konflikten (mit/ohne) + +### Problem 4: Batch-Processing nicht möglich +**Aktuell**: Jede Raumbuchung muss einzeln bestĂ€tigt werden, keine Bulk-Aktion +**Folge**: Viele Zeit fĂŒr repetitive Aktionen (z.B. eine Woche mit unkritischen Buchungen bestĂ€tigen) +**Lösung**: Zeilen-Auswahl und Bulk-Operationen (BestĂ€tigen/Ablehnen mehrerer Buchungen gleichzeitig) + +### Problem 5: Unklare Termine fĂŒr Bucher +**Aktuell**: Nutzer sehen manchmal nur "unbekannter Termin" im Kalender, weil sie die Berechtigung zum Einsehen haben +**Folge**: Nutzer verstehen nicht, warum ihre Buchung abgelehnt wurde; Konfusion beim Buchen +**Lösung**: +- Klare Fehlermedlung/Hinweis beim Buchen, wenn Termin nicht sichtbar ist +- Evtl. in Ablehnungsmail erwĂ€hnen: "Der geplante Termin war fĂŒr Sie nicht einsehbar" +- (Ggfs. im Rahmen separater Verbesserung an BuchungsoberflĂ€che) + ## 🎯 Funktionale Anforderungen ### 1. Raumbuchungsanfragen Übersicht (Dashboard Card) From 25cf3abbfa448afdb04589d8083353d65a3c6347 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:43:31 +0200 Subject: [PATCH 03/21] docs: API-Analyse mit verifizierten Endpoints aus OpenAPI-Doku Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- docs/API_ANALYSE_Raumbuchungen.md | 285 ++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/API_ANALYSE_Raumbuchungen.md diff --git a/docs/API_ANALYSE_Raumbuchungen.md b/docs/API_ANALYSE_Raumbuchungen.md new file mode 100644 index 0000000..c885673 --- /dev/null +++ b/docs/API_ANALYSE_Raumbuchungen.md @@ -0,0 +1,285 @@ +# API-Analyse: Raumbuchungsanfragen (Resource Bookings) + +## 🔍 Findings + +### Bestehende API-Endpoints fĂŒr Raumbuchungen + +Das ChurchTools System hat bereits eine vollstĂ€ndige **Booking/Resource API** im OpenAPI-Schema (`ct-types.d.ts`): + +#### 1. **Datenmodelle vorhanden** ✅ + +- `BookingBase` - Basis-Buchung mit vollstĂ€ndigen Details +- `BookingCalculated` - Buchung mit berechneten Dates +- `BookingCalculatedWithIncludes` - **Mit Konflikten!** ⭐ +- `BookingConflict` - Konflikt-Informationen (bookingId, startDate, endDate, statusId, title) +- `BookingCreate` - Schema zum Erstellen einer Buchung +- `Resource` - Ressource/Raum-Definition +- `ResourceType` - Typ der Ressource + +#### 2. **Booking-Status & Konflikt-Handling** ✅ + +```typescript +// BookingBase enthĂ€lt: +- id: number +- resourceId: number +- startDate: ZuluDate +- endDate: ZuluDate +- statusId: number +- title: string +- description: string | null +- createdBy: DomainObjectPerson +- onBehalfOfPid: number | null +- onBehalfOf: DomainObjectPerson (in involvedPersonsDomainObjects) + +// Konflikte sind explizit im Type: +BookingCalculatedWithIncludes: { + booking: BookingCalculated + conflicts?: Array +} + +// BookingConflict: +{ + bookingId: number + startDate: ZuluDate + endDate: ZuluDate + statusId: StatusId + title: string +} +``` + +#### 3. **Status-Handling** ✅ + +Das System hat `StatusId` Typen: +- Verschiedene Booking-Status sind ĂŒber `statusId` definiert +- API gibt Status explizit zurĂŒck +- Konflikte mit jeweiligem Status einzeln auflisten + +### ✅ Verifizierte API-Endpoints (aus OpenAPI-Doku) + +```typescript +// 1. Alle Bookings abrufen (mit Filtern & Includes) +GET /bookings + params: { + resource_ids[]: number[] // ERFORDERLICH! Array von Resource-IDs + status_ids[]?: number[] // Optional, default: [1, 2] (pending, approved) + // 1=PENDING, 2=APPROVED, 3=CANCELED, 99=DELETED + person_id?: number // Filter: Creator oder "im Auftrag von" + query?: string // Freitext-Suche + include[]: string[] // ["conflicts", "involvedPersonsDomainObjects"] + } + response: { + data: Array<{ + booking: BookingCalculated + conflicts?: Array + involvedPersonsDomainObjects?: { + createdBy?: DomainObjectPerson + onBehalfOf?: DomainObjectPerson + } + }> + meta: { count: number } + } + +// 2. Einzelne Booking abrufen +GET /bookings/{bookingId} + response: { + data: { + booking: BookingCalculated + calculatedDates: { startDate, endDate } + additionalInfos: string[] + } + } + +// 3. Status einer Booking Ă€ndern (✅ AKZEPTIEREN/ABLEHNEN) +PUT /bookings/{bookingId}/{answer} + params: { + answer: "accept" | "reject" + } + body: {} // Leerer Body + response: Booking aktualisiert + +// ODER Update via PUT (mehr FlexibilitĂ€t): +PUT /bookings/{bookingId} + body: { + statusId: 2 // APPROVED (2) oder CANCELED (3) + description?: string + ... weitere Felder + } + +// 4. Konflikte fĂŒr NEUE Booking berechnen +POST /bookings/conflicts + body: BookingConflictRequestBody { + resourceId: number + startDate: DateString + endDate: DateString + // ... (weitere optionale Felder) + } + response: { data: Array, meta: { count } } + +// 5. Konflikte fĂŒr BESTEHENDE Booking berechnen (bei Update) +POST /bookings/{bookingId}/conflicts + body: (wie oben) + +// 6. Alle Resources abrufen +GET /resources + response: { data: Array, meta: { count } } + +// 7. Resource Master Data +GET /resources/masterdata + response: { resources: Array, resourceTypes: Array } +``` + +### ⚠ Wichtige Erkenntnisse + +1. **`resource_ids[]` ist ERFORDERLICH!** + - Bookings können NICHT ohne Resource-IDs abgerufen werden + - MĂŒssen zuerst alle Resources laden, dann fĂŒr jede Bookings abfragen + - Oder mehrere Queries kombinieren + +2. **Status-IDs sind standardisiert:** + - 1 = PENDING (offene Anfrage) + - 2 = APPROVED/CONFIRMED (genehmigt) + - 3 = CANCELED (abgelehnt) + - 99 = DELETED (gelöscht) + +3. **Update-Endpoints:** + - `PUT /bookings/{bookingId}/{answer}` mit "accept"/"reject" + - `PUT /bookings/{bookingId}` mit statusId im Body (flexibler) + +4. **Include-Parameter:** + - `conflicts` - Zeigt conflicting bookings + - `involvedPersonsDomainObjects` - Zeigt createdBy + onBehalfOf + +5. **Person-Daten sind INCLUDED:** + - Bei `include[]=involvedPersonsDomainObjects` kommt das direkt mit + - Keine separaten `/person` Calls nötig + +## 📊 Datenfluss fĂŒr Feature + +### 1. **Initiales Laden: Dashboard Card** +``` +GET /bookings?status_id=pending&include=conflicts,persons +└─ Liefert: Array + - booking.base.resourceId, title, dates + - booking.base.createdBy (ersteller) + - booking.base.onBehalfOf (im Auftrag von) + - conflicts[] (Array, kann leer sein) +``` + +### 2. **Filterung & Sortierung** (Client-side nach Fetch) +- Nach Status: `filterByStatus(statusId)` +- Nach Konflikt: `filter(b => b.conflicts && b.conflicts.length > 0)` +- Nach Datum/Raum: `filter(b => b.booking.base.resourceId === id)` +- Freitext: `filter(b => title.includes(text))` +- **Sortierung: Spalten-Header Klick → Datensatz nach Feld sortieren** + +### 3. **Bulk-Aktion: BestĂ€tigen** +```typescript +// FĂŒr jede markierte Booking: +PUT /bookings/{bookingId} + body: { statusId: APPROVED_STATUS_ID } + +// Dann: GET /bookings (refresh) +``` + +### 4. **Bulk-Aktion: Ablehnen mit Bemerkung** +```typescript +// FĂŒr jede markierte Booking: +PUT /bookings/{bookingId} + body: { + statusId: REJECTED_STATUS_ID, + description: "Admin-Bemerkung" // Optional - mĂŒsste ĂŒbergeben werden + } + +// Dann: Trigger E-Mail an createdBy + onBehalfOf + conflictPersons +``` + +### 5. **E-Mail bei Ablehnung** +Needed zusĂ€tzlich: +- E-Mail API-Endpoint (existiert wahrscheinlich in ChurchTools) +- Template fĂŒr Ablehnungsmail +- Person-Daten auflösen (aus Booking: createdBy, onBehalfOf, Konflikt-Creator) + +## ✅ Offene Fragen geklĂ€rt + +| Frage | Antwort | +|-------|--------| +| **Status-IDs** | 1=PENDING, 2=APPROVED, 3=CANCELED, 99=DELETED ✅ | +| **include-Parameter** | `conflicts`, `involvedPersonsDomainObjects` ✅ | +| **Person-Daten** | Mit `involvedPersonsDomainObjects` enthalten (createdBy, onBehalfOf) ✅ | +| **Konflikt-Daten** | Mit `include[]=conflicts` in Booking enthalten ✅ | + +## ⚠ Noch zu klĂ€ren + +1. **E-Mail API in ChurchTools**: + - Gibt es einen Endpoint zum Mailen? + - Oder externe Mail-Service (nodemailer, SendGrid)? + - SMTP-Konfiguration in ChurchTools? + +2. **Person-E-Mail in DomainObjectPerson**: + - Enthalten die `createdBy` und `onBehalfOf` Objekte direkt eine `email` Property? + - Oder ist nur `id` und `name` vorhanden → separat `/persons/{id}` aufrufen? + +3. **Konflikt-Creator auflösen**: + - Konflikt enthĂ€lt nur `bookingId`, `title`, `startDate`, `endDate` + - Um E-Mail des Konflikt-Creators zu bekommen: + - Option A: `GET /bookings/{conflictBookingId}` mit `include[]=involvedPersonsDomainObjects` + - Option B: Ist Creator schon im Konflikt-Objekt enthalten? + - **→ MĂŒssen wir mit Test-API prĂŒfen** + +4. **Bulk Email-Versand**: + - Mehrere Personen in Ablehnungs-Mail (createdBy + onBehalfOf + conflictCreators) + - Alle in einer Mail? Separate Mails? (Mit E-Mail-Template TBD) + +## 🎯 NĂ€chste Schritte + +1. **API-Endpoints testen/validieren**: + - Doku der ChurchTools API checken + - Oder in Live-Instanz mit DevTools testen + - Status-IDs herausfinden + +2. **E-Mail-Integration klĂ€ren**: + - Welcher Endpunkt? Welche Template-Sprache? + - Lokale Lösung (nodemailer) vs. ChurchTools-API? + +3. **Composable implementieren** (`useRoomBookings.ts`): + - `fetchBookings(filter, sort)` + - `approveBooking(id)` + - `rejectBooking(id, reason)` + - `sendRejectionEmail(booking, reason, conflictPersons)` + +4. **AdminTable Pattern studieren**: + - `/src/components/tags/TagsAdmin.vue` + - `/src/components/automatic-groups/AutomaticGroupsAdmin.vue` + - Checkboxes, Bulk-Buttons, Actions Spalte + +## 📝 API Pattern in diesem Projekt + +**BestĂ€tigte Muster aus existendem Code:** + +```typescript +// ✅ CORRECT (nach AGENTS.md): +const response = await churchtoolsClient.get("/api/endpoint", { param1: "value1" }) +// Client unwraps data - use response directly! + +// ❌ WRONG: +const response = await churchtoolsClient.get("/api/endpoint", { params: { ... } }) + +// Delete-Operationen: +await (churchtoolsClient as any).deleteApi(`/tags/${tagId}`) + +// Pagination: +const response = await churchtoolsClient.get(`/resource?limit=${limit}&page=${page}`) +``` + +**Composable Pattern** (aus `useAutomaticGroups.ts`, `useTags.ts`): +- Vue 3 Composable mit `ref`, `computed` +- Oder `@tanstack/vue-query` fĂŒr komplexere Daten +- Async Funktionen fĂŒr API-Calls +- Error Handling & Logging +- Type-safe mit TypeScript Interfaces + +--- + +**Status**: 📋 Analyse +**Aktuell**: 2026-04-16 +**NĂ€chster Schritt**: API-Endpoints verifizieren & Composable starten From 93db8cac1c5a26b40e71dc7dc5a9e193b7f80ea6 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:51:12 +0200 Subject: [PATCH 04/21] docs: E-Mail API (Legacy Ajax Endpoint) dokumentiert Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- docs/API_ANALYSE_Raumbuchungen.md | 66 +++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/docs/API_ANALYSE_Raumbuchungen.md b/docs/API_ANALYSE_Raumbuchungen.md index c885673..1156103 100644 --- a/docs/API_ANALYSE_Raumbuchungen.md +++ b/docs/API_ANALYSE_Raumbuchungen.md @@ -208,27 +208,59 @@ Needed zusĂ€tzlich: | **Person-Daten** | Mit `involvedPersonsDomainObjects` enthalten (createdBy, onBehalfOf) ✅ | | **Konflikt-Daten** | Mit `include[]=conflicts` in Booking enthalten ✅ | -## ⚠ Noch zu klĂ€ren +## ✅ E-Mail API (Legacy Ajax Endpoint) -1. **E-Mail API in ChurchTools**: - - Gibt es einen Endpoint zum Mailen? - - Oder externe Mail-Service (nodemailer, SendGrid)? - - SMTP-Konfiguration in ChurchTools? +ChurchTools hat einen **Legacy Ajax Endpoint** zum Versenden von E-Mails: -2. **Person-E-Mail in DomainObjectPerson**: - - Enthalten die `createdBy` und `onBehalfOf` Objekte direkt eine `email` Property? - - Oder ist nur `id` und `name` vorhanden → separat `/persons/{id}` aufrufen? +```typescript +POST /index.php?q=churchdb/ajax +Content-Type: application/x-www-form-urlencoded + +Parameter: +- ids: string // Komma-getrennte Person-IDs (545,892) +- betreff: string // Subject (URL-encoded) +- inhalt: string // HTML-Body (URL-encoded) +- template_id: number // Template-ID (z.B. 11) +- func: "sendEMailToPersonIds" // Funktions-Identifier +- attachments?: string // Optional: Datei-Hashes +- domain_id?: number // Optional +- group_id?: number // Optional +- browsertabId?: string // Session-Info + +Beispiel: +ids=545%2C892&betreff=%5BBG+Korntal%5D+&inhalt=...&template_id=11&func=sendEMailToPersonIds +``` -3. **Konflikt-Creator auflösen**: - - Konflikt enthĂ€lt nur `bookingId`, `title`, `startDate`, `endDate` - - Um E-Mail des Konflikt-Creators zu bekommen: - - Option A: `GET /bookings/{conflictBookingId}` mit `include[]=involvedPersonsDomainObjects` - - Option B: Ist Creator schon im Konflikt-Objekt enthalten? - - **→ MĂŒssen wir mit Test-API prĂŒfen** +**Wichtig:** +- Dieser Endpoint ist **NICHT** REST API, sondern Legacy AJAX +- Funktioniert nur mit aktiver Session (Cookie + CSRF-Token) +- IDs mĂŒssen komma-getrennt sein +- HTML-Content wird direkt versendet +- Mehrere Personen in einer Request (Batch) -4. **Bulk Email-Versand**: - - Mehrere Personen in Ablehnungs-Mail (createdBy + onBehalfOf + conflictCreators) - - Alle in einer Mail? Separate Mails? (Mit E-Mail-Template TBD) +## ⚠ Noch zu klĂ€ren + +1. **Person-E-Mail in DomainObjectPerson**: + - Enthalten die `createdBy` und `onBehalfOf` Objekte `id` und `name`? + - Wir verwenden die `id` zum Versenden via `/index.php?q=churchdb/ajax` + - **→ Mit Test-API prĂŒfen** + +2. **Konflikt-Creator auflösen**: + - Konflikt enthĂ€lt nur `bookingId`, `title`, `startDate`, `endDate` - **KEINE Person-Daten** + - Um E-Mail des Konflikt-Creators zu bekommen: + - `GET /bookings/{conflictBookingId}` mit `include[]=involvedPersonsDomainObjects` + - Dann `id` der createdBy Person extrahieren + - **→ Implementation wird mehrere nested Calls benötigen** + +3. **Template-Verwaltung**: + - Wo sind die Templates definiert? (Template-ID `11` in Beispiel) + - Können wir Templates dynamisch laden? + - Oder mĂŒssen wir HTML manuell zusammenstellen? + +4. **Bulk Email-Versand Design**: + - Mehrere Personen (createdBy + onBehalfOf + conflictCreators) in einer Mail versenden + - Ein API-Call mit all den IDs (optimal) + - ODER separate Calls pro Person (einfacher zu implementieren) ## 🎯 NĂ€chste Schritte From e4e59d8be0f08c72e70e15aee61b83a35c6ce7ad Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:52:40 +0200 Subject: [PATCH 05/21] feat: useRoomBookings composable implementiert Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- .../room-bookings/useRoomBookings.ts | 462 ++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 src/components/room-bookings/useRoomBookings.ts diff --git a/src/components/room-bookings/useRoomBookings.ts b/src/components/room-bookings/useRoomBookings.ts new file mode 100644 index 0000000..546d2b0 --- /dev/null +++ b/src/components/room-bookings/useRoomBookings.ts @@ -0,0 +1,462 @@ +import { ref, computed, reactive } from 'vue' +import { churchtoolsClient } from '@churchtools/churchtools-client' + +// ============================================================================ +// TYPES +// ============================================================================ + +export interface RoomBookingPerson { + id: number + name: string + email?: string +} + +export interface RoomBookingConflict { + bookingId: number + title: string + startDate: string + endDate: string + statusId: number +} + +export interface RoomBooking { + id: number + resourceId: number + resourceName: string + startDate: string + endDate: string + title: string + description?: string + statusId: number + createdBy?: RoomBookingPerson + onBehalfOf?: RoomBookingPerson + conflicts?: RoomBookingConflict[] +} + +export interface RoomBookingsFilter { + statusIds: number[] + resourceIds: number[] + conflictStatus?: 'all' | 'with' | 'without' + searchQuery?: string +} + +export interface RoomBookingsSort { + field: string + direction: 'asc' | 'desc' +} + +// Status Constants +export const BOOKING_STATUS = { + PENDING: 1, + APPROVED: 2, + CANCELED: 3, + DELETED: 99, +} as const + +// ============================================================================ +// COMPOSABLE +// ============================================================================ + +export function useRoomBookings() { + // State + const bookings = ref([]) + const resources = ref([]) + const loading = ref(false) + const error = ref(null) + + // Filter & Sort State + const filter = reactive({ + statusIds: [BOOKING_STATUS.PENDING], + resourceIds: [], + conflictStatus: 'all', + searchQuery: '', + }) + + const sort = reactive({ + field: 'startDate', + direction: 'asc', + }) + + // ======================================================================== + // API CALLS + // ======================================================================== + + /** + * Fetch all resources + */ + const fetchResources = async () => { + try { + const response = (await churchtoolsClient.get('/resources')) as any[] + resources.value = Array.isArray(response) ? response : [] + return resources.value + } catch (err: any) { + console.error('Error fetching resources:', err) + throw new Error('Fehler beim Laden der RĂ€ume') + } + } + + /** + * Fetch bookings for given resource IDs + */ + const fetchBookings = async ( + resourceIds: number[], + statusIds: number[] = [BOOKING_STATUS.PENDING] + ) => { + if (resourceIds.length === 0) { + console.warn('No resource IDs provided for booking fetch') + return [] + } + + loading.value = true + error.value = null + + try { + const response = (await churchtoolsClient.get('/bookings', { + 'resource_ids[]': resourceIds, + 'status_ids[]': statusIds, + 'include[]': ['conflicts', 'involvedPersonsDomainObjects'], + })) as any[] + + const data = Array.isArray(response) ? response : [] + + // Transform API response to RoomBooking format + const transformed = data.map((item: any) => { + const booking = item.booking || item + const base = booking.base || booking + + return { + id: base.id, + resourceId: base.resourceId, + resourceName: base.resource?.name || 'Unbekannter Raum', + startDate: base.startDate, + endDate: base.endDate, + title: base.title || '', + description: base.description || base.subtitle || '', + statusId: base.statusId, + createdBy: item.involvedPersonsDomainObjects?.createdBy + ? { + id: item.involvedPersonsDomainObjects.createdBy.id, + name: item.involvedPersonsDomainObjects.createdBy.name, + email: item.involvedPersonsDomainObjects.createdBy.email, + } + : undefined, + onBehalfOf: item.involvedPersonsDomainObjects?.onBehalfOf + ? { + id: item.involvedPersonsDomainObjects.onBehalfOf.id, + name: item.involvedPersonsDomainObjects.onBehalfOf.name, + email: item.involvedPersonsDomainObjects.onBehalfOf.email, + } + : undefined, + conflicts: item.conflicts || [], + } + }) + + bookings.value = transformed + return transformed + } catch (err: any) { + const msg = err.message || 'Fehler beim Laden der Raumbuchungen' + error.value = msg + console.error('Error fetching bookings:', err) + throw new Error(msg) + } finally { + loading.value = false + } + } + + /** + * Fetch single booking with details + */ + const fetchBookingDetails = async (bookingId: number) => { + try { + const response = await churchtoolsClient.get(`/bookings/${bookingId}`) + return response + } catch (err: any) { + console.error(`Error fetching booking ${bookingId}:`, err) + throw new Error('Fehler beim Laden der Buchungsdetails') + } + } + + /** + * Approve booking(s) + */ + const approveBooking = async (bookingId: number) => { + try { + const response = await churchtoolsClient.put(`/bookings/${bookingId}`, { + statusId: BOOKING_STATUS.APPROVED, + }) + // Refresh list + if (filter.resourceIds.length > 0) { + await fetchBookings(filter.resourceIds, filter.statusIds) + } + return response + } catch (err: any) { + console.error(`Error approving booking ${bookingId}:`, err) + throw new Error('Fehler beim Genehmigen der Buchung') + } + } + + /** + * Reject booking with remarks + */ + const rejectBooking = async (bookingId: number, remarks: string) => { + try { + const response = await churchtoolsClient.put(`/bookings/${bookingId}`, { + statusId: BOOKING_STATUS.CANCELED, + description: remarks, + }) + // Refresh list + if (filter.resourceIds.length > 0) { + await fetchBookings(filter.resourceIds, filter.statusIds) + } + return response + } catch (err: any) { + console.error(`Error rejecting booking ${bookingId}:`, err) + throw new Error('Fehler beim Ablehnen der Buchung') + } + } + + /** + * Send rejection email to person(s) + * Uses legacy AJAX endpoint: /index.php?q=churchdb/ajax + */ + const sendRejectionEmail = async ( + personIds: number[], + subject: string, + htmlContent: string, + templateId: number = 11 + ) => { + if (personIds.length === 0) { + console.warn('No person IDs provided for email') + return + } + + try { + // Use raw fetch for legacy AJAX endpoint + const params = new URLSearchParams({ + ids: personIds.join(','), + betreff: subject, + inhalt: htmlContent, + template_id: templateId.toString(), + func: 'sendEMailToPersonIds', + }) + + const response = await fetch('/index.php?q=churchdb/ajax', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + credentials: 'include', // Send cookies + body: params.toString(), + }) + + if (!response.ok) { + throw new Error(`Email send failed: ${response.statusText}`) + } + + return response + } catch (err: any) { + console.error('Error sending rejection email:', err) + throw new Error('Fehler beim Versenden der Ablehnungs-E-Mail') + } + } + + /** + * Resolve conflict creator details + * Fetches the creator of a conflicting booking + */ + const resolveConflictCreator = async ( + conflictBookingId: number + ): Promise => { + try { + const response = (await churchtoolsClient.get(`/bookings/${conflictBookingId}`, { + 'include[]': ['involvedPersonsDomainObjects'], + })) as any + + const booking = response?.booking || response + const createdBy = booking?.involvedPersonsDomainObjects?.createdBy + + if (!createdBy) { + console.warn(`No creator found for conflict booking ${conflictBookingId}`) + return null + } + + return { + id: createdBy.id, + name: createdBy.name, + email: createdBy.email, + } + } catch (err: any) { + console.warn(`Error resolving conflict creator ${conflictBookingId}:`, err) + return null + } + } + + // ======================================================================== + // COMPUTED PROPERTIES & FILTERING + // ======================================================================== + + /** + * Filter bookings based on current filter state + */ + const filteredBookings = computed(() => { + let result = [...bookings.value] + + // Filter by conflict status + if (filter.conflictStatus === 'with') { + result = result.filter((b) => b.conflicts && b.conflicts.length > 0) + } else if (filter.conflictStatus === 'without') { + result = result.filter((b) => !b.conflicts || b.conflicts.length === 0) + } + + // Filter by search query + if (filter.searchQuery) { + const query = filter.searchQuery.toLowerCase() + result = result.filter( + (b) => + b.title.toLowerCase().includes(query) || + b.resourceName.toLowerCase().includes(query) || + b.createdBy?.name.toLowerCase().includes(query) || + b.onBehalfOf?.name.toLowerCase().includes(query) || + b.description?.toLowerCase().includes(query) + ) + } + + // Sort + result.sort((a, b) => { + let aVal: any = a[sort.field as keyof RoomBooking] + let bVal: any = b[sort.field as keyof RoomBooking] + + if (aVal === undefined || aVal === null) return 1 + if (bVal === undefined || bVal === null) return -1 + + if (typeof aVal === 'string') { + aVal = aVal.toLowerCase() + bVal = (bVal as string).toLowerCase() + } + + const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0 + return sort.direction === 'asc' ? comparison : -comparison + }) + + return result + }) + + /** + * Count bookings by conflict status + */ + const bookingStats = computed(() => ({ + total: bookings.value.length, + withConflicts: bookings.value.filter((b) => b.conflicts && b.conflicts.length > 0).length, + withoutConflicts: bookings.value.filter((b) => !b.conflicts || b.conflicts.length === 0).length, + })) + + /** + * Get unique resources with open bookings + */ + const resourcesWithBookings = computed(() => { + const resourceIds = new Set(filteredBookings.value.map((b) => b.resourceId)) + return resources.value.filter((r) => resourceIds.has(r.id)) + }) + + // ======================================================================== + // HELPERS + // ======================================================================== + + const setSort = (field: string, direction?: 'asc' | 'desc') => { + if (sort.field === field && !direction) { + sort.direction = sort.direction === 'asc' ? 'desc' : 'asc' + } else { + sort.field = field + sort.direction = direction || 'asc' + } + } + + const updateFilter = (updates: Partial) => { + Object.assign(filter, updates) + } + + // ======================================================================== + // BULK OPERATIONS + // ======================================================================== + + const bulkApprove = async (bookingIds: number[]) => { + let successCount = 0 + let errorCount = 0 + const errors: string[] = [] + + for (const bookingId of bookingIds) { + try { + await approveBooking(bookingId) + successCount++ + } catch (err: any) { + errorCount++ + errors.push(`Booking ${bookingId}: ${err.message}`) + } + } + + if (errorCount > 0) { + const message = `${successCount} genehmigt, ${errorCount} Fehler: ${errors.join('; ')}` + throw new Error(message) + } + + return { successCount, errorCount } + } + + const bulkReject = async (bookingIds: number[], remarks: string) => { + let successCount = 0 + let errorCount = 0 + const errors: string[] = [] + + for (const bookingId of bookingIds) { + try { + await rejectBooking(bookingId, remarks) + successCount++ + } catch (err: any) { + errorCount++ + errors.push(`Booking ${bookingId}: ${err.message}`) + } + } + + if (errorCount > 0) { + const message = `${successCount} abgelehnt, ${errorCount} Fehler: ${errors.join('; ')}` + throw new Error(message) + } + + return { successCount, errorCount } + } + + // ======================================================================== + // RETURN + // ======================================================================== + + return { + // State + bookings, + resources, + loading, + error, + filter, + sort, + + // Computed + filteredBookings, + bookingStats, + resourcesWithBookings, + + // API Methods + fetchResources, + fetchBookings, + fetchBookingDetails, + approveBooking, + rejectBooking, + sendRejectionEmail, + resolveConflictCreator, + + // Helper Methods + setSort, + updateFilter, + bulkApprove, + bulkReject, + } +} From cf69589dc241d3568bf4ca1f8c652368e651a1e2 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:54:00 +0200 Subject: [PATCH 06/21] feat: RoomBookingsCard und RoomBookingsAdmin implementiert Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- .../room-bookings/RoomBookingsAdmin.vue | 736 ++++++++++++++++++ .../room-bookings/RoomBookingsCard.vue | 113 +++ 2 files changed, 849 insertions(+) create mode 100644 src/components/room-bookings/RoomBookingsAdmin.vue create mode 100644 src/components/room-bookings/RoomBookingsCard.vue diff --git a/src/components/room-bookings/RoomBookingsAdmin.vue b/src/components/room-bookings/RoomBookingsAdmin.vue new file mode 100644 index 0000000..14ce331 --- /dev/null +++ b/src/components/room-bookings/RoomBookingsAdmin.vue @@ -0,0 +1,736 @@ + + + + + diff --git a/src/components/room-bookings/RoomBookingsCard.vue b/src/components/room-bookings/RoomBookingsCard.vue new file mode 100644 index 0000000..42c5d72 --- /dev/null +++ b/src/components/room-bookings/RoomBookingsCard.vue @@ -0,0 +1,113 @@ + + + From 14b8664cedec253cf9fa64157150b410a4202b39 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:54:30 +0200 Subject: [PATCH 07/21] =?UTF-8?q?feat:=20RoomBookings=20Modul=20zu=20App.v?= =?UTF-8?q?ue=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- src/App.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/App.vue b/src/App.vue index 039c15e..9278b60 100644 --- a/src/App.vue +++ b/src/App.vue @@ -56,6 +56,8 @@ import BeispielCard from './components/beispiel/BeispielCard.vue' import ColorPickerExample from './components/common/ColorPickerExample.vue' import LoggerSummaryCard from './components/loggerSummary/LoggerSummaryCard.vue' import LoggerSummaryAdmin from './components/loggerSummary/LoggerSummaryAdmin.vue' +import RoomBookingsCard from './components/room-bookings/RoomBookingsCard.vue' +import RoomBookingsAdmin from './components/room-bookings/RoomBookingsAdmin.vue' import Toast from './components/common/Toast.vue' import { useToast } from './composables/useToast' @@ -94,6 +96,14 @@ const modules: DashboardModule[] = [ cardComponent: LoggerSummaryCard, adminComponent: LoggerSummaryAdmin, }, + { + id: 'room-bookings', + title: 'Raumbuchungsanfragen', + icon: 'đŸ›ïž', + description: 'Verwaltung von Raumbuchungsanfragen', + cardComponent: RoomBookingsCard, + adminComponent: RoomBookingsAdmin, + }, ] const userDisplayName = ref('') From 27cdf60069ac42c6bdb9d4c93e87096c6ab497b6 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Thu, 16 Apr 2026 19:55:54 +0200 Subject: [PATCH 08/21] fix: Variable naming conflict in RoomBookingsAdmin Amp-Thread-ID: https://ampcode.com/threads/T-019d951a-df03-766a-b7f4-3fea8f33d840 Co-authored-by: Amp --- src/components/room-bookings/RoomBookingsAdmin.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/room-bookings/RoomBookingsAdmin.vue b/src/components/room-bookings/RoomBookingsAdmin.vue index 14ce331..15f0517 100644 --- a/src/components/room-bookings/RoomBookingsAdmin.vue +++ b/src/components/room-bookings/RoomBookingsAdmin.vue @@ -101,7 +101,7 @@