Compare commits

...

32 Commits

Author SHA1 Message Date
b5ac5df38d Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-12-19 16:09:02 +01:00
c982f34823 Add renovate.json 2025-12-19 15:58:37 +01:00
Torsten Schulz (local)
c390228ed9 Update database configuration and improve WorshipRender component: Change database connection details to use 'miriamgemeinde' and 'localhost', and enhance conditional rendering in WorshipRender.vue for better handling of event place information. 2025-11-23 00:32:23 +01:00
Torsten Schulz (local)
9c924071f4 Update package-lock.json and configuration files: Upgrade lockfile version to 3, update @vue/cli-plugin-babel and @vue/cli-plugin-eslint to version 5.0.0, and change database connection details to use 'miriam_user' and host 'tsschulz.de' with the new password. 2025-11-23 00:24:05 +01:00
Torsten Schulz (local)
2d1ed43312 Refactor babel.config.js: Simplify Babel preset configuration by using the package name instead of a direct path for improved readability and maintainability. 2025-11-22 23:37:34 +01:00
Torsten Schulz (local)
11cf961ed8 Update package.json: Upgrade @vue/cli-plugin-babel and @vue/cli-plugin-eslint to version 5.0.0, removing the previous version 3.12.1 for better compatibility. 2025-11-22 23:33:04 +01:00
Torsten Schulz (local)
cd9e645941 Update babel.config.js: Use direct path for Babel preset to ensure reliable resolution across different Node/npm versions. 2025-11-22 23:29:40 +01:00
Torsten Schulz (local)
7bd236aa82 Update package.json: Re-add @vue/cli-plugin-babel dependency to version 3.12.1 in devDependencies. 2025-11-22 23:24:53 +01:00
Torsten Schulz (local)
f96e0a1f22 Update package dependencies: Downgrade @vue/cli and its plugins to version 4.2.2 and 3.12.1 respectively, and update various Babel packages to version 7.28.5. Remove unused Apollo packages and add new dependencies for vue-jscodeshift-adapter and vue-sfc-descriptor-to-string. 2025-11-22 23:17:23 +01:00
Torsten Schulz (local)
45190ed7a5 Füge eine Überprüfung hinzu, um endlose Wiederholungen beim Laden von Menü-Daten zu verhindern: Implementiere eine Flagge, die sicherstellt, dass die Menü-Daten nur einmal geladen werden, bevor die Routen generiert werden. 2025-11-22 22:47:51 +01:00
Torsten Schulz (local)
79f2ca8382 Füge eine Überprüfung hinzu, um endlose Wiederholungen beim Laden von Menü-Daten zu verhindern: Implementiere eine Flagge, die sicherstellt, dass die Menü-Daten nur einmal geladen werden, bevor die Routen generiert werden. 2025-11-22 22:31:28 +01:00
Torsten Schulz (local)
550ed97a11 Aktualisiere die Axios-Konfiguration: Setze die Basis-URL standardmäßig auf '/api' und ermögliche die Verwendung einer Umgebungsvariablen in Entwicklungsumgebungen. Verhindere Mixed-Content-Probleme durch Umstellung auf HTTPS. Ändere den Import von Axios im Store, um die neue Konfiguration zu nutzen. 2025-11-22 22:23:08 +01:00
Torsten Schulz (local)
718bcabea3 Verbessere die Axios-Konfiguration: Setze die Basis-URL dynamisch basierend auf der Umgebung und verhindere Mixed-Content-Probleme durch Umstellung auf HTTPS. Aktualisiere die Menü-Datenabfrage, um Axios für den API-Zugriff zu verwenden und füge Fehlerbehandlung hinzu. 2025-11-22 22:16:50 +01:00
Torsten Schulz (local)
44c978f21e Füge Import- und Exportfunktionen für Gottesdienste hinzu: Implementiere die Möglichkeit, Gottesdienste aus .doc und .docx-Dateien zu importieren und in verschiedenen Formaten zu exportieren. Verbessere die Benutzeroberfläche des Worship Management-Formulars mit neuen Schaltflächen für Import und Export sowie Dialogen zur Bearbeitung importierter Daten. Aktualisiere die Datenbankstruktur, um neue Felder für die Genehmigung und das Orgelspiel zu unterstützen. 2025-11-22 22:07:36 +01:00
Torsten Schulz (local)
6c54bc9d49 Aktualisiere die Datenschutzerklärung und die Nutzungsbedingungen: Präzisiere die rechtlichen Grundlagen für den Datenschutz, füge eine detaillierte Auflistung der Nutzerrechte hinzu und aktualisiere das Datum der Datenschutzerklärung auf Januar 2025. 2025-10-27 09:35:09 +01:00
Torsten Schulz (local)
d4fab1ceb3 Füge Ablaufdatum zu Kontaktpersonen hinzu: Implementiere die Möglichkeit, ein Ablaufdatum für Kontaktpersonen zu speichern und anzuzeigen. Aktualisiere die Filterlogik, um nur nicht abgelaufene Kontaktpersonen anzuzeigen, und passe die Benutzeroberfläche an, um das Ablaufdatum darzustellen. 2025-10-07 17:45:45 +02:00
Torsten Schulz (local)
ead4dbdd3f Ändere die Hintergrundfarbe aktiver Schaltflächen im EditPagesComponent von grün zu dunkelgrau, um die Sichtbarkeit zu verbessern und ein einheitlicheres Design zu gewährleisten. 2025-10-07 17:40:33 +02:00
Torsten Schulz (local)
b18c911500 Verbessere die Benutzeroberfläche des EditPagesComponent: Füge aktive Klassen zu Schaltflächen hinzu, um den aktuellen Status der Textformatierungen anzuzeigen. Aktualisiere die Stile für aktive Schaltflächen zur besseren Sichtbarkeit. 2025-10-07 17:37:16 +02:00
Torsten Schulz (local)
5e4471a936 Optimiere das Worship Management-Formular: Entferne redundante Codeabschnitte, verbessere die Benutzeroberfläche durch Anpassungen der Abstände und Padding-Werte, und vereinheitliche die Struktur der Eingabefelder. Füge eine neue Auswahl für das Jahr hinzu, um die liturgischen Daten zu laden. 2025-10-07 17:22:39 +02:00
Torsten Schulz (local)
459dd3168a Verbessere die Auswahl von liturgischen Tagen im Worship Management: Filtere nur zukünftige Tage, formatiere die Anzeige mit Datum und Namen und aktualisiere die Logik zur Auswahl und Speicherung des liturgischen Tages. Optimiere die Handhabung von Tags und deren Zuordnung im Formular. 2025-10-07 17:20:17 +02:00
Torsten Schulz (local)
3af7089e06 Verbessere das Layout des Worship Management-Formulars: Ändere das Layout von Flexbox zu Grid, optimiere die Stile für Labels, Eingabefelder und Schaltflächen, um eine bessere Benutzererfahrung zu gewährleisten. 2025-10-07 17:18:37 +02:00
Torsten Schulz (local)
156d89a45d Füge Logik zur Aktualisierung des Datums basierend auf dem ausgewählten liturgischen Tag hinzu: Implementiere einen Watcher für die Änderung des Tagesnamens und vermeide Endlosschleifen durch ein Flag. Protokolliere das gesetzte Datum und den entsprechenden liturgischen Tag in der Konsole. 2025-10-07 17:16:42 +02:00
Torsten Schulz (local)
bf0b7f1dad Verbessere die Logik zur Ermittlung des liturgischen Tages: Normalisiere das Datum und vergleiche nur das Datum ohne Zeitstempel. Füge Konsolenausgaben hinzu, um die Ergebnisse der Suche nach liturgischen Tagen zu protokollieren. 2025-10-07 17:14:24 +02:00
Torsten Schulz (local)
6de8cac0bc Optimiere die Funktion zum Abrufen von Worship-Optionen: Reduziere die Anzahl der Datenbankabfragen, indem Organizer und SacristanService in einer Abfrage zusammengefasst werden. Verbessere die Fehlerbehandlung durch detailliertere Fehlermeldungen. 2025-10-07 17:10:18 +02:00
Torsten Schulz (local)
ecd03d29f4 Verbessere das Worship Management: Implementiere die Auswahl von liturgischen Tagen mit Multiselect und aktualisiere das Formular zur dynamischen Anzeige basierend auf dem Datum. 2025-10-07 17:06:48 +02:00
Torsten Schulz (local)
0238fffd3d 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. 2025-10-07 17:03:58 +02:00
Torsten Schulz (local)
fd84112cef Füge neue Funktionalität zum Abrufen von Worship-Optionen hinzu: Implementiere Endpunkt für eindeutige Gestalter und Küsterdienste. Aktualisiere das Worship Management-Formular zur Verwendung von Multiselect-Komponenten für die Auswahl und Eingabe von Gestaltern und Küstern. Integriere die Fetch-Funktion für Worship-Optionen im Vue-Komponenten-Lifecycle. 2025-10-07 16:52:52 +02:00
Torsten Schulz (local)
3303c749e2 Füge 'actualize.sh' zur .gitignore hinzu, um sicherzustellen, dass die Datei nicht ins Repository aufgenommen wird. 2025-10-07 16:46:05 +02:00
Torsten Schulz (local)
9cadaf3f51 Füge Such- und Filteroptionen für Gottesdienste hinzu: Ermögliche die Suche nach Datum und die Anzeige vergangener Gottesdienste im Worship Management-Bereich. Implementiere eine Funktion zum Zurücksetzen der Suche. 2025-10-07 16:42:11 +02:00
Torsten Schulz (local)
5d32c46e38 Füge Such- und Filterfunktionen für Veranstaltungen hinzu: Ermögliche die Suche nach Name, Typ und Beschreibung sowie die Anzeige vergangener Events im Event-Management-Bereich. 2025-10-07 16:37:30 +02:00
Torsten Schulz (local)
7e6128dec4 Füge Funktionalität zum Fokussieren des ersten Eingabefelds im Event-Formular hinzu und implementiere das Scrollen zum Formular bei der Erstellung und Bearbeitung von Veranstaltungen. 2025-10-07 16:06:17 +02:00
Torsten Schulz (local)
cff48550ae Changed 2025-10-07 16:00:49 +02:00
39 changed files with 10945 additions and 18485 deletions

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ server.key
server.cert server.cert
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
actualize.sh
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx

