Files
miriamgemeinde/src/components/EventForm.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>