diff --git a/controllers/worshipController.js b/controllers/worshipController.js index fa6b4aa..487546e 100644 --- a/controllers/worshipController.js +++ b/controllers/worshipController.js @@ -1323,7 +1323,13 @@ function normalizeText(input) { } function isHeading(line, heading) { - return normalizeText(line) === normalizeText(heading); + const normalizedLine = normalizeText(line).replace(/[:\-–]\s*$/g, ''); + const normalizedHeading = normalizeText(heading).replace(/[:\-–]\s*$/g, ''); + return ( + normalizedLine === normalizedHeading || + normalizedLine.startsWith(`${normalizedHeading} `) || + normalizedHeading.startsWith(`${normalizedLine} `) + ); } function getSectionByHeading(lines, startHeading, endHeadings = []) { @@ -1370,26 +1376,97 @@ function hasDateOrTime(line) { function buildDetailedItems(lines) { const result = []; const seen = new Set(); - for (let i = 0; i < lines.length; i++) { - const current = lines[i]; - if (!hasDateOrTime(current)) continue; + const isSectionLabel = (line) => /^(gottesdienste|veranstaltungen)\s*:?\s*$/i.test(String(line || '').trim()); + const youthAnchorPattern = /\b(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden|vorkonfirmandenkurs)\b/i; + const splitForYouthAnchors = (line) => { + const compact = String(line || '').replace(/\s+/g, ' ').trim(); + if (!compact) return []; + const withCuts = compact + .replace(/\s+(?=(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden|vorkonfirmandenkurs)\b)/gi, ' || ') + .replace(/\s+(?=(montag:|dienstag:|mittwoch:|donnerstag:|freitag:|samstag:|sonntag:))/gi, ' || '); + return withCuts.split('||').map((s) => s.trim()).filter(Boolean); + }; + const expandedLines = lines.flatMap(splitForYouthAnchors); + const isHardSectionBoundary = (line) => { + const n = normalizeText(line); + if (!n) return false; + return ( + n.startsWith('besondere gottesdienste und veranstaltungen') || + n.includes('nieder-erlenbach und harheim') || + n.startsWith('wunderbarer norden') || + n.startsWith('leben vor dem tod') || + /^seite?\s*\d+$/i.test(String(line || '').trim()) || + /^\d{1,3}$/.test(String(line || '').trim()) + ); + }; + const isEntryStart = (line) => { + if (!line || looksLikeHeading(line) || isSectionLabel(line) || isHardSectionBoundary(line)) return false; + if (/^(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag):/i.test(String(line).trim())) return true; + if (youthAnchorPattern.test(line)) return true; + const hasScheduleSignal = + hasDateOrTime(line) || + /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(line) || + /\b(von|um)\s+\d{1,2}[:.]\d{2}\b/i.test(line) || + /\b(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)s?\b/i.test(line) || + /\btermine[:\s]/i.test(line); + if (youthAnchorPattern.test(line) && hasScheduleSignal) return true; + // Klassische Startzeilen in den PDFs: + // "So., 08.02. 11.00 Uhr ..." oder "Mi., 18.02. 19.00 - 20.30 Uhr ..." + if (/^(so|mo|di|mi|do|fr|sa)\.,?\s+\d{1,2}\.\d{1,2}\./i.test(line)) return true; + // Fallback: enthalt Datum + Uhrzeit in derselben Zeile. + return /\b\d{1,2}\.\d{1,2}\./.test(line) && /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(line); + }; + const hasYouthScheduleSignal = (line) => + hasDateOrTime(line) || + /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(String(line || '')) || + /\b(von|um)\s+\d{1,2}[:.]\d{2}\b/i.test(String(line || '')) || + /\b(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)\b/i.test(String(line || '')) || + /\btermine[:\s]/i.test(String(line || '')); - const prev = i > 0 ? lines[i - 1] : ''; - const next = i + 1 < lines.length ? lines[i + 1] : ''; + for (let i = 0; i < expandedLines.length; i++) { + const current = expandedLines[i]; + if (!isEntryStart(current)) continue; - const parts = []; - if (prev && !hasDateOrTime(prev) && !looksLikeHeading(prev) && prev.length < 120) { - parts.push(prev); - } - parts.push(current); - if (next && !hasDateOrTime(next) && !looksLikeHeading(next) && next.length < 120) { + const parts = [current]; + for (let j = i + 1; j < expandedLines.length; j++) { + const next = expandedLines[j]; + if (!next) break; + if (looksLikeHeading(next) || isSectionLabel(next) || isHardSectionBoundary(next)) break; + if (isEntryStart(next)) { + const currentIsYouthAnchor = youthAnchorPattern.test(current); + const currentHasSchedule = hasYouthScheduleSignal(current); + const nextIsStandaloneScheduleLine = !youthAnchorPattern.test(next); + if (currentIsYouthAnchor && !currentHasSchedule && nextIsStandaloneScheduleLine) { + parts.push(next); + i = j; + continue; + } + break; + } + if (isNoiseLine(next)) break; parts.push(next); + i = j; // konsumierte Zeilen überspringen } - const text = parts.join(' | '); - const key = text.toLowerCase(); - if (!seen.has(key)) { + + const text = parts + .join(' ') + .replace(/\s*-\s+(?=[A-Za-zÄÖÜäöüß])/g, '') // harte Zeilentrennung "Gemein- desaal" heilen + .replace(/\s+\|/g, ' |') + .replace(/\s{2,}/g, ' ') + .trim(); + + // Falls eine neue Abschnittsüberschrift in derselben Zeile klebt, + // den Eintrag dort hart abschneiden. + const textCutAtInlineBoundary = text + .split(/\s+Besondere Gottesdienste und Veranstaltungen\b/i)[0] + .split(/\s+Wunderbarer Norden\b/i)[0] + .split(/\s+Leben vor dem Tod\b/i)[0] + .trim(); + + const key = textCutAtInlineBoundary.toLowerCase(); + if (!seen.has(key) && textCutAtInlineBoundary.length > 0) { seen.add(key); - result.push(text); + result.push(textCutAtInlineBoundary); } } return result; @@ -1451,12 +1528,41 @@ function extractRegularTermineDetails(lines) { /frauenfr[üu]hst[üu]ck/i, /kinder- und jugendb[üu]cherei/i, /wunderkiste/i, + /seniorenclub/i, + /seniorencaf[eé]/i, ]; + const splitRegularLineIntoSegments = (line) => { + const compact = String(line || '').replace(/\s+/g, ' ').trim(); + if (!compact) return []; + + const withAnchorCuts = compact + .replace(/\s+(?=(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden\s*[„"]|vorkonfirmandenkurs|m[aä]nnerpalaver|miriamtreff|frauenfr[üu]hst[üu]ck|wunderkiste)\b)/gi, ' || ') + .replace(/\s+(?=(montag:|dienstag:|mittwoch:|donnerstag:|freitag:|samstag:|sonntag:))/gi, ' || '); + + return withAnchorCuts + .split('||') + .map((s) => s.trim()) + .filter(Boolean); + }; + + const expandedLines = lines.flatMap(splitRegularLineIntoSegments); const details = []; const seen = new Set(); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (!anchors.some((r) => r.test(line))) continue; + const isAnchorLine = (line) => anchors.some((r) => r.test(line)); + const isSubHeadingLike = (line) => { + const t = normalizeText(line); + return ( + /^kinder und jugendliche$/.test(t) || + /^kinder und jugend$/.test(t) || + /^maenner und frauen$/.test(t) || + /^musik$/.test(t) || + /^senioren$/.test(t) + ); + }; + + for (let i = 0; i < expandedLines.length; i++) { + const line = expandedLines[i]; + if (!isAnchorLine(line)) continue; if (isNoiseLine(line)) continue; if (/start des neuen konfirmanden-jahrganges/i.test(line)) continue; if (/konfirmanden\s*\/\s*geburtstagsgr[üu][ßs]e/i.test(line)) continue; @@ -1464,9 +1570,11 @@ function extractRegularTermineDetails(lines) { const parts = [line]; let hasScheduleSignal = hasDateOrTime(line) || /termine[:\s]/i.test(line); - for (let j = i + 1; j < Math.min(lines.length, i + 3); j++) { - const next = lines[j]; - if (looksLikeHeading(next) || isNoiseLine(next)) break; + for (let j = i + 1; j < Math.min(expandedLines.length, i + 8); j++) { + const next = expandedLines[j]; + if (looksLikeHeading(next) || isSubHeadingLike(next) || isNoiseLine(next)) break; + // Sobald ein neuer Anker startet, endet der aktuelle Block. + if (isAnchorLine(next)) break; if (hasDateOrTime(next) || /termine[:\s]/i.test(next) || /\bmontag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag\b/i.test(next)) { parts.push(next); hasScheduleSignal = true; @@ -1606,6 +1714,71 @@ function extractWorshipBlocks(lines) { return [...new Set(blocks.map((b) => b.trim()).filter(Boolean))]; } +function buildEventSignature(line) { + const text = normalizeText(line); + const anchorPatterns = [ + /kinderkirche/, + /kigosabo|kindergottesdienst/, + /jungschar/, + /konfirmationsunterricht/, + /konfirmanden/, + /vorkonfirmandenkurs/, + /pfadfinder/, + /miriamtreff/, + /maennerpalaver/, + /frauenfruehstueck/, + /seniorenclub/, + /seniorencafe|senioren-cafe/, + ]; + const anchor = (anchorPatterns.find((r) => r.test(text)) || /./).source; + + const dates = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\b/g)] + .map((m) => `${String(m[1]).padStart(2, '0')}.${String(m[2]).padStart(2, '0')}`) + .sort() + .join(','); + + const range = text.match(/\b(\d{1,2})[:.](\d{2})\s*-\s*(\d{1,2})[:.](\d{2})\s*uhr\b/i); + const single = text.match(/\b(\d{1,2})[:.](\d{2})\s*uhr\b/i); + const startTime = range + ? `${String(range[1]).padStart(2, '0')}:${range[2]}` + : (single ? `${String(single[1]).padStart(2, '0')}:${single[2]}` : ''); + const endTime = range ? `${String(range[3]).padStart(2, '0')}:${range[4]}` : ''; + const openTermine = /termine\s*:\s*noch offen|noch offen/.test(text) ? 'open' : ''; + + const placePatterns = [ + /kita sternenzelt/, + /gemeindehaus bonames/, + /gemeindehaus nieder-eschbach/, + /gemeindehaus nieder-erlenbach/, + /crutzenhof kalbach/, + /bonames/, + /kalbach/, + /nieder-eschbach/, + /nieder-erlenbach/, + /harheim/, + ]; + const place = (placePatterns.find((r) => r.test(text)) || /./).source; + + return `${anchor}|${dates}|${startTime}|${endTime}|${openTermine}|${place}`; +} + +function dedupeBySignature(lines) { + const seen = new Set(); + const result = []; + for (const line of lines || []) { + const key = buildEventSignature(line); + if (seen.has(key)) continue; + seen.add(key); + result.push(line); + } + return result; +} + +function removeCrossSectionDuplicates(primaryLines, secondaryLines) { + const primaryKeys = new Set((primaryLines || []).map((line) => buildEventSignature(line))); + return (secondaryLines || []).filter((line) => !primaryKeys.has(buildEventSignature(line))); +} + exports.importNewsletterPdf = async (req, res) => { try { if (!req.file) { @@ -1638,7 +1811,8 @@ exports.importNewsletterPdf = async (req, res) => { 'Männer und Frauen', ['Musik', 'Kinder und Jugendliche'] ); - const regelmaessigLines = [...regelmaessigSection, ...maennerFrauenSection]; + const seniorenKeywordLines = extractLinesByKeyword(lines, /seniorenclub|senioren-?caf[eé]/i); + const regelmaessigLines = [...regelmaessigSection, ...maennerFrauenSection, ...seniorenKeywordLines]; const besondereLines = getSectionByHeading( lines, @@ -1662,29 +1836,39 @@ exports.importNewsletterPdf = async (req, res) => { const cleanedBesondere = filterNoise(besondereLines); const cleanedKinderJugend = filterNoise(kinderJugendLines); - const regelmaessigDetails = extractRegularTermineDetails(cleanedRegelmaessig); + const regelmaessigDetails = dedupeBySignature(extractRegularTermineDetails(cleanedRegelmaessig)); + const seniorenDetails = dedupeBySignature(extractRegularTermineDetails(filterNoise(seniorenKeywordLines))); + const mergedRegelmaessigDetails = dedupeBySignature([...regelmaessigDetails, ...seniorenDetails]); + const kinderUndJugendDetails = dedupeBySignature(buildDetailedItems(cleanedKinderJugend)); + const regelmaessigOhneSenioren = removeCrossSectionDuplicates(seniorenDetails, mergedRegelmaessigDetails); + const regelmaessigOhneJugend = removeCrossSectionDuplicates(kinderUndJugendDetails, regelmaessigOhneSenioren); + const miriamtreffDetails = dedupeBySignature(miriamtreffLines); + const regelmaessigBereinigt = removeCrossSectionDuplicates(miriamtreffDetails, regelmaessigOhneJugend); const parsedWorshipBlocks = extractWorshipBlocks(cleanedGottesdienste); const result = { gottesdienste: parsedWorshipBlocks, - regelmaessigeTermine: regelmaessigDetails, + regelmaessigeTermine: regelmaessigBereinigt, besondereGottesdienste: extractEventCandidates(cleanedBesondere), - miriamtreff: miriamtreffLines, - kinderUndJugend: extractEventCandidates(cleanedKinderJugend), + miriamtreff: miriamtreffDetails, + kinderUndJugend: kinderUndJugendDetails, frauenfruehstueck: frauenfruehstueckLines, + senioren: seniorenDetails, }; const details = { gottesdienste: parsedWorshipBlocks, - regelmaessigeTermine: regelmaessigDetails, + regelmaessigeTermine: regelmaessigBereinigt, besondereGottesdienste: buildDetailedItems(cleanedBesondere), - miriamtreff: miriamtreffLines, - kinderUndJugend: buildDetailedItems(cleanedKinderJugend), + miriamtreff: miriamtreffDetails, + kinderUndJugend: kinderUndJugendDetails, frauenfruehstueck: frauenfruehstueckLines, + senioren: seniorenDetails, sectionInfo: { gottesdiensteLines: gottesdiensteLines.length, regelmaessigLines: regelmaessigLines.length, + seniorenKeywordLines: seniorenKeywordLines.length, besondereLines: besondereLines.length, kinderJugendLines: kinderJugendLines.length, } diff --git a/src/content/admin/EventManagement.vue b/src/content/admin/EventManagement.vue index eab32b4..8f21834 100644 --- a/src/content/admin/EventManagement.vue +++ b/src/content/admin/EventManagement.vue @@ -128,6 +128,51 @@ export default { }, methods: { formatTime, + applyDraftToForm(draft) { + const resolvedEventPlace = + this.eventPlaces.find((place) => place.id === draft?.event_place_id) || + this.eventPlaces.find((place) => + draft?.event_place_name && + String(place?.name || '').toLowerCase().includes(String(draft.event_place_name).toLowerCase()) + ) || + null; + this.selectedEvent = { + name: draft?.name || '', + description: '', + date: draft?.date || '', + time: draft?.time || '', + endTime: draft?.endTime || '', + eventTypeId: draft?.eventTypeId ?? null, + event_place_id: resolvedEventPlace?.id ?? draft?.event_place_id ?? null, + eventPlace: resolvedEventPlace, + __newsletterDateMode: draft?.dateMode || null, + __newsletterBulkDates: draft?.bulkDates || '', + }; + this.showForm = true; + this.scrollToFormAndFocus(); + }, + getNextDraftFromBulkQueue() { + const rawQueue = localStorage.getItem('newsletter_import_event_bulk_queue'); + if (!rawQueue) return null; + try { + const queue = JSON.parse(rawQueue); + if (!Array.isArray(queue) || queue.length === 0) { + localStorage.removeItem('newsletter_import_event_bulk_queue'); + return null; + } + const [next, ...rest] = queue; + if (rest.length > 0) { + localStorage.setItem('newsletter_import_event_bulk_queue', JSON.stringify(rest)); + } else { + localStorage.removeItem('newsletter_import_event_bulk_queue'); + } + return next; + } catch (error) { + console.error('Fehler beim Lesen der Event-Bulk-Queue:', error); + localStorage.removeItem('newsletter_import_event_bulk_queue'); + return null; + } + }, async fetchData() { try { const [eventResponse, institutionResponse, eventPlaceResponse, contactPersonResponse, eventTypeResponse] = await Promise.all([ @@ -156,31 +201,18 @@ export default { this.$router.push('/admin/newsletter-import'); }, applyNewsletterDraft() { + const nextFromQueue = this.getNextDraftFromBulkQueue(); + if (nextFromQueue) { + this.applyDraftToForm(nextFromQueue); + return; + } + const raw = localStorage.getItem('newsletter_import_event_draft'); if (!raw) return; localStorage.removeItem('newsletter_import_event_draft'); try { const draft = JSON.parse(raw); - const resolvedEventPlace = - this.eventPlaces.find((place) => place.id === draft?.event_place_id) || - this.eventPlaces.find((place) => - draft?.event_place_name && - String(place?.name || '').toLowerCase().includes(String(draft.event_place_name).toLowerCase()) - ) || - null; - this.selectedEvent = { - name: draft?.name || '', - description: '', - date: draft?.date || '', - time: draft?.time || '', - eventTypeId: draft?.eventTypeId ?? null, - event_place_id: resolvedEventPlace?.id ?? draft?.event_place_id ?? null, - eventPlace: resolvedEventPlace, - __newsletterDateMode: draft?.dateMode || null, - __newsletterBulkDates: draft?.bulkDates || '', - }; - this.showForm = true; - this.scrollToFormAndFocus(); + this.applyDraftToForm(draft); } catch (error) { console.error('Fehler beim Übernehmen des Gemeindebrief-Entwurfs (Event):', error); } @@ -211,7 +243,12 @@ export default { } }, handleEventSaved() { - this.showForm = false; + const nextFromQueue = this.getNextDraftFromBulkQueue(); + if (nextFromQueue) { + this.applyDraftToForm(nextFromQueue); + } else { + this.showForm = false; + } this.fetchData(); }, handleEventCancelled() { diff --git a/src/content/admin/NewsletterImportManagement.vue b/src/content/admin/NewsletterImportManagement.vue index ecca76e..736d600 100644 --- a/src/content/admin/NewsletterImportManagement.vue +++ b/src/content/admin/NewsletterImportManagement.vue @@ -41,6 +41,7 @@
  • Miriamtreff: {{ newsletterImportResult.parsed?.miriamtreff?.length || 0 }}
  • Kinder und Jugend: {{ newsletterImportResult.parsed?.kinderUndJugend?.length || 0 }}
  • Frauenfrühstück: {{ newsletterImportResult.parsed?.frauenfruehstueck?.length || 0 }}
  • +
  • Senioren: {{ newsletterImportResult.parsed?.senioren?.length || 0 }}
  • @@ -60,7 +61,7 @@
    Regelmäßige Termine
    + +
    +
    Senioren
    + +
    @@ -146,40 +158,105 @@ export default { } }, methods: { + encodeBulkSelection(entry, category) { + return `${category}|||${entry}`; + }, + decodeBulkSelection(token) { + const value = String(token || ''); + const sep = '|||'; + const idx = value.indexOf(sep); + if (idx < 0) { + return { category: 'Regelmäßige Termine', entry: value }; + } + return { + category: value.slice(0, idx), + entry: value.slice(idx + sep.length), + }; + }, + buildEventDraft(entry, category) { + const parsed = this.parseDateAndTime(entry); + const mapping = this.inferEventMapping(entry, category); + const allDates = this.extractAllDates(entry); + const hasMultipleDates = allDates.length > 1; + const inferredTitle = this.extractTitle(entry); + const normalizedTitle = mapping.eventTypeId === 41 ? 'Kirche Kunterbunt' : inferredTitle; + return { + name: normalizedTitle, + description: '', + date: hasMultipleDates ? '' : parsed.isoDate, + time: parsed.time, + endTime: parsed.endTime, + dateMode: hasMultipleDates ? 'bulk' : 'date', + bulkDates: hasMultipleDates ? allDates.join(', ') : '', + category, + eventTypeId: mapping.eventTypeId, + event_place_id: mapping.event_place_id, + }; + }, + isValidDayMonth(day, month) { + const dd = Number(day); + const mm = Number(month); + return Number.isInteger(dd) && Number.isInteger(mm) && dd >= 1 && dd <= 31 && mm >= 1 && mm <= 12; + }, parseDateAndTime(rawText) { const text = String(rawText || ''); - const dateMatch = text.match(/\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b/); - const shortDateMatch = text.match(/\b(\d{1,2})\.(\d{1,2})\.\b/); - // Akzeptiert "19:30 Uhr", "19.30 Uhr" und auch "19:30"/"19.30" ohne "Uhr". - const timeMatch = text.match(/\b(\d{1,2})[:.](\d{2})(?:\s*uhr)?\b/i); + const longDateMatches = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b/g)]; + const shortDateMatches = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\.(?!\d)(?!\s*uhr\b)/gi)]; + // Zeit robust parsen: + // 1) Zeitspannen immer als Startzeit übernehmen: "17.00 - 20.00 Uhr" -> 17:00 + // 2) Einzelzeiten: "19:30" / "19:30 Uhr" / "19.30 Uhr" + const rangeMatch = + text.match(/\b(?:von\s+)?(\d{1,2})[.:](\d{2})\s*-\s*\d{1,2}[.:]\d{2}\s*uhr\b/i); + const rangeEndMatch = + text.match(/\b(?:von\s+)?\d{1,2}[.:]\d{2}\s*-\s*(\d{1,2})[.:](\d{2})\s*uhr\b/i); + const colonTimeMatch = text.match(/\b(?:um\s+|von\s+)?(\d{1,2}):(\d{2})(?:\s*uhr)?\b/i); + // Punkt-Zeiten nur mit klarem Zeitkontext akzeptieren, damit 09.02. (Datum) nicht als Uhrzeit gilt. + const dotTimeWithContextMatch = + text.match(/\bum\s+(\d{1,2})\.(\d{2})\s*uhr\b/i) || + text.match(/\bvon\s+(\d{1,2})\.(\d{2})\b/i) || + text.match(/\b(\d{1,2})\.(\d{2})\s*uhr\b/i); let isoDate = ''; let time = ''; - if (dateMatch) { - const dd = String(dateMatch[1]).padStart(2, '0'); - const mm = String(dateMatch[2]).padStart(2, '0'); - const yyyy = dateMatch[3]; + let endTime = ''; + + const firstLong = longDateMatches.find((m) => this.isValidDayMonth(m[1], m[2])); + if (firstLong) { + const dd = String(firstLong[1]).padStart(2, '0'); + const mm = String(firstLong[2]).padStart(2, '0'); + const yyyy = firstLong[3]; isoDate = `${yyyy}-${mm}-${dd}`; - } else if (shortDateMatch) { + } else { + const firstShort = shortDateMatches.find((m) => this.isValidDayMonth(m[1], m[2])); + if (firstShort) { const nowYear = new Date().getFullYear(); - const dd = String(shortDateMatch[1]).padStart(2, '0'); - const mm = String(shortDateMatch[2]).padStart(2, '0'); + const dd = String(firstShort[1]).padStart(2, '0'); + const mm = String(firstShort[2]).padStart(2, '0'); isoDate = `${nowYear}-${mm}-${dd}`; + } } + const timeMatch = rangeMatch || colonTimeMatch || dotTimeWithContextMatch; if (timeMatch) { const hh = String(timeMatch[1]).padStart(2, '0'); const min = String(timeMatch[2]).padStart(2, '0'); time = `${hh}:${min}`; } - return { isoDate, time }; + if (rangeEndMatch) { + const hhEnd = String(rangeEndMatch[1]).padStart(2, '0'); + const minEnd = String(rangeEndMatch[2]).padStart(2, '0'); + endTime = `${hhEnd}:${minEnd}`; + } + return { isoDate, time, endTime }; }, extractAllDates(rawText) { const text = String(rawText || ''); const nowYear = new Date().getFullYear(); const longMatches = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b/g)]; - const shortMatches = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\.(?!\d)/g)]; + const shortMatches = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\.(?!\d)(?!\s*uhr\b)/gi)]; - const normalized = longMatches.map((m) => { + const normalized = longMatches + .filter((m) => this.isValidDayMonth(m[1], m[2])) + .map((m) => { const dd = String(m[1]).padStart(2, '0'); const mm = String(m[2]).padStart(2, '0'); const yyyy = m[3]; @@ -187,6 +264,7 @@ export default { }); shortMatches.forEach((m) => { + if (!this.isValidDayMonth(m[1], m[2])) return; const dd = String(m[1]).padStart(2, '0'); const mm = String(m[2]).padStart(2, '0'); const fallback = `${dd}.${mm}.${nowYear}`; @@ -266,8 +344,17 @@ export default { const text = String(rawText || '').trim(); const parts = text.split('|').map((p) => p.trim()).filter(Boolean); if (parts.length === 0) return 'Import aus Gemeindebrief'; + // Bevorzugt den eigentlichen Titelteil aus dem Segment mit Datum/Uhrzeit. + const firstPart = parts[0] || ''; + const fromFirstPart = firstPart + .replace(/^(?:so|mo|di|mi|do|fr|sa)\.,?\s*/i, '') + .replace(/^\d{1,2}\.\d{1,2}\.(?:\d{2,4})?\s*/i, '') + .replace(/^\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\s*/i, '') + .replace(/^\d{1,2}\.\d{1,2}\.\s*/i, '') + .trim(); const withoutDate = parts.find((p) => !/\d{1,2}\.\d{1,2}\./.test(p) && !/\d{1,2}[:.]\d{2}\s*uhr/i.test(p)); - const cleaned = (withoutDate || parts[0] || '') + const baseTitle = fromFirstPart || withoutDate || parts[0] || ''; + const cleaned = baseTitle .replace(/\[\[FLAG_NEIGHBOR_INVITATION\]\]/gi, '') .replace(/\[\[FLAG_SELF_INFORMATION\]\]/gi, '') .replace(/bitte informieren sie sich auch auf den internetseiten.*$/i, '') @@ -277,44 +364,18 @@ export default { return cleaned || 'Import aus Gemeindebrief'; }, transferToEventForm(entry, category) { - const parsed = this.parseDateAndTime(entry); - const mapping = this.inferEventMapping(entry, category); - const allDates = this.extractAllDates(entry); - const hasMultipleDates = allDates.length > 1; - const draft = { - name: this.extractTitle(entry), - description: '', - date: hasMultipleDates ? '' : parsed.isoDate, - time: parsed.time, - dateMode: hasMultipleDates ? 'bulk' : 'single', - bulkDates: hasMultipleDates ? allDates.join(', ') : '', - category, - eventTypeId: mapping.eventTypeId, - event_place_id: mapping.event_place_id, - }; + const draft = this.buildEventDraft(entry, category); localStorage.setItem('newsletter_import_event_draft', JSON.stringify(draft)); this.$router.push('/admin/events'); }, transferSelectedToEventBulk() { if (this.selectedEventEntries.length === 0) return; - const bulkDates = this.selectedEventEntries - .map((entry) => { - const parsed = this.parseDateAndTime(entry); - if (!parsed.isoDate) return ''; - const [yyyy, mm, dd] = parsed.isoDate.split('-'); - return `${dd}.${mm}.${yyyy}`; - }) - .filter(Boolean) - .join(', '); - const first = this.selectedEventEntries[0] || ''; - const draft = { - name: this.extractTitle(first), - description: '', - dateMode: 'bulk', - bulkDates, - ...this.inferEventMapping(first, 'Regelmäßige Termine'), - }; - localStorage.setItem('newsletter_import_event_draft', JSON.stringify(draft)); + const queue = this.selectedEventEntries + .map((token) => this.decodeBulkSelection(token)) + .filter((item) => item.entry) + .map((item) => this.buildEventDraft(item.entry, item.category)); + if (queue.length === 0) return; + localStorage.setItem('newsletter_import_event_bulk_queue', JSON.stringify(queue)); this.$router.push('/admin/events'); }, handleNewsletterPdfSelect(event) {