Füge Unterstützung für liturgische Tage im Worship Management hinzu: Implementiere Multiselect für die Auswahl von Tag-Namen und lade die verfügbaren liturgischen Tage. Aktualisiere das Formular zur Anzeige und Auswahl des liturgischen Tages basierend auf dem Datum.

This commit is contained in:
Torsten Schulz (local)
2025-10-07 17:03:58 +02:00
parent fd84112cef
commit 0238fffd3d
6 changed files with 365 additions and 5 deletions

View File

@@ -0,0 +1,154 @@
const { LiturgicalDay } = require('../models');
const { Op } = require('sequelize');
const axios = require('axios');
// Alle liturgischen Tage abrufen
const getAllLiturgicalDays = async (req, res) => {
try {
const days = await LiturgicalDay.findAll({
order: [['date', 'ASC']]
});
res.status(200).json(days);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Abrufen der liturgischen Tage' });
}
};
// Eindeutige Namen für Multiselect abrufen
const getLiturgicalDayNames = async (req, res) => {
try {
const days = await LiturgicalDay.findAll({
attributes: ['dayName'],
group: ['dayName'],
order: [['dayName', 'ASC']]
});
const names = days.map(day => day.dayName);
res.status(200).json(names);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Abrufen der Tag-Namen' });
}
};
// HTML von liturgischem Kalender parsen und in DB speichern
const loadLiturgicalYear = async (req, res) => {
const { year } = req.body;
if (!year) {
return res.status(400).json({ message: 'Jahr ist erforderlich' });
}
const currentYear = new Date().getFullYear();
if (year < currentYear || year > currentYear + 2) {
return res.status(400).json({ message: 'Jahr muss zwischen aktuellem Jahr und 2 Jahren in der Zukunft liegen' });
}
try {
const url = `https://www.eike-fleer.de/liturgischer-kalender/${year}.htm`;
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
const html = response.data;
// Parse HTML - suche nach Tabellenzeilen mit Datum und Name
// Format: "DD.MM.YYYY &nbsp; &nbsp; &nbsp; DayName"
const regex = /(\d{2}\.\d{2}\.\d{4})\s*(?:&nbsp;|\s)+(.+?)(?:<\/|$)/gi;
const matches = [...html.matchAll(regex)];
const liturgicalDays = [];
for (const match of matches) {
const dateStr = match[1]; // DD.MM.YYYY
let dayName = match[2];
// Bereinige den Tag-Namen von HTML-Tags und Entities
dayName = dayName
.replace(/<[^>]*>/g, '') // Entferne HTML-Tags
.replace(/&nbsp;/g, ' ') // Ersetze &nbsp;
.replace(/&auml;/g, 'ä')
.replace(/&ouml;/g, 'ö')
.replace(/&uuml;/g, 'ü')
.replace(/&Auml;/g, 'Ä')
.replace(/&Ouml;/g, 'Ö')
.replace(/&Uuml;/g, 'Ü')
.replace(/&szlig;/g, 'ß')
.trim();
// Konvertiere Datum von DD.MM.YYYY zu YYYY-MM-DD
const [day, month, yearPart] = dateStr.split('.');
const isoDate = `${yearPart}-${month}-${day}`;
if (dayName && dayName.length > 0) {
liturgicalDays.push({
date: isoDate,
dayName: dayName
});
}
}
if (liturgicalDays.length === 0) {
return res.status(500).json({ message: 'Keine liturgischen Tage gefunden. Möglicherweise hat sich das HTML-Format geändert.' });
}
// Speichere oder aktualisiere die Einträge
for (const day of liturgicalDays) {
await LiturgicalDay.upsert({
date: day.date,
dayName: day.dayName
});
}
res.status(200).json({
message: `${liturgicalDays.length} liturgische Tage für ${year} erfolgreich geladen`,
count: liturgicalDays.length
});
} catch (error) {
console.error('Fehler beim Laden der liturgischen Tage:', error);
if (error.response && error.response.status === 404) {
return res.status(404).json({ message: `Liturgischer Kalender für ${year} nicht gefunden` });
}
res.status(500).json({ message: 'Fehler beim Laden der liturgischen Tage', error: error.message });
}
};
// Einzelnen Tag erstellen
const createLiturgicalDay = async (req, res) => {
try {
const day = await LiturgicalDay.create(req.body);
res.status(201).json(day);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Erstellen des liturgischen Tags' });
}
};
// Tag löschen
const deleteLiturgicalDay = async (req, res) => {
try {
const { id } = req.params;
const deleted = await LiturgicalDay.destroy({
where: { id }
});
if (deleted) {
res.status(200).json({ message: 'Liturgischer Tag erfolgreich gelöscht' });
} else {
res.status(404).json({ message: 'Liturgischer Tag nicht gefunden' });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Löschen des liturgischen Tags' });
}
};
module.exports = {
getAllLiturgicalDays,
getLiturgicalDayNames,
loadLiturgicalYear,
createLiturgicalDay,
deleteLiturgicalDay
};

View File

@@ -0,0 +1,28 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('liturgical_days', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
date: {
type: Sequelize.DATEONLY,
allowNull: false,
unique: true
},
dayName: {
type: Sequelize.STRING,
allowNull: false
}
});
},
async down (queryInterface, Sequelize) {
await queryInterface.dropTable('liturgical_days');
}
};

