Add worship leaders functionality: Introduce worship leaders management by adding routes, controllers, and CSV import capabilities. Update worship management UI to support .csv file uploads for worship services, enhancing data handling and user experience.
All checks were successful
Deploy miriamgemeinde / deploy (push) Successful in 7s

This commit is contained in:
Torsten Schulz (local)
2026-04-29 18:04:05 +02:00
parent a2b1ebdb97
commit 7f01c004c8
11 changed files with 635 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
const { Worship, EventPlace, LiturgicalDay, Sequelize, sequelize } = require('../models'); const { Worship, EventPlace, LiturgicalDay, WorshipLeader, Sequelize, sequelize } = require('../models');
const { Op, fn, literal } = require('sequelize'); const { Op, fn, literal } = require('sequelize');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { isTokenBlacklisted, addTokenToBlacklist } = require('../utils/blacklist'); const { isTokenBlacklisted, addTokenToBlacklist } = require('../utils/blacklist');
@@ -6,6 +6,7 @@ const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
const mammoth = require('mammoth'); const mammoth = require('mammoth');
const pdfParse = require('pdf-parse'); const pdfParse = require('pdf-parse');
const { parse: parseCsv } = require('csv-parse/sync');
const { Document, Packer, Paragraph, Table, TableRow, TableCell, TextRun, WidthType, AlignmentType, VerticalAlign, ShadingType, VerticalMerge, VerticalMergeType, FontFamily, HeadingLevel, PageMargin, SectionType, BorderStyle, HeightRule } = require('docx'); const { Document, Packer, Paragraph, Table, TableRow, TableCell, TextRun, WidthType, AlignmentType, VerticalAlign, ShadingType, VerticalMerge, VerticalMergeType, FontFamily, HeadingLevel, PageMargin, SectionType, BorderStyle, HeightRule } = require('docx');
function isAuthorized(req) { function isAuthorized(req) {
@@ -235,6 +236,119 @@ function parseTime(timeString) {
return null; return null;
} }
function parseGermanDateString(dateString) {
const value = String(dateString || '').trim();
const match = value.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
if (!match) return null;
const day = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1;
const year = parseInt(match[3], 10);
return new Date(Date.UTC(year, month, day));
}
function normalizeText(value) {
return String(value || '')
.replace(/\u00a0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function buildLeaderMaps(leaders) {
const codeToName = new Map();
const normalizedToName = new Map();
for (const leader of leaders || []) {
if (!leader?.active) continue;
const code = normalizeText(leader.code);
const name = normalizeText(leader.name);
if (code) {
codeToName.set(code, name);
normalizedToName.set(code.toLowerCase(), name);
}
const aliases = String(leader.aliases || '')
.split(',')
.map((x) => normalizeText(x))
.filter(Boolean);
for (const alias of aliases) {
normalizedToName.set(alias.toLowerCase(), name);
}
}
return { codeToName, normalizedToName };
}
function resolveEventPlaceIdFromHeader(eventPlaces, headerCell) {
const raw = normalizeText(headerCell);
if (!raw) return null;
const nameOnly = normalizeText(raw.split('(')[0]);
const normalized = nameOnly.toLowerCase();
// Prefer exact name match.
const exact = (eventPlaces || []).find((p) => normalizeText(p.name).toLowerCase() === normalized);
if (exact) return exact.id;
// Fallback: contains.
const contains = (eventPlaces || []).find((p) => normalizeText(p.name).toLowerCase().includes(normalized));
if (contains) return contains.id;
// Hardcoded fallbacks for known CSV headers.
if (/am b[üu]gel/i.test(raw)) return 12;
if (/bonames/i.test(raw)) return 7;
if (/kalbach/i.test(raw)) return 2;
if (/nieder-eschbach/i.test(raw)) return 14;
if (/harheim/i.test(raw)) return 15;
if (/nieder-erlenbach/i.test(raw)) return 13;
return null;
}
function splitNbrCellToSegments(cellText) {
const text = normalizeText(cellText);
if (!text) return [];
// Often multiple items are separated by newlines or commas; keep it conservative.
return text
.split(/\n+|,\s*(?=(?:[A-ZÄÖÜa-zäöü]{1,3}\.)?\s*\d{1,2}[:.]\d{2}|Sa\.)/g)
.map((s) => normalizeText(s))
.filter(Boolean);
}
function parseNbrSegment(segment, baseDateUtc, leaderNormalizedMap) {
const raw = normalizeText(segment);
if (!raw) return null;
let dateUtc = baseDateUtc;
let text = raw;
if (/^(sa|samstag)\.?/i.test(text)) {
const d = new Date(dateUtc.getTime());
d.setUTCDate(d.getUTCDate() - 1);
dateUtc = d;
text = normalizeText(text.replace(/^(sa|samstag)\.?\s*/i, ''));
}
// Time: allow 10.30, 10:30, 10 h, 10h, 10.30 Uhr, etc.
let time = null;
const timeMatch = text.match(/(\d{1,2})\s*(?:[:.]\s*(\d{2})|h)\b/i);
if (timeMatch) {
const hours = parseInt(timeMatch[1], 10);
const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0;
time = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
text = normalizeText(text.replace(timeMatch[0], ''));
}
// Officiant: pick the last token that matches a configured leader code/alias.
let officiant = '';
const tokens = text.split(' ').map((t) => t.trim()).filter(Boolean);
for (let i = tokens.length - 1; i >= 0; i--) {
const token = tokens[i].replace(/[()]/g, '');
const resolved = leaderNormalizedMap.get(token.toLowerCase());
if (resolved) {
officiant = resolved;
tokens.splice(i, 1);
break;
}
}
const title = tokens.join(' ').trim() || 'Gottesdienst';
return { dateUtc, time, title, officiant };
}
// Hilfsfunktion zum Parsen eines Gottesdienstes aus der zweiten Spalte // Hilfsfunktion zum Parsen eines Gottesdienstes aus der zweiten Spalte
function parseWorshipFromCell(cellText, date, dayName) { function parseWorshipFromCell(cellText, date, dayName) {
// Zuerst in Zeilen aufteilen (falls Zeilenumbrüche vorhanden) // Zuerst in Zeilen aufteilen (falls Zeilenumbrüche vorhanden)
@@ -1213,6 +1327,182 @@ exports.importWorships = async (req, res) => {
} }
}; };
// Import-Funktion für Gottesdienste aus dem neuen NBR-CSV Format (2026+)
exports.importWorshipsNbrCsv = async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'Keine Datei hochgeladen.' });
}
const fileName = req.file.originalname.toLowerCase();
if (!fileName.endsWith('.csv')) {
return res.status(400).json({ message: 'Nur .csv Dateien sind erlaubt.' });
}
const csvText = req.file.buffer.toString('utf8');
const records = parseCsv(csvText, {
relax_quotes: true,
relax_column_count: true,
skip_empty_lines: false,
});
if (!Array.isArray(records) || records.length < 3) {
return res.status(400).json({ message: 'CSV hat zu wenig Zeilen.' });
}
const header = records[0] || [];
const datumCol = 1;
const groups = [];
for (let idx = 2; idx < header.length; idx += 3) {
const placeHeader = header[idx];
if (!normalizeText(placeHeader)) continue;
groups.push({
idx,
placeHeader,
musicIdx: idx + 1,
serviceIdx: idx + 2,
});
}
const eventPlaces = await EventPlace.findAll();
const leaders = await WorshipLeader.findAll();
const { normalizedToName } = buildLeaderMaps(leaders);
// existing worships for change detection
const existingWorships = await Worship.findAll({
where: {
date: {
[Op.gte]: literal('DATE_SUB(CURDATE(), INTERVAL 1 DAY)'),
},
},
});
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const imported = [];
const errors = [];
const findExisting = (dateUtc, time, eventPlaceId) => {
const y = dateUtc.getUTCFullYear();
const m = String(dateUtc.getUTCMonth() + 1).padStart(2, '0');
const d = String(dateUtc.getUTCDate()).padStart(2, '0');
const dateKey = `${y}-${m}-${d}`;
const timeKey = time ? String(time).substring(0, 5) : '';
return existingWorships.find((w) => {
const wDate = w.date instanceof Date ? w.date : new Date(w.date);
const wy = wDate.getUTCFullYear();
const wm = String(wDate.getUTCMonth() + 1).padStart(2, '0');
const wd = String(wDate.getUTCDate()).padStart(2, '0');
const wKey = `${wy}-${wm}-${wd}`;
const wTime = w.time ? String(w.time).substring(0, 5) : '';
return wKey === dateKey && wTime === timeKey && String(w.eventPlaceId || '') === String(eventPlaceId || '');
});
};
const hasChanges = (newW, existing) => {
if (!existing) return true;
const fields = ['title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId'];
for (const field of fields) {
const a = normalizeText(newW[field]);
const b = normalizeText(existing[field]);
if (a !== b) return true;
}
return false;
};
for (let r = 1; r < records.length; r++) {
const row = records[r] || [];
const dateUtc = parseGermanDateString(row[datumCol]);
if (!dateUtc) continue;
const baseDateUtc = dateUtc;
const dayName = normalizeText(row[0]).replace(/\s*\(\s*/g, ' (').trim();
// Skip past days for preview (same behavior as docx import)
const compare = new Date(baseDateUtc.getTime());
compare.setUTCHours(0, 0, 0, 0);
if (compare < today) continue;
for (const group of groups) {
const placeHeader = group.placeHeader;
const eventPlaceId = resolveEventPlaceIdFromHeader(eventPlaces, placeHeader);
const worshipCell = row[group.idx];
const music = row[group.musicIdx];
const service = row[group.serviceIdx];
const segments = splitNbrCellToSegments(worshipCell);
if (segments.length === 0) continue;
for (const seg of segments) {
const parsed = parseNbrSegment(seg, baseDateUtc, normalizedToName);
if (!parsed || !parsed.time) {
continue;
}
const worshipData = {
date: parsed.dateUtc,
dayName,
time: parsed.time,
title: parsed.title,
// "Gottesdienst haltend" ist bei uns der "Gestalter" (organizer).
organizer: parsed.officiant || '',
collection: '',
sacristanService: normalizeText(service),
organPlaying: normalizeText(music),
eventPlaceId,
};
const existing = findExisting(worshipData.date, worshipData.time, worshipData.eventPlaceId);
if (!hasChanges(worshipData, existing)) {
continue;
}
if (existing) {
worshipData._isUpdate = true;
worshipData._existingId = existing.id;
worshipData._oldValues = {
title: existing.title,
organizer: existing.organizer,
sacristanService: existing.sacristanService,
collection: existing.collection,
organPlaying: existing.organPlaying,
eventPlaceId: existing.eventPlaceId,
};
worshipData._changedFields = Object.keys(worshipData._oldValues).filter((f) => normalizeText(worshipData[f]) !== normalizeText(existing[f]));
} else {
worshipData._isNew = true;
}
imported.push(worshipData);
}
}
}
const worshipsForFrontend = imported.map((w) => {
const copy = { ...w };
if (copy.date instanceof Date) {
const year = copy.date.getUTCFullYear();
const month = String(copy.date.getUTCMonth() + 1).padStart(2, '0');
const day = String(copy.date.getUTCDate()).padStart(2, '0');
copy.date = `${year}-${month}-${day}`;
}
if (copy.time && typeof copy.time === 'string' && copy.time.length > 5) {
copy.time = copy.time.substring(0, 5);
}
return copy;
});
res.status(200).json({
message: `CSV geparst. ${worshipsForFrontend.length} Einträge mit Änderungen gefunden.`,
worships: worshipsForFrontend,
errors: errors.length ? errors : undefined,
});
} catch (error) {
console.error('Fehler beim Importieren (NBR CSV):', error);
res.status(500).json({ message: 'Fehler beim Importieren der CSV', error: error.message });
}
};
// Funktion zum Speichern der bearbeiteten Gottesdienste // Funktion zum Speichern der bearbeiteten Gottesdienste
exports.saveImportedWorships = async (req, res) => { exports.saveImportedWorships = async (req, res) => {
try { try {

View File

@@ -0,0 +1,88 @@
const { WorshipLeader } = require('../models');
const { Op } = require('sequelize');
function normalizeLeaderPayload(body) {
const code = String(body.code || '').trim();
const name = String(body.name || '').trim();
const aliases = String(body.aliases || '').trim();
const active = body.active === undefined ? true : !!body.active;
return { code, name, aliases, active };
}
exports.getAllWorshipLeaders = async (req, res) => {
try {
const includeInactive = String(req.query?.includeInactive || '').toLowerCase();
const wantsInactive = includeInactive === '1' || includeInactive === 'true' || includeInactive === 'yes';
const where = wantsInactive ? undefined : { active: true };
const leaders = await WorshipLeader.findAll({
where,
order: [['code', 'ASC']],
});
res.json(leaders);
} catch (error) {
console.error('getAllWorshipLeaders:', error);
res.status(500).json({ error: 'Failed to fetch worship leaders' });
}
};
exports.createWorshipLeader = async (req, res) => {
try {
const payload = normalizeLeaderPayload(req.body || {});
if (!payload.code || !payload.name) {
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
}
const existing = await WorshipLeader.findOne({ where: { code: payload.code } });
if (existing) {
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
}
const created = await WorshipLeader.create(payload);
res.status(201).json(created);
} catch (error) {
console.error('createWorshipLeader:', error);
res.status(500).json({ error: 'Failed to create worship leader' });
}
};
exports.updateWorshipLeader = async (req, res) => {
try {
const { id } = req.params;
const leader = await WorshipLeader.findByPk(id);
if (!leader) {
return res.status(404).json({ message: 'Worship leader not found' });
}
const payload = normalizeLeaderPayload(req.body || {});
if (!payload.code || !payload.name) {
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
}
const codeClash = await WorshipLeader.findOne({
where: { code: payload.code, id: { [Op.ne]: id } },
});
if (codeClash) {
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
}
await leader.update(payload);
res.json(leader);
} catch (error) {
console.error('updateWorshipLeader:', error);
res.status(500).json({ error: 'Failed to update worship leader' });
}
};
exports.deleteWorshipLeader = async (req, res) => {
try {
const { id } = req.params;
const deleted = await WorshipLeader.destroy({ where: { id } });
if (!deleted) {
return res.status(404).json({ message: 'Worship leader not found' });
}
res.status(204).json();
} catch (error) {
console.error('deleteWorshipLeader:', error);
res.status(500).json({ error: 'Failed to delete worship leader' });
}
};

View File

@@ -0,0 +1,47 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('worship_leaders', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
code: {
type: Sequelize.STRING(32),
allowNull: false,
unique: true,
},
name: {
type: Sequelize.STRING(255),
allowNull: false,
},
aliases: {
type: Sequelize.STRING(512),
allowNull: false,
defaultValue: '',
},
active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface) {
await queryInterface.dropTable('worship_leaders');
},
};

32
models/WorshipLeader.js Normal file
View File

@@ -0,0 +1,32 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const WorshipLeader = sequelize.define('WorshipLeader', {
code: {
type: DataTypes.STRING(32),
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
},
aliases: {
// Comma-separated list of alternative codes (kept simple to avoid join tables).
type: DataTypes.STRING(512),
allowNull: true,
defaultValue: '',
},
active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
}, {
tableName: 'worship_leaders',
timestamps: true,
});
return WorshipLeader;
};

View File

@@ -44,6 +44,7 @@
"mysql2": "^3.10.1", "mysql2": "^3.10.1",
"nodemailer": "^7.0.6", "nodemailer": "^7.0.6",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"csv-parse": "^5.6.0",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"sequelize-cli": "^6.6.2", "sequelize-cli": "^6.6.2",
"uuid": "^10.0.0", "uuid": "^10.0.0",

17
routes/worshipLeaders.js Normal file
View File

@@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware');
const {
getAllWorshipLeaders,
createWorshipLeader,
updateWorshipLeader,
deleteWorshipLeader,
} = require('../controllers/worshipLeaderController');
router.get('/', authMiddleware, getAllWorshipLeaders);
router.post('/', authMiddleware, createWorshipLeader);
router.put('/:id', authMiddleware, updateWorshipLeader);
router.delete('/:id', authMiddleware, deleteWorshipLeader);
module.exports = router;

View File

@@ -1,12 +1,13 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships, importNewsletterPdf } = require('../controllers/worshipController'); const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, importWorshipsNbrCsv, uploadImportFile, exportWorships, saveImportedWorships, importNewsletterPdf } = require('../controllers/worshipController');
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
router.get('/', getAllWorships); router.get('/', getAllWorships);
router.get('/options', getWorshipOptions); router.get('/options', getWorshipOptions);
router.post('/', authMiddleware, createWorship); router.post('/', authMiddleware, createWorship);
router.post('/import', authMiddleware, uploadImportFile, importWorships); router.post('/import', authMiddleware, uploadImportFile, importWorships);
router.post('/import/nbr-csv', authMiddleware, uploadImportFile, importWorshipsNbrCsv);
router.post('/import/newsletter-pdf', authMiddleware, uploadImportFile, importNewsletterPdf); router.post('/import/newsletter-pdf', authMiddleware, uploadImportFile, importNewsletterPdf);
router.post('/import/save', authMiddleware, saveImportedWorships); router.post('/import/save', authMiddleware, saveImportedWorships);
router.put('/:id', authMiddleware, updateWorship); router.put('/:id', authMiddleware, updateWorship);