View File

@@ -2,4 +2,4 @@ module.exports = {
presets: [ presets: [
'@vue/cli-plugin-babel/preset' '@vue/cli-plugin-babel/preset'
] ]
} };

View File

@@ -1,21 +1,21 @@
{ {
"development": { "development": {
"username": "miriam_user", "username": "miriamgemeinde",
"password": "qTCTTWwpEwy3vPDU", "password": "hitomisan",
"database": "miriamgemeinde", "database": "miriamgemeinde",
"host": "tsschulz.de", "host": "localhost",
"dialect": "mysql" "dialect": "mysql"
}, },
"test": { "test": {
"username": "miriam_user", "username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU", "password": "hitomisan",
"database": "miriamgemeinde", "database": "miriamgemeinde",
"host": "tsschulz.de", "host": "tsschulz.de",
"dialect": "mysql" "dialect": "mysql"
}, },
"production": { "production": {
"username": "miriam_user", "username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU", "password": "hitomisan",
"database": "miriamgemeinde", "database": "miriamgemeinde",
"host": "tsschulz.de", "host": "tsschulz.de",
"dialect": "mysql" "dialect": "mysql"

View File

@@ -1,7 +1,7 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', { const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan', {
host: 'tsschulz.de', host: 'localhost',
dialect: 'mysql', dialect: 'mysql',
retry: { retry: {
match: [ match: [
@@ -26,7 +26,7 @@ const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vP
async function connectWithRetry() { async function connectWithRetry() {
try { try {
await sequelize.authenticate(); await sequelize.authenticate();
console.log('Connection has been established successfully.'); console.log(`Connection has been established successfully. Database server: ${sequelize.config.host}`);
} catch (error) { } catch (error) {
console.error('Unable to connect to the database:', error); console.error('Unable to connect to the database:', error);
setTimeout(connectWithRetry, 5000); setTimeout(connectWithRetry, 5000);

View File

@@ -3,7 +3,16 @@ const { Op } = require('sequelize');
const getAllContactPersons = async (req, res) => { const getAllContactPersons = async (req, res) => {
try { try {
const today = new Date();
today.setHours(0, 0, 0, 0);
const contactPersons = await ContactPerson.findAll({ const contactPersons = await ContactPerson.findAll({
where: {
[Op.or]: [
{ expiryDate: null },
{ expiryDate: { [Op.gte]: today } }
]
},
include: [ include: [
{ {
model: Position, model: Position,
@@ -79,6 +88,14 @@ const filterContactPersons = async (req, res) => {
const where = {}; const where = {};
const having = []; const having = [];
// Filter für nicht abgelaufene Kontaktpersonen
const today = new Date();
today.setHours(0, 0, 0, 0);
where[Op.or] = [
{ expiryDate: null },
{ expiryDate: { [Op.gte]: today } }
];
if (config.selection.id && config.selection.id === 'all') { if (config.selection.id && config.selection.id === 'all') {
// No additional filter needed for "all" // No additional filter needed for "all"
} else if (config.selection.id) { } else if (config.selection.id) {

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
};

File diff suppressed because it is too large Load Diff

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');
}
};

View File

@@ -0,0 +1,16 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'organ_playing', {
type: Sequelize.STRING,
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'organ_playing');
}
};

View File

@@ -0,0 +1,17 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'approved', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'approved');
}
};

View File

@@ -25,6 +25,10 @@ module.exports = (sequelize) => {
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
},
expiryDate: {
type: DataTypes.DATEONLY,
allowNull: true
} }
}, { }, {
tableName: 'contact_persons', tableName: 'contact_persons',

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;
};

View File

@@ -61,6 +61,17 @@ module.exports = (sequelize) => {
allowNull: true, allowNull: true,
field: 'sacristan_service' field: 'sacristan_service'
}, },
organPlaying: {
type: DataTypes.STRING,
allowNull: true,
field: 'organ_playing'
},
approved: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'approved'
},
}, { }, {
tableName: 'worships', tableName: 'worships',
timestamps: true timestamps: true

25917
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
"@tiptap/extension-underline": "^2.4.0", "@tiptap/extension-underline": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0", "@tiptap/starter-kit": "^2.4.0",
"@tiptap/vue-3": "^2.4.0", "@tiptap/vue-3": "^2.4.0",
"@vue/cli": "^5.0.8", "@vue/cli": "^4.2.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
@@ -34,10 +34,12 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"docx": "^9.5.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mammoth": "^1.11.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1", "mysql2": "^3.10.1",

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

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

@@ -1,12 +1,16 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships } = require('../controllers/worshipController'); const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships } = require('../controllers/worshipController');
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
router.get('/', getAllWorships); router.get('/', getAllWorships);
router.get('/options', getWorshipOptions);
router.post('/', authMiddleware, createWorship); router.post('/', authMiddleware, createWorship);
router.post('/import', authMiddleware, uploadImportFile, importWorships);
router.post('/import/save', authMiddleware, saveImportedWorships);
router.put('/:id', authMiddleware, updateWorship); router.put('/:id', authMiddleware, updateWorship);
router.delete('/:id', authMiddleware, deleteWorship); router.delete('/:id', authMiddleware, deleteWorship);
router.get('/filtered', getFilteredWorships); router.get('/filtered', getFilteredWorships);
router.get('/export', authMiddleware, exportWorships);
module.exports = router; module.exports = router;

View File

@@ -2,8 +2,14 @@ const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const cors = require('cors'); const cors = require('cors');
const https = require('https'); const https = require('https');
const http = require('http');
const fs = require('fs'); const fs = require('fs');
require('dotenv').config(); require('dotenv').config();
// Erhöhe maxHttpHeaderSize für Node.js (Standard ist 8KB, erhöhe auf 16KB)
if (http.maxHeaderSize !== undefined) {
http.maxHeaderSize = 16384;
}
const sequelize = require('./config/database'); const sequelize = require('./config/database');
const authRouter = require('./routes/auth'); const authRouter = require('./routes/auth');
const eventTypesRouter = require('./routes/eventtypes'); const eventTypesRouter = require('./routes/eventtypes');
@@ -17,7 +23,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;
@@ -30,9 +37,50 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
app.use(cors({ app.use(cors({
origin: (origin, callback) => { origin: (origin, callback) => {
if (!origin) return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server if (!origin) {
if (allowedOrigins.length === 0) return callback(null, true); // Fallback: alles erlauben return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server
if (allowedOrigins.includes(origin)) return callback(null, true); }
if (allowedOrigins.length === 0) {
return callback(null, true); // Fallback: alles erlauben
}
// Prüfe exakte Übereinstimmung
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
// Für Entwicklung: Erlaube localhost und torstens auf jedem Port
try {
const originUrl = new URL(origin);
const hostname = originUrl.hostname.toLowerCase();
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
const isTorstens = hostname === 'torstens' || hostname.includes('torstens');
if (isLocalhost || isTorstens) {
return callback(null, true);
}
} catch (e) {
// Falls URL-Parsing fehlschlägt, prüfe mit Regex
const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:\d+)?$/.test(origin);
const isTorstens = /^https?:\/\/torstens(:\d+)?/.test(origin);
if (isLocalhost || isTorstens) {
return callback(null, true);
}
}
// Prüfe auch ohne Port (für Flexibilität)
const originWithoutPort = origin.replace(/:\d+$/, '');
const allowedWithoutPort = allowedOrigins.some(allowed => {
const allowedWithoutPort = allowed.replace(/:\d+$/, '');
return originWithoutPort === allowedWithoutPort;
});
if (allowedWithoutPort) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'), false); return callback(new Error('Not allowed by CORS'), false);
}, },
credentials: true, credentials: true,
@@ -41,7 +89,14 @@ app.use(cors({
})); }));
app.options('*', cors()); app.options('*', cors());
app.use(bodyParser.json()); // Erhöhe Header-Limits für große Requests
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
// Erhöhe maxHttpHeaderSize (Node.js 18.3.0+)
if (process.versions.node.split('.')[0] >= 18) {
require('http').maxHeaderSize = 16384; // 16KB (Standard ist 8KB)
}
app.use('/api/auth', authRouter); app.use('/api/auth', authRouter);
app.use('/api/event-types', eventTypesRouter); app.use('/api/event-types', eventTypesRouter);
@@ -56,6 +111,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'),
@@ -67,7 +123,7 @@ sequelize.sync().then(() => {
/* https.createServer(options, app).listen(PORT, () => { /* https.createServer(options, app).listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`); console.log(`Server läuft auf Port ${PORT}`);
});*/ });*/
app.listen(PORT, () => { app.listen(PORT, '0.0.0.0', () => {
console.log(`Server läuft auf Port ${PORT}`); console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
}); });
}); });

View File

@@ -0,0 +1,4 @@
-- Ablaufdatum zu Kontaktpersonen hinzufügen
ALTER TABLE `contact_persons`
ADD COLUMN `expiryDate` DATE NULL AFTER `email`;

View File

@@ -0,0 +1,12 @@
-- Tabelle für liturgische Kalendertage erstellen
CREATE TABLE IF NOT EXISTS `liturgical_days` (
`id` INT NOT NULL AUTO_INCREMENT,
`date` DATE NOT NULL,
`dayName` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Index für schnellere Suche nach dayName
CREATE INDEX `idx_dayName` ON `liturgical_days` (`dayName`);

View File

@@ -2,8 +2,11 @@ import axios from 'axios';
import store from './store'; import store from './store';
import router from './router'; import router from './router';
axios.defaults.baseURL = process.env.VUE_APP_BACKEND_URL; // Einheitliche Basis-URL:
console.log(process.env.VUE_APP_BACKEND_URL); // - immer relativ zur aktuellen Origin
// - kein absoluter http/https-Host → verhindert Mixed-Content-Probleme
axios.defaults.baseURL = '/api';
console.log('Axios baseURL:', axios.defaults.baseURL);
axios.interceptors.request.use( axios.interceptors.request.use(
config => { config => {
@@ -24,8 +27,11 @@ axios.interceptors.response.use(
}, },
error => { error => {
if (error.response && error.response.status === 401) { if (error.response && error.response.status === 401) {
store.dispatch('logout'); store.dispatch('logout').then(() => {
router.push('/auth/login'); if (router.currentRoute.value.path !== '/auth/login') {
router.replace('/auth/login');
}
});
} }
return Promise.reject(error); return Promise.reject(error);
} }

View File

@@ -36,27 +36,53 @@ export default {
<style scoped> <style scoped>
.dialog-overlay { .dialog-overlay {
top: calc(50% - 25em); position: fixed;
left: 5%; top: 0;
width: 90%; left: 0;
height: 50em; width: 100%;
background: rgba(0, 0, 0, .5); height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: auto; z-index: 1000;
} }
.dialog { .dialog {
background: white; background: white;
padding: 20px; padding: 30px;
border-radius: 5px; border-radius: 8px;
max-width: 400px; max-width: 400px;
width: 100%; width: 90%;
text-align: center; text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
button { .dialog h2 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 20px;
}
.dialog p {
margin: 15px 0;
color: #666;
line-height: 1.5;
}
.dialog button {
margin-top: 20px; margin-top: 20px;
padding: 10px 20px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.dialog button:hover {
background-color: #0056b3;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<footer class="footer"> <footer class="footer">
<div class="left-links"> <div class="left-links">
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link> <router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a> <a v-if="isLoggedIn" @click="handleLogout" class="logout-link">Logout</a>
</div> </div>
<div class="right-links"> <div class="right-links">
<router-link to="/terms">Impressum</router-link> <router-link to="/terms">Impressum</router-link>

View File

@@ -20,6 +20,9 @@
<label for="email">Email:</label> <label for="email">Email:</label>
<input type="email" id="email" v-model="localContactPerson.email"> <input type="email" id="email" v-model="localContactPerson.email">
<label for="expiryDate">Ablaufdatum (optional):</label>
<input type="date" id="expiryDate" v-model="localContactPerson.expiryDate">
<label for="positions">Positionen:</label> <label for="positions">Positionen:</label>
<multiselect <multiselect
v-model="selectedPositions" v-model="selectedPositions"
@@ -57,6 +60,7 @@ export default {
zipcode: '', zipcode: '',
city: '', city: '',
email: '', email: '',
expiryDate: null,
positions: [] positions: []
}) })
}, },
@@ -111,6 +115,7 @@ export default {
zipcode: '', zipcode: '',
city: '', city: '',
email: '', email: '',
expiryDate: null,
positions: [] positions: []
}; };
this.selectedPositions = []; this.selectedPositions = [];

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0"> <div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0">
<div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin"> <div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin">
<p>{{ contact.name }}</p> <p>{{ contact.name }} <span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span></p>
<p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</p> <p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</p>
<p v-if="displayOptions.includes('street')">Straße: {{ contact.street }}</p> <p v-if="displayOptions.includes('street')">Straße: {{ contact.street }}</p>
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p> <p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
@@ -13,7 +13,7 @@
</div> </div>
<span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0"> <span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0">
<span v-for="contact in contacts" :key="contact.id" class="bottom-margin"> <span v-for="contact in contacts" :key="contact.id" class="bottom-margin">
{{ contact.name }} {{ contact.name }}<span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
<span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</span> <span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</span>
<span v-if="displayOptions.includes('street')">, Straße: {{ contact.street }}</span> <span v-if="displayOptions.includes('street')">, Straße: {{ contact.street }}</span>
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span> <span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
@@ -58,6 +58,17 @@ export default {
this.loading = false; this.loading = false;
} }
}, },
methods: {
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
}
}; };
</script> </script>
@@ -68,4 +79,9 @@ export default {
.bottom-margin { .bottom-margin {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.expiry-date {
font-size: 0.9em;
color: #666;
font-style: italic;
}
</style> </style>

View File

@@ -312,6 +312,15 @@ export default {
this.assignedImage = null; this.assignedImage = null;
this.imageFilename = ''; this.imageFilename = '';
}, },
focusFirstField() {
// Fokussiert das erste Eingabefeld (Name)
this.$nextTick(() => {
const nameInput = document.getElementById('name');
if (nameInput) {
nameInput.focus();
}
});
}
} }
}; };
</script> </script>

View File

@@ -11,7 +11,7 @@
formatTime(event.endTime) }}</span> Uhr</div> formatTime(event.endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div> <div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div> <div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => cp.name).join(', ')}} <div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div> </div>
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div> <div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div> <div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
@@ -28,7 +28,7 @@
formatTime(events[0].endTime) }}</span> Uhr</div> formatTime(events[0].endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</div> <div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ events[0].description }}</div> <div v-if="shouldDisplay('description')" class="description">{{ events[0].description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => cp.name).join(', ')}} <div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div> </div>
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div> <div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div> <div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
@@ -102,6 +102,18 @@ export default {
const path = '/images/uploads/' + response.data.filename; const path = '/images/uploads/' + response.data.filename;
console.log(path); console.log(path);
return path; return path;
},
formatContactPerson(contactPerson) {
if (!contactPerson.expiryDate) {
return contactPerson.name;
}
const date = new Date(contactPerson.expiryDate);
const formattedDate = date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
return `${contactPerson.name} (bis ${formattedDate})`;
} }
} }
}; };

