Integrate PDF parsing functionality: Add 'pdf-parse' dependency to package.json and package-lock.json. Update worshipController to include logic for handling PDF imports, enhancing the event management process. Refactor routing to support new newsletter import features and improve event form handling for better user experience.
This commit is contained in:
@@ -101,6 +101,19 @@ button,
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Schutz vor List-Styles in vue-multiselect-Dropdowns */
|
||||
.multiselect__content-wrapper ul,
|
||||
.multiselect__content {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.multiselect__content li,
|
||||
.multiselect__element {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -168,16 +168,25 @@ export default {
|
||||
watch: {
|
||||
event(newVal) {
|
||||
this.eventData = { ...newVal };
|
||||
this.determineDateMode();
|
||||
if (newVal && typeof newVal.__newsletterDateMode === 'string') {
|
||||
this.dateMode = newVal.__newsletterDateMode;
|
||||
}
|
||||
if (newVal && typeof newVal.__newsletterBulkDates === 'string') {
|
||||
this.bulkDates = newVal.__newsletterBulkDates;
|
||||
}
|
||||
this.selectedEventType = this.eventTypes.find(type => type.id === newVal.eventTypeId) || null;
|
||||
this.selectedInstitution = newVal.institution || null;
|
||||
this.selectedEventPlace = newVal.eventPlace || null;
|
||||
this.selectedEventPlace =
|
||||
newVal.eventPlace ||
|
||||
this.localEventPlaces.find((place) => place.id === newVal.event_place_id || place.id === newVal.eventPlaceId) ||
|
||||
null;
|
||||
this.selectedContactPersons = newVal.contactPersons || [];
|
||||
this.onHomepage = newVal.alsoOnHomepage == 1 ? true : false;
|
||||
this.assignedImage = newVal.relatedImage || null;
|
||||
if (this.assignedImage) {
|
||||
this.fetchImageFilename();
|
||||
}
|
||||
this.determineDateMode();
|
||||
},
|
||||
institutions(newVal) {
|
||||
this.localInstitutions = [...newVal];
|
||||
@@ -194,10 +203,19 @@ export default {
|
||||
const eventTypeResponse = await axios.get('/event-types');
|
||||
this.eventTypes = eventTypeResponse.data;
|
||||
this.selectedEventType = this.eventTypes.find(type => type.id === this.event.eventTypeId) || null;
|
||||
if (!this.selectedEventType && this.event?.eventTypeId) {
|
||||
this.selectedEventType = this.eventTypes.find(type => type.id === this.event.eventTypeId) || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event types:', error);
|
||||
}
|
||||
this.determineDateMode();
|
||||
if (this.event && typeof this.event.__newsletterDateMode === 'string') {
|
||||
this.dateMode = this.event.__newsletterDateMode;
|
||||
}
|
||||
if (this.event && typeof this.event.__newsletterBulkDates === 'string') {
|
||||
this.bulkDates = this.event.__newsletterBulkDates;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getImagePath() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="event-management">
|
||||
<h2>Veranstaltungen</h2>
|
||||
<button v-if="hasNewsletterPreview" @click="goBackToNewsletterImport">Zurück zum Gemeindebrief-Import</button>
|
||||
<button @click="createEvent">Neue Veranstaltung</button>
|
||||
<EventForm v-if="showForm"
|
||||
ref="eventForm"
|
||||
@@ -75,6 +76,7 @@ export default {
|
||||
showForm: false,
|
||||
searchQuery: '',
|
||||
showPastEvents: false,
|
||||
hasNewsletterPreview: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -121,6 +123,8 @@ export default {
|
||||
},
|
||||
async created() {
|
||||
await this.fetchData();
|
||||
this.hasNewsletterPreview = !!localStorage.getItem('newsletter_import_last_result');
|
||||
this.applyNewsletterDraft();
|
||||
},
|
||||
methods: {
|
||||
formatTime,
|
||||
@@ -148,6 +152,39 @@ export default {
|
||||
this.showForm = true;
|
||||
this.scrollToFormAndFocus();
|
||||
},
|
||||
goBackToNewsletterImport() {
|
||||
this.$router.push('/admin/newsletter-import');
|
||||
},
|
||||
applyNewsletterDraft() {
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Übernehmen des Gemeindebrief-Entwurfs (Event):', error);
|
||||
}
|
||||
},
|
||||
editEvent(event) {
|
||||
this.selectedEvent = { ...event };
|
||||
this.showForm = true;
|
||||
|
||||
373
src/content/admin/NewsletterImportManagement.vue
Normal file
373
src/content/admin/NewsletterImportManagement.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<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>
|
||||
</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="entry" 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="entry" 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="entry" 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="entry" 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="entry" 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>
|
||||
|
||||
<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: {
|
||||
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);
|
||||
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];
|
||||
isoDate = `${yyyy}-${mm}-${dd}`;
|
||||
} else if (shortDateMatch) {
|
||||
const nowYear = new Date().getFullYear();
|
||||
const dd = String(shortDateMatch[1]).padStart(2, '0');
|
||||
const mm = String(shortDateMatch[2]).padStart(2, '0');
|
||||
isoDate = `${nowYear}-${mm}-${dd}`;
|
||||
}
|
||||
if (timeMatch) {
|
||||
const hh = String(timeMatch[1]).padStart(2, '0');
|
||||
const min = String(timeMatch[2]).padStart(2, '0');
|
||||
time = `${hh}:${min}`;
|
||||
}
|
||||
return { isoDate, time };
|
||||
},
|
||||
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 normalized = longMatches.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) => {
|
||||
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';
|
||||
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] || '')
|
||||
.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 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,
|
||||
};
|
||||
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));
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="worship-management">
|
||||
<h2>Gottesdienst Verwaltung</h2>
|
||||
<button v-if="hasNewsletterPreview" type="button" @click="goBackToNewsletterImport">
|
||||
Zurück zum Gemeindebrief-Import
|
||||
</button>
|
||||
<div class="action-buttons">
|
||||
<button type="button" @click="toggleImportSection" class="import-button">
|
||||
{{ showImportSection ? 'Import ausblenden' : 'Import' }}
|
||||
@@ -27,6 +30,7 @@
|
||||
{{ isImporting ? 'Importiere...' : 'Importieren' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="showExportSection" class="export-section">
|
||||
<h3>Gottesdienste exportieren</h3>
|
||||
@@ -323,6 +327,7 @@ export default {
|
||||
showImportDialog: false,
|
||||
importedWorships: [],
|
||||
importErrors: [],
|
||||
hasNewsletterPreview: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -375,8 +380,76 @@ export default {
|
||||
await this.fetchWorships();
|
||||
await this.fetchWorshipOptions();
|
||||
await this.fetchLiturgicalDays();
|
||||
this.hasNewsletterPreview = !!localStorage.getItem('newsletter_import_last_result');
|
||||
this.applyNewsletterDraft();
|
||||
},
|
||||
methods: {
|
||||
goBackToNewsletterImport() {
|
||||
this.$router.push('/admin/newsletter-import');
|
||||
},
|
||||
applyNewsletterDraft() {
|
||||
const bulkRaw = localStorage.getItem('newsletter_import_worship_bulk_draft');
|
||||
if (bulkRaw) {
|
||||
localStorage.removeItem('newsletter_import_worship_bulk_draft');
|
||||
try {
|
||||
const bulk = JSON.parse(bulkRaw);
|
||||
if (Array.isArray(bulk) && bulk.length > 0) {
|
||||
this.importedWorships = bulk.map((w) => {
|
||||
const eventPlace = this.eventPlaces.find((ep) => ep.id === w.eventPlaceId);
|
||||
return {
|
||||
date: w.date || '',
|
||||
dayName: w.dayName || '',
|
||||
time: w.time || '',
|
||||
title: w.title || '',
|
||||
organizer: w.organizer || '',
|
||||
collection: w.collection || '',
|
||||
sacristanService: w.sacristanService || '',
|
||||
organPlaying: w.organPlaying || '',
|
||||
approved: !!w.approved,
|
||||
eventPlace: eventPlace || null,
|
||||
eventPlaceId: w.eventPlaceId || null,
|
||||
_changedFields: [],
|
||||
_oldValues: {},
|
||||
_isUpdate: false,
|
||||
_isNew: true,
|
||||
};
|
||||
});
|
||||
this.importErrors = [];
|
||||
this.showImportDialog = true;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Übernehmen des Gemeindebrief-Bulk-Entwurfs (Gottesdienst):', error);
|
||||
}
|
||||
}
|
||||
|
||||
const raw = localStorage.getItem('newsletter_import_worship_draft');
|
||||
if (!raw) return;
|
||||
localStorage.removeItem('newsletter_import_worship_draft');
|
||||
try {
|
||||
const draft = JSON.parse(raw);
|
||||
if (draft?.title) this.worshipData.title = draft.title;
|
||||
if (draft?.date) this.worshipData.date = draft.date;
|
||||
if (draft?.time) this.worshipData.time = draft.time;
|
||||
if (draft?.organizer) {
|
||||
this.selectedOrganizers = draft.organizer.split(',').map((name) => ({ name: name.trim() })).filter((x) => x.name);
|
||||
}
|
||||
if (draft?.collection) this.worshipData.collection = draft.collection;
|
||||
if (typeof draft?.selfInformation === 'boolean') this.worshipData.selfInformation = draft.selfInformation;
|
||||
if (typeof draft?.neighborInvitation === 'boolean') this.worshipData.neighborInvitation = draft.neighborInvitation;
|
||||
if (draft?.eventPlaceId) {
|
||||
this.selectedEventPlace = this.eventPlaces.find((ep) => ep.id === draft.eventPlaceId) || null;
|
||||
}
|
||||
if (draft?.sourceText && !this.worshipData.introLine) {
|
||||
this.worshipData.introLine = draft.sourceText;
|
||||
}
|
||||
if (this.worshipData.date) {
|
||||
this.updateDayNameFromDate();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Übernehmen des Gemeindebrief-Entwurfs (Gottesdienst):', error);
|
||||
}
|
||||
},
|
||||
isFieldChanged(worship, fieldName) {
|
||||
return worship._changedFields && worship._changedFields.includes(fieldName);
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import axios from '../../axios';
|
||||
import DialogComponent from '../../common/components/DialogComponent.vue';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
@@ -53,9 +53,8 @@ export default {
|
||||
password: this.password
|
||||
});
|
||||
const token = response.data.token;
|
||||
const data = response.data;
|
||||
localStorage.setItem('token', token);
|
||||
this.login(data.user);
|
||||
const user = response.data.user;
|
||||
this.login({ user, token });
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
this.$router.push('/admin/index');
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppComponent from './AppComponent.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import axios from './axios';
|
||||
import 'vue-multiselect/dist/vue-multiselect.css';
|
||||
import './assets/css/design-tokens.css';
|
||||
import './assets/css/content-cards.css';
|
||||
import './assets/css/editor.css';
|
||||
|
||||
@@ -3,6 +3,8 @@ import store from './store';
|
||||
|
||||
const ROUTE_NAMES = {
|
||||
ADMIN_EDIT_PAGES: 'admin-edit-pages',
|
||||
ADMIN_FILE_UPLOAD: 'admin-file-upload',
|
||||
ADMIN_NEWSLETTER_IMPORT: 'admin-newsletter-import',
|
||||
REGISTER: 'register',
|
||||
FORGOT_PASSWORD: 'forgot-password',
|
||||
RESET_PASSWORD: 'reset-password',
|
||||
@@ -188,6 +190,35 @@ function addEditPagesRoute() {
|
||||
});
|
||||
}
|
||||
|
||||
function addFileUploadRoute() {
|
||||
if (router.hasRoute(ROUTE_NAMES.ADMIN_FILE_UPLOAD)) {
|
||||
router.removeRoute(ROUTE_NAMES.ADMIN_FILE_UPLOAD);
|
||||
}
|
||||
router.addRoute({
|
||||
path: '/admin/fileupload',
|
||||
meta: { requiresAuth: true },
|
||||
components: {
|
||||
default: loadComponent('admin/UploadFileManagement'),
|
||||
rightColumn: loadComponent('ImageContent')
|
||||
},
|
||||
name: ROUTE_NAMES.ADMIN_FILE_UPLOAD
|
||||
});
|
||||
}
|
||||
|
||||
function addNewsletterImportRoute() {
|
||||
if (router.hasRoute(ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT)) {
|
||||
router.removeRoute(ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT);
|
||||
}
|
||||
router.addRoute({
|
||||
path: '/admin/newsletter-import',
|
||||
components: {
|
||||
default: loadComponent('admin/NewsletterImportManagement'),
|
||||
rightColumn: loadComponent('ImageContent')
|
||||
},
|
||||
name: ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT
|
||||
});
|
||||
}
|
||||
|
||||
function addRegisterRoute() {
|
||||
if (router.hasRoute(ROUTE_NAMES.REGISTER)) {
|
||||
router.removeRoute(ROUTE_NAMES.REGISTER);
|
||||
@@ -302,6 +333,8 @@ function ensureNotFoundRoute() {
|
||||
|
||||
function ensureCoreRoutes() {
|
||||
if (!router.hasRoute(ROUTE_NAMES.ADMIN_EDIT_PAGES)) addEditPagesRoute();
|
||||
if (!router.hasRoute(ROUTE_NAMES.ADMIN_FILE_UPLOAD)) addFileUploadRoute();
|
||||
if (!router.hasRoute(ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT)) addNewsletterImportRoute();
|
||||
if (!router.hasRoute(ROUTE_NAMES.REGISTER)) addRegisterRoute();
|
||||
if (!router.hasRoute(ROUTE_NAMES.FORGOT_PASSWORD)) addForgotPasswordRoute();
|
||||
if (!router.hasRoute(ROUTE_NAMES.RESET_PASSWORD)) addResetPasswordRoute();
|
||||
@@ -313,6 +346,8 @@ function ensureCoreRoutes() {
|
||||
}
|
||||
|
||||
addEditPagesRoute();
|
||||
addFileUploadRoute();
|
||||
addNewsletterImportRoute();
|
||||
addRegisterRoute();
|
||||
addForgotPasswordRoute();
|
||||
addResetPasswordRoute();
|
||||
|
||||
@@ -2,6 +2,18 @@ import { createStore } from 'vuex';
|
||||
import axios from '../axios';
|
||||
import router from '../router';
|
||||
|
||||
const rawStoredToken = localStorage.getItem('token');
|
||||
const storedToken =
|
||||
rawStoredToken && rawStoredToken !== 'undefined' && rawStoredToken !== 'null'
|
||||
? rawStoredToken
|
||||
: '';
|
||||
|
||||
if (!storedToken) {
|
||||
// Altlasten aus früheren Login-Bugs bereinigen
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
|
||||
let user = [];
|
||||
try {
|
||||
user = JSON.parse(localStorage.getItem('user')) || null;
|
||||
@@ -11,9 +23,9 @@ try {
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
isLoggedIn: !!localStorage.getItem('isLoggedIn'),
|
||||
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true' && !!storedToken,
|
||||
user: user,
|
||||
token: localStorage.getItem('token') || '',
|
||||
token: storedToken,
|
||||
menuData: [],
|
||||
/** gesetzt, wenn /menu-data fehlschlägt (z. B. DB weg) — nicht mit „Seite nicht gefunden“ verwechseln */
|
||||
menuLoadError: null,
|
||||
|
||||
Reference in New Issue
Block a user