24
models/LiturgicalDay.js Normal file
View File

@@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
const LiturgicalDay = sequelize.define('LiturgicalDay', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
date: {
type: DataTypes.DATEONLY,
allowNull: false,
unique: true
},
dayName: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'liturgical_days',
timestamps: false
});
return LiturgicalDay;
};

19
routes/liturgicalDays.js Normal file
View File

@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();
const {
getAllLiturgicalDays,
getLiturgicalDayNames,
loadLiturgicalYear,
createLiturgicalDay,
deleteLiturgicalDay
} = require('../controllers/liturgicalDayController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', getAllLiturgicalDays);
router.get('/names', getLiturgicalDayNames);
router.post('/load-year', authMiddleware, loadLiturgicalYear);
router.post('/', authMiddleware, createLiturgicalDay);
router.delete('/:id', authMiddleware, deleteLiturgicalDay);
module.exports = router;

View File

@@ -17,7 +17,8 @@ const worshipRouter = require('./routes/worships');
const pageRouter = require('./routes/pages'); const pageRouter = require('./routes/pages');
const userRouter = require('./routes/users'); const userRouter = require('./routes/users');
const imageRouter = require('./routes/image'); const imageRouter = require('./routes/image');
const filesRouter = require('./routes/files'); const filesRouter = require('./routes/files');
const liturgicalDaysRouter = require('./routes/liturgicalDays');
const app = express(); const app = express();
const PORT = parseInt(process.env.PORT, 10) || 3000; const PORT = parseInt(process.env.PORT, 10) || 3000;
@@ -56,6 +57,7 @@ app.use('/api/page-content', pageRouter);
app.use('/api/users', userRouter); app.use('/api/users', userRouter);
app.use('/api/image', imageRouter); app.use('/api/image', imageRouter);
app.use('/api/files', filesRouter); app.use('/api/files', filesRouter);
app.use('/api/liturgical-days', liturgicalDaysRouter);
const options = { const options = {
key: fs.readFileSync('server.key'), key: fs.readFileSync('server.key'),

View File

@@ -7,10 +7,29 @@
placeholder="Veranstaltungsort wählen"></multiselect> placeholder="Veranstaltungsort wählen"></multiselect>
<label for="date">Datum:</label> <label for="date">Datum:</label>
<input type="date" id="date" v-model="worshipData.date" required> <input type="date" id="date" v-model="worshipData.date" required @change="updateDayNameFromDate">
<label for="dayName">Name des Tags:</label> <label for="dayName">Name des Tags:</label>
<input type="text" id="dayName" v-model="worshipData.dayName" required> <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 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>
</div>
<label for="time">Uhrzeit:</label> <label for="time">Uhrzeit:</label>
<input type="time" id="time" v-model="worshipData.time" required> <input type="time" id="time" v-model="worshipData.time" required>
@@ -103,6 +122,7 @@ export default {
name: 'WorshipManagement', name: 'WorshipManagement',
components: { Multiselect }, components: { Multiselect },
data() { data() {
const currentYear = new Date().getFullYear();
return { return {
worships: [], worships: [],
eventPlaces: [], eventPlaces: [],
@@ -110,6 +130,12 @@ export default {
sacristanOptions: [], sacristanOptions: [],
selectedOrganizers: [], selectedOrganizers: [],
selectedSacristans: [], selectedSacristans: [],
dayNameOptions: [],
liturgicalDays: [],
selectedDayName: null,
selectedYear: currentYear,
availableYears: [currentYear, currentYear + 1, currentYear + 2],
isLoading: false,
worshipData: { worshipData: {
eventPlaceId: null, eventPlaceId: null,
date: '', date: '',
@@ -124,6 +150,7 @@ export default {
introLine: '', introLine: '',
sacristanService: '', sacristanService: '',
website: '', website: '',
dayName: '',
}, },
selectedEventPlace: null, selectedEventPlace: null,
editMode: false, editMode: false,
@@ -173,6 +200,7 @@ export default {
await this.fetchEventPlaces(); await this.fetchEventPlaces();
await this.fetchWorships(); await this.fetchWorships();
await this.fetchWorshipOptions(); await this.fetchWorshipOptions();
await this.fetchLiturgicalDays();
}, },
methods: { methods: {
formatTime, formatTime,
@@ -202,13 +230,62 @@ export default {
console.error('Fehler beim Abrufen der Worship-Optionen:', error); console.error('Fehler beim Abrufen der Worship-Optionen:', error);
} }
}, },
async fetchLiturgicalDays() {
try {
const response = await axios.get('/liturgical-days');
this.liturgicalDays = response.data;
// Erstelle Optionen für Multiselect
const uniqueNames = [...new Set(response.data.map(day => day.dayName))];
this.dayNameOptions = uniqueNames.sort().map(name => ({ name }));
} 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;
}
// Finde liturgischen Tag für das gewählte Datum
const liturgicalDay = this.liturgicalDays.find(day => day.date === this.worshipData.date);
if (liturgicalDay) {
this.selectedDayName = { name: liturgicalDay.dayName };
this.worshipData.dayName = liturgicalDay.dayName;
}
},
async saveWorship() { async saveWorship() {
try { try {
const payload = { const payload = {
...this.worshipData, ...this.worshipData,
eventPlaceId: this.selectedEventPlace ? this.selectedEventPlace.id : null, eventPlaceId: this.selectedEventPlace ? this.selectedEventPlace.id : null,
organizer: this.selectedOrganizers.map(org => org.name).join(', '), organizer: this.selectedOrganizers.map(org => org.name).join(', '),
sacristanService: this.selectedSacristans.map(sac => sac.name).join(', ') sacristanService: this.selectedSacristans.map(sac => sac.name).join(', '),
dayName: this.selectedDayName ? this.selectedDayName.name : ''
}; };
if (this.editMode) { if (this.editMode) {
@@ -239,6 +316,9 @@ export default {
? worship.sacristanService.split(',').map(sac => ({ name: sac.trim() })) ? worship.sacristanService.split(',').map(sac => ({ name: sac.trim() }))
: []; : [];
// Setze dayName
this.selectedDayName = worship.dayName ? { name: worship.dayName } : null;
this.editMode = true; this.editMode = true;
this.editId = worship.id; this.editId = worship.id;
}, },
@@ -262,11 +342,13 @@ export default {
selfInformation: false, selfInformation: false,
highlightTime: false, highlightTime: false,
neighborInvitation: false, neighborInvitation: false,
introLine: '' introLine: '',
dayName: ''
}; };
this.selectedEventPlace = null; this.selectedEventPlace = null;
this.selectedOrganizers = []; this.selectedOrganizers = [];
this.selectedSacristans = []; this.selectedSacristans = [];
this.selectedDayName = null;
this.editMode = false; this.editMode = false;
this.editId = null; this.editId = null;
}, },
@@ -291,6 +373,11 @@ export default {
const tag = { name: newTag }; const tag = { name: newTag };
this.sacristanOptions.push(tag); this.sacristanOptions.push(tag);
this.selectedSacristans.push(tag); this.selectedSacristans.push(tag);
},
addDayNameTag(newTag) {
const tag = { name: newTag };
this.dayNameOptions.push(tag);
this.selectedDayName = tag;
} }
} }
}; };
@@ -373,6 +460,52 @@ button {
background-color: #d32f2f; background-color: #d32f2f;
} }
.liturgical-day-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.liturgical-loader {
display: flex;
gap: 10px;
align-items: center;
}
.year-select {
padding: 8px 12px;
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: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
margin: 0;
}
.load-year-button:hover:not(:disabled) {
background-color: #1976D2;
}
.load-year-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
ul { ul {
margin-top: 20px; margin-top: 20px;
} }