View File

@@ -11,21 +11,35 @@
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im <div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
Nachbarschaftsraum:</div> Nachbarschaftsraum:</div>
<h3> <h3>
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time) <span
}}</span>&nbsp;-&nbsp; :class="worship.highlightTime ? 'highlight-time' : ''"
{{ worship.title ? worship.title : `Gottesdienst in ${worship.eventPlace.name}` }} >{{ formatTime(worship.time) }}</span>&nbsp;-&nbsp;
{{
worship.title
? worship.title
: (worship.eventPlace && worship.eventPlace.name
? `Gottesdienst in ${worship.eventPlace.name}`
: 'Gottesdienst')
}}
</h3> </h3>
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div> <div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
<div v-if="worship.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div> <div v-if="worship.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div>
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div> <div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>
<div v-if="worship.organPlaying" class="internal-information">Orgelspiel: {{ worship.organPlaying }}</div>
<div v-if="worship.address">{{ worship.address }}</div> <div v-if="worship.address">{{ worship.address }}</div>
<div v-if="!worship.address && worship.eventPlace.id && worship.eventPlace.id"> <div
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ v-if="!worship.address && worship.eventPlace && worship.eventPlace.id"
worship.eventPlace.city }} >
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
</div> </div>
<div v-if="worship.selfInformation" class="selfinformation">Bitte informieren Sie sich auch auf den <div v-if="worship.selfInformation" class="selfinformation">
<a v-if="worship.eventPlace.website" :href="worship.eventPlace.website" target="_blank">Internetseiten dieser Gemeinde!</a><span Bitte informieren Sie sich auch auf den
v-else>Internetseiten dieser Gemeinde!</span> <a
v-if="worship.eventPlace && worship.eventPlace.website"
:href="worship.eventPlace.website"
target="_blank"
>Internetseiten dieser Gemeinde!</a>
<span v-else>Internetseiten dieser Gemeinde!</span>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -33,6 +33,7 @@ export default {
zipcode: '', zipcode: '',
city: '', city: '',
email: '', email: '',
expiryDate: null,
positions: [] positions: []
}, },
positions: [] positions: []

