416 lines
14 KiB
Vue
416 lines
14 KiB
Vue
<template>
|
|
<div class="event-form">
|
|
<h2>Veranstaltung Formular</h2>
|
|
<form @submit.prevent="saveEvent">
|
|
<table>
|
|
<tbody>
|
|
<tr>
|
|
<td><label for="name">Name:</label></td>
|
|
<td><input type="text" id="name" v-model="eventData.name" required></td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="eventType">Typ:</label></td>
|
|
<td>
|
|
<multiselect v-model="selectedEventType" :options="eventTypes" label="caption" track-by="id"
|
|
placeholder="Typ wählen"></multiselect>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="dateMode">Datum-Modus:</label></td>
|
|
<td>
|
|
<select v-model="dateMode">
|
|
<option value="date">Datum</option>
|
|
<option value="weekday">Wochentag</option>
|
|
<option value="interval">Intervall</option>
|
|
<option value="bulk">Bulk-Datum</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="dateMode === 'date' || dateMode === 'interval'">
|
|
<td><label for="date">Datum:</label></td>
|
|
<td>
|
|
<input type="date" id="date" v-model="eventData.date">
|
|
</td>
|
|
</tr>
|
|
<tr v-if="dateMode === 'bulk'">
|
|
<td><label for="bulkDates">Bulk-Daten:</label></td>
|
|
<td>
|
|
<textarea id="bulkDates" v-model="bulkDates"
|
|
placeholder="Mehrere Daten, z.B. 27.03.2025,03.04.2025 oder je Zeile ein Datum"></textarea>
|
|
<div style="font-size: 0.9em; color: #888;">Format: TT.MM.JJJJ (optional weiterhin JJJJ-MM-TT). Trennen mit Komma oder Zeilenumbruch.</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="dateMode === 'weekday' || dateMode === 'interval'">
|
|
<td><label for="dayOfWeek">Wochentag:</label></td>
|
|
<td>
|
|
<multiselect v-model="eventData.dayOfWeek" :options="weekdays" label="name" track-by="value"
|
|
placeholder="Wochentag wählen"></multiselect>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="time">Uhrzeit:</label></td>
|
|
<td><input type="time" id="time" v-model="eventData.time"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="endTime">Ende-Uhrzeit:</label></td>
|
|
<td><input type="time" id="endTime" v-model="eventData.endTime"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="description">Beschreibung:</label></td>
|
|
<td><textarea id="description" v-model="eventData.description" class="descriptionedit"></textarea></td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="institution">Institution:</label></td>
|
|
<td>
|
|
<multiselect v-model="selectedInstitution" :options="localInstitutions" label="name" track-by="id"
|
|
placeholder="Institution wählen"></multiselect>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="eventPlace">Veranstaltungsort:</label></td>
|
|
<td>
|
|
<multiselect v-model="selectedEventPlace" :options="localEventPlaces" label="name" track-by="id"
|
|
placeholder="Veranstaltungsort wählen"></multiselect>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><label for="contactPersons">Kontaktpersonen:</label></td>
|
|
<td>
|
|
<multiselect v-model="selectedContactPersons" :options="localContactPersons" :multiple="true" label="name"
|
|
track-by="id" placeholder="Kontaktpersonen wählen"></multiselect>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="2"><label><input type="checkbox" v-model="onHomepage">Auf der Startseite anzeigen</label></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Zugewiesenes Bild:</td>
|
|
<td>
|
|
<div v-if="assignedImage != null && imageFilename">
|
|
<img :src="getImagePath" class="preview-image" />
|
|
<button @click="removeImage" type="button">Bild entfernen</button>
|
|
</div>
|
|
<div v-else>
|
|
<button type="button" @click="openAddImageDialog">Bild auswählen</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="2">
|
|
<button type="submit">Speichern</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</form>
|
|
</div>
|
|
<AddImageDialog ref="addImageDialog" @confirm="setImage" />
|
|
</template>
|
|
|
|
<script>
|
|
import axios from 'axios';
|
|
import Multiselect from 'vue-multiselect';
|
|
import { formatTime } from '@/utils/strings';
|
|
import AddImageDialog from '@/components/AddImageDialog.vue';
|
|
|
|
export default {
|
|
name: 'EventForm',
|
|
components: { Multiselect, AddImageDialog },
|
|
props: {
|
|
event: {
|
|
type: Object,
|
|
required: true,
|
|
default: () => ({})
|
|
},
|
|
institutions: {
|
|
type: Array,
|
|
required: true,
|
|
default: () => []
|
|
},
|
|
eventPlaces: {
|
|
type: Array,
|
|
required: true,
|
|
default: () => []
|
|
},
|
|
contactPersons: {
|
|
type: Array,
|
|
required: true,
|
|
default: () => []
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
eventData: { ...this.event },
|
|
selectedEventType: null,
|
|
selectedInstitution: this.event.institution || null,
|
|
selectedEventPlace: this.event.eventPlace || null,
|
|
selectedContactPersons: this.event.contactPersons || [],
|
|
eventTypes: [],
|
|
dateMode: 'date',
|
|
weekdays: [
|
|
{ name: 'Montag', value: 1 },
|
|
{ name: 'Dienstag', value: 2 },
|
|
{ name: 'Mittwoch', value: 3 },
|
|
{ name: 'Donnerstag', value: 4 },
|
|
{ name: 'Freitag', value: 5 },
|
|
{ name: 'Samstag', value: 6 },
|
|
{ name: 'Sonntag', value: 7 },
|
|
],
|
|
localInstitutions: [...this.institutions],
|
|
localEventPlaces: [...this.eventPlaces],
|
|
localContactPersons: [...this.contactPersons],
|
|
onHomepage: false,
|
|
assignedImage: null,
|
|
imageFilename: '',
|
|
bulkDates: '',
|
|
};
|
|
},
|
|
watch: {
|
|
event(newVal) {
|
|
this.eventData = this.normalizeEventForForm(newVal);
|
|
this.determineDateMode();
|
|
if (newVal && typeof newVal.__newsletterDateMode === 'string') {
|
|
this.dateMode = newVal.__newsletterDateMode;
|
|
}
|
|
if (newVal && typeof newVal.__newsletterBulkDates === 'string') {
|
|
this.bulkDates = newVal.__newsletterBulkDates;
|
|
}
|
|
this.selectedEventType = this.eventTypes.find(type => type.id === newVal.eventTypeId) || null;
|
|
this.selectedInstitution = newVal.institution || null;
|
|
this.selectedEventPlace =
|
|
newVal.eventPlace ||
|
|
this.localEventPlaces.find((place) => place.id === newVal.event_place_id || place.id === newVal.eventPlaceId) ||
|
|
null;
|
|
this.selectedContactPersons = newVal.contactPersons || [];
|
|
this.onHomepage = newVal.alsoOnHomepage == 1 ? true : false;
|
|
this.assignedImage = newVal.relatedImage || null;
|
|
if (this.assignedImage) {
|
|
this.fetchImageFilename();
|
|
}
|
|
},
|
|
institutions(newVal) {
|
|
this.localInstitutions = [...newVal];
|
|
},
|
|
eventPlaces(newVal) {
|
|
this.localEventPlaces = [...newVal];
|
|
},
|
|
contactPersons(newVal) {
|
|
this.localContactPersons = [...newVal];
|
|
}
|
|
},
|
|
async created() {
|
|
try {
|
|
const eventTypeResponse = await axios.get('/event-types');
|
|
this.eventTypes = eventTypeResponse.data;
|
|
this.selectedEventType = this.eventTypes.find(type => type.id === this.event.eventTypeId) || null;
|
|
if (!this.selectedEventType && this.event?.eventTypeId) {
|
|
this.selectedEventType = this.eventTypes.find(type => type.id === this.event.eventTypeId) || null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch event types:', error);
|
|
}
|
|
this.determineDateMode();
|
|
if (this.event && typeof this.event.__newsletterDateMode === 'string') {
|
|
this.dateMode = this.event.__newsletterDateMode;
|
|
}
|
|
if (this.event && typeof this.event.__newsletterBulkDates === 'string') {
|
|
this.bulkDates = this.event.__newsletterBulkDates;
|
|
}
|
|
},
|
|
computed: {
|
|
getImagePath() {
|
|
return this.imageFilename ? `/images/uploads/${this.imageFilename}` : '';
|
|
}
|
|
},
|
|
methods: {
|
|
formatTime,
|
|
async saveEvent() {
|
|
try {
|
|
const dayOfWeekValue = this.extractDayOfWeekValue(this.eventData.dayOfWeek);
|
|
const basePayload = {
|
|
...this.eventData,
|
|
eventTypeId: this.selectedEventType ? this.selectedEventType.id : null,
|
|
institution_id: this.selectedInstitution ? this.selectedInstitution.id : null,
|
|
event_place_id: this.selectedEventPlace ? this.selectedEventPlace.id : null,
|
|
contactPersonIds: this.selectedContactPersons.map(person => person.id),
|
|
dayOfWeek: dayOfWeekValue,
|
|
relatedImage: this.assignedImage,
|
|
alsoOnHomepage: this.onHomepage ? 1 : 0
|
|
};
|
|
if (this.dateMode === 'bulk' && this.bulkDates) {
|
|
// Aufteilen und deutsche Datumsformate (TT.MM.JJJJ) sowie ISO (JJJJ-MM-TT) akzeptieren
|
|
const parts = this.bulkDates.split(/,|\n/).map(d => d.trim()).filter(d => d.length > 0);
|
|
const isoDates = [];
|
|
const invalid = [];
|
|
const pad = n => n.toString().padStart(2, '0');
|
|
for (const p of parts) {
|
|
// dd.mm.yyyy
|
|
let m = p.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
if (m) {
|
|
const dd = parseInt(m[1], 10);
|
|
const mm = parseInt(m[2], 10);
|
|
const yyyy = parseInt(m[3], 10);
|
|
// einfache Plausibilitätsprüfung
|
|
if (mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) {
|
|
isoDates.push(`${yyyy}-${pad(mm)}-${pad(dd)}`);
|
|
continue;
|
|
} else {
|
|
invalid.push(p);
|
|
continue;
|
|
}
|
|
}
|
|
// ISO yyyy-mm-dd
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(p)) {
|
|
isoDates.push(p);
|
|
} else {
|
|
invalid.push(p);
|
|
}
|
|
}
|
|
if (isoDates.length === 0) {
|
|
alert('Keine gültigen Datumsangaben erkannt. Erlaubt: TT.MM.JJJJ oder JJJJ-MM-TT');
|
|
return;
|
|
}
|
|
if (invalid.length > 0) {
|
|
// Hinweis für Benutzer über ignorierte Werte
|
|
console.warn('Ungültige Datumsangaben ignoriert:', invalid);
|
|
alert('Folgende Einträge wurden ignoriert: ' + invalid.join(', '));
|
|
}
|
|
const results = [];
|
|
for (const date of isoDates) {
|
|
const payload = { ...basePayload, date };
|
|
const response = await axios.post('/events', payload);
|
|
results.push(response.data);
|
|
}
|
|
this.$emit('saved', results);
|
|
} else {
|
|
let response;
|
|
if (this.eventData.id) {
|
|
response = await axios.put(`/events/${this.eventData.id}`, basePayload);
|
|
} else {
|
|
response = await axios.post('/events', basePayload);
|
|
}
|
|
this.$emit('saved', response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save event:', error);
|
|
}
|
|
},
|
|
determineDateMode() {
|
|
// Bei bestehenden Events hat dayOfWeek oft einen Wert, obwohl "Datum" gemeint ist.
|
|
// Deshalb hat fixes Datum Vorrang vor Intervall.
|
|
if (this.eventData.date) {
|
|
this.dateMode = 'date';
|
|
} else if (this.extractDayOfWeekValue(this.eventData.dayOfWeek) > -1) {
|
|
this.dateMode = 'weekday';
|
|
} else {
|
|
this.dateMode = 'date';
|
|
}
|
|
},
|
|
normalizeEventForForm(event) {
|
|
const normalized = { ...(event || {}) };
|
|
if (typeof normalized.date === 'string') {
|
|
normalized.date = normalized.date.split('T')[0];
|
|
}
|
|
const dayValue = this.extractDayOfWeekValue(normalized.dayOfWeek);
|
|
if (dayValue > -1) {
|
|
const mappedValue = dayValue === 0 ? 7 : dayValue;
|
|
normalized.dayOfWeek =
|
|
this.weekdays.find((d) => d.value === mappedValue) || null;
|
|
} else {
|
|
normalized.dayOfWeek = null;
|
|
}
|
|
return normalized;
|
|
},
|
|
extractDayOfWeekValue(dayOfWeek) {
|
|
if (dayOfWeek === null || dayOfWeek === undefined || dayOfWeek === '') {
|
|
return -1;
|
|
}
|
|
if (typeof dayOfWeek === 'object') {
|
|
if (dayOfWeek.value === null || dayOfWeek.value === undefined || dayOfWeek.value === '') {
|
|
return -1;
|
|
}
|
|
return Number(dayOfWeek.value);
|
|
}
|
|
const parsed = Number(dayOfWeek);
|
|
return Number.isFinite(parsed) ? parsed : -1;
|
|
},
|
|
async fetchImageFilename() {
|
|
try {
|
|
const response = await axios.get('/image/' + this.assignedImage);
|
|
this.imageFilename = response.data.filename;
|
|
} catch (error) {
|
|
console.error('Bild konnte nicht geladen werden:', error);
|
|
}
|
|
},
|
|
openAddImageDialog() {
|
|
this.$refs.addImageDialog.openAddImageDialog();
|
|
},
|
|
|
|
async setImage(imageId) {
|
|
this.assignedImage = imageId;
|
|
try {
|
|
const response = await axios.get('/image/' + imageId);
|
|
this.imageFilename = response.data.filename;
|
|
} catch (error) {
|
|
console.error('Bild konnte nicht geladen werden:', error);
|
|
this.imageFilename = '';
|
|
}
|
|
},
|
|
|
|
removeImage() {
|
|
this.assignedImage = null;
|
|
this.imageFilename = '';
|
|
},
|
|
focusFirstField() {
|
|
// Fokussiert das erste Eingabefeld (Name)
|
|
this.$nextTick(() => {
|
|
const nameInput = document.getElementById('name');
|
|
if (nameInput) {
|
|
nameInput.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
@import 'vue-multiselect/dist/vue-multiselect.css';
|
|
|
|
.event-form {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
td {
|
|
padding: 8px;
|
|
vertical-align: top;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
}
|
|
|
|
button {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.descriptionedit {
|
|
width: 100%;
|
|
height: 10em;
|
|
}
|
|
|
|
.preview-image {
|
|
max-width: 50px;
|
|
max-height: 50px;
|
|
object-fit: contain;
|
|
}
|
|
</style>
|