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.

This commit is contained in:
Torsten Schulz (local)
2025-11-22 22:07:36 +01:00
parent 6c54bc9d49
commit 44c978f21e
19 changed files with 2810 additions and 63 deletions

3
.gitignore vendored
View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

369
package-lock.json generated
View File

@@ -33,10 +33,12 @@
"cors": "^2.8.5",
"crypto": "^1.0.1",
"date-fns": "^3.6.0",
"docx": "^9.5.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"file-saver": "^2.0.5",
"jsonwebtoken": "^9.0.2",
"mammoth": "^1.11.0",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1",
@@ -3617,11 +3619,12 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "20.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~7.16.0"
}
},
"node_modules/@types/node-forge": {
@@ -4862,6 +4865,15 @@
"@xtuc/long": "4.2.2"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -7873,6 +7885,12 @@
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -7908,6 +7926,41 @@
"node": ">=6.0.0"
}
},
"node_modules/docx": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
"license": "MIT",
"dependencies": {
"@types/node": "^24.0.1",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -8056,6 +8109,15 @@
"node": ">=4"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -10457,7 +10519,6 @@
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@@ -10795,6 +10856,12 @@
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -11867,6 +11934,18 @@
"node": ">=10"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -11951,6 +12030,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -12350,6 +12438,17 @@
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lowdb": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz",
@@ -12423,6 +12522,54 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mammoth": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz",
"integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mammoth/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/mammoth/node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/mammoth/node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -12723,8 +12870,7 @@
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
@@ -13464,6 +13610,12 @@
"opener": "bin/opener-bin.js"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -13687,6 +13839,12 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -16130,6 +16288,12 @@
"node": ">=0.10.0"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -17694,10 +17858,17 @@
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
@@ -19097,6 +19268,24 @@
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
@@ -21751,11 +21940,11 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"@types/node": {
"version": "20.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"requires": {
"undici-types": "~5.26.4"
"undici-types": "~7.16.0"
}
},
"@types/node-forge": {
@@ -22763,6 +22952,11 @@
"@xtuc/long": "4.2.2"
}
},
"@xmldom/xmldom": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="
},
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -24960,6 +25154,11 @@
}
}
},
"dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -24986,6 +25185,26 @@
"esutils": "^2.0.2"
}
},
"docx": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
"requires": {
"@types/node": "^24.0.1",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"dependencies": {
"nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="
}
}
},
"dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -25105,6 +25324,14 @@
"rimraf": "^3.0.0"
}
},
"duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"requires": {
"underscore": "^1.13.1"
}
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -26875,7 +27102,6 @@
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@@ -27108,6 +27334,11 @@
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -27876,6 +28107,17 @@
}
}
},
"jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -27948,6 +28190,14 @@
"type-check": "~0.4.0"
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"requires": {
"immediate": "~3.0.5"
}
},
"lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -28263,6 +28513,16 @@
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"requires": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"lowdb": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz",
@@ -28322,6 +28582,43 @@
"semver": "^6.0.0"
}
},
"mammoth": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz",
"integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==",
"requires": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="
}
}
},
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -28544,8 +28841,7 @@
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
@@ -29088,6 +29384,11 @@
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true
},
"option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="
},
"optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -29247,6 +29548,11 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -31024,6 +31330,11 @@
}
}
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -32204,10 +32515,15 @@
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
},
"underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
},
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
@@ -33204,6 +33520,19 @@
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"requires": {}
},
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",

View File

@@ -34,10 +34,12 @@
"cors": "^2.8.5",
"crypto": "^1.0.1",
"date-fns": "^3.6.0",
"docx": "^9.5.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"file-saver": "^2.0.5",
"jsonwebtoken": "^9.0.2",
"mammoth": "^1.11.0",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1",

View File

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

View File