View File

@@ -8,29 +8,29 @@
</select> </select>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<button @click="toggleHeading(3)">H3</button> <button @click="toggleHeading(3)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 3 }) }">H3</button>
<button @click="toggleHeading(4)">H4</button> <button @click="toggleHeading(4)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 4 }) }">H4</button>
<button @click="toggleHeading(5)">H5</button> <button @click="toggleHeading(5)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 5 }) }">H5</button>
<button @click="toggleHeading(6)">H6</button> <button @click="toggleHeading(6)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 6 }) }">H6</button>
<button @click="toggleBold()" width="24" height="24"> <button @click="toggleBold()" :class="{ 'is-active': editor && editor.isActive('bold') }" width="24" height="24">
<BoldIcon width="24" height="24" /> <BoldIcon width="24" height="24" />
</button> </button>
<button @click="toggleItalic()"> <button @click="toggleItalic()" :class="{ 'is-active': editor && editor.isActive('italic') }">
<ItalicIcon width="24" height="24" /> <ItalicIcon width="24" height="24" />
</button> </button>
<button @click="toggleUnderline()"> <button @click="toggleUnderline()" :class="{ 'is-active': editor && editor.isActive('underline') }">
<UnderlineIcon width="24" height="24" /> <UnderlineIcon width="24" height="24" />
</button> </button>
<button @click="toggleStrike()"> <button @click="toggleStrike()" :class="{ 'is-active': editor && editor.isActive('strike') }">
<StrikethroughIcon width="24" height="24" /> <StrikethroughIcon width="24" height="24" />
</button> </button>
<button @click="insertTable()"> <button @click="insertTable()">
<TableIcon width="24" height="24" /> <TableIcon width="24" height="24" />
</button> </button>
<button @click="toggleBulletList()"> <button @click="toggleBulletList()" :class="{ 'is-active': editor && editor.isActive('bulletList') }">
<ListIcon width="24" height="24" /> <ListIcon width="24" height="24" />
</button> </button>
<button @click="toggleOrderedList()"> <button @click="toggleOrderedList()" :class="{ 'is-active': editor && editor.isActive('orderedList') }">
<NumberedListLeftIcon width="24" height="24" /> <NumberedListLeftIcon width="24" height="24" />
</button> </button>
<button @click="openAddImageDialog"> <button @click="openAddImageDialog">
@@ -511,4 +511,15 @@ export default {
.align-top { .align-top {
vertical-align: top; vertical-align: top;
} }
.toolbar button.is-active,
.table-toolbar button.is-active {
background-color: #333;
color: white;
}
.toolbar button.is-active svg,
.table-toolbar button.is-active svg {
fill: white;
}
</style> </style>

View File

@@ -3,12 +3,30 @@
<h2>Veranstaltungen</h2> <h2>Veranstaltungen</h2>
<button @click="createEvent">Neue Veranstaltung</button> <button @click="createEvent">Neue Veranstaltung</button>
<EventForm v-if="showForm" <EventForm v-if="showForm"
ref="eventForm"
:event="selectedEvent" :event="selectedEvent"
:institutions="institutions" :institutions="institutions"
:eventPlaces="eventPlaces" :eventPlaces="eventPlaces"
:contactPersons="contactPersons" :contactPersons="contactPersons"
@saved="handleEventSaved" @saved="handleEventSaved"
@cancelled="handleEventCancelled" /> @cancelled="handleEventCancelled" />
<div class="filter-section">
<input
v-model="searchQuery"
type="text"
placeholder="Suche nach Name, Typ, Beschreibung..."
class="search-input"
/>
<label class="checkbox-label">
<input
v-model="showPastEvents"
type="checkbox"
/>
Vergangene Events anzeigen
</label>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -22,7 +40,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="event in events" :key="event.id"> <tr v-for="event in filteredEvents" :key="event.id">
<td>{{ event.name }}</td> <td>{{ event.name }}</td>
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td> <td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
<td>{{ event.date }}</td> <td>{{ event.date }}</td>
@@ -55,8 +73,52 @@ export default {
eventTypes: [], eventTypes: [],
selectedEvent: null, selectedEvent: null,
showForm: false, showForm: false,
searchQuery: '',
showPastEvents: false,
}; };
}, },
computed: {
filteredEvents() {
let filtered = this.events;
// Filter vergangene Events aus
if (!this.showPastEvents) {
const today = new Date();
today.setHours(0, 0, 0, 0);
filtered = filtered.filter(event => {
// Events mit Wochentag (ohne festes Datum) immer anzeigen
if (event.dayOfWeek !== null && event.dayOfWeek !== undefined && !event.date) {
return true;
}
// Events mit Datum: nur zukünftige oder heutige anzeigen
if (event.date) {
const eventDate = new Date(event.date);
eventDate.setHours(0, 0, 0, 0);
return eventDate >= today;
}
// Events ohne Datum und ohne Wochentag anzeigen
return true;
});
}
// Suchfilter anwenden
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(event => {
const name = event.name ? event.name.toLowerCase() : '';
const description = event.description ? event.description.toLowerCase() : '';
const eventType = this.getEventTypeCaption(event.eventTypeId).toLowerCase();
return name.includes(query) ||
description.includes(query) ||
eventType.includes(query);
});
}
return filtered;
}
},
async created() { async created() {
await this.fetchData(); await this.fetchData();
}, },
@@ -84,10 +146,24 @@ export default {
createEvent() { createEvent() {
this.selectedEvent = {}; this.selectedEvent = {};
this.showForm = true; this.showForm = true;
this.scrollToFormAndFocus();
}, },
editEvent(event) { editEvent(event) {
this.selectedEvent = { ...event }; this.selectedEvent = { ...event };
this.showForm = true; this.showForm = true;
this.scrollToFormAndFocus();
},
scrollToFormAndFocus() {
// Wartet auf das Rendern des Formulars und scrollt dann nach oben
this.$nextTick(() => {
// Nach oben scrollen
window.scrollTo({ top: 0, behavior: 'smooth' });
// Das erste Feld fokussieren
if (this.$refs.eventForm) {
this.$refs.eventForm.focusFirstField();
}
});
}, },
async deleteEvent(id) { async deleteEvent(id) {
try { try {
@@ -110,7 +186,7 @@ export default {
}, },
getWeekdayName(dayOfWeek) { getWeekdayName(dayOfWeek) {
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return weekdays[dayOfWeek - 1]; return weekdays[dayOfWeek];
}, },
} }
}; };
@@ -122,6 +198,40 @@ export default {
margin: 0 auto; margin: 0 auto;
} }
.filter-section {
margin: 20px 0;
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 250px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: #4CAF50;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
cursor: pointer;
}
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,14 @@
<div class="privacy-policy"> <div class="privacy-policy">
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1> <h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
<p> <p>
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend der kirchlichen Datenschutzgesetze sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle. Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
</p> </p>
<h2>Anbieter:</h2> <h2>Anbieter:</h2>
<p> <p>
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br /> Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<br /> Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<br />
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br /> Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br />
Inhaltlich Verantwortlicher gemäß § 6 MDStV: Torsten Schulz Inhaltlich Verantwortlicher gemäß § 6 DDG: Torsten Schulz
</p> </p>
<p> <p>
Die Nutzung der Webseite der Miriamgemeinde Frankfurt am Main ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Die nachfolgende Erklärung gibt Ihnen einen Überblick darüber, wie dieser Schutz gewährleistet werden soll und welche Art von Daten zu welchem Zweck von Ihnen erhoben werden. Die Nutzung der Webseite der Miriamgemeinde Frankfurt am Main ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Die nachfolgende Erklärung gibt Ihnen einen Überblick darüber, wie dieser Schutz gewährleistet werden soll und welche Art von Daten zu welchem Zweck von Ihnen erhoben werden.
@@ -63,11 +63,23 @@
</p> </p>
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3> <h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
<p> <p>
Sie haben als Nutzer das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt. Darüber hinaus können Sie unrichtige Daten berichtigen oder solche Daten löschen lassen, deren Speicherung unzulässig oder nicht mehr erforderlich ist. Sie haben die Rechte auf Datenübertragbarkeit, Einschränkung der Verarbeitung und Widerspruch. Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist Der Beauftragte für den Datenschutz der EKD Adresse siehe unten. Sie haben als Nutzer nach dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) folgende Rechte:
</p> </p>
<ul>
<li><strong>Recht auf Auskunft:</strong> Sie haben das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt.</li>
<li><strong>Recht auf Berichtigung:</strong> Sie können unrichtige oder unvollständige Daten berichtigen lassen.</li>
<li><strong>Recht auf Löschung:</strong> Sie können die Löschung Ihrer Daten verlangen, wenn diese nicht mehr erforderlich sind oder wenn die Verarbeitung rechtswidrig war.</li>
<li><strong>Recht auf Einschränkung der Verarbeitung:</strong> Sie können eine Einschränkung der Verarbeitung Ihrer Daten verlangen.</li>
<li><strong>Recht auf Widerspruch:</strong> Sie haben das Recht, der Verarbeitung Ihrer personenbezogenen Daten zu widersprechen, wenn diese auf Grund von berechtigtem Interesse erfolgt.</li>
<li><strong>Recht auf Datenübertragbarkeit:</strong> Sie können verlangen, dass die Daten, die Sie bereitgestellt haben, in einem strukturierten, gängigen und maschinenlesbaren Format ausgehändigt werden.</li>
<li><strong>Recht auf Widerruf:</strong> Sie können jederzeit die Einwilligung zur Datenverarbeitung widerrufen, ohne dass die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung berührt wird.</li>
</ul>
<p> <p>
Sie erhalten jederzeit ohne Angabe von Gründen kostenfrei Auskunft über Ihre bei uns gespeicherten Daten. Sie können jederzeit Ihre bei uns erhobenen Daten sperren, berichtigen oder löschen lassen. Auch können Sie jederzeit die uns erteilte Einwilligung zur Datenerhebung und Verwendung ohne Angaben von Gründen widerrufen. Wenden Sie sich hierzu bitte an die auf dieser Seite angegebene Kontaktadresse des Datenschutzbeauftragten. Wir stehen Ihnen jederzeit gern für weitergehende Fragen zu unserem Hinweisen zum Datenschutz und zur Verarbeitung Ihrer persönlichen Daten zur Verfügung. Sie erhalten jederzeit ohne Angabe von Gründen kostenfrei Auskunft über Ihre bei uns gespeicherten Daten. Sie können jederzeit Ihre bei uns erhobenen Daten sperren, berichtigen oder löschen lassen. Auch können Sie jederzeit die uns erteilte Einwilligung zur Datenerhebung und Verwendung ohne Angaben von Gründen widerrufen. Wenden Sie sich hierzu bitte an die auf dieser Seite angegebene Kontaktadresse des Datenschutzbeauftragten. Wir stehen Ihnen jederzeit gern für weitergehende Fragen zu unserem Hinweisen zum Datenschutz und zur Verarbeitung Ihrer persönlichen Daten zur Verfügung.
</p> </p>
<p>
Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist der Beauftragte für den Datenschutz der EKD Adresse siehe unten.
</p>
<h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3> <h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3>
<p> <p>
Die Aufsicht über die Einhaltung der Vorschriften zum Datenschutz obliegt im kirchlichen Bereich dem Beauftragten für den Datenschutz der EKD. Für den Bereich der Evangelischen Kirche in Hessen und Nassau (EKHN) ist zuständig die Außenstelle Dortmund für die Datenschutzregion Mitte-West Die Aufsicht über die Einhaltung der Vorschriften zum Datenschutz obliegt im kirchlichen Bereich dem Beauftragten für den Datenschutz der EKD. Für den Bereich der Evangelischen Kirche in Hessen und Nassau (EKHN) ist zuständig die Außenstelle Dortmund für die Datenschutzregion Mitte-West
@@ -96,7 +108,7 @@
<p> <p>
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren. Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
</p> </p>
<p>Stand: 24. Mai 2018</p> <p>Stand: Januar 2025</p>
</div> </div>
</template> </template>

