1529 lines
58 KiB
JavaScript
1529 lines
58 KiB
JavaScript
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 = /<table[^>]*>([\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 = /<tr[^>]*>([\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 = /<t[dh][^>]*>([\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 <br> und <br/> durch Zeilenumbrüche
|
||
cellText = cellText.replace(/<br\s*\/?>/gi, '\n');
|
||
|
||
// Ersetze </p> und <p> durch Zeilenumbrüche (aber nicht doppelte)
|
||
cellText = cellText.replace(/<\/p>/gi, '\n');
|
||
cellText = cellText.replace(/<p[^>]*>/gi, '');
|
||
|
||
// Ersetze </div> und <div> durch Zeilenumbrüche
|
||
cellText = cellText.replace(/<\/div>/gi, '\n');
|
||
cellText = cellText.replace(/<div[^>]*>/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 <Ort>"
|
||
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 });
|
||
}
|
||
};
|