1854 lines
56 KiB
Vue
1854 lines
56 KiB
Vue
<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' }}
|
||
</button>
|
||
<button type="button" @click="toggleExportSection" class="export-button">
|
||
{{ showExportSection ? 'Export ausblenden' : 'Export' }}
|
||
</button>
|
||
</div>
|
||
<div v-if="showImportSection" class="import-section">
|
||
<h3>Gottesdienste importieren</h3>
|
||
<div class="import-content">
|
||
<label for="import-file">Datei auswählen (.doc, .docx, .xlsx):</label>
|
||
<input
|
||
type="file"
|
||
id="import-file"
|
||
ref="fileInput"
|
||
@change="handleFileSelect"
|
||
accept=".doc,.docx,.xlsx"
|
||
/>
|
||
<div v-if="selectedFile" class="selected-file">
|
||
Ausgewählte Datei: {{ selectedFile.name }}
|
||
</div>
|
||
<button type="button" @click="importWorships" :disabled="!selectedFile || isImporting" class="submit-import-button">
|
||
{{ isImporting ? 'Importiere...' : 'Importieren' }}
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
<div v-if="showExportSection" class="export-section">
|
||
<h3>Gottesdienste exportieren</h3>
|
||
<div class="export-content">
|
||
<label for="export-date-from">Von Datum:</label>
|
||
<input
|
||
type="date"
|
||
id="export-date-from"
|
||
v-model="exportDateFrom"
|
||
/>
|
||
<label for="export-date-to">Bis Datum:</label>
|
||
<input
|
||
type="date"
|
||
id="export-date-to"
|
||
v-model="exportDateTo"
|
||
/>
|
||
<label for="export-format">Export-Format:</label>
|
||
<select id="export-format" v-model="exportFormat">
|
||
<option value="editing">Für Bearbeitung</option>
|
||
<option value="newsletter">Für Gemeindebrief</option>
|
||
</select>
|
||
<button type="button" @click="exportWorships" :disabled="!exportDateFrom || !exportDateTo || isExporting" class="submit-export-button">
|
||
{{ isExporting ? 'Exportiere...' : 'Exportieren' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dialog zur Bearbeitung der importierten Gottesdienste -->
|
||
<div v-if="showImportDialog" class="import-dialog-overlay" @click.self="closeImportDialog">
|
||
<div class="import-dialog-content">
|
||
<div class="import-dialog-header">
|
||
<h3>Importierte Gottesdienste bearbeiten</h3>
|
||
<button class="close-button" @click="closeImportDialog">×</button>
|
||
</div>
|
||
<div class="import-dialog-body">
|
||
<div v-if="importErrors && importErrors.length > 0" class="import-errors">
|
||
<h4>Fehler beim Parsen:</h4>
|
||
<ul>
|
||
<li v-for="(error, index) in importErrors" :key="index">{{ error }}</li>
|
||
</ul>
|
||
</div>
|
||
<div class="import-filter-bar">
|
||
<p class="import-range">
|
||
Importierter Zeitraum: {{ importedDateRangeLabel }}
|
||
</p>
|
||
<div class="import-filter-fields">
|
||
<label>
|
||
Von:
|
||
<input type="date" v-model="importFilterFrom" />
|
||
</label>
|
||
<label>
|
||
Bis:
|
||
<input type="date" v-model="importFilterTo" />
|
||
</label>
|
||
<button type="button" class="clear-button" @click="clearImportDateFilter">
|
||
Filter zurücksetzen
|
||
</button>
|
||
</div>
|
||
<p class="import-range">
|
||
Angezeigt: {{ filteredImportedWorships.length }} von {{ importedWorships.length }}
|
||
</p>
|
||
</div>
|
||
<div class="imported-worships-list">
|
||
<div v-for="(worship, index) in filteredImportedWorships" :key="worship._tempId || index" class="imported-worship-item">
|
||
<h4>
|
||
Gottesdienst {{ index + 1 }}
|
||
<span v-if="worship._isNew" class="new-badge">NEU</span>
|
||
<span v-else-if="worship._isUpdate" class="update-badge">ÄNDERUNG</span>
|
||
</h4>
|
||
<div class="worship-edit-fields">
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'date') }">
|
||
<label>
|
||
Datum:
|
||
<span v-if="isFieldChanged(worship, 'date')" class="old-value">(alt: {{ getOldValue(worship, 'date') }})</span>
|
||
</label>
|
||
<input type="date" v-model="worship.date" />
|
||
</div>
|
||
<div class="field-group">
|
||
<label>Tag-Name:</label>
|
||
<input type="text" v-model="worship.dayName" />
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'time') }">
|
||
<label>
|
||
Uhrzeit:
|
||
<span v-if="isFieldChanged(worship, 'time')" class="old-value">(alt: {{ getOldValue(worship, 'time') }})</span>
|
||
</label>
|
||
<input type="time" v-model="worship.time" step="60" />
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'eventPlaceId') }">
|
||
<label>
|
||
Ort:
|
||
<span v-if="isFieldChanged(worship, 'eventPlaceId')" class="old-value">(alt: {{ getOldValue(worship, 'eventPlaceName') || getOldValue(worship, 'eventPlaceId') }})</span>
|
||
</label>
|
||
<multiselect
|
||
v-model="worship.eventPlace"
|
||
:options="eventPlaces"
|
||
label="name"
|
||
track-by="id"
|
||
placeholder="Veranstaltungsort wählen"
|
||
@update:modelValue="(value) => { if (value) worship.eventPlaceId = value.id; }"
|
||
></multiselect>
|
||
</div>
|
||
<div v-if="worship._sourceText" class="field-group source-field">
|
||
<label>Original aus Excel:</label>
|
||
<textarea :value="worship._sourceText" readonly rows="2"></textarea>
|
||
<small v-if="worship._unparsedText">Nicht automatisch übernommen: {{ worship._unparsedText }}</small>
|
||
</div>
|
||
<div v-if="worship._hasDayPlaceConflict" class="field-group conflict-field">
|
||
<label>Es gibt bereits Gottesdienst(e) an diesem Tag und Ort:</label>
|
||
<ul>
|
||
<li v-for="existing in worship._conflictingWorships" :key="existing.id">
|
||
{{ formatConflictWorship(existing) }}
|
||
</li>
|
||
</ul>
|
||
<label class="radio-label">
|
||
<input type="radio" value="keepExisting" v-model="worship._importChoice" />
|
||
Bestehenden Eintrag behalten, importierten Eintrag nicht speichern
|
||
</label>
|
||
<label class="radio-label">
|
||
<input type="radio" value="replaceExisting" v-model="worship._importChoice" />
|
||
Importierten Eintrag speichern und bestehende(n) ersetzen
|
||
</label>
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'title') }">
|
||
<label>
|
||
Titel:
|
||
<span v-if="isFieldChanged(worship, 'title')" class="old-value">(alt: {{ getOldValue(worship, 'title') }})</span>
|
||
</label>
|
||
<input type="text" v-model="worship.title" />
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'organizer') }">
|
||
<label>
|
||
Gestalter:
|
||
<span v-if="isFieldChanged(worship, 'organizer')" class="old-value">(alt: {{ getOldValue(worship, 'organizer') }})</span>
|
||
</label>
|
||
<multiselect
|
||
v-model="worship._selectedOrganizer"
|
||
:options="worshipLeaderOptions"
|
||
label="displayName"
|
||
track-by="displayName"
|
||
:multiple="false"
|
||
:taggable="true"
|
||
:allow-empty="true"
|
||
placeholder="Gestalter wählen oder eingeben"
|
||
@tag="(newTag) => addImportOrganizerTag(worship, newTag)"
|
||
@update:modelValue="(value) => setImportOrganizer(worship, value)"
|
||
/>
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'collection') }">
|
||
<label>
|
||
Kollekte:
|
||
<span v-if="isFieldChanged(worship, 'collection')" class="old-value">(alt: {{ getOldValue(worship, 'collection') }})</span>
|
||
</label>
|
||
<input type="text" v-model="worship.collection" />
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'sacristanService') }">
|
||
<label>
|
||
Dienst:
|
||
<span v-if="isFieldChanged(worship, 'sacristanService')" class="old-value">(alt: {{ getOldValue(worship, 'sacristanService') }})</span>
|
||
</label>
|
||
<input type="text" v-model="worship.sacristanService" />
|
||
</div>
|
||
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'organPlaying') }">
|
||
<label>
|
||
Orgelspiel:
|
||
<span v-if="isFieldChanged(worship, 'organPlaying')" class="old-value">(alt: {{ getOldValue(worship, 'organPlaying') }})</span>
|
||
</label>
|
||
<input type="text" v-model="worship.organPlaying" />
|
||
</div>
|
||
<div class="field-group">
|
||
<label>
|
||
<input type="checkbox" v-model="worship.approved" />
|
||
Freigegeben
|
||
</label>
|
||
</div>
|
||
<div class="field-group">
|
||
<label>
|
||
<input type="checkbox" v-model="worship.neighborInvitation" @change="handleImportNeighborInvitationChange(worship)" />
|
||
Einladung zum Nachbarschaftsraum
|
||
</label>
|
||
</div>
|
||
<div class="field-group">
|
||
<label>
|
||
<input type="checkbox" v-model="worship.selfInformation" />
|
||
Selbstinformation
|
||
</label>
|
||
</div>
|
||
<button type="button" @click="removeWorship(worship)" class="remove-button">Entfernen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="import-dialog-footer">
|
||
<button type="button" @click="closeImportDialog" class="cancel-button">Abbrechen</button>
|
||
<button type="button" @click="saveImportedWorships" :disabled="importedWorships.length === 0 || isImporting" class="save-button">
|
||
{{ isImporting ? 'Speichere...' : 'Speichern' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="liturgical-loader">
|
||
<select v-model="selectedYear" class="year-select">
|
||
<option v-for="year in availableYears" :key="year" :value="year">{{ year }}</option>
|
||
</select>
|
||
<button type="button" @click="loadLiturgicalYear" class="load-year-button" :disabled="isLoading">
|
||
{{ isLoading ? 'Lade...' : 'Kirchenjahr laden' }}
|
||
</button>
|
||
</div>
|
||
<form @submit.prevent="saveWorship">
|
||
<label for="eventPlaceId">Veranstaltungsort:</label>
|
||
<multiselect v-model="selectedEventPlace" :options="eventPlaces" label="name" track-by="id"
|
||
placeholder="Veranstaltungsort wählen"></multiselect>
|
||
|
||
<label for="dayName">Name des Tags:</label>
|
||
<div class="liturgical-day-section">
|
||
<multiselect v-model="selectedDayName" :options="dayNameOptions" :multiple="false" :taggable="true"
|
||
@tag="addDayNameTag" placeholder="Tag-Name wählen oder eingeben" label="name" track-by="name">
|
||
</multiselect>
|
||
</div>
|
||
|
||
<label for="date">Datum:</label>
|
||
<input type="date" id="date" v-model="worshipData.date" required @change="updateDayNameFromDate">
|
||
|
||
<label for="time">Uhrzeit:</label>
|
||
<input type="time" id="time" v-model="worshipData.time" required>
|
||
|
||
<label for="title">Titel:</label>
|
||
<input type="text" id="title" v-model="worshipData.title">
|
||
|
||
<label for="organizer">Gestalter:</label>
|
||
<multiselect v-model="selectedOrganizers" :options="organizerOptions" :multiple="true" :taggable="true"
|
||
@tag="addOrganizerTag" placeholder="Gestalter wählen oder neu eingeben" label="name" track-by="name">
|
||
</multiselect>
|
||
|
||
<label for="sacristanService">Küsterdienst:</label>
|
||
<multiselect v-model="selectedSacristans" :options="sacristanOptions" :multiple="true" :taggable="true"
|
||
@tag="addSacristanTag" placeholder="Küsterdienst wählen oder neu eingeben" label="name" track-by="name">
|
||
</multiselect>
|
||
|
||
<label for="collection">Kollekte:</label>
|
||
<input type="text" id="collection" v-model="worshipData.collection">
|
||
|
||
<label for="organPlaying">Orgelspiel / Organist:</label>
|
||
<input type="text" id="organPlaying" v-model="worshipData.organPlaying">
|
||
|
||
<label for="address">Adresse:</label>
|
||
<input type="text" id="address" v-model="worshipData.address">
|
||
|
||
<label for="selfInformation">Selbstinformation:</label>
|
||
<input type="checkbox" id="selfInformation" v-model="worshipData.selfInformation">
|
||
|
||
<label for="highlightTime">Uhrzeit hervorheben:</label>
|
||
<input type="checkbox" id="highlightTime" v-model="worshipData.highlightTime">
|
||
|
||
<label for="neighborInvitation">Einladung zum Nachbarschaftsraum:</label>
|
||
<input type="checkbox" id="neighborInvitation" v-model="worshipData.neighborInvitation">
|
||
|
||
<label for="approved">Freigegeben:</label>
|
||
<input type="checkbox" id="approved" v-model="worshipData.approved">
|
||
|
||
<label for="introLine">Einleitungszeile:</label>
|
||
<input type="text" id="introLine" v-model="worshipData.introLine">
|
||
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" @click="resetForm">Neuer Gottesdienst</button>
|
||
</form>
|
||
|
||
<div class="filter-section">
|
||
<input v-model="searchDate" type="date" class="search-input" placeholder="Nach Datum suchen..." />
|
||
<label class="checkbox-label">
|
||
<input v-model="showPastWorships" type="checkbox" />
|
||
Vergangene Gottesdienste anzeigen
|
||
</label>
|
||
<button v-if="searchDate" @click="clearSearch" type="button" class="clear-button">
|
||
Suche zurücksetzen
|
||
</button>
|
||
</div>
|
||
|
||
<ul>
|
||
<li
|
||
v-for="worship in filteredWorships"
|
||
:key="worship.id"
|
||
:class="[
|
||
'worship-list-item',
|
||
{ 'old-items': dateIsLowerCurrentDate(worship.date) },
|
||
{ 'not-approved': !worship.approved }
|
||
]"
|
||
>
|
||
<span>
|
||
{{ worship.title }} - {{ formatDate(worship.date) }}, {{ formatTime(worship.time) }}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
class="approve-toggle-button"
|
||
@click="toggleApproved(worship)"
|
||
>
|
||
{{ worship.approved ? 'Freigabe zurücknehmen' : 'Freigeben' }}
|
||
</button>
|
||
<button type="button" @click="editWorship(worship)">Bearbeiten</button>
|
||
<button type="button" @click="deleteWorship(worship.id)">Löschen</button>
|
||
<div class="tooltip">{{ getEventPlaceName(worship.eventPlaceId) }}</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import axios from 'axios';
|
||
import Multiselect from 'vue-multiselect';
|
||
import { formatTime, formatDate } from '../../utils/strings'; // Importieren der Methode
|
||
|
||
export default {
|
||
name: 'WorshipManagement',
|
||
components: { Multiselect },
|
||
data() {
|
||
const currentYear = new Date().getFullYear();
|
||
return {
|
||
worships: [],
|
||
eventPlaces: [],
|
||
organizerOptions: [],
|
||
worshipLeaderOptions: [],
|
||
sacristanOptions: [],
|
||
selectedOrganizers: [],
|
||
selectedSacristans: [],
|
||
dayNameOptions: [],
|
||
liturgicalDays: [],
|
||
selectedDayName: null,
|
||
selectedYear: currentYear,
|
||
availableYears: [currentYear, currentYear + 1, currentYear + 2],
|
||
isLoading: false,
|
||
isUpdatingFromDate: false,
|
||
worshipData: {
|
||
eventPlaceId: null,
|
||
date: '',
|
||
time: '',
|
||
title: '',
|
||
organizer: '',
|
||
collection: '',
|
||
organPlaying: '',
|
||
address: '',
|
||
selfInformation: false,
|
||
highlightTime: false,
|
||
neighborInvitation: false,
|
||
introLine: '',
|
||
sacristanService: '',
|
||
website: '',
|
||
dayName: '',
|
||
approved: false,
|
||
},
|
||
selectedEventPlace: null,
|
||
editMode: false,
|
||
editId: null,
|
||
searchDate: '',
|
||
showPastWorships: false,
|
||
showImportSection: false,
|
||
selectedFile: null,
|
||
isImporting: false,
|
||
showExportSection: false,
|
||
exportDateFrom: '',
|
||
exportDateTo: '',
|
||
exportFormat: 'editing',
|
||
isExporting: false,
|
||
showImportDialog: false,
|
||
importedWorships: [],
|
||
importErrors: [],
|
||
hasNewsletterPreview: false,
|
||
importFilterFrom: '',
|
||
importFilterTo: '',
|
||
};
|
||
},
|
||
computed: {
|
||
filteredWorships() {
|
||
let filtered = this.worships;
|
||
|
||
// Filter vergangene Gottesdienste aus
|
||
if (!this.showPastWorships) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
filtered = filtered.filter(worship => {
|
||
if (worship.date) {
|
||
const worshipDate = new Date(worship.date);
|
||
worshipDate.setHours(0, 0, 0, 0);
|
||
return worshipDate >= today;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
// Datumsfilter anwenden
|
||
if (this.searchDate) {
|
||
const searchDateObj = new Date(this.searchDate);
|
||
searchDateObj.setHours(0, 0, 0, 0);
|
||
|
||
filtered = filtered.filter(worship => {
|
||
if (worship.date) {
|
||
const worshipDate = new Date(worship.date);
|
||
worshipDate.setHours(0, 0, 0, 0);
|
||
return worshipDate.getTime() === searchDateObj.getTime();
|
||
}
|
||
return false;
|
||
});
|
||
}
|
||
|
||
return filtered;
|
||
},
|
||
filteredImportedWorships() {
|
||
return this.importedWorships.filter((w) => {
|
||
const dateValue = this.normalizeDateOnly(w.date);
|
||
if (!dateValue) return false;
|
||
|
||
// 1) Vergangene Termine grundsätzlich ausblenden.
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const worshipDate = new Date(dateValue);
|
||
worshipDate.setHours(0, 0, 0, 0);
|
||
if (worshipDate < today) return false;
|
||
|
||
// 2) Zusätzlicher Von/Bis-Filter.
|
||
if (this.importFilterFrom && dateValue < this.importFilterFrom) return false;
|
||
if (this.importFilterTo && dateValue > this.importFilterTo) return false;
|
||
return true;
|
||
});
|
||
},
|
||
importedDateRangeLabel() {
|
||
const dates = this.importedWorships
|
||
.map((w) => this.normalizeDateOnly(w.date))
|
||
.filter(Boolean)
|
||
.sort();
|
||
if (dates.length === 0) return '-';
|
||
const from = dates[0];
|
||
const to = dates[dates.length - 1];
|
||
return `${this.formatDate(from)} bis ${this.formatDate(to)}`;
|
||
}
|
||
},
|
||
watch: {
|
||
selectedDayName(newValue, oldValue) {
|
||
// Nur wenn sich der Wert wirklich ändert und nicht beim initialen Laden
|
||
if (newValue && newValue !== oldValue && !this.isUpdatingFromDate) {
|
||
this.updateDateFromDayName();
|
||
}
|
||
}
|
||
},
|
||
async created() {
|
||
await this.fetchEventPlaces();
|
||
await this.fetchWorships();
|
||
await this.fetchWorshipOptions();
|
||
await this.fetchWorshipLeaders();
|
||
await this.fetchLiturgicalDays();
|
||
this.hasNewsletterPreview = !!localStorage.getItem('newsletter_import_last_result');
|
||
this.applyNewsletterDraft();
|
||
},
|
||
methods: {
|
||
normalizeDateOnly(value) {
|
||
if (!value) return '';
|
||
if (typeof value === 'string') return value.split('T')[0];
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return '';
|
||
const y = date.getFullYear();
|
||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
return `${y}-${m}-${d}`;
|
||
},
|
||
clearImportDateFilter() {
|
||
this.importFilterFrom = '';
|
||
this.importFilterTo = '';
|
||
},
|
||
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 || '',
|
||
_selectedOrganizer: this.resolveImportOrganizerOption(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,
|
||
_tempId: `bulk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
};
|
||
});
|
||
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);
|
||
},
|
||
getOldValue(worship, fieldName) {
|
||
if (worship._oldValues && worship._oldValues[fieldName]) {
|
||
return worship._oldValues[fieldName];
|
||
}
|
||
return '';
|
||
},
|
||
formatConflictWorship(worship) {
|
||
const time = worship.time ? `${worship.time} Uhr` : 'ohne Uhrzeit';
|
||
const title = worship.title || 'Gottesdienst';
|
||
const organizer = worship.organizer ? `, ${worship.organizer}` : '';
|
||
return `${time} – ${title}${organizer}`;
|
||
},
|
||
formatTime,
|
||
formatDate,
|
||
async fetchWorships() {
|
||
try {
|
||
const response = await axios.get('/worships');
|
||
this.worships = response.data;
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der Gottesdienste:', error);
|
||
}
|
||
},
|
||
async fetchEventPlaces() {
|
||
try {
|
||
const response = await axios.get('/event-places');
|
||
this.eventPlaces = response.data;
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der Veranstaltungsorte:', error);
|
||
}
|
||
},
|
||
async fetchWorshipOptions() {
|
||
try {
|
||
const response = await axios.get('/worships/options');
|
||
this.organizerOptions = response.data.organizers.map(org => ({ name: org }));
|
||
this.sacristanOptions = response.data.sacristanServices.map(sac => ({ name: sac }));
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der Worship-Optionen:', error);
|
||
}
|
||
},
|
||
async fetchWorshipLeaders() {
|
||
try {
|
||
const response = await axios.get('/worship-leaders');
|
||
this.worshipLeaderOptions = (response.data || []).map((leader) => {
|
||
const name = String(leader.name || '').trim();
|
||
const code = String(leader.code || '').trim();
|
||
return {
|
||
id: leader.id,
|
||
name,
|
||
code,
|
||
displayName: code ? `${name} (${code})` : name,
|
||
};
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der Worship-Leads:', error);
|
||
this.worshipLeaderOptions = [];
|
||
}
|
||
},
|
||
resolveImportOrganizerOption(organizerValue) {
|
||
const value = String(organizerValue || '').trim();
|
||
if (!value) return null;
|
||
const lower = value.toLowerCase();
|
||
const option = this.worshipLeaderOptions.find(
|
||
(o) => o.name.toLowerCase() === lower || (o.code && o.code.toLowerCase() === lower)
|
||
);
|
||
if (option) return option;
|
||
return { id: null, name: value, code: '', displayName: value };
|
||
},
|
||
setImportOrganizer(worship, selected) {
|
||
if (!selected) {
|
||
worship._selectedOrganizer = null;
|
||
worship.organizer = '';
|
||
return;
|
||
}
|
||
worship._selectedOrganizer = selected;
|
||
worship.organizer = selected.name || selected.displayName || '';
|
||
},
|
||
addImportOrganizerTag(worship, newTag) {
|
||
const value = String(newTag || '').trim();
|
||
if (!value) return;
|
||
const option = { id: null, name: value, code: '', displayName: value };
|
||
this.worshipLeaderOptions.push(option);
|
||
this.setImportOrganizer(worship, option);
|
||
},
|
||
async fetchLiturgicalDays() {
|
||
try {
|
||
const response = await axios.get('/liturgical-days');
|
||
this.liturgicalDays = response.data;
|
||
|
||
// Nur zukünftige Tage anzeigen
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const futureDays = response.data.filter(day => {
|
||
const dayDate = new Date(day.date);
|
||
dayDate.setHours(0, 0, 0, 0);
|
||
return dayDate >= today;
|
||
});
|
||
|
||
// Sortiere nach Datum
|
||
futureDays.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||
|
||
// Erstelle Optionen mit Datum und Name: "30.11.2025 - 1. Advent"
|
||
this.dayNameOptions = futureDays.map(day => {
|
||
const date = new Date(day.date);
|
||
const formattedDate = date.toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
return {
|
||
name: `${formattedDate} - ${day.dayName}`,
|
||
dayName: day.dayName,
|
||
date: day.date
|
||
};
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der liturgischen Tage:', error);
|
||
}
|
||
},
|
||
async loadLiturgicalYear() {
|
||
if (!this.selectedYear) {
|
||
alert('Bitte wählen Sie ein Jahr aus');
|
||
return;
|
||
}
|
||
|
||
this.isLoading = true;
|
||
try {
|
||
const response = await axios.post('/liturgical-days/load-year', {
|
||
year: this.selectedYear
|
||
});
|
||
alert(response.data.message);
|
||
await this.fetchLiturgicalDays();
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden des Kirchenjahres:', error);
|
||
if (error.response && error.response.data && error.response.data.message) {
|
||
alert('Fehler: ' + error.response.data.message);
|
||
} else {
|
||
alert('Fehler beim Laden des Kirchenjahres');
|
||
}
|
||
} finally {
|
||
this.isLoading = false;
|
||
}
|
||
},
|
||
updateDayNameFromDate() {
|
||
if (!this.worshipData.date) {
|
||
return;
|
||
}
|
||
|
||
// Setze Flag, um Endlosschleife zu vermeiden
|
||
this.isUpdatingFromDate = true;
|
||
|
||
// Normalisiere das Datum (HTML input gibt YYYY-MM-DD zurück)
|
||
const selectedDate = this.worshipData.date;
|
||
|
||
// Finde liturgischen Tag für das gewählte Datum
|
||
const liturgicalDay = this.liturgicalDays.find(day => {
|
||
// Vergleiche nur das Datum (ignoriere mögliche Zeitstempel)
|
||
const dayDate = typeof day.date === 'string' ? day.date : day.date.split('T')[0];
|
||
return dayDate === selectedDate;
|
||
});
|
||
|
||
if (liturgicalDay) {
|
||
// Finde die passende Option mit formatiertem Datum
|
||
const option = this.dayNameOptions.find(opt => opt.date === selectedDate);
|
||
if (option) {
|
||
this.selectedDayName = option;
|
||
}
|
||
this.worshipData.dayName = liturgicalDay.dayName;
|
||
console.log('Liturgischer Tag gefunden:', liturgicalDay.dayName);
|
||
} else {
|
||
console.log('Kein liturgischer Tag gefunden für:', selectedDate);
|
||
}
|
||
|
||
// Reset Flag nach kurzer Verzögerung
|
||
this.$nextTick(() => {
|
||
this.isUpdatingFromDate = false;
|
||
});
|
||
},
|
||
updateDateFromDayName() {
|
||
if (!this.selectedDayName || !this.selectedDayName.date) {
|
||
return;
|
||
}
|
||
|
||
// Das Datum ist bereits in der Option enthalten
|
||
this.worshipData.date = this.selectedDayName.date;
|
||
this.worshipData.dayName = this.selectedDayName.dayName;
|
||
console.log('Datum gesetzt auf:', this.selectedDayName.date, 'für', this.selectedDayName.dayName);
|
||
},
|
||
async saveWorship() {
|
||
try {
|
||
if (!this.worshipData.date || !this.worshipData.time || !this.selectedEventPlace) {
|
||
alert('Bitte Datum, Uhrzeit und Veranstaltungsort ausfüllen.');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
...this.worshipData,
|
||
eventPlaceId: this.selectedEventPlace ? this.selectedEventPlace.id : null,
|
||
title: this.worshipData.title && this.worshipData.title.trim() ? this.worshipData.title.trim() : 'Gottesdienst',
|
||
organizer: this.selectedOrganizers.map(org => org.name).join(', '),
|
||
sacristanService: this.selectedSacristans.map(sac => sac.name).join(', '),
|
||
dayName: this.selectedDayName ? this.selectedDayName.dayName : '',
|
||
approved: !!this.worshipData.approved,
|
||
};
|
||
|
||
if (this.editMode) {
|
||
await axios.put(`/worships/${this.editId}`, payload);
|
||
} else {
|
||
await axios.post('/worships', payload);
|
||
}
|
||
|
||
this.resetForm();
|
||
await this.fetchWorships();
|
||
await this.fetchWorshipOptions();
|
||
} catch (error) {
|
||
console.error('Fehler beim Speichern des Gottesdienstes:', error);
|
||
}
|
||
},
|
||
async toggleApproved(worship) {
|
||
try {
|
||
const newApproved = !worship.approved;
|
||
await axios.put(`/worships/${worship.id}`, { approved: newApproved });
|
||
worship.approved = newApproved;
|
||
} catch (error) {
|
||
console.error('Fehler beim Aktualisieren des Freigabe-Status:', error);
|
||
alert('Fehler beim Aktualisieren des Freigabe-Status.');
|
||
}
|
||
},
|
||
editWorship(worship) {
|
||
this.worshipData = { ...worship };
|
||
this.worshipData.date = formatDate(worship.date).split(".").reverse().join("-");
|
||
this.worshipData.time = formatTime(worship.time);
|
||
console.log(this.worshipData);
|
||
this.selectedEventPlace = this.eventPlaces.find(ep => ep.id === worship.eventPlaceId);
|
||
|
||
// Konvertiere kommaseparierte Strings zu Arrays für Multiselect
|
||
this.selectedOrganizers = worship.organizer
|
||
? worship.organizer.split(',').map(org => ({ name: org.trim() }))
|
||
: [];
|
||
this.selectedSacristans = worship.sacristanService
|
||
? worship.sacristanService.split(',').map(sac => ({ name: sac.trim() }))
|
||
: [];
|
||
|
||
// Setze dayName - finde die passende Option
|
||
if (worship.dayName) {
|
||
const option = this.dayNameOptions.find(opt =>
|
||
opt.dayName === worship.dayName && opt.date === this.worshipData.date
|
||
);
|
||
this.selectedDayName = option || null;
|
||
} else {
|
||
this.selectedDayName = null;
|
||
}
|
||
|
||
this.editMode = true;
|
||
this.editId = worship.id;
|
||
},
|
||
async deleteWorship(id) {
|
||
try {
|
||
await axios.delete(`/worships/${id}`);
|
||
await this.fetchWorships();
|
||
} catch (error) {
|
||
console.error('Fehler beim Löschen des Gottesdienstes:', error);
|
||
}
|
||
},
|
||
resetForm() {
|
||
this.worshipData = {
|
||
eventPlaceId: null,
|
||
date: '',
|
||
time: '',
|
||
title: '',
|
||
organizer: '',
|
||
collection: '',
|
||
organPlaying: '',
|
||
address: '',
|
||
selfInformation: false,
|
||
highlightTime: false,
|
||
neighborInvitation: false,
|
||
introLine: '',
|
||
dayName: '',
|
||
approved: false,
|
||
};
|
||
this.selectedEventPlace = null;
|
||
this.selectedOrganizers = [];
|
||
this.selectedSacristans = [];
|
||
this.selectedDayName = null;
|
||
this.editMode = false;
|
||
this.editId = null;
|
||
},
|
||
getEventPlaceName(eventPlaceId) {
|
||
const place = this.eventPlaces.find(place => place.id === eventPlaceId);
|
||
return place ? place.name : 'Unbekannter Ort';
|
||
},
|
||
dateIsLowerCurrentDate(date) {
|
||
const currentDate = new Date();
|
||
const inputDate = new Date(date);
|
||
return inputDate < currentDate;
|
||
},
|
||
clearSearch() {
|
||
this.searchDate = '';
|
||
},
|
||
addOrganizerTag(newTag) {
|
||
const tag = { name: newTag };
|
||
this.organizerOptions.push(tag);
|
||
this.selectedOrganizers.push(tag);
|
||
},
|
||
addSacristanTag(newTag) {
|
||
const tag = { name: newTag };
|
||
this.sacristanOptions.push(tag);
|
||
this.selectedSacristans.push(tag);
|
||
},
|
||
addDayNameTag(newTag) {
|
||
// Wenn manuell ein Tag eingegeben wird, ohne Datum
|
||
const tag = {
|
||
name: newTag,
|
||
dayName: newTag,
|
||
date: this.worshipData.date || null
|
||
};
|
||
this.dayNameOptions.push(tag);
|
||
this.selectedDayName = tag;
|
||
this.worshipData.dayName = newTag;
|
||
},
|
||
toggleImportSection() {
|
||
this.showImportSection = !this.showImportSection;
|
||
if (!this.showImportSection) {
|
||
// Reset beim Ausblenden
|
||
this.selectedFile = null;
|
||
if (this.$refs.fileInput) {
|
||
this.$refs.fileInput.value = '';
|
||
}
|
||
}
|
||
},
|
||
handleFileSelect(event) {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
// Validierung: .docx (alt) oder .xlsx (neue NBR-Planung) erlauben
|
||
const allowedExtensions = ['.doc', '.docx', '.xlsx'];
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext));
|
||
|
||
if (!isValidFile) {
|
||
alert('Bitte wählen Sie eine .doc/.docx oder .xlsx Datei aus.');
|
||
event.target.value = '';
|
||
this.selectedFile = null;
|
||
return;
|
||
}
|
||
|
||
this.selectedFile = file;
|
||
} else {
|
||
this.selectedFile = null;
|
||
}
|
||
},
|
||
async importWorships() {
|
||
if (!this.selectedFile) {
|
||
alert('Bitte wählen Sie eine Datei aus.');
|
||
return;
|
||
}
|
||
|
||
this.isImporting = true;
|
||
const formData = new FormData();
|
||
formData.append('file', this.selectedFile);
|
||
|
||
try {
|
||
const fileName = this.selectedFile.name.toLowerCase();
|
||
const endpoint = fileName.endsWith('.xlsx')
|
||
? '/worships/import/nbr-planning'
|
||
: '/worships/import';
|
||
|
||
const response = await axios.post(endpoint, formData, {
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
});
|
||
|
||
// Geparste Daten im Dialog anzeigen
|
||
if (response.data.worships && response.data.worships.length > 0) {
|
||
// EventPlace-Objekte zuordnen
|
||
// Das Datum kommt bereits im YYYY-MM-DD Format vom Backend
|
||
this.importedWorships = response.data.worships.map(w => {
|
||
const eventPlace = this.eventPlaces.find(ep => ep.id === w.eventPlaceId);
|
||
// Normalisiere Uhrzeit: entferne Sekunden für Anzeige
|
||
let timeValue = w.time;
|
||
if (timeValue && typeof timeValue === 'string' && timeValue.length > 5) {
|
||
timeValue = timeValue.substring(0, 5);
|
||
}
|
||
return {
|
||
...w,
|
||
// Stelle sicher, dass das Datum ein String im YYYY-MM-DD Format ist
|
||
date: typeof w.date === 'string' ? w.date.split('T')[0] : w.date,
|
||
time: timeValue,
|
||
eventPlace: eventPlace || null,
|
||
approved: false,
|
||
// Stelle sicher, dass _changedFields und _oldValues erhalten bleiben
|
||
_changedFields: w._changedFields || [],
|
||
_oldValues: w._oldValues || {},
|
||
_isUpdate: w._isUpdate || false,
|
||
_isNew: w._isNew || false,
|
||
_existingId: w._existingId || null,
|
||
_sourceText: w._sourceText || '',
|
||
_unparsedText: w._unparsedText || '',
|
||
_hasDayPlaceConflict: w._hasDayPlaceConflict || false,
|
||
_conflictingWorships: w._conflictingWorships || [],
|
||
_replaceExistingIds: w._replaceExistingIds || [],
|
||
_importChoice: w._importChoice || (w._hasDayPlaceConflict ? 'keepExisting' : 'import'),
|
||
neighborInvitation: !!w.neighborInvitation,
|
||
selfInformation: !!w.selfInformation,
|
||
_tempId: `imp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
_selectedOrganizer: this.resolveImportOrganizerOption(w.organizer || '')
|
||
};
|
||
});
|
||
this.importErrors = response.data.errors || [];
|
||
this.showImportDialog = true;
|
||
} else {
|
||
alert('Keine Gottesdienste in der Datei gefunden.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Importieren der Gottesdienste:', error);
|
||
const errorMessage = error.response?.data?.message || 'Fehler beim Importieren der Datei.';
|
||
alert('Fehler: ' + errorMessage);
|
||
} finally {
|
||
this.isImporting = false;
|
||
}
|
||
},
|
||
closeImportDialog() {
|
||
this.showImportDialog = false;
|
||
this.importedWorships = [];
|
||
this.importErrors = [];
|
||
this.clearImportDateFilter();
|
||
this.selectedFile = null;
|
||
if (this.$refs.fileInput) {
|
||
this.$refs.fileInput.value = '';
|
||
}
|
||
},
|
||
removeWorship(worshipToRemove) {
|
||
const index = this.importedWorships.findIndex((w) => w === worshipToRemove || (w._tempId && w._tempId === worshipToRemove._tempId));
|
||
if (index >= 0) {
|
||
this.importedWorships.splice(index, 1);
|
||
}
|
||
},
|
||
handleImportNeighborInvitationChange(worship) {
|
||
if (worship?.neighborInvitation) {
|
||
worship.selfInformation = true;
|
||
}
|
||
},
|
||
async saveImportedWorships() {
|
||
const worshipsForSave = this.filteredImportedWorships;
|
||
if (worshipsForSave.length === 0) {
|
||
alert('Keine Gottesdienste zum Speichern vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const invalidWorship = worshipsForSave.find(w => {
|
||
if (w._hasDayPlaceConflict && w._importChoice === 'keepExisting') {
|
||
return false;
|
||
}
|
||
const eventPlaceId = w.eventPlace ? w.eventPlace.id : (w.eventPlaceId || null);
|
||
return !w.date || !w.time || !eventPlaceId;
|
||
});
|
||
if (invalidWorship) {
|
||
alert('Bitte bei allen zu speichernden Gottesdiensten Datum, Uhrzeit und Ort ausfüllen.');
|
||
return;
|
||
}
|
||
|
||
this.isImporting = true;
|
||
|
||
// Daten für das Backend vorbereiten
|
||
const worshipsToSave = worshipsForSave.map(w => {
|
||
// Stelle sicher, dass das Datum im richtigen Format ist (YYYY-MM-DD)
|
||
let dateStr = w.date;
|
||
if (w.date instanceof Date) {
|
||
const year = w.date.getFullYear();
|
||
const month = String(w.date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(w.date.getDate()).padStart(2, '0');
|
||
dateStr = `${year}-${month}-${day}`;
|
||
} else if (typeof w.date === 'string') {
|
||
// Falls bereits String, verwende direkt (sollte YYYY-MM-DD sein)
|
||
dateStr = w.date.split('T')[0];
|
||
}
|
||
|
||
// Stelle sicher, dass die Uhrzeit im Format HH:MM:00 ist (mit Sekunden für DB)
|
||
let timeValue = w.time;
|
||
if (timeValue && typeof timeValue === 'string') {
|
||
// Wenn nur HH:MM vorhanden, füge :00 hinzu
|
||
if (timeValue.length === 5) {
|
||
timeValue = timeValue + ':00';
|
||
}
|
||
}
|
||
|
||
const worshipData = {
|
||
date: dateStr,
|
||
dayName: w.dayName,
|
||
time: timeValue,
|
||
title: w.title && w.title.trim() ? w.title.trim() : 'Gottesdienst',
|
||
organizer: w.organizer,
|
||
collection: w.collection,
|
||
sacristanService: w.sacristanService,
|
||
organPlaying: w.organPlaying,
|
||
approved: w.approved || false,
|
||
neighborInvitation: !!w.neighborInvitation,
|
||
selfInformation: !!w.selfInformation || !!w.neighborInvitation,
|
||
eventPlaceId: w.eventPlace ? w.eventPlace.id : (w.eventPlaceId || null),
|
||
_importChoice: w._importChoice || 'import',
|
||
_replaceExistingIds: w._replaceExistingIds || [],
|
||
};
|
||
return worshipData;
|
||
});
|
||
|
||
try {
|
||
const response = await axios.post('/worships/import/save', {
|
||
worships: worshipsToSave
|
||
});
|
||
|
||
// Erfolgsmeldung anzeigen
|
||
let message = response.data.message || 'Import erfolgreich abgeschlossen!';
|
||
if (response.data.imported !== undefined || response.data.updated !== undefined) {
|
||
message = `Import abgeschlossen!\n`;
|
||
if (response.data.imported !== undefined) {
|
||
message += `- ${response.data.imported} neue Gottesdienste erstellt\n`;
|
||
}
|
||
if (response.data.updated !== undefined) {
|
||
message += `- ${response.data.updated} Gottesdienste aktualisiert\n`;
|
||
}
|
||
if (response.data.skipped !== undefined && response.data.skipped > 0) {
|
||
message += `- ${response.data.skipped} übersprungen (vergangene Daten)\n`;
|
||
}
|
||
}
|
||
if (response.data.errors && response.data.errors.length > 0) {
|
||
message += `\nFehler: ${response.data.errors.length}`;
|
||
}
|
||
alert(message);
|
||
|
||
// Dialog schließen und Daten aktualisieren
|
||
this.closeImportDialog();
|
||
await this.fetchWorships();
|
||
await this.fetchWorshipOptions();
|
||
await this.fetchLiturgicalDays();
|
||
} catch (error) {
|
||
console.error('Fehler beim Speichern der Gottesdienste:', error);
|
||
const errorMessage = error.response?.data?.message || 'Fehler beim Speichern der Gottesdienste.';
|
||
alert('Fehler: ' + errorMessage);
|
||
} finally {
|
||
this.isImporting = false;
|
||
}
|
||
},
|
||
toggleExportSection() {
|
||
this.showExportSection = !this.showExportSection;
|
||
if (!this.showExportSection) {
|
||
// Reset beim Ausblenden
|
||
this.exportDateFrom = '';
|
||
this.exportDateTo = '';
|
||
this.exportFormat = 'editing';
|
||
}
|
||
},
|
||
async exportWorships() {
|
||
if (!this.exportDateFrom || !this.exportDateTo) {
|
||
alert('Bitte wählen Sie einen Datumsbereich aus.');
|
||
return;
|
||
}
|
||
|
||
if (new Date(this.exportDateFrom) > new Date(this.exportDateTo)) {
|
||
alert('Das "Von Datum" muss vor dem "Bis Datum" liegen.');
|
||
return;
|
||
}
|
||
|
||
this.isExporting = true;
|
||
try {
|
||
const response = await axios.get('/worships/export', {
|
||
params: {
|
||
from: this.exportDateFrom,
|
||
to: this.exportDateTo,
|
||
format: this.exportFormat
|
||
},
|
||
responseType: 'blob'
|
||
});
|
||
|
||
// Datei herunterladen
|
||
const blob = new Blob([response.data], {
|
||
type: response.headers['content-type'] || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||
});
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
|
||
// Dateiname aus Content-Disposition Header extrahieren oder Standardnamen verwenden
|
||
const contentDisposition = response.headers['content-disposition'];
|
||
let filename = `gottesdienste_${this.exportDateFrom}_${this.exportDateTo}.docx`;
|
||
if (contentDisposition) {
|
||
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
|
||
if (filenameMatch) {
|
||
filename = filenameMatch[1];
|
||
}
|
||
}
|
||
|
||
link.download = filename;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
} catch (error) {
|
||
console.error('Fehler beim Exportieren der Gottesdienste:', error);
|
||
const errorMessage = error.response?.data?.message || 'Fehler beim Exportieren der Datei.';
|
||
alert('Fehler: ' + errorMessage);
|
||
} finally {
|
||
this.isExporting = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import 'vue-multiselect/dist/vue-multiselect.css';
|
||
|
||
.worship-management {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
form {
|
||
display: grid;
|
||
grid-template-columns: 180px 1fr;
|
||
gap: 8px 20px;
|
||
align-items: start;
|
||
}
|
||
|
||
form>label {
|
||
margin: 0;
|
||
padding-top: 6px;
|
||
text-align: right;
|
||
font-weight: 500;
|
||
}
|
||
|
||
form>input[type="text"],
|
||
form>input[type="date"],
|
||
form>input[type="time"] {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
padding: 6px 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
form>.multiselect,
|
||
form>.liturgical-day-section {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
form>input[type="checkbox"] {
|
||
justify-self: start;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
form>button {
|
||
grid-column: 1 / -1;
|
||
justify-self: start;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.filter-section {
|
||
margin: 30px 0 20px;
|
||
padding: 15px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
background-color: white;
|
||
}
|
||
|
||
.search-input:focus {
|
||
outline: none;
|
||
border-color: #4CAF50;
|
||
}
|
||
|
||
.checkbox-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.checkbox-label input[type="checkbox"] {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.clear-button {
|
||
padding: 8px 16px;
|
||
background-color: #f44336;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin: 0;
|
||
}
|
||
|
||
.clear-button:hover {
|
||
background-color: #d32f2f;
|
||
}
|
||
|
||
.liturgical-day-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.liturgical-loader {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.year-select {
|
||
padding: 6px 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
background-color: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.year-select:focus {
|
||
outline: none;
|
||
border-color: #4CAF50;
|
||
}
|
||
|
||
.load-year-button {
|
||
padding: 6px 12px;
|
||
background-color: #2196F3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
margin: 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.load-year-button:hover:not(:disabled) {
|
||
background-color: #1976D2;
|
||
}
|
||
|
||
.load-year-button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
ul {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
li {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px;
|
||
border-bottom: 1px solid rgba(224, 224, 224, 0.9);
|
||
position: relative;
|
||
}
|
||
|
||
.worship-list-item {
|
||
transition: background-color 0.2s ease, border-left-color 0.2s ease;
|
||
}
|
||
|
||
button {
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.tooltip {
|
||
visibility: hidden;
|
||
width: auto;
|
||
background-color: rgba(224, 224, 224, 0.6);
|
||
color: #000;
|
||
text-align: center;
|
||
padding: 5px 0;
|
||
position: absolute;
|
||
z-index: 1;
|
||
bottom: 75%;
|
||
left: 50%;
|
||
margin-left: -100px;
|
||
padding: 5px;
|
||
border: 1px solid #000;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
li:hover .tooltip {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
li>span {
|
||
flex: 1;
|
||
}
|
||
|
||
.old-items {
|
||
color: #aaa;
|
||
}
|
||
|
||
.not-approved {
|
||
background-color: #fff8e1; /* zartes Gelb für noch nicht freigegebene Gottesdienste */
|
||
border-left: 4px solid #ffb300;
|
||
}
|
||
|
||
.approve-toggle-button {
|
||
padding: 6px 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid #4CAF50;
|
||
background-color: #e8f5e9;
|
||
color: #2e7d32;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.approve-toggle-button:hover {
|
||
background-color: #c8e6c9;
|
||
}
|
||
|
||
.import-button {
|
||
padding: 8px 16px;
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
margin: 0;
|
||
height: 36px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.import-button:hover {
|
||
background-color: #45a049;
|
||
}
|
||
|
||
.import-section {
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 20px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.import-section h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.import-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.import-content label {
|
||
font-weight: 500;
|
||
color: #555;
|
||
margin: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.import-content input[type="file"] {
|
||
padding: 6px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.import-content input[type="file"]:focus {
|
||
outline: none;
|
||
border-color: #4CAF50;
|
||
}
|
||
|
||
.selected-file {
|
||
padding: 8px;
|
||
background-color: #e8f5e9;
|
||
border: 1px solid #4CAF50;
|
||
border-radius: 4px;
|
||
color: #2e7d32;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.submit-import-button {
|
||
padding: 8px 16px;
|
||
background-color: #2196F3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
align-self: flex-start;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.submit-import-button:hover:not(:disabled) {
|
||
background-color: #1976D2;
|
||
}
|
||
|
||
.submit-import-button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.export-button {
|
||
padding: 8px 16px;
|
||
background-color: #FF9800;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
margin: 0;
|
||
height: 36px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.export-button:hover {
|
||
background-color: #F57C00;
|
||
}
|
||
|
||
.export-section {
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 20px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.export-section h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.export-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.export-content label {
|
||
font-weight: 500;
|
||
color: #555;
|
||
margin: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.export-content input[type="date"],
|
||
.export-content select {
|
||
padding: 6px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.export-content input[type="date"]:focus,
|
||
.export-content select:focus {
|
||
outline: none;
|
||
border-color: #FF9800;
|
||
}
|
||
|
||
.submit-export-button {
|
||
padding: 8px 16px;
|
||
background-color: #FF9800;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
align-self: flex-start;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.submit-export-button:hover:not(:disabled) {
|
||
background-color: #F57C00;
|
||
}
|
||
|
||
.submit-export-button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.import-dialog-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.import-dialog-content {
|
||
background: white;
|
||
border-radius: 8px;
|
||
max-width: 90%;
|
||
max-height: 90vh;
|
||
width: 1200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.import-dialog-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
|
||
.import-dialog-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
|
||
.close-button {
|
||
background: none;
|
||
border: none;
|
||
font-size: 28px;
|
||
cursor: pointer;
|
||
color: #aaa;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
line-height: 30px;
|
||
}
|
||
|
||
.close-button:hover {
|
||
color: #000;
|
||
}
|
||
|
||
.import-dialog-body {
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.import-errors {
|
||
background-color: #ffebee;
|
||
border: 1px solid #f44336;
|
||
border-radius: 4px;
|
||
padding: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.import-errors h4 {
|
||
margin-top: 0;
|
||
color: #c62828;
|
||
}
|
||
|
||
.import-errors ul {
|
||
margin: 10px 0 0 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.import-errors li {
|
||
color: #c62828;
|
||
padding: 5px 0;
|
||
border: none;
|
||
}
|
||
|
||
.imported-worships-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.imported-worship-item {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.imported-worship-item h4 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
border-bottom: 1px solid #ddd;
|
||
padding-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.new-badge {
|
||
background-color: #4caf50;
|
||
color: white;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.update-badge {
|
||
background-color: #ff9800;
|
||
color: white;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.worship-edit-fields {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 15px;
|
||
}
|
||
|
||
.field-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.field-group label {
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.field-group input[type="text"],
|
||
.field-group input[type="date"],
|
||
.field-group input[type="time"],
|
||
.field-group textarea {
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.source-field {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.source-field textarea {
|
||
background: #f8f9fa;
|
||
resize: vertical;
|
||
}
|
||
|
||
.source-field small {
|
||
margin-top: 4px;
|
||
color: #666;
|
||
}
|
||
|
||
.conflict-field {
|
||
grid-column: 1 / -1;
|
||
padding: 10px;
|
||
border: 1px solid #f0ad4e;
|
||
border-radius: 4px;
|
||
background: #fff8e5;
|
||
}
|
||
|
||
.conflict-field ul {
|
||
margin: 0 0 8px 18px;
|
||
padding: 0;
|
||
}
|
||
|
||
.conflict-field .radio-label {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-top: 6px;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.field-group.field-changed {
|
||
background-color: #fff3cd;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
border: 1px solid #ffc107;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.field-group.field-changed label {
|
||
color: #856404;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.field-group .old-value {
|
||
font-size: 12px;
|
||
color: #856404;
|
||
font-weight: normal;
|
||
font-style: italic;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
.field-group input[type="checkbox"] {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.field-group .multiselect {
|
||
width: 100%;
|
||
}
|
||
|
||
.remove-button {
|
||
grid-column: 1 / -1;
|
||
padding: 8px 16px;
|
||
background-color: #f44336;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.remove-button:hover {
|
||
background-color: #d32f2f;
|
||
}
|
||
|
||
.import-dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
padding: 20px;
|
||
border-top: 1px solid #ddd;
|
||
}
|
||
|
||
.cancel-button,
|
||
.save-button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.cancel-button {
|
||
background-color: #ccc;
|
||
color: #333;
|
||
}
|
||
|
||
.cancel-button:hover {
|
||
background-color: #bbb;
|
||
}
|
||
|
||
.save-button {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.save-button:hover:not(:disabled) {
|
||
background-color: #45a049;
|
||
}
|
||
|
||
.save-button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|