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:
Torsten Schulz (local)
2026-04-08 13:04:38 +02:00
parent e6f87184b2
commit 1be6fe0afc
264 changed files with 3188 additions and 18 deletions

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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;

View 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>

View File

@@ -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);
},

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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();

View File

@@ -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,