diff --git a/controllers/worshipController.js b/controllers/worshipController.js index e8651bd..9acf610 100644 --- a/controllers/worshipController.js +++ b/controllers/worshipController.js @@ -402,6 +402,7 @@ async function parseXlsxToRecords(buffer) { function buildLeaderMaps(leaders) { const codeToName = new Map(); const normalizedToName = new Map(); + const matchEntries = []; for (const leader of leaders || []) { if (!leader?.active) continue; const code = normalizeText(leader.code); @@ -409,6 +410,11 @@ function buildLeaderMaps(leaders) { if (code) { codeToName.set(code, name); normalizedToName.set(code.toLowerCase(), name); + matchEntries.push({ key: code.toLowerCase(), name }); + } + if (name) { + normalizedToName.set(name.toLowerCase(), name); + matchEntries.push({ key: name.toLowerCase(), name }); } const aliases = String(leader.aliases || '') .split(',') @@ -416,9 +422,55 @@ function buildLeaderMaps(leaders) { .filter(Boolean); for (const alias of aliases) { normalizedToName.set(alias.toLowerCase(), name); + matchEntries.push({ key: alias.toLowerCase(), name }); } } - return { codeToName, normalizedToName }; + // Längere Keys zuerst (z.B. ganzer Name vor Kürzel). + matchEntries.sort((a, b) => b.key.length - a.key.length); + return { codeToName, normalizedToName, matchEntries }; +} + +function escapeRegex(text) { + return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function resolveLeaderFromText(text, leaderMaps) { + const source = normalizeText(text); + const lowerSource = source.toLowerCase(); + let resolvedName = ''; + let cleaned = source; + + for (const entry of leaderMaps?.matchEntries || []) { + const key = entry.key; + if (!key) continue; + const boundaryPattern = new RegExp(`(^|\\s|[(/,-])${escapeRegex(key)}($|\\s|[)/,-])`, 'i'); + if (!boundaryPattern.test(lowerSource)) continue; + resolvedName = entry.name; + cleaned = cleaned.replace(new RegExp(`\\b${escapeRegex(key)}\\b`, 'ig'), ' ').replace(/\s+/g, ' ').trim(); + break; + } + + return { resolvedName, cleanedText: cleaned }; +} + +function buildWorshipTitleFromPlace(placeName, fallback = 'Gottesdienst') { + const raw = normalizeText(placeName); + if (!raw) return fallback; + let church = raw; + + // Typische Muster: "Evangelische Kirche Nieder-Eschbach" -> "Nieder-Eschbach" + const afterKirche = raw.match(/kirche\s+(.+)$/i); + if (afterKirche && afterKirche[1]) { + church = normalizeText(afterKirche[1]); + } else { + const parts = raw.split(/\s*(?:,|\/|->|\(|\)|-)\s*/).filter(Boolean); + if (parts.length > 0) { + church = normalizeText(parts[parts.length - 1]); + } + } + + if (!church) return fallback; + return `Gottesdienst in ${church}`; } function resolveEventPlaceIdFromHeader(eventPlaces, headerCell) { @@ -455,7 +507,7 @@ function splitNbrCellToSegments(cellText) { .filter(Boolean); } -function parseNbrSegment(segment, baseDateUtc, leaderNormalizedMap) { +function parseNbrSegment(segment, baseDateUtc, leaderMaps) { const raw = normalizeText(segment); if (!raw) return null; @@ -478,19 +530,10 @@ function parseNbrSegment(segment, baseDateUtc, leaderNormalizedMap) { text = normalizeText(text.replace(timeMatch[0], '')); } - // Officiant: pick the last token that matches a configured leader code/alias. - let officiant = ''; - const tokens = text.split(' ').map((t) => t.trim()).filter(Boolean); - for (let i = tokens.length - 1; i >= 0; i--) { - const token = tokens[i].replace(/[()]/g, ''); - const resolved = leaderNormalizedMap.get(token.toLowerCase()); - if (resolved) { - officiant = resolved; - tokens.splice(i, 1); - break; - } - } - const unparsedText = tokens.join(' ').trim(); + // Gestalter aus beliebigem Teilstring auflösen (Kürzel/Name/Alias). + const { resolvedName, cleanedText } = resolveLeaderFromText(text, leaderMaps); + const officiant = resolvedName || ''; + const unparsedText = cleanedText; return { dateUtc, @@ -1501,7 +1544,7 @@ async function parseNbrPlanningRecords(records) { const eventPlaces = await EventPlace.findAll(); const leaders = await WorshipLeader.findAll(); - const { normalizedToName } = buildLeaderMaps(leaders); + const leaderMaps = buildLeaderMaps(leaders); // existing worships for change detection const existingWorships = await Worship.findAll({ @@ -1574,6 +1617,8 @@ async function parseNbrPlanningRecords(records) { for (const group of groups) { const placeHeader = group.placeHeader; const eventPlaceId = resolveEventPlaceIdFromHeader(eventPlaces, placeHeader); + const eventPlace = eventPlaces.find((ep) => String(ep.id) === String(eventPlaceId)); + const titleFromPlace = buildWorshipTitleFromPlace(eventPlace?.name || placeHeader, 'Gottesdienst'); const worshipCell = row[group.idx]; const music = row[group.musicIdx]; @@ -1582,7 +1627,7 @@ async function parseNbrPlanningRecords(records) { if (segments.length === 0) continue; for (const seg of segments) { - const parsed = parseNbrSegment(seg, baseDateUtc, normalizedToName); + const parsed = parseNbrSegment(seg, baseDateUtc, leaderMaps); if (!parsed || !parsed.time) { continue; } @@ -1590,7 +1635,7 @@ async function parseNbrPlanningRecords(records) { date: parsed.dateUtc, dayName, time: parsed.time, - title: parsed.title, + title: titleFromPlace || parsed.title || 'Gottesdienst', // "Gottesdienst haltend" ist bei uns der "Gestalter" (organizer). organizer: parsed.officiant || '', collection: '',