const { Worship, EventPlace, LiturgicalDay, Sequelize, sequelize } = require('../models'); const { Op, fn, literal } = require('sequelize'); const jwt = require('jsonwebtoken'); const { isTokenBlacklisted, addTokenToBlacklist } = require('../utils/blacklist'); const multer = require('multer'); const upload = multer({ storage: multer.memoryStorage() }); const mammoth = require('mammoth'); const { Document, Packer, Paragraph, Table, TableRow, TableCell, TextRun, WidthType, AlignmentType, VerticalAlign, ShadingType, VerticalMerge, VerticalMergeType, FontFamily, HeadingLevel, PageMargin, SectionType, BorderStyle, HeightRule } = require('docx'); function isAuthorized(req) { const authHeader = req.header('Authorization'); if (!authHeader) { return false; } const token = authHeader.replace('Bearer ', ''); if (isTokenBlacklisted(token)) { console.log('Token is blacklisted'); return false; } try { const decoded = jwt.verify(token, 'zTxVgptmPl9!_dr%xxx9999(dd)'); req.user = decoded; return true; } catch (err) { // Token ist ungültig oder abgelaufen – Benutzer gilt einfach als nicht autorisiert. // Wichtig: Wir setzen abgelaufene/ungültige Tokens hier NICHT mehr auf die Blacklist, // damit ein Seiten-Reload nicht dazu führt, dass der Token als "gesperrt" behandelt wird. console.log('Token verification failed:', err.message); return false; } } exports.getAllWorships = async (req, res) => { try { const authorized = isAuthorized(req); const worships = await Worship.findAll({ where: { date: { [Op.gt]: literal("DATE_SUB(NOW(), INTERVAL 4 WEEK)") }, }, attributes: authorized ? undefined : { exclude: ['sacristanService'] }, order: [ ['date', 'ASC'], ['time', 'ASC'] ], }); res.status(200).json(worships); } catch (error) { console.error('Fehler beim Abrufen der Gottesdienste:', error); res.status(500).json({ message: 'Fehler beim Abrufen der Gottesdienste', error: error.message }); } }; exports.createWorship = async (req, res) => { try { const worship = await Worship.create(req.body); res.status(201).json(worship); } catch (error) { console.log(error); res.status(500).json({ message: 'Fehler beim Erstellen des Gottesdienstes' }); } }; exports.updateWorship = async (req, res) => { try { const worship = await Worship.findByPk(req.params.id); if (worship) { await worship.update(req.body); res.status(200).json(worship); } else { res.status(404).json({ message: 'Gottesdienst nicht gefunden' }); } } catch (error) { res.status(500).json({ message: 'Fehler beim Aktualisieren des Gottesdienstes' }); } }; exports.deleteWorship = async (req, res) => { try { const worship = await Worship.findByPk(req.params.id); if (worship) { await worship.destroy(); res.status(200).json({ message: 'Gottesdienst erfolgreich gelöscht' }); } else { res.status(404).json({ message: 'Gottesdienst nicht gefunden' }); } } catch (error) { res.status(500).json({ message: 'Fehler beim Löschen des Gottesdienstes' }); } }; exports.getFilteredWorships = async (req, res) => { const { location, order } = req.query; const where = {}; if (order && order.trim() === '') { order = 'date DESC'; } const locations = location ? JSON.parse(location) : []; if (location && locations.length > 0) { where.eventPlaceId = { [Sequelize.Op.in]: locations } } where.date = { [Op.gte]: fn('CURDATE'), }; // Nur freigegebene Gottesdienste anzeigen where.approved = true; try { const authorized = isAuthorized(req); // Attribute: organPlaying und sacristanService nur für nicht-autorisierte Benutzer ausschließen const attributesExclude = []; if (!authorized) { attributesExclude.push('organPlaying', 'sacristanService'); } const worships = await Worship.findAll({ where, attributes: { exclude: attributesExclude }, include: { model: EventPlace, as: 'eventPlace', }, order: [ ['date', 'ASC'], ['time', 'ASC'] ], }); res.status(200).json(worships); } catch (error) { console.log(error); res.status(500).json({ message: 'Fehler beim Abrufen der gefilterten Gottesdienste' }); } }; exports.getWorshipOptions = async (req, res) => { try { // Alle Worships mit organizer und sacristanService abrufen const worships = await Worship.findAll({ attributes: ['organizer', 'sacristanService'], raw: true }); // Strings aufteilen (kommasepariert) und alle eindeutigen Werte sammeln const organizerSet = new Set(); const sacristanSet = new Set(); worships.forEach(worship => { // Organizer verarbeiten if (worship.organizer && worship.organizer.trim() !== '') { worship.organizer.split(',').forEach(org => { const trimmed = org.trim(); if (trimmed) organizerSet.add(trimmed); }); } // SacristanService verarbeiten if (worship.sacristanService && worship.sacristanService.trim() !== '') { worship.sacristanService.split(',').forEach(sac => { const trimmed = sac.trim(); if (trimmed) sacristanSet.add(trimmed); }); } }); res.status(200).json({ organizers: Array.from(organizerSet).sort(), sacristanServices: Array.from(sacristanSet).sort() }); } catch (error) { console.error('Fehler beim Abrufen der Worship-Optionen:', error); res.status(500).json({ message: 'Fehler beim Abrufen der Worship-Optionen', error: error.message }); } }; // Multer middleware für File-Upload exports.uploadImportFile = upload.single('file'); // Hilfsfunktion zum Parsen eines Datums aus dem Tag-String function parseDateFromDayString(dayString) { // Erwartetes Format: "24.08.2025 - 1. Advent" oder "24.08.2025" const dateMatch = dayString.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/); if (dateMatch) { const day = parseInt(dateMatch[1], 10); const month = parseInt(dateMatch[2], 10) - 1; // Monate sind 0-indexiert const year = parseInt(dateMatch[3], 10); // Erstelle Date-Objekt mit UTC, um Zeitzonenprobleme zu vermeiden const date = new Date(Date.UTC(year, month, day)); return date; } return null; } // Hilfsfunktion zum Extrahieren des Tag-Namens function extractDayName(dayString) { // Erwartetes Format: "24.08.2025 - 1. Advent" oder "24.08.2025\n10. Sonntag nach Trinitatis" // Zuerst versuche mit " - " Trennzeichen const parts = dayString.split(' - '); if (parts.length > 1) { return parts.slice(1).join(' - ').trim(); } // Falls nicht gefunden, versuche mit Zeilenumbrüchen const lines = dayString.split('\n').map(line => line.trim()).filter(line => line.length > 0); if (lines.length > 1) { // Erste Zeile ist das Datum, weitere Zeilen sind der Tag-Name return lines.slice(1).join(' ').trim(); } return ''; } // Hilfsfunktion zum Parsen der Uhrzeit function parseTime(timeString) { // Erwartetes Format: "10:00", "10.00", "10:00 Uhr" oder "10.00 Uhr" // Versuche zuerst mit Doppelpunkt let timeMatch = timeString.match(/(\d{1,2}):(\d{2})/); if (!timeMatch) { // Versuche mit Punkt timeMatch = timeString.match(/(\d{1,2})\.(\d{2})/); } if (timeMatch) { const hours = parseInt(timeMatch[1], 10); const minutes = parseInt(timeMatch[2], 10); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`; } return null; } // Hilfsfunktion zum Parsen eines Gottesdienstes aus der zweiten Spalte function parseWorshipFromCell(cellText, date, dayName) { // Zuerst in Zeilen aufteilen (falls Zeilenumbrüche vorhanden) const lines = cellText.split('\n').map(line => line.trim()).filter(line => line.length > 0); if (lines.length === 0) { return null; } // Für Debugging: Zeige die Zeilen console.log(` parseWorshipFromCell: ${lines.length} Zeilen gefunden`); lines.forEach((line, idx) => { console.log(` Zeile ${idx + 1}: "${line.substring(0, 100)}${line.length > 100 ? '...' : ''}"`); }); const fullText = cellText.trim(); // Wenn Zeilenumbrüche vorhanden sind, verwende die zeilenbasierte Logik if (lines.length > 1) { return parseWorshipFromCellWithLines(lines, date, dayName); } const worship = { date: date, dayName: dayName, time: null, title: '', organizer: '', sacristanService: '', collection: '', organPlaying: '', eventPlaceId: null, address: '', selfInformation: false, highlightTime: false, neighborInvitation: false, introLine: '' }; console.log(` parseWorshipFromCell: Volltext: "${fullText.substring(0, 200)}..."`); // Suche nach Uhrzeit am Anfang (Format: "11.15 Uhr" oder "11:15 Uhr") const timeMatch = fullText.match(/^(\d{1,2})[:.](\d{2})\s*Uhr/i); if (timeMatch) { const hours = parseInt(timeMatch[1], 10); const minutes = parseInt(timeMatch[2], 10); worship.time = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`; console.log(` parseWorshipFromCell: Uhrzeit geparst: ${worship.time}`); // Text nach der Uhrzeit extrahieren const textAfterTime = fullText.substring(timeMatch[0].length).trim(); // Titel extrahieren: Alles bis zum ersten "Gestaltung:", "Dienst:", "Kollekte:" oder "Orgel:" const titleEndMatch = textAfterTime.match(/(Gestaltung|Dienst|Kollekte|Orgel):/i); if (titleEndMatch) { let title = textAfterTime.substring(0, titleEndMatch.index).trim(); // Entferne häufige Wörter am Anfang title = title.replace(/^(Gottesdienst|Gemeinsamer Gottesdienst|Einladung zum Gottesdienst)\s*/i, ''); // Entferne "in", "am", "zu" + Ort am Ende, wenn vorhanden (aber behalte den Rest) title = title.replace(/\s+(in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)$/, ''); worship.title = title || 'Gottesdienst'; } else { // Falls keine Markierungen gefunden, nimm den gesamten Text als Titel let title = textAfterTime; title = title.replace(/^(Gottesdienst|Gemeinsamer Gottesdienst|Einladung zum Gottesdienst)\s*/i, ''); worship.title = title.substring(0, 100) || 'Gottesdienst'; } console.log(` parseWorshipFromCell: Titel extrahiert: "${worship.title}"`); // Ort extrahieren (aus dem Titel-Bereich) const locationMatch = textAfterTime.match(/(?:in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)/i); if (locationMatch) { const locationName = locationMatch[1].trim(); const normalizedLocation = locationName.split(/\s*-\s*/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() ).join('-'); worship.extractedLocation = `Kirche ${normalizedLocation}`; console.log(` parseWorshipFromCell: Ort extrahiert: "${worship.extractedLocation}"`); } // Gestalter extrahieren const organizerMatch = fullText.match(/Gestaltung:\s*([^DienstKollekteOrgelVideoschnitt]+?)(?=Dienst:|Kollekte:|Orgel:|Videoschnitt:|$)/i); if (organizerMatch) { worship.organizer = organizerMatch[1].trim(); console.log(` parseWorshipFromCell: Gestalter extrahiert: "${worship.organizer}"`); } // Dienst extrahieren const serviceMatch = fullText.match(/Dienst:\s*([^KollekteOrgelVideoschnitt]+?)(?=Kollekte:|Orgel:|Videoschnitt:|$)/i); if (serviceMatch) { worship.sacristanService = serviceMatch[1].trim(); console.log(` parseWorshipFromCell: Dienst extrahiert: "${worship.sacristanService}"`); } // Kollekte extrahieren const collectionMatch = fullText.match(/Kollekte:\s*([^OrgelVideoschnitt]+?)(?=Orgel:|Videoschnitt:|$)/i); if (collectionMatch) { let collection = collectionMatch[1].trim(); // Falls am Ende eine Nummer in Klammern steht (z.B. "für XY (12345)"), diese entfernen collection = collection.replace(/\s*\(\d+\)\s*$/, '').trim(); worship.collection = collection; console.log(` parseWorshipFromCell: Kollekte extrahiert: "${worship.collection}"`); } // Orgelspiel extrahieren const organMatch = fullText.match(/Orgel:\s*([^GestaltungDienstKollekteVideoschnitt]+?)(?=Gestaltung:|Dienst:|Kollekte:|Videoschnitt:|$)/i); if (organMatch) { worship.organPlaying = organMatch[1].trim(); console.log(` parseWorshipFromCell: Orgelspiel extrahiert: "${worship.organPlaying}"`); } } else { // Falls keine Uhrzeit gefunden, versuche Titel direkt zu extrahieren const titleMatch = fullText.match(/^(.+?)(?=Gestaltung:|Dienst:|Kollekte:|Orgel:|$)/i); if (titleMatch) { worship.title = titleMatch[1].trim(); } else { worship.title = fullText.substring(0, 100); } } // Nur Datum ist Pflichtfeld - alle anderen Felder sind optional // Das Datum wird bereits vor dem Aufruf dieser Funktion geprüft // Falls keine Uhrzeit gefunden wurde, setze einen Standardwert oder lasse es leer if (!worship.time) { worship.time = null; // Optional } // Falls kein Titel gefunden wurde, setze einen Standardwert if (!worship.title || worship.title.trim().length === 0) { worship.title = 'Gottesdienst'; // Standardtitel } console.log(` parseWorshipFromCell: Erfolgreich - date: ${worship.date}, time: ${worship.time || 'keine'}, title: "${worship.title}"`); return worship; } // Hilfsfunktion zum Parsen eines Gottesdienstes mit Zeilenumbrüchen function parseWorshipFromCellWithLines(lines, date, dayName) { const worship = { date: date, dayName: dayName, time: null, title: '', organizer: '', sacristanService: '', collection: '', organPlaying: '', eventPlaceId: null, address: '', selfInformation: false, highlightTime: false, neighborInvitation: false, introLine: '' }; // Suche nach Uhrzeit in allen Zeilen (beginne mit der ersten) let timeFound = false; let timeLineIndex = -1; for (let i = 0; i < lines.length; i++) { const time = parseTime(lines[i]); if (time) { worship.time = time; timeLineIndex = i; timeFound = true; console.log(` parseWorshipFromCellWithLines: Uhrzeit geparst: ${worship.time} (Zeile ${i + 1})`); break; } } if (!timeFound) { // Keine Uhrzeit gefunden return null; } // Titel aus der Zeile mit der Uhrzeit extrahieren if (timeLineIndex >= 0 && lines[timeLineIndex]) { let timeLine = lines[timeLineIndex]; let title = timeLine.replace(/\d{1,2}[:.]\d{2}/, '').replace(/Uhr/gi, '').trim(); // Entferne häufige Wörter am Anfang, aber behalte "Gottesdienst" title = title.replace(/^(Gemeinsamer|Einladung zum)\s+/i, ''); // Entferne Ort am Ende (wenn vorhanden) title = title.replace(/\s+(?:in|am|zu)\s+[A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+$/i, ''); // Falls Titel leer ist, verwende "Gottesdienst" worship.title = title.trim() || 'Gottesdienst'; // Ort aus der Zeile mit der Uhrzeit extrahieren const locationMatch = timeLine.match(/(?:in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)/i); if (locationMatch) { const locationName = locationMatch[1].trim(); const normalizedLocation = locationName.split(/\s*-\s*/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() ).join('-'); worship.extractedLocation = `Kirche ${normalizedLocation}`; console.log(` parseWorshipFromCellWithLines: Ort extrahiert: "${worship.extractedLocation}"`); } } // Weitere Zeilen durchgehen (beginne nach der Zeile mit der Uhrzeit) for (let i = timeLineIndex + 1; i < lines.length; i++) { const line = lines[i]; // Wenn eine neue Uhrzeit gefunden wird, stoppe hier (dieser Block gehört zu einem anderen Gottesdienst) if (parseTime(line)) { break; } // Gestalter if (line.toLowerCase().includes('gestaltung:')) { worship.organizer = line.replace(/^.*gestaltung:\s*/i, '').trim(); console.log(` parseWorshipFromCellWithLines: Gestalter: "${worship.organizer}"`); } // Dienst else if (line.toLowerCase().includes('dienst:') && !line.toLowerCase().includes('gestaltung')) { worship.sacristanService = line.replace(/^.*dienst:\s*/i, '').trim(); console.log(` parseWorshipFromCellWithLines: Dienst: "${worship.sacristanService}"`); } // Kollekte else if (line.toLowerCase().includes('kollekte:')) { let collection = line.replace(/^.*kollekte:\s*/i, '').trim(); // Falls am Ende eine Nummer in Klammern steht (z.B. "für XY (12345)"), diese entfernen collection = collection.replace(/\s*\(\d+\)\s*$/, '').trim(); worship.collection = collection; console.log(` parseWorshipFromCellWithLines: Kollekte: "${worship.collection}"`); } // Orgelspiel else if (line.toLowerCase().includes('orgel:')) { worship.organPlaying = line.replace(/^.*orgel:\s*/i, '').trim(); console.log(` parseWorshipFromCellWithLines: Orgelspiel: "${worship.organPlaying}"`); } // Falls keine spezifische Markierung und Titel noch nicht vollständig else if (line && !worship.title.includes(line) && !line.toLowerCase().includes('videoschnitt')) { if (worship.title && !worship.title.includes(line)) { worship.title += ' ' + line; } } } // Mindestanforderungen prüfen if (!worship.time || !worship.title) { console.log(` parseWorshipFromCellWithLines: Fehlgeschlagen - time: ${worship.time}, title: "${worship.title}"`); return null; } console.log(` parseWorshipFromCellWithLines: Erfolgreich - time: ${worship.time}, title: "${worship.title}"`); return worship; } // Import-Funktion für Gottesdienste aus .doc/.docx Dateien exports.importWorships = async (req, res) => { try { if (!req.file) { return res.status(400).json({ message: 'Keine Datei hochgeladen.' }); } // Validierung: Nur .doc und .docx Dateien erlauben const fileName = req.file.originalname.toLowerCase(); const allowedExtensions = ['.doc', '.docx']; const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext)); if (!isValidFile) { return res.status(400).json({ message: 'Nur .doc und .docx Dateien sind erlaubt.' }); } // Nur .docx wird aktuell unterstützt (mammoth unterstützt nur .docx) if (!fileName.endsWith('.docx')) { return res.status(400).json({ message: 'Aktuell werden nur .docx Dateien unterstützt.' }); } // .docx Datei mit mammoth parsen const result = await mammoth.convertToHtml({ buffer: req.file.buffer }); const html = result.value; // Tabelle aus HTML extrahieren const tableRegex = /]*>([\s\S]*?)<\/table>/gi; const tableMatch = tableRegex.exec(html); if (!tableMatch) { return res.status(400).json({ message: 'Keine Tabelle in der Datei gefunden.' }); } const tableHtml = tableMatch[1]; // Zeilen aus der Tabelle extrahieren const rowRegex = /]*>([\s\S]*?)<\/tr>/gi; const rows = []; let rowMatch; while ((rowMatch = rowRegex.exec(tableHtml)) !== null) { rows.push(rowMatch[1]); } const importedWorships = []; const errors = []; // Heutiges Datum (ohne Uhrzeit) zur Filterung von Vergangenheits-Terminen const today = new Date(); today.setHours(0, 0, 0, 0); console.log(`Gefundene Tabellenzeilen: ${rows.length}`); // Durch jede Zeile iterieren for (const row of rows) { // Zellen aus der Zeile extrahieren const cellRegex = /]*>([\s\S]*?)<\/t[dh]>/gi; const cells = []; let cellMatch; while ((cellMatch = cellRegex.exec(row)) !== null) { // HTML-Tags entfernen und Text extrahieren, aber Zeilenumbrüche erhalten let cellText = cellMatch[1]; // Ersetze
und
durch Zeilenumbrüche cellText = cellText.replace(//gi, '\n'); // Ersetze

und

durch Zeilenumbrüche (aber nicht doppelte) cellText = cellText.replace(/<\/p>/gi, '\n'); cellText = cellText.replace(/]*>/gi, ''); // Ersetze und

durch Zeilenumbrüche cellText = cellText.replace(/<\/div>/gi, '\n'); cellText = cellText.replace(/]*>/gi, ''); // Entferne andere HTML-Tags cellText = cellText.replace(/<[^>]+>/g, ''); // HTML-Entities ersetzen cellText = cellText.replace(/ /g, ' '); cellText = cellText.replace(/&/g, '&'); cellText = cellText.replace(/</g, '<'); cellText = cellText.replace(/>/g, '>'); cellText = cellText.replace(/"/g, '"'); // Mehrfache Zeilenumbrüche auf einen reduzieren cellText = cellText.replace(/\n\s*\n\s*\n+/g, '\n\n'); // Trim cellText = cellText.trim(); cells.push(cellText); } // Mindestens 2 Spalten erwartet if (cells.length < 2) { continue; } const dayString = cells[0]; const worshipCell = cells[1]; console.log(`Zeile ${rows.indexOf(row) + 1}: dayString="${dayString}", worshipCell="${worshipCell.substring(0, 100)}..."`); // Wenn Spalte 1 leer ist, Zeile überspringen if (!dayString || dayString.trim().length === 0) { console.log(` -> Spalte 1 ist leer, Zeile wird übersprungen`); continue; } // Datum aus der ersten Spalte extrahieren (Pflichtfeld) const date = parseDateFromDayString(dayString); if (!date) { console.log(` -> Datum konnte nicht geparst werden aus: "${dayString}"`); const rowNum = rows.indexOf(row) + 1; errors.push(`Zeile ${rowNum}: Konnte Datum nicht aus "${dayString}" extrahieren.\nVollständige Zeile:\nSpalte 1: "${dayString}"\nSpalte 2: "${worshipCell.substring(0, 200)}${worshipCell.length > 200 ? '...' : ''}"`); continue; } console.log(` -> Datum geparst: ${date}`); // Gottesdienste in der Vergangenheit nicht weiter verarbeiten/anzeigen const worshipDate = new Date(date); worshipDate.setHours(0, 0, 0, 0); if (worshipDate < today) { console.log(` -> Datum liegt in der Vergangenheit, Zeile wird übersprungen`); continue; } // Gottesdienst(e) aus der zweiten Spalte extrahieren // Hinweis: Vergangene Daten werden erst beim Speichern herausgefiltert, // damit sie im Dialog zur Bearbeitung angezeigt werden können // Suche nach allen Uhrzeiten im Format "XX.XX Uhr" oder "XX:XX Uhr" const timeMatches = [...worshipCell.matchAll(/(\d{1,2})[:.](\d{2})\s*Uhr/gi)]; if (timeMatches.length === 0) { // Keine Uhrzeit gefunden, versuche trotzdem zu parsen (Uhrzeit ist optional) const worship = parseWorshipFromCell(worshipCell, date, ''); if (worship) { importedWorships.push(worship); } else { // Falls Parsing komplett fehlschlägt, erstelle trotzdem einen minimalen Eintrag mit Datum const dateStr = date ? date.toISOString().split('T')[0] : 'unbekannt'; const minimalWorship = { date: date, dayName: '', time: null, title: 'Gottesdienst', organizer: '', sacristanService: '', collection: '', organPlaying: '', eventPlaceId: null }; importedWorships.push(minimalWorship); console.log(` -> Minimaler Gottesdienst erstellt (nur Datum): ${dateStr}`); } continue; } // Teile die Zelle an jeder Uhrzeit const worshipBlocks = []; for (let i = 0; i < timeMatches.length; i++) { const match = timeMatches[i]; const startIndex = match.index; const endIndex = i < timeMatches.length - 1 ? timeMatches[i + 1].index : worshipCell.length; const block = worshipCell.substring(startIndex, endIndex).trim(); if (block.length > 0) { worshipBlocks.push(block); } } // Jeden Gottesdienst-Block parsen console.log(` -> ${worshipBlocks.length} Gottesdienst-Blöcke gefunden`); for (const block of worshipBlocks) { console.log(` -> Parse Block: "${block.substring(0, 100)}..."`); const worship = parseWorshipFromCell(block, date, ''); if (worship) { console.log(` -> Gottesdienst erfolgreich geparst: ${worship.time || 'keine Uhrzeit'} - ${worship.title}`); importedWorships.push(worship); } else { // Falls Parsing fehlschlägt, erstelle trotzdem einen minimalen Eintrag mit Datum console.log(` -> Gottesdienst konnte nicht geparst werden, erstelle minimalen Eintrag`); const dateStr = date ? date.toISOString().split('T')[0] : 'unbekannt'; const minimalWorship = { date: date, dayName: '', time: null, title: 'Gottesdienst', organizer: '', sacristanService: '', collection: '', organPlaying: '', eventPlaceId: null }; importedWorships.push(minimalWorship); } } } console.log(`Geparste Gottesdienste: ${importedWorships.length}`); console.log(`Fehler beim Parsen: ${errors.length}`); // EventPlace- und Tag-Name-Zuordnung für alle geparsten Gottesdienste durchführen // Hinweis: Vergangene Daten werden erst beim Speichern herausgefiltert, // damit sie im Dialog zur Bearbeitung angezeigt werden können // Sammle alle Daten der geparsten Gottesdienste für Datenbankabfrage const datesToCheck = new Set(); for (const worshipData of importedWorships) { if (worshipData.date) { let dateStr; if (worshipData.date instanceof Date) { const year = worshipData.date.getFullYear(); const month = String(worshipData.date.getMonth() + 1).padStart(2, '0'); const day = String(worshipData.date.getDate()).padStart(2, '0'); dateStr = `${year}-${month}-${day}`; } else if (typeof worshipData.date === 'string') { dateStr = worshipData.date.split('T')[0]; } if (dateStr) { datesToCheck.add(dateStr); } } } // Lade alle bestehenden Gottesdienste für die relevanten Daten // Verwende DATE() Funktion, um nur das Datum zu vergleichen (ohne Zeit) // Lade auch EventPlace-Beziehung für Vergleich // WICHTIG: Lade alle Felder inklusive organPlaying für Vergleich let existingWorships = []; if (datesToCheck.size > 0) { existingWorships = await Worship.findAll({ where: { [Op.or]: Array.from(datesToCheck).map(dateStr => sequelize.where(sequelize.fn('DATE', sequelize.col('date')), dateStr) ) }, // Lade alle Felder (organPlaying ist standardmäßig enthalten) include: { model: EventPlace, as: 'eventPlace' } }); console.log(` -> ${existingWorships.length} bestehende Gottesdienste geladen für Vergleich`); } // Hilfsfunktion: Prüft ob ein Feld mehr Daten hat const hasMoreData = (newValue, oldValue) => { const newHasData = newValue && String(newValue).trim().length > 0; const oldHasData = oldValue && String(oldValue).trim().length > 0; return newHasData && !oldHasData; }; // Hilfsfunktion: Prüft ob ein Feld weniger Daten hat const hasLessData = (newValue, oldValue) => { const newHasData = newValue && String(newValue).trim().length > 0; const oldHasData = oldValue && String(oldValue).trim().length > 0; return !newHasData && oldHasData; }; // Hilfsfunktion: Prüft ob ein Eintrag relevante Änderungen hat const hasRelevantChanges = (newWorship, existingWorship) => { // Wenn kein bestehender Eintrag existiert, prüfe ob der neue Eintrag relevante Daten hat if (!existingWorship) { // Prüfe ob mindestens ein Feld außer Datum, Zeit und Ort gefüllt ist const hasRelevantData = newWorship.title || newWorship.organizer || newWorship.sacristanService || newWorship.collection || newWorship.organPlaying; return !!hasRelevantData; // Nur relevant wenn mindestens ein Feld gefüllt ist } let hasMoreOrChanged = false; let hasLess = false; // Vergleiche alle relevanten Felder const fieldsToCompare = ['time', 'title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId']; // Hilfsfunktion zum Normalisieren von Uhrzeiten (ignoriere Sekunden) const normalizeTimeForComparison = (timeStr) => { if (!timeStr) return ''; return String(timeStr).substring(0, 5); // Nur HH:MM }; for (const field of fieldsToCompare) { let newValue = newWorship[field]; let oldValue = existingWorship[field]; // Spezielle Behandlung für Uhrzeit: ignoriere Sekunden beim Vergleich if (field === 'time') { newValue = normalizeTimeForComparison(newValue); oldValue = normalizeTimeForComparison(oldValue); } // Normalisiere Werte für Vergleich // Behandle null, undefined und leere Strings als gleich const normalizeValue = (val) => { if (val === null || val === undefined) return ''; const str = String(val).trim(); return str.length === 0 ? '' : str; }; const newValueStr = normalizeValue(newValue); const oldValueStr = normalizeValue(oldValue); // Wenn beide Werte identisch sind (oder beide leer), überspringe if (newValueStr === oldValueStr) { continue; } // Prüfe ob neuer Wert mehr Daten hat (war leer, jetzt gefüllt) if (newValueStr && !oldValueStr) { hasMoreOrChanged = true; console.log(` -> Feld "${field}": Mehr Daten (neu: "${newValueStr}", alt: leer)`); } // Prüfe ob neuer Wert weniger Daten hat (war gefüllt, jetzt leer) if (!newValueStr && oldValueStr) { hasLess = true; console.log(` -> Feld "${field}": Weniger Daten (neu: leer, alt: "${oldValueStr}")`); } // Prüfe ob Werte unterschiedlich sind (beide gefüllt) if (newValueStr && oldValueStr && newValueStr !== oldValueStr) { hasMoreOrChanged = true; console.log(` -> Feld "${field}": Geändert (neu: "${newValueStr}", alt: "${oldValueStr}")`); } } // Nur relevant wenn mehr/andere Daten vorhanden sind UND nicht weniger Daten const isRelevant = hasMoreOrChanged && !hasLess; console.log(` -> hasRelevantChanges: hasMoreOrChanged=${hasMoreOrChanged}, hasLess=${hasLess}, result=${isRelevant}`); return isRelevant; }; // Hilfsfunktion: Finde passenden bestehenden Eintrag const findExistingWorship = (worshipData) => { if (!worshipData.date) return null; let dateStr; if (worshipData.date instanceof Date) { const year = worshipData.date.getFullYear(); const month = String(worshipData.date.getMonth() + 1).padStart(2, '0'); const day = String(worshipData.date.getDate()).padStart(2, '0'); dateStr = `${year}-${month}-${day}`; } else if (typeof worshipData.date === 'string') { dateStr = worshipData.date.split('T')[0]; } else { return null; } // Suche nach passendem Eintrag (Datum, Uhrzeit, Ort) return existingWorships.find(existing => { // Konvertiere bestehendes Datum zu YYYY-MM-DD let existingDateStr; if (existing.date instanceof Date) { const year = existing.date.getFullYear(); const month = String(existing.date.getMonth() + 1).padStart(2, '0'); const day = String(existing.date.getDate()).padStart(2, '0'); existingDateStr = `${year}-${month}-${day}`; } else if (typeof existing.date === 'string') { existingDateStr = existing.date.split('T')[0]; } else { return false; } if (existingDateStr !== dateStr) return false; // Vergleiche Uhrzeit (normalisiere beide, ignoriere Sekunden) const normalizeTime = (timeStr) => { if (!timeStr) return null; const str = String(timeStr); // Extrahiere nur HH:MM (erste 5 Zeichen) return str.substring(0, 5); }; const newTime = normalizeTime(worshipData.time); const existingTime = normalizeTime(existing.time); if (newTime !== existingTime) return false; // Vergleiche Ort const newPlaceId = worshipData.eventPlaceId || null; const existingPlaceId = existing.eventPlaceId || null; if (newPlaceId !== existingPlaceId) return false; return true; }); }; for (const worshipData of importedWorships) { // EventPlace zuordnen, falls ein Ort explizit extrahiert wurde if (worshipData.extractedLocation) { const eventPlace = await EventPlace.findOne({ where: { name: worshipData.extractedLocation } }); if (eventPlace) { worshipData.eventPlaceId = eventPlace.id; } else { // Falls nicht gefunden, versuche auch ohne "Kirche " Präfix const locationWithoutKirche = worshipData.extractedLocation.replace(/^Kirche\s+/i, '').trim(); // Zuerst versuchen wir, einen Ort zu finden, der sowohl den Ortsnamen // als auch "Kirche" im Namen enthält (z.B. "Ev. Kirche Nieder-Erlenbach"). // Damit wird bei mehreren Treffern (Kirche / Gemeindehaus) bevorzugt die Kirche gewählt. let eventPlaceAlt = await EventPlace.findOne({ where: { [Op.and]: [ { name: { [Op.like]: `%${locationWithoutKirche}%` } }, { name: { [Op.like]: `%Kirche%` } } ] } }); // Falls keine passende Kirche gefunden wurde, auf die bisherige Logik zurückfallen if (!eventPlaceAlt) { eventPlaceAlt = await EventPlace.findOne({ where: { name: { [Op.like]: `%${locationWithoutKirche}%` } } }); } if (eventPlaceAlt) { worshipData.eventPlaceId = eventPlaceAlt.id; } } // Entferne extractedLocation, da es nicht in der Datenbank gespeichert wird delete worshipData.extractedLocation; } // Fallback: Wenn noch kein Ort gesetzt ist, versuche den Titel als Ortsnamen zu interpretieren if (!worshipData.eventPlaceId && worshipData.title && typeof worshipData.title === 'string') { const titleTrimmed = worshipData.title.trim(); // "Einfache" Titel als mögliche Ortsnamen zulassen: // - Beginn mit Großbuchstaben // - nur Buchstaben, Bindestriche und Leerzeichen const isSimpleLocationLike = /^[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*(?:\s+[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*)*$/.test(titleTrimmed); if (isSimpleLocationLike) { const normalizedLocation = titleTrimmed .split(/\s*-\s*/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join('-'); // 1. Versuche: exakter Name "Kirche " let eventPlaceFromTitle = await EventPlace.findOne({ where: { name: `Kirche ${normalizedLocation}` } }); // 2. Versuche: Name enthält Ortsnamen und "Kirche" if (!eventPlaceFromTitle) { eventPlaceFromTitle = await EventPlace.findOne({ where: { [Op.and]: [ { name: { [Op.like]: `%${normalizedLocation}%` } }, { name: { [Op.like]: `%Kirche%` } } ] } }); } // 3. Versuche: nur der Ortsname (z.B. wenn EventPlace "Nieder-Eschbach" heißt) if (!eventPlaceFromTitle) { eventPlaceFromTitle = await EventPlace.findOne({ where: { name: { [Op.like]: `%${normalizedLocation}%` } } }); } if (eventPlaceFromTitle) { worshipData.eventPlaceId = eventPlaceFromTitle.id; } } } // Tag-Name aus der Datenbank basierend auf dem Datum holen if (worshipData.date) { let dateStr; if (worshipData.date instanceof Date) { // Konvertiere Date zu YYYY-MM-DD Format const year = worshipData.date.getFullYear(); const month = String(worshipData.date.getMonth() + 1).padStart(2, '0'); const day = String(worshipData.date.getDate()).padStart(2, '0'); dateStr = `${year}-${month}-${day}`; } else if (typeof worshipData.date === 'string') { // Falls bereits String, verwende direkt (sollte YYYY-MM-DD sein) dateStr = worshipData.date.split('T')[0]; } else { dateStr = null; } if (dateStr) { console.log(` -> Suche liturgischen Tag für Datum: ${dateStr}`); const liturgicalDay = await LiturgicalDay.findOne({ where: { date: dateStr } }); if (liturgicalDay) { worshipData.dayName = liturgicalDay.dayName; console.log(` -> Tag-Name gefunden: "${liturgicalDay.dayName}"`); } else { // Falls kein liturgischer Tag gefunden, leer lassen worshipData.dayName = ''; console.log(` -> Kein liturgischer Tag für Datum ${dateStr} gefunden`); } } else { worshipData.dayName = ''; } } } // Filtere Gottesdienste: Nur die mit relevanten Änderungen behalten // Dies geschieht bereits im Backend, bevor Daten an das Frontend gesendet werden const worshipsWithChanges = []; let skippedCount = 0; for (const worshipData of importedWorships) { const existingWorship = findExistingWorship(worshipData); // Konvertiere Datum für Vergleich let dateStrForCompare = ''; if (worshipData.date instanceof Date) { const year = worshipData.date.getFullYear(); const month = String(worshipData.date.getMonth() + 1).padStart(2, '0'); const day = String(worshipData.date.getDate()).padStart(2, '0'); dateStrForCompare = `${year}-${month}-${day}`; } else if (typeof worshipData.date === 'string') { dateStrForCompare = worshipData.date.split('T')[0]; } console.log(` -> Prüfe Gottesdienst: Datum=${dateStrForCompare}, Zeit=${worshipData.time}, Ort=${worshipData.eventPlaceId}`); console.log(` -> organPlaying im neuen: "${worshipData.organPlaying || '(leer)'}" (Typ: ${typeof worshipData.organPlaying}, Wert: ${JSON.stringify(worshipData.organPlaying)})`); if (existingWorship) { console.log(` -> Bestehender Eintrag gefunden: ID=${existingWorship.id}`); console.log(` -> organPlaying im bestehenden: "${existingWorship.organPlaying || '(leer)'}" (Typ: ${typeof existingWorship.organPlaying}, Wert: ${JSON.stringify(existingWorship.organPlaying)})`); } else { console.log(` -> Kein bestehender Eintrag gefunden (neuer Eintrag)`); // Debug: Zeige alle bestehenden Einträge für dieses Datum const existingForDate = existingWorships.filter(ex => { let exDateStr = ''; if (ex.date instanceof Date) { const year = ex.date.getFullYear(); const month = String(ex.date.getMonth() + 1).padStart(2, '0'); const day = String(ex.date.getDate()).padStart(2, '0'); exDateStr = `${year}-${month}-${day}`; } else if (typeof ex.date === 'string') { exDateStr = ex.date.split('T')[0]; } return exDateStr === dateStrForCompare; }); console.log(` -> Bestehende Einträge für dieses Datum: ${existingForDate.length}`); existingForDate.forEach(ex => { const exTime = ex.time ? String(ex.time).substring(0, 5) : 'keine'; const exPlace = ex.eventPlaceId || 'kein Ort'; console.log(` -> ID=${ex.id}, Zeit=${exTime}, Ort=${exPlace}`); }); } // Prüfe ob relevante Änderungen vorhanden sind if (hasRelevantChanges(worshipData, existingWorship)) { // Markiere als Update, falls bestehender Eintrag existiert if (existingWorship) { worshipData._isUpdate = true; worshipData._isNew = false; worshipData._existingId = existingWorship.id; // Speichere alte Werte für Vergleich const fieldsToCompare = ['time', 'title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId']; worshipData._oldValues = {}; worshipData._changedFields = []; // Verwende die gleiche Normalisierungslogik wie in hasRelevantChanges const normalizeValueForComparison = (val) => { if (val === null || val === undefined) return ''; const str = String(val).trim(); return str.length === 0 ? '' : str; }; const normalizeTimeForComparison = (timeStr) => { if (!timeStr) return ''; return String(timeStr).substring(0, 5); // Nur HH:MM }; for (const field of fieldsToCompare) { let newValue = worshipData[field]; let oldValue = existingWorship[field]; // Spezielle Behandlung für Uhrzeit: ignoriere Sekunden beim Vergleich if (field === 'time') { newValue = normalizeTimeForComparison(newValue); oldValue = normalizeTimeForComparison(oldValue); } // Normalisiere Werte für Vergleich (behandle null, undefined und leere Strings als gleich) const newValueStr = normalizeValueForComparison(newValue); const oldValueStr = normalizeValueForComparison(oldValue); if (newValueStr !== oldValueStr) { // Für Anzeige: verwende formatierte Version if (field === 'time' && oldValue) { // Zeige alte Uhrzeit ohne Sekunden const oldTimeStr = String(oldValue); worshipData._oldValues[field] = oldTimeStr.substring(0, 5) || '(leer)'; } else { // Zeige alten Wert, oder "(leer)" wenn leer/null const displayValue = oldValue ? String(oldValue).trim() : ''; worshipData._oldValues[field] = displayValue || '(leer)'; } worshipData._changedFields.push(field); console.log(` -> Feld "${field}" als geändert markiert: neu="${newValueStr}", alt="${oldValueStr}"`); } } // Speichere auch alten EventPlace-Namen, falls vorhanden if (existingWorship.eventPlace && existingWorship.eventPlace.name) { worshipData._oldValues.eventPlaceName = existingWorship.eventPlace.name; } console.log(` -> Relevante Änderungen gefunden für bestehenden Eintrag ID ${existingWorship.id}: ${worshipData._changedFields.join(', ')}`); } else { // Komplett neuer Eintrag (Datum, Uhrzeit, Ort existieren noch nicht) worshipData._isUpdate = false; worshipData._isNew = true; worshipData._oldValues = {}; worshipData._changedFields = []; console.log(` -> Neuer Eintrag wird hinzugefügt (Datum, Uhrzeit, Ort existieren noch nicht)`); } worshipsWithChanges.push(worshipData); } else { skippedCount++; if (existingWorship) { console.log(` -> Eintrag übersprungen (weniger oder keine relevanten Änderungen) für ID ${existingWorship.id}`); } else { console.log(` -> Eintrag übersprungen (keine relevanten Daten)`); } } } console.log(`Gottesdienste mit relevanten Änderungen: ${worshipsWithChanges.length}`); console.log(`Übersprungene Gottesdienste: ${skippedCount}`); // Geparste Daten zurückgeben (ohne zu speichern) // Hinweis: Nur Gottesdienste mit relevanten Änderungen werden zurückgegeben // Konvertiere Date-Objekte zu YYYY-MM-DD Strings für das Frontend const worshipsForFrontend = worshipsWithChanges.map(w => { const worshipCopy = { ...w }; // Konvertiere Datum zu YYYY-MM-DD Format if (worshipCopy.date instanceof Date) { const year = worshipCopy.date.getFullYear(); const month = String(worshipCopy.date.getMonth() + 1).padStart(2, '0'); const day = String(worshipCopy.date.getDate()).padStart(2, '0'); worshipCopy.date = `${year}-${month}-${day}`; } else if (typeof worshipCopy.date === 'string') { // Falls bereits String, stelle sicher, dass es YYYY-MM-DD ist worshipCopy.date = worshipCopy.date.split('T')[0]; } return worshipCopy; }); res.status(200).json({ message: `Datei erfolgreich geparst. ${worshipsWithChanges.length} Gottesdienste mit relevanten Änderungen gefunden (${skippedCount} übersprungen).`, worships: worshipsForFrontend, errors: errors.length > 0 ? errors : undefined }); } catch (error) { console.error('Fehler beim Importieren der Gottesdienste:', error); res.status(500).json({ message: 'Fehler beim Importieren der Gottesdienste', error: error.message }); } }; // Funktion zum Speichern der bearbeiteten Gottesdienste exports.saveImportedWorships = async (req, res) => { try { const { worships } = req.body; if (!worships || !Array.isArray(worships)) { return res.status(400).json({ message: 'Keine Gottesdienste zum Speichern übergeben.' }); } let savedCount = 0; let updatedCount = 0; const errors = []; const today = new Date(); today.setHours(0, 0, 0, 0); for (const worshipData of worships) { try { // Prüfen ob Datum in der Vergangenheit liegt const worshipDate = new Date(worshipData.date); worshipDate.setHours(0, 0, 0, 0); if (worshipDate < today) { continue; // Überspringe vergangene Daten } // approved auf false setzen worshipData.approved = false; // Prüfen ob bereits ein Eintrag für dieses Datum und diese Uhrzeit existiert const whereClause = { date: { [Op.eq]: sequelize.fn('DATE', worshipData.date) }, time: worshipData.time }; // Wenn eventPlaceId gesetzt ist, auch danach suchen if (worshipData.eventPlaceId) { whereClause.eventPlaceId = worshipData.eventPlaceId; } else { // Wenn kein eventPlaceId, suche nach Einträgen ohne eventPlaceId whereClause.eventPlaceId = { [Op.is]: null }; } const existingWorship = await Worship.findOne({ where: whereClause }); if (existingWorship) { // Update bestehenden Eintrag await existingWorship.update(worshipData); updatedCount++; } else { // Neuen Eintrag erstellen await Worship.create(worshipData); savedCount++; } } catch (error) { console.error('Fehler beim Speichern eines Gottesdienstes:', error); errors.push(`Fehler beim Speichern: ${worshipData.date} ${worshipData.time} - ${worshipData.title || 'Unbekannt'}`); } } const totalProcessed = savedCount + updatedCount; res.status(200).json({ message: `Import abgeschlossen. ${savedCount} neue Gottesdienste erstellt, ${updatedCount} aktualisiert.`, imported: savedCount, updated: updatedCount, total: totalProcessed, skipped: worships.length - totalProcessed, errors: errors }); } catch (error) { console.error('Fehler beim Speichern der importierten Gottesdienste:', error); res.status(500).json({ message: 'Fehler beim Speichern der Gottesdienste', error: error.message }); } }; // Export-Funktion für Gottesdienste exports.exportWorships = async (req, res) => { try { const { from, to, format } = req.query; if (!from || !to) { return res.status(400).json({ message: 'Von- und Bis-Datum müssen angegeben werden.' }); } const fromDate = new Date(from); const toDate = new Date(to); toDate.setHours(23, 59, 59, 999); // Bis Ende des Tages if (fromDate > toDate) { return res.status(400).json({ message: 'Das Von-Datum muss vor dem Bis-Datum liegen.' }); } // Gottesdienste im Datumsbereich abrufen const worships = await Worship.findAll({ where: { date: { [Op.between]: [fromDate, toDate] } }, include: { model: EventPlace, as: 'eventPlace', }, order: [ ['date', 'ASC'], ['time', 'ASC'] ], }); if (worships.length === 0) { return res.status(404).json({ message: 'Keine Gottesdienste im angegebenen Zeitraum gefunden.' }); } // Datum formatieren für Anzeige const formatDate = (date) => { const d = new Date(date); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; const formatTime = (time) => { if (!time) return ''; const parts = time.split(':'); return `${parts[0]}:${parts[1]}`; }; // Hex zu RGB konvertieren const hexToRgb = (hex) => { if (!hex) return null; const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }; // Gottesdienste nach Datum gruppieren für rowspan-Berechnung const worshipsByDate = {}; worships.forEach(worship => { const dateKey = formatDate(worship.date); if (!worshipsByDate[dateKey]) { worshipsByDate[dateKey] = []; } worshipsByDate[dateKey].push(worship); }); // Tabellenzeilen erstellen const tableRows = []; worships.forEach((worship, index) => { const dateKey = formatDate(worship.date); const isFirstWorshipOfDay = worshipsByDate[dateKey][0] === worship; const rowspan = worshipsByDate[dateKey].length; const dateStr = formatDate(worship.date); const dayNameStr = worship.dayName || ''; // Erste Spalte: Datum + Tag-Name // Formatierungen aus der geparsten Datei: Schriftart Arial, Größe 11pt, fett // Breite: 3,45 cm = 1956 DXA (1 cm = 567 DXA) let firstCell; if (isFirstWorshipOfDay) { // Erste Zelle des Tages: VerticalMerge RESTART (startet die Verbindung) // Tag-Name in neuer Zeile const firstCellChildren = [ new TextRun({ text: dateStr, color: '000000', font: 'Arial', size: 22, // 11pt = 22 half-points bold: true }) ]; if (dayNameStr) { firstCellChildren.push(new TextRun({ text: '', break: 1 // Zeilenumbruch })); firstCellChildren.push(new TextRun({ text: dayNameStr, color: '000000', font: 'Arial', size: 22, bold: true })); } firstCell = new TableCell({ children: [new Paragraph({ children: firstCellChildren, alignment: AlignmentType.CENTER, // Spalte 1 zentriert })], // Breite wird über columnWidths auf Tabellenebene gesetzt margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding verticalMerge: rowspan > 1 ? VerticalMergeType.RESTART : undefined, verticalAlign: VerticalAlign.TOP }); } else { // Nachfolgende Zellen: mit VerticalMerge CONTINUE verbinden firstCell = new TableCell({ children: [new Paragraph({ text: '', alignment: AlignmentType.CENTER, // Spalte 1 zentriert })], // Breite wird über columnWidths auf Tabellenebene gesetzt margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding verticalMerge: VerticalMergeType.CONTINUE }); } // Zweite Spalte: Gottesdienst-Details // Hintergrundfarbe aus EventPlace-Einstellungen const backgroundColor = worship.eventPlace?.backgroundColor || '#ffffff'; const hexColor = backgroundColor.replace('#', '').toUpperCase(); // Hintergrundfarbe immer setzen (auch wenn weiß) // Prüfen, ob die Farbe gültig ist (6-stelliger Hex-Code) const validHexColor = hexColor.length === 6 ? hexColor : 'FFFFFF'; const shading = { fill: '#' + validHexColor, // Fill muss mit # beginnen type: ShadingType.SOLID, color: '#' + validHexColor // Color sollte die gleiche Farbe wie fill haben }; // Format: **bold** Uhrzeit + Titel, dann Gestaltung (bold), dann Kollekte (nicht bold) const timeStr = formatTime(worship.time); const titleStr = worship.title || 'Gottesdienst'; const organizerStr = worship.organizer || ''; const collectionStr = worship.collection || ''; const secondCellChildren = []; // Uhrzeit + Titel (bold, schwarz, Arial 11pt) secondCellChildren.push(new TextRun({ text: `${timeStr} ${titleStr}`, bold: true, color: '000000', font: 'Arial', size: 22 // 11pt = 22 half-points })); // Gestaltung (bold, schwarz, Arial 11pt) if (organizerStr) { secondCellChildren.push(new TextRun({ text: '', break: 1 // Zeilenumbruch })); secondCellChildren.push(new TextRun({ text: 'Gestaltung: ', bold: true, color: '000000', font: 'Arial', size: 22 })); secondCellChildren.push(new TextRun({ text: organizerStr, bold: true, color: '000000', font: 'Arial', size: 22 })); } // Kollekte (nicht bold, schwarz, Arial 11pt) if (collectionStr) { secondCellChildren.push(new TextRun({ text: '', break: 1 // Zeilenumbruch })); secondCellChildren.push(new TextRun({ text: 'Kollekte: ', color: '000000', font: 'Arial', size: 22 })); secondCellChildren.push(new TextRun({ text: collectionStr, color: '000000', font: 'Arial', size: 22 })); } const secondCell = new TableCell({ children: [new Paragraph({ children: secondCellChildren, alignment: AlignmentType.CENTER, // Spalte 2 zentriert })], // Breite wird über columnWidths auf Tabellenebene gesetzt shading: shading, // Shading vor margins setzen margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding verticalAlign: VerticalAlign.TOP }); // Zeile erstellen - immer beide Spalten const rowChildren = [firstCell, secondCell]; tableRows.push(new TableRow({ children: rowChildren, height: { value: 1559, rule: HeightRule.EXACT } // 2,75 cm = 1559 DXA - Zeilenhöhe auf TableRow setzen })); }); // Word-Dokument erstellen // Seitenränder: 2 cm = 1134 DXA (1 cm = 567 DXA) // Verfügbare Breite: 21 cm (A4) - 2 cm links - 2 cm rechts = 17 cm = 9638 DXA // Spalte 1: 3,45 cm = 1956 DXA // Spalte 2: 13,55 cm = 7682 DXA const doc = new Document({ sections: [{ properties: { page: { margin: { top: 1134, // 2 cm right: 1134, // 2 cm bottom: 1134, // 2 cm left: 1134 // 2 cm } } }, children: [ new Paragraph({ text: `Gottesdienste ${formatDate(fromDate)} - ${formatDate(toDate)}`, heading: 'Heading1', alignment: AlignmentType.CENTER }), new Paragraph({ text: '' }), // Leerzeile new Table({ width: { size: 9638, type: WidthType.DXA }, // 17 cm = 3,45 cm (1956) + 13,55 cm (7682) columnWidths: [1956, 7682], // Explizite Spaltenbreiten in DXA borders: { top: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }, bottom: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }, left: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }, right: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }, insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }, insideVertical: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' } }, rows: tableRows }) ] }] }); // Dokument als Buffer generieren const buffer = await Packer.toBuffer(doc); // Dateiname generieren const filename = `gottesdienste_${from}_${to}_${format}.docx`; // Response senden res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(buffer); } catch (error) { console.error('Fehler beim Exportieren der Gottesdienste:', error); res.status(500).json({ message: 'Fehler beim Exportieren der Gottesdienste', error: error.message }); } };