web - refactor core modules, harden authorization, simplify endpoints

This commit is contained in:
2026-08-29 11:15:54 +02:00
parent e1109a07ef
commit 79d857d686
71 changed files with 8269 additions and 9119 deletions
@@ -0,0 +1,15 @@
function openBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'flex';
}
function closeBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'none';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.standard_overlay[id^="bsModal-"]').forEach(function(m) {
m.style.display = 'none';
});
}
});
@@ -0,0 +1,72 @@
'use strict';
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-auth-toast]').forEach(function (toast) {
requestAnimationFrame(function () {
toast.classList.add('nak-toast--visible');
});
});
var otpRequestForm = document.getElementById('otpRequestForm');
if (otpRequestForm) {
otpRequestForm.addEventListener('submit', function () {
var button = document.getElementById('requestBtn');
if (button) {
button.classList.add('is-loading');
button.disabled = true;
}
});
}
var signupForm = document.getElementById('signup');
if (!signupForm || typeof window.JustValidate !== 'function') {
return;
}
var validation = new window.JustValidate('#signup');
validation
.addField('#first_name', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{ rule: 'maxLength', value: 100, errorMessage: 'Zu lang' }
])
.addField('#last_name', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{ rule: 'maxLength', value: 100, errorMessage: 'Zu lang' }
])
.addField('#email', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{ rule: 'email', errorMessage: 'Ungültig' },
{ rule: 'maxLength', value: 254, errorMessage: 'Zu lang' }
])
.addField('#phone', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{
validator: function (value) {
var digits = String(value).replace(/\D+/g, '');
return /^[1-9][0-9]{5,14}$/.test(digits);
},
errorMessage: 'Ohne führende 0, 6–15 Ziffern'
}
])
.addField('#password', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{
validator: function (value) {
return String(value).length >= 8 && /[a-z]/i.test(value) && /[0-9]/.test(value);
},
errorMessage: 'Mind. 8 Zeichen, Buchstabe und Zahl'
}
])
.addField('#password_confirmation', [
{ rule: 'required', errorMessage: 'Pflichtfeld' },
{
validator: function (value, fields) {
return value === fields['#password'].elem.value;
},
errorMessage: 'Passwörter müssen übereinstimmen'
}
])
.onSuccess(function () {
signupForm.submit();
});
});
@@ -0,0 +1,601 @@
/* ══════════════════════════════════════
NAVIGATION
══════════════════════════════════════ */
var bsCurrentSection = 0;
var bsTotalSections = 6; // 0=intro, 1-5=pillars
var bsCurrentSub = {}; // track sub-steps per section
// Init sub-step tracking
for (var i = 1; i <= 5; i++) bsCurrentSub[i] = 1;
function bsGoToSection(section) {
var scoreEl = document.getElementById('bsSectionScore');
if (scoreEl) scoreEl.style.display = 'none';
// Hide all sections
for (var i = 0; i < bsTotalSections; i++) {
var el = document.getElementById('bsSection' + i);
if (el) el.style.display = 'none';
}
// Show target section
var target = document.getElementById('bsSection' + section);
if (target) target.style.display = '';
bsCurrentSection = section;
bsUpdateMainProgress(section);
// Reset to first sub-step when entering a section
if (section >= 1 && section <= 5) {
bsCurrentSub[section] = 1;
bsShowSubStep(section, 1);
}
bsScrollToTop();
}
function bsUpdateMainProgress(section) {
var steps = document.querySelectorAll('#bsMainProgress .bs-progress-step');
var lines = document.querySelectorAll('#bsMainProgress .bs-progress-line');
steps.forEach(function(el, idx) {
el.classList.remove('active', 'done');
if (idx === section) el.classList.add('active');
else if (idx < section) el.classList.add('done');
});
lines.forEach(function(el, idx) {
el.classList.toggle('done', idx < section);
});
}
/* ── Sub-step navigation ── */
function bsSubStep(section, step) {
bsCurrentSub[section] = step;
bsShowSubStep(section, step);
bsScrollToTop();
}
function bsShowSubStep(section, step) {
// Count sub-steps
var maxSub = document.querySelectorAll('#bsSection' + section + ' .bs-substep').length;
for (var i = 1; i <= maxSub; i++) {
var el = document.getElementById('bsSub' + section + '_' + i);
if (el) el.style.display = (i === step) ? '' : 'none';
}
}
function bsScrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
var headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
var elemHeaderMenue = document.getElementById('header_menue');
var elemHeaderTitle = document.getElementById('header_title');
if (elemHeaderMenue) elemHeaderMenue.style.top = '0';
if (elemHeaderTitle) elemHeaderTitle.style.top = headerHeight + 'px';
}
/* ── Dropdown Matching ── */
function bsCheckDropdownMatch() {
var container = document.getElementById('match3');
var rows = container.querySelectorAll('.bs-dropdown-match-row');
var correct = 0;
var total = rows.length;
rows.forEach(function(row) {
var select = row.querySelector('select');
row.classList.remove('correct', 'wrong');
if (select.value === select.dataset.correct) {
row.classList.add('correct');
correct++;
} else {
row.classList.add('wrong');
}
});
var fb = document.getElementById('match3_fb');
if (correct === total) {
fb.className = 'bs-quiz-feedback show correct';
fb.textContent = 'Richtig: Alle Zuordnungen korrekt! Gut gemacht!';
} else {
fb.className = 'bs-quiz-feedback show wrong';
fb.textContent = correct + ' von ' + total + ' richtig. Bitte überprüfen Sie Ihre Auswahl.';
}
return correct === total;
}
/* ══════════════════════════════════════
FILL-IN-THE-BLANK
══════════════════════════════════════ */
function bsCheckFill(ids) {
var correct = 0;
var total = ids.length;
ids.forEach(function(id) {
var input = document.getElementById(id);
var answer = input.dataset.answer.toLowerCase().trim();
var value = input.value.toLowerCase().trim();
input.classList.remove('correct', 'wrong');
if (value === answer) {
input.classList.add('correct');
correct++;
} else {
input.classList.add('wrong');
}
});
// Find feedback element near the button
var fb = document.getElementById('fill2_fb');
if (fb) {
fb.classList.remove('correct', 'wrong');
fb.classList.add('show');
if (correct === total) {
fb.classList.add('correct');
fb.textContent = 'Richtig: Alle Lücken richtig ausgefüllt!';
} else {
fb.classList.add('wrong');
fb.textContent = correct + ' von ' + total + ' richtig. Korrekte Antworten: 112, Klixstraße 17, 13403';
}
}
return correct === total;
}
/* ══════════════════════════════════════
MULTI-SELECT CHECKBOX QUIZ
══════════════════════════════════════ */
function bsToggleCheckbox(optionEl) {
var cb = optionEl.querySelector('input[type="checkbox"]');
cb.checked = !cb.checked;
optionEl.classList.toggle('selected', cb.checked);
}
function bsCheckMultiGeneric(quizId, correctMsg, wrongMsg) {
var quiz = document.getElementById(quizId);
var options = quiz.querySelectorAll('.bs-quiz-option');
var allCorrect = true;
options.forEach(function(opt) {
var cb = opt.querySelector('input[type="checkbox"]');
var isCorrect = cb.dataset.correct === 'true';
opt.classList.remove('correct', 'wrong');
if (isCorrect && cb.checked) {
opt.classList.add('correct');
} else if (isCorrect && !cb.checked) {
opt.classList.add('correct');
allCorrect = false;
} else if (!isCorrect && cb.checked) {
opt.classList.add('wrong');
allCorrect = false;
}
});
var fb = document.getElementById(quizId + '_fb');
fb.classList.remove('correct', 'wrong');
fb.classList.add('show');
if (allCorrect) {
fb.classList.add('correct');
fb.textContent = correctMsg;
} else {
fb.classList.add('wrong');
fb.textContent = wrongMsg;
}
return allCorrect;
}
/* ══════════════════════════════════════
INDIVIDUAL QUIZ GATES
══════════════════════════════════════ */
var bsQuizPassed = {};
var bsQuizAttempts = {};
var bsQuizCorrect = {}; // true = correct, false = nach drei Versuchen beendet
var bsQuizGates = [
{ section: 1, nextBtnId: 'bsNextS1Q', quizIds: ['quiz1_1', 'quiz1_2', 'quiz1_3'] },
{ section: 2, nextBtnId: 'bsNextS2Q', quizIds: ['scenario2_1', 'fill2'] },
{ section: 3, nextBtnId: 'bsNextS3Q', quizIds: ['match3', 'quiz3_1', 'quiz3_2'] },
{ section: 4, nextBtnId: 'bsNextS4Q', quizIds: ['tf4', 'scenario4_1'] },
{ section: 5, nextBtnId: 'bsNextS5P1', quizIds: ['bs5q1', 'bs5q2', 'bs5q3', 'bs5q4', 'bs5q5'] },
{ section: 5, nextBtnId: 'bsNextS5P2', quizIds: ['bs5q6', 'bs5q7', 'bs5q8', 'bs5q9', 'bs5q10'] }
];
var bsQuizMessages = {
scenario2_1: [
'Richtig! Nicht durch das verrauchte Treppenhaus gehen und keinen Löschversuch im Rauch unternehmen. Richtig ist: 112 anrufen, warnen, in einem rauchfreien Raum bleiben und sich am Fenster bemerkbar machen.',
'Falsch. Gesucht sind die Verhaltensweisen, die man nicht tun sollte: durch das verrauchte Treppenhaus laufen oder im verrauchten Bereich selbst löschen.'
],
tf4: [
'Richtig: Alle Antworten richtig! Ausgezeichnet!',
'Nicht ganz richtig. Die korrekten Antworten sind markiert.'
],
scenario4_1: [
'Richtig! Bei Elektrobränden niemals Wasser verwenden, Stromschlaggefahr! Der CO₂-Löscher ist die richtige Wahl.',
'Falsch. Bei Elektrobränden darf kein Wasser verwendet werden (Stromschlaggefahr). Ein CO₂-Löscher ist hier das richtige Löschmittel.'
],
bs5q1: [
'Richtig! Die Feuerlöscher befinden sich im Vorraum am seitlichen Ausgang, im Treppenhaus 1. OG und im hinteren Kirchenanbau neben der Toilette.',
'Falsch: Nicht ganz. Die korrekten Standorte sind: Vorraum am seitlichen Ausgang, Treppenhaus 1. OG und hinterer Kirchenanbau neben der Toilette.'
],
bs5q2: [
'Richtig! Alle drei Ausgänge stehen zur Verfügung: Haupteingang, hinterer Kirchenanbau und seitlicher Eingang. Achte auf die Stufen und den Gegenverkehr von der Empore!',
'Falsch: Nicht ganz. Die korrekten Rettungswege sind: Haupteingang (Stufen!), hinterer Kirchenanbau (Stufen!) und seitlicher Eingang (Emporenverkehr!). Fenster sind keine Rettungswege.'
],
bs5q3: [
'Richtig! Wir haben keine Brandmeldeanlage. Deshalb ist es besonders wichtig, dass alle Anwesenden wachsam sind und die Abläufe kennen.',
'Falsch. Wir haben keine Brandmeldeanlage, daher müssen wir uns umso mehr auf geschultes Verhalten und aufmerksame Personen verlassen.'
],
bs5q4: [
'Richtig! Die Adresse für den Notruf findest du innen an der rechten Tür des Haupteingangs. Merke dir diesen Ort!',
'Falsch. Die Adresse befindet sich innen an der rechten Tür des Haupteingangs (Fluchttür), nicht auf dem Altar oder im Treppenhaus.'
],
bs5q5: [
'Richtig! Unsere Kirchengemeinde fasst 180 bis 200 Personen.',
'Falsch. Unsere Kirchengemeinde fasst 180 bis 200 Personen, nicht mehr.'
],
bs5q6: [
'Richtig! Die Gefahrenstellen sind: Stufen zum Anbau, Stufen am Haupteingang und das Treppenhaus an der Empore (Gegenverkehr).',
'Falsch: Nicht ganz. Die Gefahrenstellen sind: Stufen zwischen Kirchenschiff und Anbau, Stufen vor dem Haupteingang und das Treppenhaus an der Empore. Die Rampe ist keine Gefahrenstelle.'
],
bs5q7: [
'Richtig! Die zuständige vBsU koordiniert, eine klare Zuständigkeit verhindert Chaos und widersprüchliche Anweisungen.',
'Falsch. Nur die zuständige vBsU koordiniert die Aktionen. Das verhindert widersprüchliche Anweisungen und sorgt für eine geordnete Evakuierung.'
],
bs5q8: [
'Richtig! Notleuchten befinden sich an sechs Standorten: Sakristei, unterm Altar, seitliche Klappen, Empore, Eltern-Kind-Raum und Vorraum.',
'Falsch: Nicht ganz. Die Notleuchten befinden sich an: Sakristei, unterm Altar, letzte seitliche Klappen im Kirchenschiff, Empore (Tisch), Eltern-Kind-Raum (Schrank unter TV) und „Diakonie-Schrank" im Vorraum.'
],
bs5q9: [
'Richtig! Der vorbereitete Text wird vom Pult aus verlesen, so erreicht die Information alle Anwesenden gleichzeitig und klar verständlich.',
'Falsch. Die Räumung wird eingeleitet, indem der vorbereitete Text „zur Räumung des Gebäudes" vom Pult aus verlesen wird (rotes, laminiertes DIN-A4-Blatt). So wird sichergestellt, dass alle die gleiche, klare Ansage hören.'
],
bs5q10: [
'Richtig! Anwesende Ersthelfer*innen übernehmen die weitere Erste Hilfe. Die vBsU koordiniert die Evakuierung, nicht die medizinische Versorgung.',
'Falsch. Für die weitere Erste Hilfe sind die anwesenden Ersthelfer*innen zuständig, nicht die vBsU. Für weitergehende Erste Hilfe ist eine gesonderte Schulung notwendig. Die erste Hilfeleistung ist natürlich von jedem erwünscht und gesetzlich vorgeschrieben.'
]
};
function bsGetQuizGate(quizId) {
return bsQuizGates.find(function(gate) {
return gate.quizIds.includes(quizId);
}) || null;
}
function bsGetAllQuizIds() {
return bsQuizGates.reduce(function(ids, gate) { return ids.concat(gate.quizIds); }, []);
}
function bsRenderQuizResult(quizId) {
if (quizId === 'fill2') {
return bsCheckFill(['fill2_1', 'fill2_2', 'fill2_3']);
}
if (quizId === 'match3') {
return bsCheckDropdownMatch();
}
var messages = bsQuizMessages[quizId] || ['Richtig!', 'Falsch: Leider falsch. Die richtige Antwort ist markiert.'];
return bsCheckMultiGeneric(quizId, messages[0], messages[1]);
}
function bsGetScoreStats() {
var allIds = bsGetAllQuizIds();
var total = allIds.length;
var correct = 0;
var wrong = 0;
var open = 0;
allIds.forEach(function(id) {
if (bsQuizCorrect[id] === true) correct++;
else if (bsQuizCorrect[id] === false) wrong++;
else open++;
});
var percent = total > 0 ? Math.round((correct / total) * 100) : 0;
return { correct: correct, wrong: wrong, open: open, total: total, percent: percent };
}
function bsShowSectionScore(completedSection, nextAction) {
var stats = bsGetScoreStats();
var answered = stats.correct + stats.wrong;
var maxReachable = stats.total > 0 ? 100 - Math.round((stats.wrong / stats.total) * 100) : 100;
var scoreEl = document.getElementById('bsSectionScore');
var body = scoreEl.querySelector('.bs-body');
var barColor = maxReachable >= 70 ? 'var(--bs-green)' : 'var(--bs-red)';
body.innerHTML =
'<div class="bs-chapter-title">Zwischenstand nach Säule ' + completedSection + '</div>' +
'<div class="bs-score-bar-wrap">' +
'<div class="bs-score-bar"><div class="bs-score-bar-fill" style="width:' + maxReachable + '%;background:' + barColor + '"></div></div>' +
'<div class="bs-score-bar-label">Max. erreichbar: ' + maxReachable + '% (mindestens 70% erforderlich)</div>' +
'</div>' +
'<div class="bs-score-grid">' +
'<div class="bs-score-card bs-score-correct"><div class="bs-score-card-num">' + stats.correct + '</div><div class="bs-score-card-label">Richtig</div></div>' +
'<div class="bs-score-card bs-score-wrong"><div class="bs-score-card-num">' + stats.wrong + '</div><div class="bs-score-card-label">Falsch</div></div>' +
'<div class="bs-score-card bs-score-open"><div class="bs-score-card-num">' + stats.open + '</div><div class="bs-score-card-label">Offen</div></div>' +
'<div class="bs-score-card"><div class="bs-score-card-num">' + answered + '/' + stats.total + '</div><div class="bs-score-card-label">Gesamt</div></div>' +
'</div>';
var actionsRight = scoreEl.querySelector('.bs-actions-right');
actionsRight.innerHTML = '';
if (typeof nextAction === 'function') {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'bs-btn bs-btn-primary';
btn.textContent = completedSection < 5 ? 'Weiter zu Säule ' + (completedSection + 1) + ' >' : 'Zum Ergebnis >';
btn.onclick = nextAction;
actionsRight.appendChild(btn);
}
var actionsLeft = scoreEl.querySelector('.bs-actions-left');
actionsLeft.innerHTML = '<button type="button" class="bs-btn" onclick="bsGoToSection(' + completedSection + ')">< Zurück zu Säule ' + completedSection + '</button>';
for (var i = 0; i < 6; i++) {
var el = document.getElementById('bsSection' + i);
if (el) el.style.display = 'none';
}
scoreEl.style.display = '';
bsScrollToTop();
}
function bsFinalResultAction() {
document.getElementById('bsSectionScore').style.display = 'none';
bsShowFinalResult();
document.getElementById('bsSection5').style.display = '';
bsCurrentSub[5] = 5;
bsShowSubStep(5, 5);
bsScrollToTop();
}
function bsShowFinalResult() {
var stats = bsGetScoreStats();
var passed = stats.percent >= 70;
var section = document.getElementById('bsSub5_5');
var body = section.querySelector('.bs-body');
var barColor = passed ? 'var(--bs-green)' : 'var(--bs-red)';
var html = '<div class="bs-chapter-title">' + (passed ? 'Schulung bestanden!' : 'Schulung nicht bestanden') + '</div>';
html += '<div class="bs-score-bar-wrap">' +
'<div class="bs-score-bar"><div class="bs-score-bar-fill" style="width:' + stats.percent + '%;background:' + barColor + '"></div></div>' +
'<div class="bs-score-bar-label">' + stats.percent + '% richtig (' + stats.correct + ' von ' + stats.total + ' Fragen)</div>' +
'</div>';
html += '<div class="bs-score-grid">' +
'<div class="bs-score-card bs-score-correct"><div class="bs-score-card-num">' + stats.correct + '</div><div class="bs-score-card-label">Richtig</div></div>' +
'<div class="bs-score-card bs-score-wrong"><div class="bs-score-card-num">' + stats.wrong + '</div><div class="bs-score-card-label">Falsch</div></div>' +
'</div>';
if (passed) {
html += '<div class="bs-successbox" style="margin-top:20px;">' +
'<strong>Herzlichen Glückwunsch!</strong> Du hast die Brandschutzunterweisung erfolgreich absolviert. ' +
'Du kennst jetzt die Grundlagen der Brandverhütung, weißt, wie du dich im Brandfall verhältst, ' +
'kennst die Aufgaben der vBsU, kannst einen Feuerlöscher richtig einsetzen und weißt, wo sich die wichtigsten ' +
'Einrichtungen in unserer Kirchengemeinde befinden.' +
'</div>';
html += '<div style="margin-top:24px;padding:20px;background:var(--gray-50);border:1px solid var(--gray-200);">' +
'<label style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;">' +
'<input type="checkbox" id="bsConfirmCheckbox" onchange="bsToggleConfirm()" style="width:20px;height:20px;margin-top:2px;">' +
'<span style="font-size:var(--font-size-sm);color:var(--gray-800);line-height:1.6;">' +
'Ich bestätige, dass ich die Inhalte der <a href="/intranet_repo/projects/brandschutz/Brandschutzordnung.pdf" target="_blank" onclick="event.stopPropagation();">Brandschutzordnung</a> verstanden und verinnerlicht habe. ' +
'Ich bin bereit, die Aufgaben als vBsU zu übernehmen, sofern ich dazu eingeteilt werde.' +
'</span>' +
'</label>' +
'</div>';
html += '<button type="button" class="bs-btn bs-btn-primary bs-btn-locked" id="bsFinishBtn" onclick="bsFinishTraining()" style="margin-top:24px;width:100%;padding:16px;font-size:var(--font-size-md);">Schulung abschließen</button>';
} else {
html += '<div class="bs-warnbox" style="margin-top:20px;">' +
'<div class="bs-warnbox-title">Leider nicht bestanden</div>' +
'Du hast ' + stats.percent + '% der Fragen richtig beantwortet. Zum Bestehen sind mindestens 70% erforderlich. ' +
'Bitte wiederhole die Schulung, um alle Inhalte zu verinnerlichen.' +
'</div>';
html += '<button type="button" class="bs-btn bs-btn-primary" onclick="bsRestartTraining()" style="margin-top:24px;width:100%;padding:16px;font-size:var(--font-size-md);">Schulung neu starten</button>';
}
body.innerHTML = html;
}
function bsSetQuizButtonState(quizId, state) {
var btn = document.getElementById('bsCheck_' + quizId);
if (!btn) return;
btn.classList.remove('bs-btn-success', 'bs-btn-failed', 'bs-btn-check-disabled');
if (state === 'passed') {
btn.textContent = 'Richtig';
btn.classList.add('bs-btn-success', 'bs-btn-check-disabled');
btn.onclick = null;
return;
}
if (state === 'failed') {
btn.textContent = 'Falsch';
btn.classList.add('bs-btn-failed', 'bs-btn-check-disabled');
btn.onclick = null;
return;
}
btn.textContent = 'Antwort prüfen';
btn.onclick = function() { bsCheckIndividualQuiz(quizId); };
}
function bsUpdatePageGate(nextBtnId, quizIds) {
var nextBtn = document.getElementById(nextBtnId);
if (!nextBtn) return;
var allPassed = quizIds.every(function(id) { return !!bsQuizPassed[id]; });
nextBtn.classList.toggle('bs-btn-locked', !allPassed);
}
/* Validate quiz without marking options */
function bsValidateQuiz(quizId) {
var quiz = document.getElementById(quizId);
if (!quiz) return false;
// Checkbox-based quizzes
var cbOptions = quiz.querySelectorAll('.bs-quiz-option input[type="checkbox"]');
if (cbOptions.length > 0) {
var ok = true;
cbOptions.forEach(function(cb) {
var isCorrect = cb.dataset.correct === 'true';
if ((isCorrect && !cb.checked) || (!isCorrect && cb.checked)) ok = false;
});
return ok;
}
// Fill-in-the-blank
var fills = quiz.querySelectorAll('.bs-fill-input');
if (fills.length > 0) {
var ok = true;
fills.forEach(function(input) {
if (input.value.toLowerCase().trim() !== input.dataset.answer.toLowerCase().trim()) ok = false;
});
return ok;
}
// Dropdown matching
var selects = quiz.querySelectorAll('.bs-dropdown-match-select');
if (selects.length > 0) {
var ok = true;
selects.forEach(function(sel) {
if (sel.value !== sel.dataset.correct) ok = false;
});
return ok;
}
return false;
}
function bsCheckIndividualQuiz(quizId) {
var gate = bsGetQuizGate(quizId);
if (!gate) return;
if (!bsQuizAttempts[quizId]) bsQuizAttempts[quizId] = 0;
bsQuizAttempts[quizId]++;
var attempt = bsQuizAttempts[quizId];
var passed = bsValidateQuiz(quizId);
if (passed) {
// Correct — show markings and lock
bsRenderQuizResult(quizId);
bsQuizPassed[quizId] = true;
bsQuizCorrect[quizId] = true;
bsSetQuizButtonState(quizId, 'passed');
document.getElementById(quizId).classList.add('bs-quiz-locked');
} else if (attempt >= 3) {
// Third wrong attempt — show solution, lock as failed
bsRenderQuizResult(quizId);
bsQuizPassed[quizId] = true; // done (failed but completed)
bsQuizCorrect[quizId] = false;
bsSetQuizButtonState(quizId, 'failed');
document.getElementById(quizId).classList.add('bs-quiz-locked');
} else {
// Wrong but retries left — show hint, keep selections
var remaining = 3 - attempt;
var fb = document.getElementById(quizId + '_fb');
if (fb) {
fb.classList.remove('correct', 'wrong');
fb.classList.add('show', 'wrong');
fb.textContent = 'Leider falsch. Du hast noch ' + remaining + (remaining === 1 ? ' Versuch.' : ' Versuche.');
}
}
bsUpdatePageGate(gate.nextBtnId, gate.quizIds);
}
function bsResetQuizContainer(containerId) {
var container = document.getElementById(containerId);
if (!container) return;
container.querySelectorAll('input[type="checkbox"]').forEach(function(input) {
input.checked = false;
});
container.querySelectorAll('.bs-quiz-option').forEach(function(opt) {
opt.classList.remove('correct', 'wrong', 'selected');
});
container.querySelectorAll('select').forEach(function(select) {
select.selectedIndex = 0;
});
container.querySelectorAll('.bs-dropdown-match-row').forEach(function(row) {
row.classList.remove('correct', 'wrong');
});
container.querySelectorAll('input[type="text"]').forEach(function(input) {
input.value = '';
input.classList.remove('correct', 'wrong');
});
container.querySelectorAll('.bs-quiz-feedback, .bs-feedback').forEach(function(fb) {
fb.classList.remove('show', 'correct', 'wrong');
fb.textContent = '';
});
}
function bsUpdateChecklistNext(checklistId, buttonId) {
const checklist = document.getElementById(checklistId);
const button = document.getElementById(buttonId);
if (!checklist || !button) return;
const items = checklist.querySelectorAll('li');
const checkedItems = checklist.querySelectorAll('li.checked');
const allChecked = items.length > 0 && items.length === checkedItems.length;
button.classList.toggle('bs-btn-locked', !allChecked);
}
function bsRestartTraining() {
bsQuizPassed = {};
bsQuizAttempts = {};
bsQuizCorrect = {};
bsGetAllQuizIds().forEach(function(id) {
bsResetQuizContainer(id);
bsSetQuizButtonState(id, 'default');
var quiz = document.getElementById(id);
if (quiz) quiz.classList.remove('bs-quiz-locked');
});
['bsNextS1Q', 'bsNextS2Q', 'bsNextS3Q', 'bsNextS4Q', 'bsNextS5P1', 'bsNextS5P2'].forEach(function(id) {
var btn = document.getElementById(id);
if (btn) btn.classList.add('bs-btn-locked');
});
var checklist = document.getElementById('bsChecklist3_2');
if (checklist) {
checklist.querySelectorAll('li.checked').forEach(function(li) { li.classList.remove('checked'); });
var clBtn = document.getElementById('bsNextSub3_2');
if (clBtn) clBtn.classList.add('bs-btn-locked');
}
bsGoToSection(0);
}
/* ══════════════════════════════════════
ABSCHLUSS, BESTÄTIGUNG
══════════════════════════════════════ */
function bsToggleConfirm() {
var checkbox = document.getElementById('bsConfirmCheckbox');
var finishBtn = document.getElementById('bsFinishBtn');
if (checkbox.checked) {
finishBtn.classList.remove('bs-btn-locked');
} else {
finishBtn.classList.add('bs-btn-locked');
}
}
function bsFinishTraining() {
var checkbox = document.getElementById('bsConfirmCheckbox');
var form = document.getElementById('bsCompleteForm');
if (!checkbox || !checkbox.checked || !form) return;
form.submit();
}
/* ══════════════════════════════════════
SHUFFLE QUIZ OPTIONS ON LOAD
══════════════════════════════════════ */
(function() {
var quizBlocks = document.querySelectorAll('.bs-quiz-options');
quizBlocks.forEach(function(container) {
var options = Array.from(container.querySelectorAll(':scope > .bs-quiz-option'));
if (options.length < 2) return;
// Fisher-Yates shuffle
for (var i = options.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = options[i];
options[i] = options[j];
options[j] = temp;
}
options.forEach(function(opt) { container.appendChild(opt); });
});
})();
@@ -0,0 +1,47 @@
(function () {
function switchTab(tabName, updateHash) {
const tab = document.querySelector('.content-tab-bar__item[data-tab="' + tabName + '"]');
const content = document.getElementById('tab-' + tabName);
if (!tab || !content) return;
document.querySelectorAll('.content-tab-bar__item').forEach(function (item) {
item.classList.toggle('active', item === tab);
});
document.querySelectorAll('.sb-tab-content').forEach(function (item) {
item.classList.toggle('active', item === content);
});
if (updateHash) {
history.replaceState(null, '', '#' + tabName);
}
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.content-tab-bar__item[data-tab]').forEach(function (tab) {
tab.addEventListener('click', function () {
switchTab(tab.dataset.tab, true);
});
});
const hash = window.location.hash.slice(1);
if (hash) {
switchTab(hash, false);
}
document.querySelectorAll('[data-confirm]').forEach(function (button) {
button.addEventListener('click', function (event) {
if (!window.confirm(button.dataset.confirm || 'Wirklich fortfahren?')) {
event.preventDefault();
}
});
});
document.querySelectorAll('[data-upload-form]').forEach(function (form) {
form.addEventListener('submit', function () {
form.classList.add('sb-uploading');
const button = form.querySelector('.sb-upload-button');
if (button) button.disabled = true;
});
});
});
})();
@@ -169,17 +169,13 @@ function showHideText(element) {
// Per Taste Overlay verlassen
document.addEventListener('keydown', function(event) {
const profileVisitOverlay = document.getElementById('profile_visit_overlay');
const profileCalendarOverlay = document.getElementById('profile_calendar_overlay');
const seelsorgeProfileOverlay = document.getElementById('profile_seelsorge_overlay');
const addNewUser = document.getElementById('addNewUser');
const showMessage = document.getElementById('showMessage');
if (event.key === 'Escape') {
if (profileVisitOverlay && profileVisitOverlay.style.display === 'flex') {
openCloseProfile(0, 'profile_visit', 'profile_visit_overlay');
}
if (profileCalendarOverlay && profileCalendarOverlay.style.display === 'flex') {
openCloseProfile(0, 'profile_calendar', 'profile_calendar_overlay');
if (seelsorgeProfileOverlay && seelsorgeProfileOverlay.style.display === 'flex') {
openCloseProfile(0, 'profile_seelsorge', 'profile_seelsorge_overlay');
}
if (addNewUser && addNewUser.style.display === 'flex') {
openCloseOverlay('addNewUser');
@@ -0,0 +1,173 @@
(function () {
'use strict';
var currentStep = 1;
function byId(id) {
return document.getElementById(id);
}
function selectedGroup(form) {
return form.querySelector('input[name="group"]:checked');
}
function showAlert(message) {
window.alert(message);
return false;
}
function validateStepOne(form) {
var selected = selectedGroup(form);
if (!selected) {
return showAlert('Bitte wählen Sie eine Terminart aus.');
}
if (selected.dataset.kalliCustom === '1' && !byId('kalliUserInput').value.trim()) {
return showAlert('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.');
}
return true;
}
function validateStepTwo() {
var dateStart = byId('kalliDateS').value;
var dateEnd = byId('kalliDateE').value;
var timeStart = byId('kalliTimeS').value;
var timeEnd = byId('kalliTimeE').value;
if (!dateStart) {
return showAlert('Bitte geben Sie ein Datum ein.');
}
if (dateEnd && dateEnd < dateStart) {
return showAlert('Das Enddatum darf nicht vor dem Startdatum liegen.');
}
if (timeStart && timeEnd && (!dateEnd || dateEnd === dateStart) && timeEnd < timeStart) {
return showAlert('Die Endzeit darf am selben Tag nicht vor der Startzeit liegen.');
}
return true;
}
function updateProgress(step) {
document.querySelectorAll('.kalli-progress-step').forEach(function (element, index) {
var elementStep = index + 1;
element.classList.toggle('active', elementStep === step);
element.classList.toggle('done', elementStep < step);
});
document.querySelectorAll('.kalli-progress-line').forEach(function (element, index) {
element.classList.toggle('done', index < step - 1);
});
}
function showHeader() {
var rootStyle = getComputedStyle(document.documentElement);
var headerHeight = parseFloat(rootStyle.getPropertyValue('--header-height')) || 64;
var menu = byId('header_menue');
var title = byId('header_title');
if (menu) menu.style.top = '0';
if (title) title.style.top = headerHeight + 'px';
}
function goToStep(step) {
for (var index = 1; index <= 4; index++) {
byId('kalliStep' + index).hidden = index !== step;
}
currentStep = step;
updateProgress(step);
window.scrollTo({ top: 0, behavior: 'smooth' });
showHeader();
}
function formatDate(isoDate) {
if (!isoDate) return '–';
var parts = isoDate.split('-');
return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : isoDate;
}
function showSummaryRow(rowId, valueId, value, displayValue) {
var row = byId(rowId);
row.hidden = !value;
if (value) byId(valueId).textContent = displayValue;
}
function buildSummary(form) {
var selected = selectedGroup(form);
var groupLabel = selected ? selected.value : '–';
if (selected && selected.dataset.kalliCustom === '1') {
var customTitle = byId('kalliUserInput').value.trim();
var visibility = selected.value === 'oeffentlich' ? 'Öffentlich' : 'Intern';
groupLabel = customTitle ? customTitle + ' (' + visibility + ')' : groupLabel;
}
byId('sumGroup').textContent = groupLabel;
byId('sumDateS').textContent = formatDate(byId('kalliDateS').value);
showSummaryRow('sumDateERow', 'sumDateE', byId('kalliDateE').value, formatDate(byId('kalliDateE').value));
showSummaryRow('sumTimeSRow', 'sumTimeS', byId('kalliTimeS').value, byId('kalliTimeS').value);
showSummaryRow('sumTimeERow', 'sumTimeE', byId('kalliTimeE').value, byId('kalliTimeE').value);
showSummaryRow('sumTextRow', 'sumText', byId('kalliFurtherText').value.trim(), byId('kalliFurtherText').value.trim());
showSummaryRow('sumLocRow', 'sumLoc', byId('kalliLocation').value.trim(), byId('kalliLocation').value.trim());
showSummaryRow('sumRespRow', 'sumResp', byId('kalliResponsibility').value.trim(), byId('kalliResponsibility').value.trim());
}
function validateForStep(form, nextStep) {
if (nextStep === 2) return validateStepOne(form);
if (nextStep === 3) return validateStepTwo();
if (nextStep === 4) {
if (!validateStepOne(form) || !validateStepTwo()) return false;
buildSummary(form);
}
return true;
}
function initialise() {
var form = byId('kalliForm');
if (!form) return;
form.querySelectorAll('[data-kalli-next]').forEach(function (button) {
button.addEventListener('click', function () {
var nextStep = Number(button.dataset.kalliNext);
if (validateForStep(form, nextStep)) goToStep(nextStep);
});
});
form.querySelectorAll('[data-kalli-prev]').forEach(function (button) {
button.addEventListener('click', function () {
goToStep(Number(button.dataset.kalliPrev));
});
});
form.addEventListener('submit', function (event) {
if (currentStep !== 4) {
event.preventDefault();
var nextStep = Math.min(currentStep + 1, 4);
if (validateForStep(form, nextStep)) goToStep(nextStep);
return;
}
if (!validateStepOne(form)) {
event.preventDefault();
goToStep(1);
return;
}
if (!validateStepTwo()) {
event.preventDefault();
goToStep(2);
return;
}
var submitButton = form.querySelector('button[type="submit"]');
if (submitButton) submitButton.disabled = true;
});
updateProgress(1);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialise);
} else {
initialise();
}
}());
@@ -38,6 +38,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Zeige den entsprechenden Inhalt an
var contentId = item.getAttribute('data-target');
document.getElementById(contentId).classList.add('active');
const url = new URL(window.location.href);
if (contentId === 'content_visit') url.searchParams.set('tab', 'visit');
else url.searchParams.delete('tab');
window.history.replaceState(null, '', url);
});
});
@@ -68,130 +73,108 @@ function switchCalendar(type) {
}
// Tages-Modal oeffnen
async function openDayModal(calendarDate) {
if (!calendarDate) return;
let _seelsorgeConfig = null;
function getSeelsorgeConfig() {
if (_seelsorgeConfig) return _seelsorgeConfig;
const element = document.getElementById('seelsorge-config');
_seelsorgeConfig = element ? JSON.parse(element.textContent) : { csrfToken: '' };
return _seelsorgeConfig;
}
async function loadCalendarDay(calendarDate, action = 'view') {
if (!calendarDate) return null;
const modal = document.getElementById('seelsorge-day-modal');
const body = document.getElementById('modal-day-body');
const title = document.getElementById('modal-day-title');
const subtitle = document.getElementById('modal-day-subtitle');
if (!modal || !body || !title || !subtitle) return null;
if (!modal || !body || !title || !subtitle) {
console.error('openDayModal: Modal-Elemente nicht gefunden.');
return;
if (action === 'view') {
title.textContent = 'Laden...';
subtitle.innerHTML = '';
}
// Modal sofort anzeigen mit Ladezustand
title.textContent = 'Laden...';
subtitle.innerHTML = '';
body.innerHTML = '<div class="seelsorge-modal-loading"><div class="loading-spinner"></div></div>';
body.dataset.calendarDate = calendarDate;
modal.style.display = 'flex';
try {
const response = await fetch('content/calendar_day.php', {
const url = 'content/calendar_day.php?date=' + encodeURIComponent(calendarDate);
const options = action === 'toggle'
? {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'calendarDate=' + encodeURIComponent(calendarDate)
});
const html = await response.text();
if (response.ok) {
body.innerHTML = html;
// Metadaten aus dem eingebetteten JSON lesen
const metaEl = body.querySelector('#modal-meta');
if (metaEl) {
const meta = JSON.parse(metaEl.textContent);
title.textContent = meta.title;
let chips = '<span class="seelsorge-modal-chip">KW ' + meta.kw + '</span>';
if (meta.feiertag) {
chips += '<span class="seelsorge-modal-chip seelsorge-chip-holiday">' + meta.feiertag + '</span>';
}
subtitle.innerHTML = chips;
metaEl.remove();
}
} else {
// PHP-Fehler direkt im Modal anzeigen
title.textContent = 'Fehler ' + response.status;
body.innerHTML = '<div style="padding:16px;font-family:monospace;font-size:0.85em;color:#d21400;white-space:pre-wrap;word-break:break-word;background:#fff5f5;border:1px solid #fcc;">'
+ (html || 'Keine Antwort vom Server.') + '</div>';
body: new URLSearchParams({
date: calendarDate,
action: 'toggle',
csrf_token: getSeelsorgeConfig().csrfToken
})
}
: { method: 'GET' };
try {
const response = await fetch(url, options);
const html = await response.text();
if (!response.ok) {
throw new Error(html || ('HTTP Fehler: ' + response.status));
}
body.innerHTML = html;
const metaEl = body.querySelector('#modal-meta');
if (!metaEl) throw new Error('Tagesdaten sind unvollständig.');
const meta = JSON.parse(metaEl.textContent);
title.textContent = meta.title;
let chips = '<span class="seelsorge-modal-chip">KW ' + meta.kw + '</span>';
if (meta.feiertag) {
chips += '<span class="seelsorge-modal-chip seelsorge-chip-holiday">' + escHtml(meta.feiertag) + '</span>';
}
subtitle.innerHTML = chips;
metaEl.remove();
return meta;
} catch (error) {
body.innerHTML = '<div class="seelsorge-modal-empty">Fehler beim Laden der Daten: ' + error.message + '</div>';
title.textContent = 'Fehler';
body.innerHTML = '<div class="seelsorge-modal-empty">' + escHtml(error.message || 'Fehler beim Laden der Daten.') + '</div>';
console.error('Fehler beim Laden der Tagesdetails:', error);
return null;
}
}
function openDayModal(calendarDate) {
return loadCalendarDay(calendarDate, 'view');
}
// Modal schliessen
function closeDayModal() {
const modal = document.getElementById('seelsorge-day-modal');
if (modal) modal.style.display = 'none';
}
// Backdrop-Klick schliesst Modal
function closeDayModalOnBackdrop(event) {
if (event.target === event.currentTarget) {
closeDayModal();
}
if (event.target === event.currentTarget) closeDayModal();
}
// Verfuegbarkeit im Modal togglen (Modal bleibt offen)
async function toggleAvailabilityModal(date) {
const btn = document.getElementById('modal-toggle-btn');
if (!btn) return;
const button = document.getElementById('modal-toggle-btn');
if (button) button.disabled = true;
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Wird gespeichert\u2026';
try {
const response = await fetch("toggle_availability.php", {
method: "POST",
headers: { "Content-type": "application/x-www-form-urlencoded" },
body: "date=" + encodeURIComponent(date)
});
const data = await response.json();
if (data.ok) {
// Modal-Inhalt neu laden um aktuellen Stand zu zeigen
await openDayModal(date);
// Kalender-Tageszelle visuell aktualisieren
updateCalendarCell(date, data.nowAvailable, data.availCount);
} else {
btn.textContent = originalText;
btn.disabled = false;
console.error("Fehler:", data.error);
}
} catch (error) {
btn.textContent = originalText;
btn.disabled = false;
console.error("Fehler beim Aktualisieren der Verfuegbarkeit:", error);
}
const meta = await loadCalendarDay(date, 'toggle');
if (meta) updateCalendarCell(date, Number(meta.availCount) || 0);
}
// Kalender-Tageszelle visuell aktualisieren ohne Page-Reload
function updateCalendarCell(date, nowAvailable, availCount) {
var cells = document.querySelectorAll('#calendar_day_item');
function updateCalendarCell(date, availCount) {
const cells = document.querySelectorAll(`[data-calendar-date="${date}"]`);
cells.forEach(function(cell) {
var onclick = cell.getAttribute('onclick') || '';
if (onclick.indexOf(date) === -1) return;
// Alle Verfuegbarkeits-Klassen entfernen
cell.classList.remove('available_single', 'available_multiple', 'normalday');
cell.classList.remove(
'available_single', 'available_multiple', 'normalday',
'today', 'sunday', 'localholiday', 'saturday'
);
if (availCount >= 2) {
cell.classList.add('available_multiple');
} else if (availCount === 1) {
cell.classList.add('available_single');
} else {
cell.classList.add('normalday');
cell.classList.add(cell.dataset.emptyClass || 'normalday');
}
});
}
@@ -201,26 +184,16 @@ function updateCalendarCell(date, nowAvailable, availCount) {
var _lastCalendarDate = null;
function openCalendarProfile(userId) {
// Datum merken, damit wir beim Schliessen zurueckkehren koennen
var metaEl = document.getElementById('modal-meta');
var bodyEl = document.getElementById('modal-day-body');
if (!_lastCalendarDate && bodyEl) {
// Datum aus dem Toggle-Button extrahieren
var toggleBtn = bodyEl.querySelector('#modal-toggle-btn');
if (toggleBtn) {
var onclickAttr = toggleBtn.getAttribute('onclick') || '';
var match = onclickAttr.match(/'(\d{4}-\d{2}-\d{2})'/);
if (match) _lastCalendarDate = match[1];
}
}
const body = document.getElementById('modal-day-body');
_lastCalendarDate = body?.dataset.calendarDate || null;
closeDayModal();
openCloseProfile(userId, 'profile_calendar', 'profile_calendar_overlay');
openCloseProfile(userId, 'profile_seelsorge', 'profile_seelsorge_overlay');
}
// Globalen Listener fuer Kalender-Profil-Overlay: bei Schliessen Tagesmodal wieder oeffnen
document.addEventListener('DOMContentLoaded', function() {
// MutationObserver auf das Kalender-Profil-Overlay
var calOverlay = document.getElementById('profile_calendar_overlay');
var calOverlay = document.getElementById('profile_seelsorge_overlay');
if (calOverlay) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
@@ -244,4 +217,297 @@ function openCalendarHelp() {
function closeCalendarHelp() {
var modal = document.getElementById('calendar-help-modal');
if (modal) modal.style.display = 'none';
}
}
// Besuchsstellen ------------------------------------------------------------
let _visitConfig = null;
function getVisitConfig() {
if (_visitConfig) return _visitConfig;
const element = document.getElementById('visit-config');
_visitConfig = element ? JSON.parse(element.textContent) : { entries: [], currentUserId: 0 };
return _visitConfig;
}
function openVisitProfile(userId) {
closeHouseholdDetail();
openCloseProfile(userId, 'profile_seelsorge', 'profile_seelsorge_overlay');
}
// --- Detail Modal ---
function openHouseholdDetail(data) {
const overlay = document.getElementById('visitDetailOverlay');
const title = document.getElementById('detailHeroTitle');
const sub = document.getElementById('detailHeroSub');
const body = document.getElementById('detailBody');
const members = data.members || [];
const lastName = members[0]?.last_name || '';
const firstNames = members.map(m => m.first_name).join(' & ');
title.textContent = lastName + ', ' + firstNames;
sub.textContent = data.address_street + ' · ' + data.address_plz;
let html = '';
// Members section
html += '<div class="visit-detail-section-title">Haushaltsmitglieder</div>';
members.forEach(m => {
html += `<div class="visit-detail-member">
<div class="visit-detail-member-info">
${escHtml(m.first_name)} ${escHtml(m.last_name)}
<span class="age">geb. ${escHtml(m.birth_formatted)} (${m.age} Jahre)</span>
</div>
<button class="visit-detail-member-edit" onclick="event.stopPropagation(); editMemberFromDetail(${m.besuch_id})" title="Bearbeiten"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
</div>`;
});
// Info grid
html += '<div class="visit-detail-section-title">Details</div>';
html += '<div class="visit-detail-info-grid">';
html += `<div class="visit-detail-info-item"><div class="visit-detail-info-label">Adresse</div><div class="visit-detail-info-value"><a href="${escHtml(data.address_link)}" target="_blank" style="color:var(--theme-color);text-decoration:none;">${escHtml(data.address_street)}, ${escHtml(data.address_plz)}</a></div></div>`;
const phone = members[0]?.phone || data.phone || '';
const phoneFormatted = phone ? ('0' + phone) : '—';
html += `<div class="visit-detail-info-item"><div class="visit-detail-info-label">Telefon</div><div class="visit-detail-info-value">${phone ? '<a href="tel:0'+escHtml(phone)+'" style="color:var(--theme-color);text-decoration:none;">'+escHtml(phoneFormatted)+'</a>' : '—'}</div></div>`;
const priestName = members[0]?.priest_name || '—';
const priestId = members[0]?.priest_id || 0;
const priestImg = members[0]?.priest_img || '/intranet_repo/general_files/profile_pictures/default.jpg';
if (priestId > 0) {
html += `<div class="visit-detail-info-item"><div class="visit-detail-info-label">Priester</div><div class="visit-detail-info-value">`;
html += `<span style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;" onclick="openVisitProfile(${priestId})">`;
html += `<img src="${escHtml(priestImg)}" alt="" style="width:28px;height:28px;border-radius:50%;object-fit:cover;">`;
html += `<span style="color:var(--theme-color);text-decoration:none;">${escHtml(priestName)}</span>`;
html += `</span></div></div>`;
} else {
html += `<div class="visit-detail-info-item"><div class="visit-detail-info-label">Priester</div><div class="visit-detail-info-value">${escHtml(priestName)}</div></div>`;
}
const comment = members.map(m => m.comment).filter(c => c).join('; ') || '—';
html += `<div class="visit-detail-info-item"><div class="visit-detail-info-label">Hinweis</div><div class="visit-detail-info-value">${escHtml(comment)}</div></div>`;
html += '</div>';
// Reservation info
if (data.reservation) {
html += '<div class="visit-detail-section-title">Geplanter Besuch</div>';
const resName = data.reservation_name || ('Nutzer #' + data.reservation);
const resImg = data.reservation_img || '/intranet_repo/general_files/profile_pictures/default.jpg';
const resId = parseInt(data.reservation);
html += '<div class="visit-detail-info-item" style="grid-column:1/-1;"><div class="visit-detail-info-label">Geplant von</div><div class="visit-detail-info-value">';
html += `<span style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;" onclick="openVisitProfile(${resId})">`;
html += `<img src="${escHtml(resImg)}" alt="" style="width:28px;height:28px;border-radius:50%;object-fit:cover;">`;
html += `<span style="color:var(--theme-color);text-decoration:none;">${escHtml(resName)}</span>`;
html += `</span>`;
html += '</div></div>';
}
// Actions
const firstBesuchId = members[0]?.besuch_id || 0;
html += '<div class="visit-detail-actions">';
if (!data.reservation) {
html += `<button class="visit-action-btn visit-action-btn-primary" onclick="doReservation(${firstBesuchId})">Reservieren</button>`;
} else if (parseInt(data.reservation) === getVisitConfig().currentUserId) {
html += `<button class="visit-action-btn" style="color:hsl(145,55%,34%);border-color:hsl(145,55%,80%);background:hsl(145,55%,96%);" onclick="doComplete(${firstBesuchId})"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>Abschließen</button>`;
}
html += `<button class="visit-action-btn" onclick="doPrioritize(${firstBesuchId})">Priorisieren</button>`;
html += '</div>';
body.innerHTML = html;
overlay.style.display = 'flex';
}
function closeHouseholdDetail() {
document.getElementById('visitDetailOverlay').style.display = 'none';
}
function escHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function editMemberFromDetail(besuchId) {
closeHouseholdDetail();
const entry = getVisitConfig().entries.find(e => e.besuch_id === besuchId);
if (entry) openVisitEditModalFromEntry(entry);
}
function submitVisitAction(action, besuchId) {
const form = document.getElementById('visitActionForm');
const command = document.getElementById('visit_action_command');
const id = document.getElementById('visit_action_id');
if (!form || !command || !id) return;
command.value = action;
id.value = besuchId;
form.submit();
}
function doReservation(besuchId) { submitVisitAction('reserve', besuchId); }
function doComplete(besuchId) { submitVisitAction('complete', besuchId); }
function doPrioritize(besuchId) { submitVisitAction('prioritize', besuchId); }
// --- Picker Modal ---
function openVisitPickerModal() {
renderVisitPickerList('');
document.getElementById('visitPickerModalOverlay').style.display = 'flex';
const inp = document.getElementById('visitPickerSearch');
inp.value = '';
setTimeout(() => inp.focus(), 30);
}
function closeVisitPickerModal() {
document.getElementById('visitPickerModalOverlay').style.display = 'none';
}
function onVisitPickerSearchInput() {
renderVisitPickerList(document.getElementById('visitPickerSearch').value || '');
}
function normalizeStr(s) { return (s || '').toString().toLowerCase(); }
function matchesQuery(entry, query) {
const q = normalizeStr(query).trim();
if (!q) return true;
const ln = normalizeStr(entry.last_name);
const fn = normalizeStr(entry.first_name);
const a = (ln + ' ' + fn).trim();
const b = (fn + ' ' + ln).trim();
const tokens = q.split(/\s+/).filter(Boolean);
return tokens.every(t => a.includes(t) || b.includes(t));
}
function renderVisitPickerList(query) {
const list = document.getElementById('visitPickerList');
list.innerHTML = '';
if (!Array.isArray(getVisitConfig().entries) || getVisitConfig().entries.length === 0) {
list.innerHTML = '<div style="opacity:.75;padding:8px;">Keine Einträge vorhanden.</div>';
return;
}
const filtered = getVisitConfig().entries.filter(e => matchesQuery(e, query));
if (filtered.length === 0) {
list.innerHTML = '<div style="opacity:.75;padding:8px;">Keine Treffer.</div>';
return;
}
filtered.forEach(entry => {
const item = document.createElement('div');
item.className = 'visit-picker-item';
item.onclick = () => { closeVisitPickerModal(); openVisitEditModalFromEntry(entry); };
const left = document.createElement('div');
const name = document.createElement('div');
name.className = 'visit-picker-name';
name.textContent = (entry.last_name || '') + ', ' + (entry.first_name || '');
left.appendChild(name);
const sub = document.createElement('div');
sub.className = 'visit-picker-sub';
sub.textContent = (entry.address_street || '') + (entry.address_plz ? (' · ' + entry.address_plz) : '');
left.appendChild(sub);
const right = document.createElement('div');
right.style.cssText = 'opacity:0.5;font-size:0.8em;font-weight:700;';
right.textContent = '#' + entry.besuch_id;
item.appendChild(left);
item.appendChild(right);
list.appendChild(item);
});
}
// --- Edit Modal ---
const EMPTY_VISIT_ENTRY = {
besuch_id: '',
last_name: '',
first_name: '',
birth_date: '',
address_street: '',
address_plz: '',
address_link: '',
phone: '',
comment: '',
letzter_besuch: '2024-07-11',
priority: 0,
pr_bezirk: ''
};
function openVisitEntryModal(entry = null, errorMessage = '') {
const values = Object.assign({}, EMPTY_VISIT_ENTRY, entry || {});
const isEdit = Number(values.besuch_id) > 0;
document.getElementById('visitModalTitle').innerText = isEdit ? 'Eintrag bearbeiten' : 'Neue Besuchsstelle anlegen';
document.getElementById('modal_besuch_id').value = isEdit ? values.besuch_id : '';
document.getElementById('modal_last_name').value = values.last_name || '';
document.getElementById('modal_first_name').value = values.first_name || '';
document.getElementById('modal_birth_date').value = values.birth_date || '';
document.getElementById('modal_address_street').value = values.address_street || '';
document.getElementById('modal_address_plz').value = values.address_plz || '';
document.getElementById('modal_address_link').value = values.address_link || '';
document.getElementById('modal_phone').value = values.phone || '';
document.getElementById('modal_comment').value = values.comment || '';
document.getElementById('modal_letzter_besuch').value = values.letzter_besuch || EMPTY_VISIT_ENTRY.letzter_besuch;
document.getElementById('modal_priority').value = values.priority ?? 0;
document.getElementById('modal_pr_bezirk').value = values.pr_bezirk ?? '';
document.getElementById('modal_delete_btn').style.display = isEdit ? 'inline-block' : 'none';
const error = document.getElementById('visitFormError');
if (error) {
error.textContent = errorMessage;
error.style.display = errorMessage ? 'block' : 'none';
}
showVisitModal();
}
function openVisitEditModalFromEntry(entry) {
openVisitEntryModal(entry);
}
function openVisitAddModal() {
openVisitEntryModal();
}
function showVisitModal() {
const overlay = document.getElementById('visitEntryModalOverlay');
overlay.style.display = 'flex';
overlay.setAttribute('aria-hidden', 'false');
setTimeout(() => document.getElementById('modal_last_name')?.focus(), 30);
}
function closeVisitModal() {
const overlay = document.getElementById('visitEntryModalOverlay');
if (overlay && overlay.contains(document.activeElement)) document.activeElement.blur();
overlay.setAttribute('aria-hidden', 'true');
overlay.style.display = 'none';
}
// Close on backdrop/escape
document.addEventListener('click', function(e) {
const picker = document.getElementById('visitPickerModalOverlay');
const edit = document.getElementById('visitEntryModalOverlay');
if (picker && picker.style.display === 'flex' && e.target === picker) closeVisitPickerModal();
if (edit && edit.style.display === 'flex' && e.target === edit) closeVisitModal();
});
document.addEventListener('keydown', function(e) {
if (e.key !== 'Escape') return;
const detail = document.getElementById('visitDetailOverlay');
const edit = document.getElementById('visitEntryModalOverlay');
const picker = document.getElementById('visitPickerModalOverlay');
if (edit && edit.style.display === 'flex') return closeVisitModal();
if (picker && picker.style.display === 'flex') return closeVisitPickerModal();
if (detail && detail.style.display === 'flex') return closeHouseholdDetail();
});
function deleteVisitEntryFromModal() {
const besuchId = document.getElementById('modal_besuch_id').value;
if (besuchId && confirm('Eintrag wirklich löschen?')) {
submitVisitAction('delete', besuchId);
}
}
// Server-seitige Validierungsfehler nach Post/Redirect/Get im Formular wieder anzeigen.
document.addEventListener('DOMContentLoaded', function() {
const config = getVisitConfig();
if (config.formError && config.formData) {
openVisitEntryModal(config.formData, config.formError);
} else if (config.formError) {
alert(config.formError);
}
});