@@ -2,8 +2,14 @@ const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const https = require('https');
const http = require('http');
const fs = require('fs');
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 authRouter = require('./routes/auth');
const eventTypesRouter = require('./routes/eventtypes');
@@ -31,9 +37,50 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server
if (allowedOrigins.length === 0) return callback(null, true); // Fallback: alles erlauben
if (allowedOrigins.includes(origin)) return callback(null, true);
if (!origin) {
return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server
}
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);
},
credentials: true,
@@ -42,7 +89,14 @@ app.use(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/event-types', eventTypesRouter);
@@ -69,7 +123,7 @@ sequelize.sync().then(() => {
/* https.createServer(options, app).listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
});*/
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
});
});

View File

@@ -24,8 +24,11 @@ axios.interceptors.response.use(
},
error => {
if (error.response && error.response.status === 401) {
store.dispatch('logout');
router.push('/auth/login');
store.dispatch('logout').then(() => {
if (router.currentRoute.value.path !== '/auth/login') {
router.replace('/auth/login');
}
});
}
return Promise.reject(error);
}

View File

@@ -36,27 +36,53 @@ export default {
<style scoped>
.dialog-overlay {
top: calc(50% - 25em);
left: 5%;
width: 90%;
height: 50em;
background: rgba(0, 0, 0, .5);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
z-index: 1000;
}
.dialog {
background: white;
padding: 20px;
border-radius: 5px;
padding: 30px;
border-radius: 8px;
max-width: 400px;
width: 100%;
width: 90%;
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;
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>

View File

@@ -2,7 +2,7 @@
<footer class="footer">
<div class="left-links">
<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 class="right-links">
<router-link to="/terms">Impressum</router-link>

View File

@@ -18,6 +18,7 @@
<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.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.eventPlace.id && worship.eventPlace.id">
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{

View File

@@ -1,6 +1,168 @@
<template>
<div class="worship-management">
<h2>Gottesdienst Verwaltung</h2>
<div class="action-buttons">
<button type="button" @click="toggleImportSection" class="import-button">
{{ showImportSection ? 'Import ausblenden' : 'Import' }}
</button>
<button type="button" @click="toggleExportSection" class="export-button">
{{ showExportSection ? 'Export ausblenden' : 'Export' }}
</button>
</div>
<div v-if="showImportSection" class="import-section">
<h3>Gottesdienste importieren</h3>
<div class="import-content">
<label for="import-file">Datei auswählen (.doc, .docx):</label>
<input
type="file"
id="import-file"
ref="fileInput"
@change="handleFileSelect"
accept=".doc,.docx"
/>
<div v-if="selectedFile" class="selected-file">
Ausgewählte Datei: {{ selectedFile.name }}
</div>
<button type="button" @click="importWorships" :disabled="!selectedFile || isImporting" class="submit-import-button">
{{ isImporting ? 'Importiere...' : 'Importieren' }}
</button>
</div>
</div>
<div v-if="showExportSection" class="export-section">
<h3>Gottesdienste exportieren</h3>
<div class="export-content">
<label for="export-date-from">Von Datum:</label>
<input
type="date"
id="export-date-from"
v-model="exportDateFrom"
/>
<label for="export-date-to">Bis Datum:</label>
<input
type="date"
id="export-date-to"
v-model="exportDateTo"
/>
<label for="export-format">Export-Format:</label>
<select id="export-format" v-model="exportFormat">
<option value="editing">Für Bearbeitung</option>
<option value="newsletter">Für Gemeindebrief</option>
</select>
<button type="button" @click="exportWorships" :disabled="!exportDateFrom || !exportDateTo || isExporting" class="submit-export-button">
{{ isExporting ? 'Exportiere...' : 'Exportieren' }}
</button>
</div>
</div>
<!-- Dialog zur Bearbeitung der importierten Gottesdienste -->
<div v-if="showImportDialog" class="import-dialog-overlay" @click.self="closeImportDialog">
<div class="import-dialog-content">
<div class="import-dialog-header">
<h3>Importierte Gottesdienste bearbeiten</h3>
<button class="close-button" @click="closeImportDialog">&times;</button>
</div>
<div class="import-dialog-body">
<div v-if="importErrors && importErrors.length > 0" class="import-errors">
<h4>Fehler beim Parsen:</h4>
<ul>
<li v-for="(error, index) in importErrors" :key="index">{{ error }}</li>
</ul>
</div>
<div class="imported-worships-list">
<div v-for="(worship, index) in importedWorships" :key="index" class="imported-worship-item">
<h4>
Gottesdienst {{ index + 1 }}
<span v-if="worship._isNew" class="new-badge">NEU</span>
<span v-else-if="worship._isUpdate" class="update-badge">ÄNDERUNG</span>
</h4>
<div class="worship-edit-fields">
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'date') }">
<label>
Datum:
<span v-if="isFieldChanged(worship, 'date')" class="old-value">(alt: {{ getOldValue(worship, 'date') }})</span>
</label>
<input type="date" v-model="worship.date" />
</div>
<div class="field-group">
<label>Tag-Name:</label>
<input type="text" v-model="worship.dayName" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'time') }">
<label>
Uhrzeit:
<span v-if="isFieldChanged(worship, 'time')" class="old-value">(alt: {{ getOldValue(worship, 'time') }})</span>
</label>
<input type="time" v-model="worship.time" step="60" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'eventPlaceId') }">
<label>
Ort:
<span v-if="isFieldChanged(worship, 'eventPlaceId')" class="old-value">(alt: {{ getOldValue(worship, 'eventPlaceName') || getOldValue(worship, 'eventPlaceId') }})</span>
</label>
<multiselect
v-model="worship.eventPlace"
:options="eventPlaces"
label="name"
track-by="id"
placeholder="Veranstaltungsort wählen"
@update:modelValue="(value) => { if (value) worship.eventPlaceId = value.id; }"
></multiselect>
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'title') }">
<label>
Titel:
<span v-if="isFieldChanged(worship, 'title')" class="old-value">(alt: {{ getOldValue(worship, 'title') }})</span>
</label>
<input type="text" v-model="worship.title" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'organizer') }">
<label>
Gestalter:
<span v-if="isFieldChanged(worship, 'organizer')" class="old-value">(alt: {{ getOldValue(worship, 'organizer') }})</span>
</label>
<input type="text" v-model="worship.organizer" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'collection') }">
<label>
Kollekte:
<span v-if="isFieldChanged(worship, 'collection')" class="old-value">(alt: {{ getOldValue(worship, 'collection') }})</span>
</label>
<input type="text" v-model="worship.collection" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'sacristanService') }">
<label>
Dienst:
<span v-if="isFieldChanged(worship, 'sacristanService')" class="old-value">(alt: {{ getOldValue(worship, 'sacristanService') }})</span>
</label>
<input type="text" v-model="worship.sacristanService" />
</div>
<div class="field-group" :class="{ 'field-changed': isFieldChanged(worship, 'organPlaying') }">
<label>
Orgelspiel:
<span v-if="isFieldChanged(worship, 'organPlaying')" class="old-value">(alt: {{ getOldValue(worship, 'organPlaying') }})</span>
</label>
<input type="text" v-model="worship.organPlaying" />
</div>
<div class="field-group">
<label>
<input type="checkbox" v-model="worship.approved" />
Freigegeben
</label>
</div>
<button type="button" @click="removeWorship(index)" class="remove-button">Entfernen</button>
</div>
</div>
</div>
</div>
<div class="import-dialog-footer">
<button type="button" @click="closeImportDialog" class="cancel-button">Abbrechen</button>
<button type="button" @click="saveImportedWorships" :disabled="importedWorships.length === 0 || isImporting" class="save-button">
{{ isImporting ? 'Speichere...' : 'Speichern' }}
</button>
</div>
</div>
</div>
<div class="liturgical-loader">
<select v-model="selectedYear" class="year-select">
<option v-for="year in availableYears" :key="year" :value="year">{{ year }}</option>
@@ -55,6 +217,9 @@
<label for="neighborInvitation">Einladung zum Nachbarschaftsraum:</label>
<input type="checkbox" id="neighborInvitation" v-model="worshipData.neighborInvitation">
<label for="approved">Freigegeben:</label>
<input type="checkbox" id="approved" v-model="worshipData.approved">
<label for="introLine">Einleitungszeile:</label>
<input type="text" id="introLine" v-model="worshipData.introLine">
@@ -74,11 +239,27 @@
</div>
<ul>
<li v-for="worship in filteredWorships" :key="worship.id"
:class="dateIsLowerCurrentDate(worship.date) ? 'old-items' : ''">
<span>{{ worship.title }} - {{ formatDate(worship.date) }}, {{ formatTime(worship.time) }}</span>
<button @click="editWorship(worship)">Bearbeiten</button>
<button @click="deleteWorship(worship.id)">Löschen</button>
<li
v-for="worship in filteredWorships"
:key="worship.id"
:class="[
'worship-list-item',
{ 'old-items': dateIsLowerCurrentDate(worship.date) },
{ 'not-approved': !worship.approved }
]"
>
<span>
{{ worship.title }} - {{ formatDate(worship.date) }}, {{ formatTime(worship.time) }}
</span>
<button
type="button"
class="approve-toggle-button"
@click="toggleApproved(worship)"
>
{{ worship.approved ? 'Freigabe zurücknehmen' : 'Freigeben' }}
</button>
<button type="button" @click="editWorship(worship)">Bearbeiten</button>
<button type="button" @click="deleteWorship(worship.id)">Löschen</button>
<div class="tooltip">{{ getEventPlaceName(worship.eventPlaceId) }}</div>
</li>
</ul>
@@ -124,12 +305,24 @@ export default {
sacristanService: '',
website: '',
dayName: '',
approved: false,
},
selectedEventPlace: null,
editMode: false,
editId: null,
searchDate: '',
showPastWorships: false,
showImportSection: false,
selectedFile: null,
isImporting: false,
showExportSection: false,
exportDateFrom: '',
exportDateTo: '',
exportFormat: 'editing',
isExporting: false,
showImportDialog: false,
importedWorships: [],
importErrors: [],
};
},
computed: {
@@ -184,6 +377,15 @@ export default {
await this.fetchLiturgicalDays();
},
methods: {
isFieldChanged(worship, fieldName) {
return worship._changedFields && worship._changedFields.includes(fieldName);
},
getOldValue(worship, fieldName) {
if (worship._oldValues && worship._oldValues[fieldName]) {
return worship._oldValues[fieldName];
}
return '';
},
formatTime,
formatDate,
async fetchWorships() {
@@ -323,7 +525,8 @@ export default {
eventPlaceId: this.selectedEventPlace ? this.selectedEventPlace.id : null,
organizer: this.selectedOrganizers.map(org => org.name).join(', '),
sacristanService: this.selectedSacristans.map(sac => sac.name).join(', '),
dayName: this.selectedDayName ? this.selectedDayName.dayName : ''
dayName: this.selectedDayName ? this.selectedDayName.dayName : '',
approved: !!this.worshipData.approved,
};
if (this.editMode) {
@@ -339,6 +542,16 @@ export default {
console.error('Fehler beim Speichern des Gottesdienstes:', error);
}
},
async toggleApproved(worship) {
try {
const newApproved = !worship.approved;
await axios.put(`/worships/${worship.id}`, { approved: newApproved });
worship.approved = newApproved;
} catch (error) {
console.error('Fehler beim Aktualisieren des Freigabe-Status:', error);
alert('Fehler beim Aktualisieren des Freigabe-Status.');
}
},
editWorship(worship) {
this.worshipData = { ...worship };
this.worshipData.date = formatDate(worship.date).split(".").reverse().join("-");
@@ -388,7 +601,8 @@ export default {
highlightTime: false,
neighborInvitation: false,
introLine: '',
dayName: ''
dayName: '',
approved: false,
};
this.selectedEventPlace = null;
this.selectedOrganizers = [];
@@ -429,6 +643,249 @@ export default {
this.dayNameOptions.push(tag);
this.selectedDayName = tag;
this.worshipData.dayName = newTag;
},
toggleImportSection() {
this.showImportSection = !this.showImportSection;
if (!this.showImportSection) {
// Reset beim Ausblenden
this.selectedFile = null;
if (this.$refs.fileInput) {
this.$refs.fileInput.value = '';
}
}
},
handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
// Validierung: Nur .doc und .docx Dateien erlauben
const allowedExtensions = ['.doc', '.docx'];
const fileName = file.name.toLowerCase();
const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!isValidFile) {
alert('Bitte wählen Sie nur .doc oder .docx Dateien aus.');
event.target.value = '';
this.selectedFile = null;
return;
}
this.selectedFile = file;
} else {
this.selectedFile = null;
}
},
async importWorships() {
if (!this.selectedFile) {
alert('Bitte wählen Sie eine Datei aus.');
return;
}
this.isImporting = true;
const formData = new FormData();
formData.append('file', this.selectedFile);
try {
const response = await axios.post('/worships/import', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
// Geparste Daten im Dialog anzeigen
if (response.data.worships && response.data.worships.length > 0) {
// EventPlace-Objekte zuordnen
// Das Datum kommt bereits im YYYY-MM-DD Format vom Backend
this.importedWorships = response.data.worships.map(w => {
const eventPlace = this.eventPlaces.find(ep => ep.id === w.eventPlaceId);
// Normalisiere Uhrzeit: entferne Sekunden für Anzeige
let timeValue = w.time;
if (timeValue && typeof timeValue === 'string' && timeValue.length > 5) {
timeValue = timeValue.substring(0, 5);
}
return {
...w,
// Stelle sicher, dass das Datum ein String im YYYY-MM-DD Format ist
date: typeof w.date === 'string' ? w.date.split('T')[0] : w.date,
time: timeValue,
eventPlace: eventPlace || null,
approved: false,
// Stelle sicher, dass _changedFields und _oldValues erhalten bleiben
_changedFields: w._changedFields || [],
_oldValues: w._oldValues || {},
_isUpdate: w._isUpdate || false,
_isNew: w._isNew || false,
_existingId: w._existingId || null
};
});
this.importErrors = response.data.errors || [];
this.showImportDialog = true;
} else {
alert('Keine Gottesdienste in der Datei gefunden.');
}
} catch (error) {
console.error('Fehler beim Importieren der Gottesdienste:', error);
const errorMessage = error.response?.data?.message || 'Fehler beim Importieren der Datei.';
alert('Fehler: ' + errorMessage);
} finally {
this.isImporting = false;
}
},
closeImportDialog() {
this.showImportDialog = false;
this.importedWorships = [];
this.importErrors = [];
this.selectedFile = null;
if (this.$refs.fileInput) {
this.$refs.fileInput.value = '';
}
},
removeWorship(index) {
this.importedWorships.splice(index, 1);
},
async saveImportedWorships() {
if (this.importedWorships.length === 0) {
alert('Keine Gottesdienste zum Speichern vorhanden.');
return;
}
this.isImporting = true;
// Daten für das Backend vorbereiten
const worshipsToSave = this.importedWorships.map(w => {
// Stelle sicher, dass das Datum im richtigen Format ist (YYYY-MM-DD)
let dateStr = w.date;
if (w.date instanceof Date) {
const year = w.date.getFullYear();
const month = String(w.date.getMonth() + 1).padStart(2, '0');
const day = String(w.date.getDate()).padStart(2, '0');
dateStr = `${year}-${month}-${day}`;
} else if (typeof w.date === 'string') {
// Falls bereits String, verwende direkt (sollte YYYY-MM-DD sein)
dateStr = w.date.split('T')[0];
}
// Stelle sicher, dass die Uhrzeit im Format HH:MM:00 ist (mit Sekunden für DB)
let timeValue = w.time;
if (timeValue && typeof timeValue === 'string') {
// Wenn nur HH:MM vorhanden, füge :00 hinzu
if (timeValue.length === 5) {
timeValue = timeValue + ':00';
}
}
const worshipData = {
date: dateStr,
dayName: w.dayName,
time: timeValue,
title: w.title,
organizer: w.organizer,
collection: w.collection,
sacristanService: w.sacristanService,
organPlaying: w.organPlaying,
approved: w.approved || false,
eventPlaceId: w.eventPlace ? w.eventPlace.id : (w.eventPlaceId || null),
};
return worshipData;
});
try {
const response = await axios.post('/worships/import/save', {
worships: worshipsToSave
});
// Erfolgsmeldung anzeigen
let message = response.data.message || 'Import erfolgreich abgeschlossen!';
if (response.data.imported !== undefined || response.data.updated !== undefined) {
message = `Import abgeschlossen!\n`;
if (response.data.imported !== undefined) {
message += `- ${response.data.imported} neue Gottesdienste erstellt\n`;
}
if (response.data.updated !== undefined) {
message += `- ${response.data.updated} Gottesdienste aktualisiert\n`;
}
if (response.data.skipped !== undefined && response.data.skipped > 0) {
message += `- ${response.data.skipped} übersprungen (vergangene Daten)\n`;
}
}
if (response.data.errors && response.data.errors.length > 0) {
message += `\nFehler: ${response.data.errors.length}`;
}
alert(message);
// Dialog schließen und Daten aktualisieren
this.closeImportDialog();
await this.fetchWorships();
await this.fetchWorshipOptions();
await this.fetchLiturgicalDays();
} catch (error) {
console.error('Fehler beim Speichern der Gottesdienste:', error);
const errorMessage = error.response?.data?.message || 'Fehler beim Speichern der Gottesdienste.';
alert('Fehler: ' + errorMessage);
} finally {
this.isImporting = false;
}
},
toggleExportSection() {
this.showExportSection = !this.showExportSection;
if (!this.showExportSection) {
// Reset beim Ausblenden
this.exportDateFrom = '';
this.exportDateTo = '';
this.exportFormat = 'editing';
}
},
async exportWorships() {
if (!this.exportDateFrom || !this.exportDateTo) {
alert('Bitte wählen Sie einen Datumsbereich aus.');
return;
}
if (new Date(this.exportDateFrom) > new Date(this.exportDateTo)) {
alert('Das "Von Datum" muss vor dem "Bis Datum" liegen.');
return;
}
this.isExporting = true;
try {
const response = await axios.get('/worships/export', {
params: {
from: this.exportDateFrom,
to: this.exportDateTo,
format: this.exportFormat
},
responseType: 'blob'
});
// Datei herunterladen
const blob = new Blob([response.data], {
type: response.headers['content-type'] || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// Dateiname aus Content-Disposition Header extrahieren oder Standardnamen verwenden
const contentDisposition = response.headers['content-disposition'];
let filename = `gottesdienste_${this.exportDateFrom}_${this.exportDateTo}.docx`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Fehler beim Exportieren der Gottesdienste:', error);
const errorMessage = error.response?.data?.message || 'Fehler beim Exportieren der Datei.';
alert('Fehler: ' + errorMessage);
} finally {
this.isExporting = false;
}
}
}
};
@@ -599,6 +1056,10 @@ li {
position: relative;
}
.worship-list-item {
transition: background-color 0.2s ease, border-left-color 0.2s ease;
}
button {
margin-left: 10px;
}
@@ -633,4 +1094,436 @@ li>span {
.old-items {
color: #aaa;
}
.not-approved {
background-color: #fff8e1; /* zartes Gelb für noch nicht freigegebene Gottesdienste */
border-left: 4px solid #ffb300;
}
.approve-toggle-button {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid #4CAF50;
background-color: #e8f5e9;
color: #2e7d32;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.approve-toggle-button:hover {
background-color: #c8e6c9;
}
.import-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
margin: 0;
height: 36px;
box-sizing: border-box;
}
.import-button:hover {
background-color: #45a049;
}
.import-section {
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
background-color: #f9f9f9;
}
.import-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 16px;
}
.import-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.import-content label {
font-weight: 500;
color: #555;
margin: 0;
text-align: left;
}
.import-content input[type="file"] {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.import-content input[type="file"]:focus {
outline: none;
border-color: #4CAF50;
}
.selected-file {
padding: 8px;
background-color: #e8f5e9;
border: 1px solid #4CAF50;
border-radius: 4px;
color: #2e7d32;
font-size: 14px;
}
.submit-import-button {
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
align-self: flex-start;
white-space: nowrap;
}
.submit-import-button:hover:not(:disabled) {
background-color: #1976D2;
}
.submit-import-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.action-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.export-button {
padding: 8px 16px;
background-color: #FF9800;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
margin: 0;
height: 36px;
box-sizing: border-box;
}
.export-button:hover {
background-color: #F57C00;
}
.export-section {
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
background-color: #f9f9f9;
}
.export-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 16px;
}
.export-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.export-content label {
font-weight: 500;
color: #555;
margin: 0;
text-align: left;
}
.export-content input[type="date"],
.export-content select {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.export-content input[type="date"]:focus,
.export-content select:focus {
outline: none;
border-color: #FF9800;
}
.submit-export-button {
padding: 8px 16px;
background-color: #FF9800;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
align-self: flex-start;
white-space: nowrap;
}
.submit-export-button:hover:not(:disabled) {
background-color: #F57C00;
}
.submit-export-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.import-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.import-dialog-content {
background: white;
border-radius: 8px;
max-width: 90%;
max-height: 90vh;
width: 1200px;
display: flex;
flex-direction: column;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.import-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #ddd;
}
.import-dialog-header h3 {
margin: 0;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #aaa;
padding: 0;
width: 30px;
height: 30px;
line-height: 30px;
}
.close-button:hover {
color: #000;
}
.import-dialog-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.import-errors {
background-color: #ffebee;
border: 1px solid #f44336;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.import-errors h4 {
margin-top: 0;
color: #c62828;
}
.import-errors ul {
margin: 10px 0 0 0;
padding-left: 20px;
}
.import-errors li {
color: #c62828;
padding: 5px 0;
border: none;
}
.imported-worships-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.imported-worship-item {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
background-color: #f9f9f9;
}
.imported-worship-item h4 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.new-badge {
background-color: #4caf50;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.update-badge {
background-color: #ff9800;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.worship-edit-fields {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.field-group {
display: flex;
flex-direction: column;
}
.field-group label {
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.field-group input[type="text"],
.field-group input[type="date"],
.field-group input[type="time"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.field-group.field-changed {
background-color: #fff3cd;
padding: 8px;
border-radius: 4px;
border: 1px solid #ffc107;
margin-bottom: 5px;
}
.field-group.field-changed label {
color: #856404;
font-weight: 600;
}
.field-group .old-value {
font-size: 12px;
color: #856404;
font-weight: normal;
font-style: italic;
margin-left: 5px;
}
.field-group input[type="checkbox"] {
margin-right: 5px;
}
.field-group .multiselect {
width: 100%;
}
.remove-button {
grid-column: 1 / -1;
padding: 8px 16px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
.remove-button:hover {
background-color: #d32f2f;
}
.import-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px;
border-top: 1px solid #ddd;
}
.cancel-button,
.save-button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.cancel-button {
background-color: #ccc;
color: #333;
}
.cancel-button:hover {
background-color: #bbb;
}
.save-button {
background-color: #4CAF50;
color: white;
}
.save-button:hover:not(:disabled) {
background-color: #45a049;
}
.save-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>

View File

@@ -6,7 +6,8 @@ import axios from './axios';
import './assets/css/editor.css';
async function fetchMenuData() {
const response = await fetch(process.env.VUE_APP_BACKEND_URL + '/menu-data');
const backendUrl = process.env.VUE_APP_BACKEND_URL || '';
const response = await fetch(backendUrl + '/menu-data');
return await response.json();
}

View File

@@ -65,8 +65,12 @@ router.beforeEach(async (to, from, next) => {
next({ ...to, replace: true });
} 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) {
next('/login');
next('/auth/login');
} else {
next();
}

View File

@@ -35,7 +35,6 @@ export default createStore({
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('user');
localStorage.removeItem('token');
router.push('/auth/login');
},
setMenuData(state, menuData) {
state.menuData = menuData;
@@ -101,6 +100,10 @@ export default createStore({
console.error('Fehler beim Logout:', error);
} finally {
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: [],
devServer: {
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: {
output: { clean: true },