View File

@@ -18,6 +18,7 @@ const institutionRouter = require('./routes/institutions');
const eventRouter = require('./routes/event'); const eventRouter = require('./routes/event');
const menuDataRouter = require('./routes/menuData'); const menuDataRouter = require('./routes/menuData');
const worshipRouter = require('./routes/worships'); const worshipRouter = require('./routes/worships');
const worshipLeadersRouter = require('./routes/worshipLeaders');
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');
@@ -106,6 +107,7 @@ app.use('/api/institutions', institutionRouter);
app.use('/api/events', eventRouter); app.use('/api/events', eventRouter);
app.use('/api/menu-data', menuDataRouter); app.use('/api/menu-data', menuDataRouter);
app.use('/api/worships', worshipRouter); app.use('/api/worships', worshipRouter);
app.use('/api/worship-leaders', worshipLeadersRouter);
app.use('/api/page-content', pageRouter); 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);

View File

@@ -0,0 +1,126 @@
<template>
<div class="worship-leader-admin">
<h1>Gestalter (Kürzel)</h1>
<h2>{{ formTitle }}</h2>
<form @submit.prevent="saveLeader">
<label for="code">Kürzel (z.B. "Eif"):</label>
<input id="code" v-model="currentLeader.code" required />
<label for="name">Name (wird als Gestalter gespeichert):</label>
<input id="name" v-model="currentLeader.name" required />
<label for="aliases">Aliase (kommagetrennt):</label>
<input id="aliases" v-model="currentLeader.aliases" />
<div class="row">
<label for="active">Aktiv:</label>
<input id="active" v-model="currentLeader.active" type="checkbox" />
</div>
<button type="submit">{{ isCreating ? 'Erstellen' : 'Aktualisieren' }}</button>
<button v-if="!isCreating" type="button" @click="resetForm">Abbrechen</button>
</form>
<div class="list">
<h2>Vorhandene Kürzel</h2>
<div class="tools">
<label><input type="checkbox" v-model="includeInactive" @change="fetchLeaders" /> Inaktive anzeigen</label>
</div>
<ul v-if="leaders.length">
<li v-for="leader in leaders" :key="leader.id" class="item">
<button type="button" class="link" @click="editLeader(leader)">
<strong>{{ leader.code }}</strong> {{ leader.name }}
<span v-if="leader.aliases"> ({{ leader.aliases }})</span>
<span v-if="!leader.active"> [inaktiv]</span>
</button>
<button type="button" class="danger" @click="deleteLeader(leader)">Löschen</button>
</li>
</ul>
<p v-else>Keine Einträge.</p>
</div>
</div>
</template>
<script>
import axios from '@/axios';
export default {
name: 'WorshipLeaderAdministration',
data() {
return {
leaders: [],
includeInactive: false,
currentLeader: {
code: '',
name: '',
aliases: '',
active: true,
},
isCreating: true,
};
},
computed: {
formTitle() {
return this.isCreating ? 'Kürzel anlegen' : 'Kürzel bearbeiten';
},
},
methods: {
async fetchLeaders() {
try {
const response = await axios.get('/worship-leaders', {
params: { includeInactive: this.includeInactive ? 1 : 0 },
});
this.leaders = response.data || [];
} catch (error) {
console.error('Fehler beim Abrufen der Kürzel:', error);
}
},
async saveLeader() {
try {
if (this.isCreating) {
await axios.post('/worship-leaders', this.currentLeader);
} else {
await axios.put(`/worship-leaders/${this.currentLeader.id}`, this.currentLeader);
}
this.resetForm();
await this.fetchLeaders();
} catch (error) {
const msg = error.response?.data?.message || error.message;
alert(`Fehler: ${msg}`);
}
},
editLeader(leader) {
this.currentLeader = { ...leader };
this.isCreating = false;
},
async deleteLeader(leader) {
if (!confirm(`Kürzel "${leader.code}" wirklich löschen?`)) return;
try {
await axios.delete(`/worship-leaders/${leader.id}`);
await this.fetchLeaders();
} catch (error) {
console.error('Fehler beim Löschen:', error);
}
},
resetForm() {
this.currentLeader = { code: '', name: '', aliases: '', active: true };
this.isCreating = true;
},
},
mounted() {
this.fetchLeaders();
},
};
</script>
<style scoped>
.worship-leader-admin { padding: 20px; }
form { display: grid; gap: 8px; max-width: 520px; }
.row { display: flex; align-items: center; gap: 10px; }
.list { margin-top: 18px; }
.tools { margin: 8px 0; }
.item { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.link { background: none; border: none; padding: 0; color: #1a73e8; cursor: pointer; text-align: left; }
.danger { background: #c62828; color: #fff; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; }
</style>

View File

@@ -15,13 +15,13 @@
<div v-if="showImportSection" class="import-section"> <div v-if="showImportSection" class="import-section">
<h3>Gottesdienste importieren</h3> <h3>Gottesdienste importieren</h3>
<div class="import-content"> <div class="import-content">
<label for="import-file">Datei auswählen (.doc, .docx):</label> <label for="import-file">Datei auswählen (.doc, .docx, .csv):</label>
<input <input
type="file" type="file"
id="import-file" id="import-file"
ref="fileInput" ref="fileInput"
@change="handleFileSelect" @change="handleFileSelect"
accept=".doc,.docx" accept=".doc,.docx,.csv"
/> />
<div v-if="selectedFile" class="selected-file"> <div v-if="selectedFile" class="selected-file">
Ausgewählte Datei: {{ selectedFile.name }} Ausgewählte Datei: {{ selectedFile.name }}
@@ -730,13 +730,13 @@ export default {
handleFileSelect(event) { handleFileSelect(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
// Validierung: Nur .doc und .docx Dateien erlauben // Validierung: .docx (alt) oder .csv (neu) erlauben
const allowedExtensions = ['.doc', '.docx']; const allowedExtensions = ['.doc', '.docx', '.csv'];
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext)); const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!isValidFile) { if (!isValidFile) {
alert('Bitte wählen Sie nur .doc oder .docx Dateien aus.'); alert('Bitte wählen Sie eine .doc/.docx oder .csv Datei aus.');
event.target.value = ''; event.target.value = '';
this.selectedFile = null; this.selectedFile = null;
return; return;
@@ -758,7 +758,12 @@ export default {
formData.append('file', this.selectedFile); formData.append('file', this.selectedFile);
try { try {
const response = await axios.post('/worships/import', formData, { const fileName = this.selectedFile.name.toLowerCase();
const endpoint = fileName.endsWith('.csv')
? '/worships/import/nbr-csv'
: '/worships/import';
const response = await axios.post(endpoint, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },

View File

@@ -5,6 +5,7 @@ const ROUTE_NAMES = {
ADMIN_EDIT_PAGES: 'admin-edit-pages', ADMIN_EDIT_PAGES: 'admin-edit-pages',
ADMIN_FILE_UPLOAD: 'admin-file-upload', ADMIN_FILE_UPLOAD: 'admin-file-upload',
ADMIN_NEWSLETTER_IMPORT: 'admin-newsletter-import', ADMIN_NEWSLETTER_IMPORT: 'admin-newsletter-import',
ADMIN_WORSHIP_LEADERS: 'admin-worship-leaders',
REGISTER: 'register', REGISTER: 'register',
FORGOT_PASSWORD: 'forgot-password', FORGOT_PASSWORD: 'forgot-password',
RESET_PASSWORD: 'reset-password', RESET_PASSWORD: 'reset-password',
@@ -219,6 +220,21 @@ function addNewsletterImportRoute() {
}); });
} }
function addWorshipLeadersRoute() {
if (router.hasRoute(ROUTE_NAMES.ADMIN_WORSHIP_LEADERS)) {
router.removeRoute(ROUTE_NAMES.ADMIN_WORSHIP_LEADERS);
}
router.addRoute({
path: '/admin/worship-leaders',
meta: { requiresAuth: true },
components: {
default: loadComponent('admin/WorshipLeaderAdministration'),
rightColumn: loadComponent('ImageContent')
},
name: ROUTE_NAMES.ADMIN_WORSHIP_LEADERS
});
}
function addRegisterRoute() { function addRegisterRoute() {
if (router.hasRoute(ROUTE_NAMES.REGISTER)) { if (router.hasRoute(ROUTE_NAMES.REGISTER)) {
router.removeRoute(ROUTE_NAMES.REGISTER); router.removeRoute(ROUTE_NAMES.REGISTER);
@@ -335,6 +351,7 @@ function ensureCoreRoutes() {
if (!router.hasRoute(ROUTE_NAMES.ADMIN_EDIT_PAGES)) addEditPagesRoute(); if (!router.hasRoute(ROUTE_NAMES.ADMIN_EDIT_PAGES)) addEditPagesRoute();
if (!router.hasRoute(ROUTE_NAMES.ADMIN_FILE_UPLOAD)) addFileUploadRoute(); if (!router.hasRoute(ROUTE_NAMES.ADMIN_FILE_UPLOAD)) addFileUploadRoute();
if (!router.hasRoute(ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT)) addNewsletterImportRoute(); if (!router.hasRoute(ROUTE_NAMES.ADMIN_NEWSLETTER_IMPORT)) addNewsletterImportRoute();
if (!router.hasRoute(ROUTE_NAMES.ADMIN_WORSHIP_LEADERS)) addWorshipLeadersRoute();
if (!router.hasRoute(ROUTE_NAMES.REGISTER)) addRegisterRoute(); if (!router.hasRoute(ROUTE_NAMES.REGISTER)) addRegisterRoute();
if (!router.hasRoute(ROUTE_NAMES.FORGOT_PASSWORD)) addForgotPasswordRoute(); if (!router.hasRoute(ROUTE_NAMES.FORGOT_PASSWORD)) addForgotPasswordRoute();
if (!router.hasRoute(ROUTE_NAMES.RESET_PASSWORD)) addResetPasswordRoute(); if (!router.hasRoute(ROUTE_NAMES.RESET_PASSWORD)) addResetPasswordRoute();
@@ -348,6 +365,7 @@ function ensureCoreRoutes() {
addEditPagesRoute(); addEditPagesRoute();
addFileUploadRoute(); addFileUploadRoute();
addNewsletterImportRoute(); addNewsletterImportRoute();
addWorshipLeadersRoute();
addRegisterRoute(); addRegisterRoute();
addForgotPasswordRoute(); addForgotPasswordRoute();
addResetPasswordRoute(); addResetPasswordRoute();