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 = /