diff --git a/.gitignore b/.gitignore index 5af353d..a3858f8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ server.key server.cert public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg -actualize.sh \ No newline at end of file +actualize.sh +files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx diff --git a/config/database.js b/config/database.js index 6cce022..cd914c2 100644 --- a/config/database.js +++ b/config/database.js @@ -1,7 +1,7 @@ const { Sequelize } = require('sequelize'); -const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', { - host: 'tsschulz.de', +const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan', { + host: 'localhost', dialect: 'mysql', retry: { match: [ @@ -26,7 +26,7 @@ const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vP async function connectWithRetry() { try { await sequelize.authenticate(); - console.log('Connection has been established successfully.'); + console.log(`Connection has been established successfully. Database server: ${sequelize.config.host}`); } catch (error) { console.error('Unable to connect to the database:', error); setTimeout(connectWithRetry, 5000); diff --git a/controllers/worshipController.js b/controllers/worshipController.js index 8099505..c15b816 100644 --- a/controllers/worshipController.js +++ b/controllers/worshipController.js @@ -1,7 +1,11 @@ -const { Worship, EventPlace, Sequelize, sequelize } = require('../models'); +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'); @@ -18,8 +22,10 @@ function isAuthorized(req) { req.user = decoded; return true; } catch (err) { - console.log('Token verification failed, adding to blacklist:', err.message); - addTokenToBlacklist(token); + // 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; } } @@ -42,7 +48,8 @@ exports.getAllWorships = async (req, res) => { }); res.status(200).json(worships); } catch (error) { - res.status(500).json({ message: 'Fehler beim Abrufen der Gottesdienste' }); + console.error('Fehler beim Abrufen der Gottesdienste:', error); + res.status(500).json({ message: 'Fehler beim Abrufen der Gottesdienste', error: error.message }); } }; @@ -87,10 +94,10 @@ exports.deleteWorship = async (req, res) => { exports.getFilteredWorships = async (req, res) => { const { location, order } = req.query; const where = {}; - if (order.trim() === '') { + if (order && order.trim() === '') { order = 'date DESC'; } - const locations = JSON.parse(location); + const locations = location ? JSON.parse(location) : []; if (location && locations.length > 0) { where.eventPlaceId = { [Sequelize.Op.in]: locations @@ -99,12 +106,20 @@ exports.getFilteredWorships = async (req, res) => { where.date = { [Op.gte]: fn('CURDATE'), }; + // Nur freigegebene Gottesdienste anzeigen + where.approved = true; try { const authorized = isAuthorized(req); - console.log(authorized); + + // 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: authorized ? undefined : { exclude: ['sacristanService'] }, + attributes: { exclude: attributesExclude }, include: { model: EventPlace, as: 'eventPlace', @@ -160,3 +175,1354 @@ exports.getWorshipOptions = async (req, res) => { 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 }); + } +}; diff --git a/migrations/20251122134324-add-orgelspiel-to-worships.js b/migrations/20251122134324-add-orgelspiel-to-worships.js new file mode 100644 index 0000000..548d36f --- /dev/null +++ b/migrations/20251122134324-add-orgelspiel-to-worships.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('worships', 'organ_playing', { + type: Sequelize.STRING, + allowNull: true + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('worships', 'organ_playing'); + } +}; + diff --git a/migrations/20251122140000-add-freigegeben-to-worships.js b/migrations/20251122140000-add-freigegeben-to-worships.js new file mode 100644 index 0000000..2d28c6c --- /dev/null +++ b/migrations/20251122140000-add-freigegeben-to-worships.js @@ -0,0 +1,17 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('worships', 'approved', { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('worships', 'approved'); + } +}; + diff --git a/models/Worship.js b/models/Worship.js index b7f1177..736e65f 100644 --- a/models/Worship.js +++ b/models/Worship.js @@ -61,6 +61,17 @@ module.exports = (sequelize) => { allowNull: true, field: 'sacristan_service' }, + organPlaying: { + type: DataTypes.STRING, + allowNull: true, + field: 'organ_playing' + }, + approved: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + field: 'approved' + }, }, { tableName: 'worships', timestamps: true diff --git a/package-lock.json b/package-lock.json index 2286112..2882786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,10 +33,12 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "date-fns": "^3.6.0", + "docx": "^9.5.1", "dotenv": "^16.4.5", "express": "^4.19.2", "file-saver": "^2.0.5", "jsonwebtoken": "^9.0.2", + "mammoth": "^1.11.0", "moment": "^2.30.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.1", @@ -3617,11 +3619,12 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.14.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", - "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "node_modules/@types/node-forge": { @@ -4862,6 +4865,15 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7873,6 +7885,12 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7908,6 +7926,41 @@ "node": ">=6.0.0" } }, + "node_modules/docx": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz", + "integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.0.1", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -8056,6 +8109,15 @@ "node": ">=4" } }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -10457,7 +10519,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -10795,6 +10856,12 @@ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11867,6 +11934,18 @@ "node": ">=10" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -11951,6 +12030,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -12350,6 +12438,17 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "license": "Apache-2.0" }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lowdb": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", @@ -12423,6 +12522,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/mammoth/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/mammoth/node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -12723,8 +12870,7 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", @@ -13464,6 +13610,12 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13687,6 +13839,12 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -16130,6 +16288,12 @@ "node": ">=0.10.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -17694,10 +17858,17 @@ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -19097,6 +19268,24 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -21751,11 +21940,11 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "@types/node": { - "version": "20.14.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", - "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "requires": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "@types/node-forge": { @@ -22763,6 +22952,11 @@ "@xtuc/long": "4.2.2" } }, + "@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -24960,6 +25154,11 @@ } } }, + "dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -24986,6 +25185,26 @@ "esutils": "^2.0.2" } }, + "docx": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz", + "integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==", + "requires": { + "@types/node": "^24.0.1", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "dependencies": { + "nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==" + } + } + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -25105,6 +25324,14 @@ "rimraf": "^3.0.0" } }, + "duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "requires": { + "underscore": "^1.13.1" + } + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -26875,7 +27102,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -27108,6 +27334,11 @@ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -27876,6 +28107,17 @@ } } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -27948,6 +28190,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -28263,6 +28513,16 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "requires": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "lowdb": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", @@ -28322,6 +28582,43 @@ "semver": "^6.0.0" } }, + "mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "requires": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==" + } + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -28544,8 +28841,7 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", @@ -29088,6 +29384,11 @@ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, + "option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, "optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -29247,6 +29548,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -31024,6 +31330,11 @@ } } }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -32204,10 +32515,15 @@ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -33204,6 +33520,19 @@ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "requires": {} }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } + }, "xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", diff --git a/package.json b/package.json index c1d14e8..905bb7e 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,12 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "date-fns": "^3.6.0", + "docx": "^9.5.1", "dotenv": "^16.4.5", "express": "^4.19.2", "file-saver": "^2.0.5", "jsonwebtoken": "^9.0.2", + "mammoth": "^1.11.0", "moment": "^2.30.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.1", diff --git a/routes/worships.js b/routes/worships.js index a101b3f..9841b17 100644 --- a/routes/worships.js +++ b/routes/worships.js @@ -1,13 +1,16 @@ const express = require('express'); const router = express.Router(); -const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions } = require('../controllers/worshipController'); +const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships } = require('../controllers/worshipController'); const authMiddleware = require('../middleware/authMiddleware'); router.get('/', getAllWorships); router.get('/options', getWorshipOptions); router.post('/', authMiddleware, createWorship); +router.post('/import', authMiddleware, uploadImportFile, importWorships); +router.post('/import/save', authMiddleware, saveImportedWorships); router.put('/:id', authMiddleware, updateWorship); router.delete('/:id', authMiddleware, deleteWorship); router.get('/filtered', getFilteredWorships); +router.get('/export', authMiddleware, exportWorships); module.exports = router; diff --git a/server.js b/server.js index d4eb563..3758578 100644 --- a/server.js +++ b/server.js @@ -2,8 +2,14 @@ const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const https = require('https'); +const http = require('http'); const fs = require('fs'); require('dotenv').config(); + +// Erhöhe maxHttpHeaderSize für Node.js (Standard ist 8KB, erhöhe auf 16KB) +if (http.maxHeaderSize !== undefined) { + http.maxHeaderSize = 16384; +} const sequelize = require('./config/database'); const authRouter = require('./routes/auth'); const eventTypesRouter = require('./routes/eventtypes'); @@ -31,9 +37,50 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || '') app.use(cors({ origin: (origin, callback) => { - if (!origin) return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server - if (allowedOrigins.length === 0) return callback(null, true); // Fallback: alles erlauben - if (allowedOrigins.includes(origin)) return callback(null, true); + if (!origin) { + return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server + } + + if (allowedOrigins.length === 0) { + return callback(null, true); // Fallback: alles erlauben + } + + // Prüfe exakte Übereinstimmung + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + + // Für Entwicklung: Erlaube localhost und torstens auf jedem Port + try { + const originUrl = new URL(origin); + const hostname = originUrl.hostname.toLowerCase(); + const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'; + const isTorstens = hostname === 'torstens' || hostname.includes('torstens'); + + if (isLocalhost || isTorstens) { + return callback(null, true); + } + } catch (e) { + // Falls URL-Parsing fehlschlägt, prüfe mit Regex + const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:\d+)?$/.test(origin); + const isTorstens = /^https?:\/\/torstens(:\d+)?/.test(origin); + + if (isLocalhost || isTorstens) { + return callback(null, true); + } + } + + // Prüfe auch ohne Port (für Flexibilität) + const originWithoutPort = origin.replace(/:\d+$/, ''); + const allowedWithoutPort = allowedOrigins.some(allowed => { + const allowedWithoutPort = allowed.replace(/:\d+$/, ''); + return originWithoutPort === allowedWithoutPort; + }); + + if (allowedWithoutPort) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS'), false); }, credentials: true, @@ -42,7 +89,14 @@ app.use(cors({ })); app.options('*', cors()); -app.use(bodyParser.json()); +// Erhöhe Header-Limits für große Requests +app.use(bodyParser.json({ limit: '50mb' })); +app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); + +// Erhöhe maxHttpHeaderSize (Node.js 18.3.0+) +if (process.versions.node.split('.')[0] >= 18) { + require('http').maxHeaderSize = 16384; // 16KB (Standard ist 8KB) +} app.use('/api/auth', authRouter); app.use('/api/event-types', eventTypesRouter); @@ -69,7 +123,7 @@ sequelize.sync().then(() => { /* https.createServer(options, app).listen(PORT, () => { console.log(`Server läuft auf Port ${PORT}`); });*/ - app.listen(PORT, () => { - console.log(`Server läuft auf Port ${PORT}`); + app.listen(PORT, '0.0.0.0', () => { + console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`); }); }); diff --git a/src/axios.js b/src/axios.js index ceee5df..ac050a3 100644 --- a/src/axios.js +++ b/src/axios.js @@ -24,8 +24,11 @@ axios.interceptors.response.use( }, error => { if (error.response && error.response.status === 401) { - store.dispatch('logout'); - router.push('/auth/login'); + store.dispatch('logout').then(() => { + if (router.currentRoute.value.path !== '/auth/login') { + router.replace('/auth/login'); + } + }); } return Promise.reject(error); } diff --git a/src/common/components/DialogComponent.vue b/src/common/components/DialogComponent.vue index 4974e84..18d9874 100644 --- a/src/common/components/DialogComponent.vue +++ b/src/common/components/DialogComponent.vue @@ -36,27 +36,53 @@ export default { \ No newline at end of file diff --git a/src/common/components/FooterComponent.vue b/src/common/components/FooterComponent.vue index eb24d34..dc7f53d 100644 --- a/src/common/components/FooterComponent.vue +++ b/src/common/components/FooterComponent.vue @@ -2,7 +2,7 @@