durch Zeilenumbrüche (aber nicht doppelte)
+ cellText = cellText.replace(/<\/p>/gi, '\n');
+ cellText = cellText.replace(/
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 @@