Enhance event management and newsletter import functionality: Introduce methods for applying drafts from a bulk queue and streamline event form handling. Update event selection logic in the newsletter import management component to support encoding and decoding of bulk selections, improving user experience and data handling.

This commit is contained in:
Torsten Schulz (local)
2026-04-08 14:22:01 +02:00
parent 1be6fe0afc
commit fb4f5e42d0
3 changed files with 386 additions and 104 deletions

View File

@@ -41,6 +41,7 @@
<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">
@@ -60,7 +61,7 @@
<h5>Regelmäßige Termine</h5>
<ul>
<li v-for="(entry, idx) in newsletterImportResult.details.regelmaessigeTermine || []" :key="`r-${idx}`">
<label><input type="checkbox" :value="entry" v-model="selectedEventEntries" /></label>
<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>
@@ -71,7 +72,7 @@
<h5>Besondere Gottesdienste</h5>
<ul>
<li v-for="(entry, idx) in newsletterImportResult.details.besondereGottesdienste || []" :key="`b-${idx}`">
<label><input type="checkbox" :value="entry" v-model="selectedEventEntries" /></label>
<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>
@@ -82,7 +83,7 @@
<h5>Miriamtreff</h5>
<ul>
<li v-for="(entry, idx) in newsletterImportResult.details.miriamtreff || []" :key="`m-${idx}`">
<label><input type="checkbox" :value="entry" v-model="selectedEventEntries" /></label>
<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>
@@ -93,7 +94,7 @@
<h5>Kinder und Jugend</h5>
<ul>
<li v-for="(entry, idx) in newsletterImportResult.details.kinderUndJugend || []" :key="`k-${idx}`">
<label><input type="checkbox" :value="entry" v-model="selectedEventEntries" /></label>
<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>
@@ -104,12 +105,23 @@
<h5>Frauenfrühstück</h5>
<ul>
<li v-for="(entry, idx) in newsletterImportResult.details.frauenfruehstueck || []" :key="`f-${idx}`">
<label><input type="checkbox" :value="entry" v-model="selectedEventEntries" /></label>
<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">
@@ -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) {