435 lines
19 KiB
Vue
435 lines
19 KiB
Vue
<template>
|
|
<div class="newsletter-import">
|
|
<h2>Gemeindebrief-Import (PDF)</h2>
|
|
<div class="import-section">
|
|
<p class="hint">
|
|
Diese Seite parst den Gemeindebrief und zeigt eine Vorschau nach Kategorien.
|
|
Die finale Übernahme bauen wir auf Basis der Parser-Ergebnisse schrittweise aus.
|
|
</p>
|
|
<div class="import-content">
|
|
<label for="newsletter-import-file">Datei auswählen (.pdf):</label>
|
|
<input
|
|
id="newsletter-import-file"
|
|
ref="newsletterFileInput"
|
|
type="file"
|
|
accept=".pdf"
|
|
@change="handleNewsletterPdfSelect"
|
|
/>
|
|
<div v-if="newsletterPdfFile" class="selected-file">
|
|
Ausgewählte PDF: {{ newsletterPdfFile.name }}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="submit-import-button"
|
|
:disabled="!newsletterPdfFile || isNewsletterImporting"
|
|
@click="importNewsletterPdf"
|
|
>
|
|
{{ isNewsletterImporting ? 'Parse PDF...' : 'PDF parsen' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="newsletterImportResult" class="newsletter-preview">
|
|
<h3>Vorschau</h3>
|
|
<p class="newsletter-meta">
|
|
Seiten: {{ newsletterImportResult.meta?.pages || '-' }}, Zeilen: {{ newsletterImportResult.meta?.lineCount || '-' }}
|
|
</p>
|
|
<ul class="newsletter-counts">
|
|
<li>Gottesdienste: derzeit deaktiviert</li>
|
|
<li>Regelmäßige Termine: {{ newsletterImportResult.parsed?.regelmaessigeTermine?.length || 0 }}</li>
|
|
<li>Besondere Gottesdienste: {{ newsletterImportResult.parsed?.besondereGottesdienste?.length || 0 }}</li>
|
|
<li>Miriamtreff: {{ newsletterImportResult.parsed?.miriamtreff?.length || 0 }}</li>
|
|
<li>Kinder und Jugend: {{ newsletterImportResult.parsed?.kinderUndJugend?.length || 0 }}</li>
|
|
<li>Frauenfrühstück: {{ newsletterImportResult.parsed?.frauenfruehstueck?.length || 0 }}</li>
|
|
<li>Senioren: {{ newsletterImportResult.parsed?.senioren?.length || 0 }}</li>
|
|
</ul>
|
|
|
|
<div v-if="newsletterImportResult.details" class="details">
|
|
<h4>Gefundene Einträge (Details)</h4>
|
|
<div class="bulk-actions">
|
|
<button type="button" class="submit-import-button" :disabled="selectedEventEntries.length === 0" @click="transferSelectedToEventBulk">
|
|
Auswahl ({{ selectedEventEntries.length }}) als Event-Bulk übernehmen
|
|
</button>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Gottesdienste</h5>
|
|
<p>Der Gottesdienst-Import ist derzeit deaktiviert und wird aktuell nicht zur Übernahme angeboten.</p>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Regelmäßige Termine</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.regelmaessigeTermine || []" :key="`r-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Regelmäßige Termine')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Regelmäßige Termine')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Besondere Gottesdienste</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.besondereGottesdienste || []" :key="`b-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Besondere Gottesdienste')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Besondere Gottesdienste')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Miriamtreff</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.miriamtreff || []" :key="`m-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Miriamtreff')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Miriamtreff')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Kinder und Jugend</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.kinderUndJugend || []" :key="`k-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Kinder und Jugend')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Kinder und Jugend')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Frauenfrühstück</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.frauenfruehstueck || []" :key="`f-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Frauenfrühstück')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Frauenfrühstück')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="detail-group">
|
|
<h5>Senioren</h5>
|
|
<ul>
|
|
<li v-for="(entry, idx) in newsletterImportResult.details.senioren || []" :key="`s-${idx}`">
|
|
<label><input type="checkbox" :value="encodeBulkSelection(entry, 'Senioren')" v-model="selectedEventEntries" /></label>
|
|
<span>{{ entry }}</span>
|
|
<button type="button" class="transfer-button" @click="transferToEventForm(entry, 'Senioren')">Als Event übernehmen</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="newsletterImportResult.questions?.length" class="questions">
|
|
<h4>Offene Fragen</h4>
|
|
<ul>
|
|
<li v-for="(question, idx) in newsletterImportResult.questions" :key="idx">{{ question }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import axios from '../../axios';
|
|
|
|
export default {
|
|
name: 'NewsletterImportManagement',
|
|
data() {
|
|
return {
|
|
newsletterPdfFile: null,
|
|
isNewsletterImporting: false,
|
|
newsletterImportResult: null,
|
|
selectedEventEntries: [],
|
|
};
|
|
},
|
|
created() {
|
|
const cached = localStorage.getItem('newsletter_import_last_result');
|
|
if (cached) {
|
|
try {
|
|
this.newsletterImportResult = JSON.parse(cached);
|
|
} catch (error) {
|
|
console.error('Konnte zwischengespeicherte Vorschau nicht lesen:', error);
|
|
}
|
|
}
|
|
},
|
|
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 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 = '';
|
|
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 {
|
|
const firstShort = shortDateMatches.find((m) => this.isValidDayMonth(m[1], m[2]));
|
|
if (firstShort) {
|
|
const nowYear = new Date().getFullYear();
|
|
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}`;
|
|
}
|
|
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)(?!\s*uhr\b)/gi)];
|
|
|
|
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];
|
|
return `${dd}.${mm}.${yyyy}`;
|
|
});
|
|
|
|
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}`;
|
|
if (!normalized.some((d) => d.startsWith(`${dd}.${mm}.`))) {
|
|
normalized.push(fallback);
|
|
}
|
|
});
|
|
|
|
return [...new Set(normalized)];
|
|
},
|
|
inferEventMapping(rawText, category) {
|
|
const text = String(rawText || '');
|
|
const normalized = text.toLowerCase();
|
|
let eventTypeId = 38; // Sonstiges
|
|
if (/miriamtreff/.test(normalized)) eventTypeId = 16;
|
|
else if (/frauenfr[üu]hst[üu]ck/.test(normalized)) eventTypeId = 25;
|
|
else if (/m[aä]nnerpalaver/.test(normalized)) eventTypeId = 15;
|
|
else if (/kinderkirche/.test(normalized)) eventTypeId = 4;
|
|
else if (/kinder kirche/.test(normalized)) eventTypeId = 4;
|
|
else if (/kigosabo|kindergottesdienst/.test(normalized)) eventTypeId = 5;
|
|
else if (/jungschar/.test(normalized)) eventTypeId = 6;
|
|
else if (/konfirmationsunterricht/.test(normalized)) eventTypeId = 3;
|
|
else if (/vorkonfirmandenkurs|vorkonfis/.test(normalized)) eventTypeId = 39;
|
|
else if (/vocal ensemble/.test(normalized)) eventTypeId = 17;
|
|
else if (/konzert/.test(normalized)) eventTypeId = 40;
|
|
else if (/vortrag/.test(normalized)) eventTypeId = 42;
|
|
else if (/weihnachtsmarkt/.test(normalized)) eventTypeId = 43;
|
|
else if (/kirchekunterbunt|kirche kunterbunt/.test(normalized)) eventTypeId = 41;
|
|
else if (category === 'Besondere Gottesdienste') eventTypeId = 1;
|
|
|
|
let eventPlaceId = null;
|
|
let eventPlaceName = '';
|
|
if (/gemeindehaus bonames/.test(normalized)) {
|
|
eventPlaceId = 7;
|
|
eventPlaceName = 'Gemeindehaus Bonames';
|
|
} else if (/gemeindehaus kalbach|crutzenhof|kalbach/.test(normalized)) {
|
|
eventPlaceId = 2;
|
|
eventPlaceName = 'Crutzenhof';
|
|
} else if (/gemeindehaus harheim|gemeindesaal harheim/.test(normalized)) {
|
|
eventPlaceId = 27;
|
|
eventPlaceName = 'Gemeindesaal Harheim';
|
|
} else if (/gemeindehaus nieder-?erlenbach|nieder-erlenbach/.test(normalized)) {
|
|
eventPlaceId = 13;
|
|
eventPlaceName = 'Nieder-Erlenbach';
|
|
} else if (/gemeindehaus nieder-?eschbach|nieder-eschbach/.test(normalized)) {
|
|
eventPlaceId = 14;
|
|
eventPlaceName = 'Nieder-Eschbach';
|
|
} else if (/gemeindehaus am b[üu]gel|am b[üu]gel/.test(normalized)) {
|
|
eventPlaceId = 12;
|
|
eventPlaceName = 'Am Bügel';
|
|
} else if (/gemeindehaus/.test(normalized) && /bonames/.test(normalized)) {
|
|
eventPlaceId = 7;
|
|
eventPlaceName = 'Gemeindehaus Bonames';
|
|
} else if (/jugendkeller bonames/.test(normalized)) {
|
|
eventPlaceId = 8;
|
|
eventPlaceName = 'Jugendkeller Bonames';
|
|
} else if (/kita sternenzelt/.test(normalized)) {
|
|
eventPlaceId = 6;
|
|
eventPlaceName = 'Kita Sternenzelt';
|
|
} else if (/bonames/.test(normalized)) {
|
|
eventPlaceId = 1;
|
|
eventPlaceName = 'Bonames';
|
|
} else if (/harheim/.test(normalized)) {
|
|
eventPlaceId = 15;
|
|
eventPlaceName = 'Harheim';
|
|
} else if (/im sauern/.test(normalized)) {
|
|
eventPlaceId = 28;
|
|
eventPlaceName = 'Im Sauern';
|
|
} else if (/wunderkiste/.test(normalized)) {
|
|
eventPlaceId = 16;
|
|
eventPlaceName = 'Miriams Wunderkiste';
|
|
}
|
|
|
|
return { eventTypeId, event_place_id: eventPlaceId, event_place_name: eventPlaceName };
|
|
},
|
|
extractTitle(rawText) {
|
|
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 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, '')
|
|
.replace(/einladung zum gottesdienst im nachbarschaftsraum/gi, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
return cleaned || 'Import aus Gemeindebrief';
|
|
},
|
|
transferToEventForm(entry, category) {
|
|
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 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) {
|
|
const file = event.target.files?.[0];
|
|
if (!file) {
|
|
this.newsletterPdfFile = null;
|
|
return;
|
|
}
|
|
if (!file.name.toLowerCase().endsWith('.pdf')) {
|
|
alert('Bitte eine PDF-Datei auswählen.');
|
|
event.target.value = '';
|
|
this.newsletterPdfFile = null;
|
|
return;
|
|
}
|
|
this.newsletterPdfFile = file;
|
|
},
|
|
async importNewsletterPdf() {
|
|
if (!this.newsletterPdfFile) return;
|
|
this.isNewsletterImporting = true;
|
|
const formData = new FormData();
|
|
formData.append('file', this.newsletterPdfFile);
|
|
try {
|
|
const response = await axios.post('/worships/import/newsletter-pdf', formData);
|
|
this.newsletterImportResult = response.data;
|
|
this.selectedEventEntries = [];
|
|
localStorage.setItem('newsletter_import_last_result', JSON.stringify(response.data));
|
|
} catch (error) {
|
|
const msg = error.response?.data?.message || 'Fehler beim Parsen der PDF-Datei.';
|
|
alert(`Fehler: ${msg}`);
|
|
} finally {
|
|
this.isNewsletterImporting = false;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.newsletter-import { max-width: 900px; margin: 0 auto; }
|
|
.hint { color: #555; margin-bottom: 10px; }
|
|
.import-section { border: 2px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; }
|
|
.import-content { display: flex; flex-direction: column; gap: 10px; }
|
|
.submit-import-button { width: fit-content; }
|
|
.selected-file { padding: 8px; background: #e8f5e9; border: 1px solid #4caf50; border-radius: 4px; }
|
|
.newsletter-preview { margin-top: 16px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; background: #fff; }
|
|
.newsletter-meta { color: #666; margin: 0 0 8px; }
|
|
.newsletter-counts { padding-left: 20px; }
|
|
.questions { margin-top: 10px; }
|
|
.details { margin-top: 12px; }
|
|
.detail-group { margin-bottom: 10px; }
|
|
.detail-group h5 { margin: 0 0 4px; }
|
|
.detail-group li { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 6px; }
|
|
.transfer-button { white-space: nowrap; }
|
|
.bulk-actions { margin-bottom: 10px; }
|
|
</style>
|
|
|