Files
miriamgemeinde/src/content/admin/WorshipManagement.vue

1854 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">&times;</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>