View File

@@ -24,7 +24,7 @@
</p> </p>
</section> </section>
<section> <section>
<h3>Inhaltlich Verantwortlicher gemäß § 6 MDStV:</h3> <h3>Inhaltlich Verantwortlicher gemäß § 6 DDG:</h3>
<p>Torsten Schulz</p> <p>Torsten Schulz</p>
</section> </section>
<section> <section>

View File

@@ -5,14 +5,19 @@ import store from './store';
import axios from './axios'; import axios from './axios';
import './assets/css/editor.css'; import './assets/css/editor.css';
// Menü-Daten über das konfigurierte Axios-Backend laden
async function fetchMenuData() { async function fetchMenuData() {
const response = await fetch(process.env.VUE_APP_BACKEND_URL + '/menu-data'); const response = await axios.get('/menu-data');
return await response.json(); return response.data;
} }
fetchMenuData().then(menuData => { fetchMenuData()
store.commit('setMenuData', menuData); .then(menuData => {
}); store.commit('setMenuData', menuData);
})
.catch(err => {
console.error('Fehler beim Laden der Menü-Daten:', err);
});
const app = createApp(AppComponent); const app = createApp(AppComponent);
app.use(router); app.use(router);

View File

@@ -45,8 +45,13 @@ const router = createRouter({
routes: [] routes: []
}); });
// Verhindert endlose Wiederholungen von fehlgeschlagenen Menü-Ladeversuchen
let menuDataInitialized = false;
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
if (!store.state.menuData.length) { if (!menuDataInitialized) {
menuDataInitialized = true;
await store.dispatch('loadMenuData'); await store.dispatch('loadMenuData');
const routes = generateRoutesFromMenu(store.state.menuData); const routes = generateRoutesFromMenu(store.state.menuData);
routes.forEach(route => router.addRoute(route)); routes.forEach(route => router.addRoute(route));
@@ -65,8 +70,12 @@ router.beforeEach(async (to, from, next) => {
next({ ...to, replace: true }); next({ ...to, replace: true });
} else { } else {
// Sicherstellen, dass die Login-Route immer verfügbar ist
if (!router.hasRoute('auth-login')) {
addAuthLoginRoute();
}
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) { if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
next('/login'); next('/auth/login');
} else { } else {
next(); next();
} }

View File

@@ -1,5 +1,5 @@
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import axios from 'axios'; import axios from '../axios';
import router from '../router'; import router from '../router';
let user = []; let user = [];
@@ -35,7 +35,6 @@ export default createStore({
localStorage.removeItem('isLoggedIn'); localStorage.removeItem('isLoggedIn');
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.removeItem('token'); localStorage.removeItem('token');
router.push('/auth/login');
}, },
setMenuData(state, menuData) { setMenuData(state, menuData) {
state.menuData = menuData; state.menuData = menuData;
@@ -101,6 +100,10 @@ export default createStore({
console.error('Fehler beim Logout:', error); console.error('Fehler beim Logout:', error);
} finally { } finally {
commit('logout'); commit('logout');
// Navigation nach Logout mit replace, damit die Login-Seite direkt erreichbar ist
if (router.currentRoute.value.path !== '/auth/login') {
router.replace('/auth/login');
}
} }
} }
}, },

View File

@@ -5,7 +5,24 @@ module.exports = defineConfig({
transpileDependencies: [], transpileDependencies: [],
devServer: { devServer: {
host: 'localhost', host: 'localhost',
port: 8080 // Port kann über VUE_APP_FRONTEND_PORT oder FRONTEND_PORT in .env gesetzt werden
port: parseInt(process.env.VUE_APP_FRONTEND_PORT || process.env.FRONTEND_PORT || '8080', 10),
// Proxy für API-Requests zum Backend-Server
// Backend sollte auf einem anderen Port laufen (z.B. 3010)
proxy: {
'/api': {
target: process.env.VUE_APP_BACKEND_PROXY || 'http://torstens:3010',
changeOrigin: true,
secure: false,
logLevel: 'debug',
// Erhöhe Header-Limits für Proxy
headers: {
'Connection': 'keep-alive'
},
// Erhöhe Timeout für große Requests
timeout: 60000
}
}
}, },
configureWebpack: { configureWebpack: {
output: { clean: true }, output: { clean: true },