camera - (real) remote control, brandschutz - configurable course, dashboard - vbb licensed API (a real vbb license, how cool)

This commit is contained in:
2026-09-08 18:21:51 +02:00
parent 3f98cf2663
commit ce90820094
24 changed files with 3044 additions and 2065 deletions
@@ -1,15 +1,680 @@
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';
});
}
});
(function () {
'use strict';
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';
}
window.openBsModal = openBsModal;
window.closeBsModal = closeBsModal;
var dataElement = document.getElementById('bsEditorData');
if (!dataElement) return;
var editorData = JSON.parse(dataElement.textContent);
var config = editorData.config;
var images = editorData.images || [];
var revision = Number(editorData.revision || 1);
var currentChapter = 0;
var currentPage = 0;
var modalState = null;
var dirty = false;
var saveInFlight = false;
var saveQueued = false;
var dragged = null;
var typeLabels = {
text: 'Textblock', infobox: 'Infobox', warnbox: 'Warnbox', list: 'Liste', accordion: 'Akkordeon',
checklist: 'Checkliste', scenario: 'Szenario', multiple_choice: 'Multiple-Choice-Frage',
fill_blank: 'Lückentext', matching: 'Zuordnung', image: 'Bild'
};
var chapterList = document.getElementById('bsChapterList');
var pageList = document.getElementById('bsPageList');
var elementList = document.getElementById('bsElementList');
var modal = document.getElementById('bsEditorModal');
var modalTitle = document.getElementById('bsEditorModalTitle');
var modalBody = document.getElementById('bsEditorModalBody');
var status = document.getElementById('bsEditorStatus');
var courseEditorOverlay = document.getElementById('bsCourseEditorOverlay');
function id(prefix) {
return prefix + '-' + (window.crypto && crypto.randomUUID ? crypto.randomUUID() : Date.now() + '-' + Math.random().toString(16).slice(2));
}
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function button(label, action, className, title) {
var node = el('button', className || 'bs-editor-icon-button', label);
node.type = 'button';
if (title) node.title = title;
node.addEventListener('click', function (event) { event.stopPropagation(); action(); });
return node;
}
function markDirty() {
dirty = true;
showStatus('Die Änderung wird verarbeitet.', 'info');
saveCourse();
}
function showStatus(message, kind) {
status.textContent = message;
status.className = 'bs-editor-notice bs-editor-notice--' + (kind || 'info');
status.hidden = false;
}
async function saveCourse() {
if (saveInFlight) {
saveQueued = true;
return;
}
saveInFlight = true;
saveQueued = false;
var snapshot = JSON.stringify(config);
var saved = false;
var form = new FormData();
form.append('action', 'save_course');
form.append('csrf_token', editorData.csrfToken);
form.append('course_revision', String(revision));
form.append('course_config', snapshot);
try {
var response = await fetch('admin_brandschutz.php', {method: 'POST', body: form});
var result = await response.json();
if (!response.ok || !result.ok) throw new Error(result.error || 'Speichern fehlgeschlagen.');
revision = Number(result.revision);
saved = true;
if (JSON.stringify(config) === snapshot) {
dirty = false;
showStatus('Die Änderung wurde erfolgreich gespeichert.', 'success');
} else {
saveQueued = true;
}
} catch (error) {
dirty = true;
showStatus(error.message || 'Die Änderung konnte nicht gespeichert werden.', 'error');
} finally {
saveInFlight = false;
if (saved && saveQueued) saveCourse();
}
}
function selectedChapter() { return config.chapters[currentChapter] || null; }
function selectedPage() {
var chapter = selectedChapter();
return chapter && chapter.pages[currentPage] ? chapter.pages[currentPage] : null;
}
function move(array, from, to) {
if (from < 0 || to < 0 || from >= array.length || to >= array.length || from === to) return;
array.splice(to, 0, array.splice(from, 1)[0]);
markDirty();
}
function controls(kind, index, length, editAction, deleteAction) {
var group = el('div', 'bs-editor-item-controls');
group.append(button('✎', editAction, '', 'Bearbeiten'));
group.append(button('↑', function () { reorder(kind, index, index - 1); }, '', 'Nach oben'));
group.lastChild.disabled = index === 0;
group.append(button('↓', function () { reorder(kind, index, index + 1); }, '', 'Nach unten'));
group.lastChild.disabled = index === length - 1;
group.append(button('×', deleteAction, 'bs-editor-icon-button bs-editor-icon-button--danger', 'Löschen'));
return group;
}
function dragEvents(node, kind, index) {
node.draggable = true;
node.addEventListener('dragstart', function () { dragged = {kind: kind, index: index}; node.classList.add('dragging'); });
node.addEventListener('dragend', function () { dragged = null; node.classList.remove('dragging'); });
node.addEventListener('dragover', function (event) { if (dragged && dragged.kind === kind) event.preventDefault(); });
node.addEventListener('drop', function (event) {
event.preventDefault();
if (dragged && dragged.kind === kind) reorder(kind, dragged.index, index);
});
}
function reorder(kind, from, to) {
if (kind === 'chapter') {
move(config.chapters, from, to);
currentChapter = Math.max(0, Math.min(to, config.chapters.length - 1));
currentPage = 0;
} else if (kind === 'page') {
move(selectedChapter().pages, from, to);
currentPage = Math.max(0, Math.min(to, selectedChapter().pages.length - 1));
} else {
move(selectedPage().elements, from, to);
}
render();
}
function render() {
currentChapter = Math.max(0, Math.min(currentChapter, Math.max(0, config.chapters.length - 1)));
var chapter = selectedChapter();
if (chapter) currentPage = Math.max(0, Math.min(currentPage, Math.max(0, chapter.pages.length - 1)));
renderChapters();
renderPages();
renderElements();
}
function renderEmpty(container, title, text) {
container.replaceChildren();
var empty = el('div', 'bs-editor-empty');
empty.append(el('strong', '', title), el('p', '', text));
container.append(empty);
}
function renderChapters() {
chapterList.replaceChildren();
if (config.chapters.length === 0) {
renderEmpty(chapterList, 'Noch keine Kapitel', 'Lege das erste Kapitel an, um die Schulung aufzubauen.');
return;
}
config.chapters.forEach(function (chapter, index) {
var item = el('div', 'bs-editor-list-item' + (index === currentChapter ? ' active' : ''));
var text = el('div', 'bs-editor-item-text');
text.append(el('strong', '', chapter.short_title || chapter.title), el('span', '', chapter.title));
item.append(text, controls('chapter', index, config.chapters.length,
function () { openEntityEditor('chapter', index); },
function () {
if (!confirm('Kapitel „' + chapter.title + '“ einschließlich aller Seiten löschen?')) return;
config.chapters.splice(index, 1); currentChapter = Math.max(0, index - 1); currentPage = 0; markDirty(); render();
}));
item.addEventListener('click', function () { currentChapter = index; currentPage = 0; render(); });
dragEvents(item, 'chapter', index);
chapterList.append(item);
});
}
function renderPages() {
pageList.replaceChildren();
var chapter = selectedChapter();
if (!chapter) {
renderEmpty(pageList, 'Kein Kapitel ausgewählt', 'Lege zuerst ein Kapitel an.');
return;
}
if (chapter.pages.length === 0) {
renderEmpty(pageList, 'Leeres Kapitel', 'Füge diesem Kapitel eine Seite hinzu.');
return;
}
chapter.pages.forEach(function (page, index) {
var item = el('div', 'bs-editor-list-item' + (index === currentPage ? ' active' : ''));
var text = el('div', 'bs-editor-item-text');
text.append(el('strong', '', page.title), el('span', '', page.elements.length + (page.elements.length === 1 ? ' Element' : ' Elemente')));
item.append(text, controls('page', index, chapter.pages.length,
function () { openEntityEditor('page', index); },
function () {
if (!confirm('Seite „' + page.title + '“ einschließlich aller Elemente löschen?')) return;
chapter.pages.splice(index, 1); currentPage = Math.max(0, index - 1); markDirty(); render();
}));
item.addEventListener('click', function () { currentPage = index; render(); });
dragEvents(item, 'page', index);
pageList.append(item);
});
}
function elementSummary(element) {
if (element.question) return element.question;
if (element.title) return element.title;
if (element.content) return String(element.content).replace(/<[^>]*>/g, '').slice(0, 120);
if (element.items && element.items.length) return element.items.length + ' Einträge';
if (element.file) return element.caption || element.alt || element.file;
return 'Noch nicht ausgefüllt';
}
function renderElements() {
elementList.replaceChildren();
var page = selectedPage();
document.getElementById('bsElementPanelTitle').textContent = page ? 'Elemente · ' + page.title : 'Elemente';
document.getElementById('bsAddPage').disabled = !selectedChapter();
document.getElementById('bsAddElement').disabled = !page;
if (!page) {
renderEmpty(elementList, 'Keine Seite ausgewählt', selectedChapter() ? 'Füge dem Kapitel eine Seite hinzu.' : 'Lege zuerst Kapitel und Seite an.');
return;
}
if (page.elements.length === 0) {
renderEmpty(elementList, 'Leere Seite', 'Füge über „+ Baustein“ Inhalte oder interaktive Elemente hinzu.');
return;
}
page.elements.forEach(function (element, index) {
var card = el('article', 'bs-element-card');
var heading = el('div', 'bs-element-card-heading');
heading.append(el('span', 'bs-element-type', typeLabels[element.type] || element.type));
heading.append(controls('element', index, page.elements.length,
function () { openElementEditor(index, false); },
function () {
if (!confirm((typeLabels[element.type] || 'Element') + ' wirklich löschen?')) return;
page.elements.splice(index, 1); markDirty(); render();
}));
card.append(heading, el('p', 'bs-element-summary', elementSummary(element)));
dragEvents(card, 'element', index);
elementList.append(card);
});
}
function openModal(title, state) {
modalState = state;
modalTitle.textContent = title;
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
setTimeout(function () { var first = modalBody.querySelector('input, textarea, select, button'); if (first) first.focus(); }, 0);
}
function closeModal() {
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
modalState = null;
}
function openCourseEditor() {
courseEditorOverlay.style.display = 'flex';
courseEditorOverlay.setAttribute('aria-hidden', 'false');
document.body.classList.add('bs-editor-open');
render();
document.getElementById('bsCloseCourseEditor').focus();
}
function closeCourseEditor() {
if (modal.style.display === 'flex') closeModal();
courseEditorOverlay.style.display = 'none';
courseEditorOverlay.setAttribute('aria-hidden', 'true');
document.body.classList.remove('bs-editor-open');
document.getElementById('bsOpenCourseEditor').focus();
}
function field(label, name, value, options) {
options = options || {};
var wrapper = el(options.richText ? 'div' : 'label', 'bs-editor-field');
wrapper.append(el('span', '', label));
if (options.richText) {
wrapper.append(richTextEditor(label, name, value));
if (options.hint) wrapper.append(el('small', '', options.hint));
return wrapper;
}
var input = document.createElement(options.multiline ? 'textarea' : 'input');
input.name = name;
if (!options.multiline) input.type = options.type || 'text';
if (options.placeholder) input.placeholder = options.placeholder;
if (options.required) input.required = true;
input.value = value === undefined || value === null ? '' : value;
wrapper.append(input);
if (options.hint) wrapper.append(el('small', '', options.hint));
return wrapper;
}
function richTextEditor(label, name, value) {
var box = el('div', 'bs-richtext');
var toolbar = el('div', 'bs-richtext-toolbar');
var editor = el('div', 'bs-richtext-editor');
editor.contentEditable = 'true';
editor.dataset.fieldName = name;
editor.setAttribute('role', 'textbox');
editor.setAttribute('aria-multiline', 'true');
editor.setAttribute('aria-label', label);
editor.innerHTML = value === undefined || value === null ? '' : value;
function commandButton(label, title, command, argument) {
var control = button(label, function () {
editor.focus();
document.execCommand(command, false, argument || null);
}, 'bs-richtext-button', title);
control.setAttribute('aria-label', title);
control.addEventListener('mousedown', function (event) { event.preventDefault(); });
return control;
}
var linkButton = button('Link', function () {
var selection = window.getSelection();
var range = selection && selection.rangeCount ? selection.getRangeAt(0).cloneRange() : null;
if (!range || range.collapsed || !editor.contains(range.commonAncestorContainer)) {
showStatus('Bitte zuerst den gewünschten Text markieren.', 'error');
return;
}
var selectedLink = Array.from(editor.querySelectorAll('a')).some(function (link) {
return range.intersectsNode(link);
});
editor.focus();
selection.removeAllRanges();
selection.addRange(range);
if (selectedLink) {
document.execCommand('unlink', false, null);
return;
}
var href = window.prompt('Linkziel (https://…, mailto:… oder /interner-pfad):', 'https://');
if (href === null) return;
href = href.trim();
if (!/^(https?:\/\/|mailto:|\/[^/])/i.test(href)) {
showStatus('Bitte ein gültiges Linkziel eingeben.', 'error');
return;
}
editor.focus();
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('createLink', false, href);
}, 'bs-richtext-button bs-richtext-button--text', 'Link hinzufügen oder entfernen');
linkButton.setAttribute('aria-label', 'Link hinzufügen oder entfernen');
linkButton.addEventListener('mousedown', function (event) { event.preventDefault(); });
toolbar.append(
commandButton('B', 'Fett', 'bold'),
commandButton('I', 'Kursiv', 'italic'),
commandButton('U', 'Unterstrichen', 'underline'),
commandButton('¶', 'Absatz', 'formatBlock', 'p'),
linkButton,
commandButton('Tx', 'Formatierung entfernen', 'removeFormat')
);
editor.addEventListener('focus', function () { document.execCommand('defaultParagraphSeparator', false, 'p'); });
editor.addEventListener('paste', function (event) {
event.preventDefault();
var text = (event.clipboardData || window.clipboardData).getData('text/plain');
document.execCommand('insertText', false, text);
});
box.append(toolbar, editor);
return box;
}
function controlValue(control) {
if (control.classList.contains('bs-richtext-editor')) {
if (!control.textContent.trim()) return '';
var normalized = control.cloneNode(true);
Array.from(normalized.children).forEach(function (child) {
if (child.tagName === 'DIV') {
var paragraph = document.createElement('p');
while (child.firstChild) paragraph.append(child.firstChild);
child.replaceWith(paragraph);
}
});
return normalized.innerHTML.trim();
}
return control.type === 'checkbox' ? control.checked : control.value.trim();
}
function fieldControl(root, name) {
return root.querySelector('[name="' + name + '"], [data-field-name="' + name + '"]');
}
function checkbox(label, name, checked) {
var wrapper = el('label', 'bs-editor-check');
var input = document.createElement('input');
input.type = 'checkbox'; input.name = name; input.checked = !!checked;
wrapper.append(input, el('span', '', label));
return wrapper;
}
function openEntityEditor(kind, index, isNew) {
modalBody.replaceChildren();
if (kind === 'chapter') {
var chapter = isNew
? {id: id('chapter'), title: 'Neues Kapitel', short_title: 'Kapitel', pages: []}
: JSON.parse(JSON.stringify(config.chapters[index]));
modalBody.append(field('Kapitelname', 'title', chapter.title, {required: true}), field('Kurztitel in der Navigation', 'short_title', chapter.short_title, {required: true, hint: 'Kurz halten, damit die Fortschrittsanzeige auf Mobilgeräten lesbar bleibt.'}));
openModal(isNew ? 'Kapitel hinzufügen' : 'Kapitel bearbeiten', {kind: kind, index: index, isNew: !!isNew, entity: chapter});
} else {
var page = isNew
? {id: id('page'), title: 'Neue Seite', elements: []}
: JSON.parse(JSON.stringify(selectedChapter().pages[index]));
modalBody.append(field('Seitentitel', 'title', page.title, {required: true}));
openModal(isNew ? 'Seite hinzufügen' : 'Seite bearbeiten', {kind: kind, index: index, isNew: !!isNew, entity: page});
}
}
function defaultElement(type) {
var base = {id: id('element'), type: type};
if (type === 'text') base.content = '';
if (['infobox', 'warnbox', 'scenario'].includes(type)) Object.assign(base, {title: '', content: ''});
if (type === 'list') Object.assign(base, {ordered: false, items: ['Neuer Listeneintrag']});
if (type === 'checklist') base.items = ['Neuer Checklistenpunkt'];
if (type === 'accordion') base.items = [{title: 'Neuer Abschnitt', content: ''}];
if (type === 'multiple_choice') Object.assign(base, assessmentDefaults(), {answers: [{text: 'Richtige Antwort', correct: true}, {text: 'Falsche Antwort', correct: false}]});
if (type === 'fill_blank') Object.assign(base, assessmentDefaults(), {text: 'Ergänzen Sie {{1}}.', blanks: [{answers: ['Lösung']}]});
if (type === 'matching') Object.assign(base, assessmentDefaults(), {choices: [{value: 'a', label: 'Kategorie A'}, {value: 'b', label: 'Kategorie B'}], pairs: [{item: 'Neuer Begriff', correct: 'a'}]});
if (type === 'image') Object.assign(base, {file: '', alt: '', caption: ''});
return base;
}
function assessmentDefaults() {
return {question: 'Neue Frage', feedback_correct: 'Richtig!', feedback_incorrect: 'Leider nicht richtig.', score: true};
}
function addRepeater(container, values, columns) {
container.replaceChildren();
values.forEach(function (value, index) {
var row = el('div', 'bs-repeat-row');
columns.forEach(function (column) {
if (column.type === 'checkbox') {
var check = checkbox(column.label, column.name, value[column.key]);
check.querySelector('input').dataset.key = column.key;
row.append(check);
} else if (column.type === 'select') {
var selectLabel = el('label', 'bs-editor-field');
selectLabel.append(el('span', '', column.label));
var select = document.createElement('select'); select.dataset.key = column.key;
column.options().forEach(function (option) { var o = new Option(option.label, option.value); o.selected = option.value === value[column.key]; select.add(o); });
selectLabel.append(select); row.append(selectLabel);
} else {
var inputField = field(column.label, column.name, value[column.key] || '', {multiline: column.multiline, richText: column.richText});
inputField.querySelector('input,textarea,.bs-richtext-editor').dataset.key = column.key;
row.append(inputField);
}
});
row.append(button('×', function () {
var currentValues = readRepeater(container);
currentValues.splice(index, 1);
addRepeater(container, currentValues, columns);
}, 'bs-editor-icon-button bs-editor-icon-button--danger', 'Eintrag entfernen'));
container.append(row);
});
container._values = values;
container._columns = columns;
}
function readRepeater(container) {
return Array.from(container.querySelectorAll(':scope > .bs-repeat-row')).map(function (row) {
var value = {};
row.querySelectorAll('[data-key]').forEach(function (input) { value[input.dataset.key] = controlValue(input); });
return value;
});
}
function repeaterBlock(title, values, columns, emptyValue) {
var block = el('div', 'bs-editor-repeat-block');
var heading = el('div', 'bs-editor-repeat-title');
var container = el('div', 'bs-repeat-list');
heading.append(el('strong', '', title), button('+ Eintrag', function () {
values = readRepeater(container); values.push(Object.assign({}, emptyValue)); addRepeater(container, values, columns);
}, 'bs-editor-button'));
block.append(heading, container);
addRepeater(container, JSON.parse(JSON.stringify(values || [])), columns);
return block;
}
function renderElementFields(element, isNew) {
modalBody.replaceChildren();
if (isNew) {
var typeField = el('label', 'bs-editor-field');
typeField.append(el('span', '', 'Bausteintyp'));
var typeSelect = document.createElement('select'); typeSelect.name = 'type';
Object.keys(typeLabels).forEach(function (type) { typeSelect.add(new Option(typeLabels[type], type, false, type === element.type)); });
typeSelect.addEventListener('change', function () { modalState.element = defaultElement(typeSelect.value); renderElementFields(modalState.element, true); });
typeField.append(typeSelect); modalBody.append(typeField);
}
if (element.type === 'text') modalBody.append(field('Inhalt', 'content', element.content, {richText: true}));
if (['infobox', 'warnbox', 'scenario'].includes(element.type)) modalBody.append(field('Titel', 'title', element.title), field('Inhalt', 'content', element.content, {richText: true}));
if (element.type === 'list') modalBody.append(checkbox('Nummerierte Liste', 'ordered', element.ordered));
if (['list', 'checklist'].includes(element.type)) {
modalBody.append(repeaterBlock('Einträge', (element.items || []).map(function (item) { return {text: item}; }), [{key: 'text', label: 'Text', richText: true}], {text: 'Neuer Eintrag'}));
}
if (element.type === 'accordion') {
modalBody.append(repeaterBlock('Akkordeon-Abschnitte', element.items || [], [{key: 'title', label: 'Titel'}, {key: 'content', label: 'Inhalt', richText: true}], {title: 'Neuer Abschnitt', content: ''}));
}
if (['multiple_choice', 'fill_blank', 'matching'].includes(element.type)) {
modalBody.append(field('Frage / Arbeitsauftrag', 'question', element.question, {multiline: true, required: true}));
}
if (element.type === 'multiple_choice') {
modalBody.append(repeaterBlock('Antwortmöglichkeiten', element.answers || [], [{key: 'text', label: 'Antwort'}, {key: 'correct', label: 'Korrekt', type: 'checkbox'}], {text: 'Neue Antwort', correct: false}));
}
if (element.type === 'fill_blank') {
modalBody.append(field('Lückentext', 'text', element.text, {multiline: true, hint: 'Platzhalter fortlaufend als {{1}}, {{2}}, … einfügen.'}));
modalBody.append(repeaterBlock('Erwartete Lösungen', (element.blanks || []).map(function (blank) { return {answers: (blank.answers || []).join(' | ')}; }), [{key: 'answers', label: 'Gültige Lösungen (mit | trennen)'}], {answers: 'Lösung'}));
}
if (element.type === 'matching') {
modalBody.append(repeaterBlock('Kategorien', element.choices || [], [{key: 'value', label: 'Interner Wert'}, {key: 'label', label: 'Bezeichnung'}], {value: 'neu', label: 'Neue Kategorie'}));
var options = function () { return [{value: '', label: '-- wählen --'}].concat((modalState.element.choices || []).map(function (choice) { return {value: choice.value, label: choice.label}; })); };
modalBody.append(repeaterBlock('Zuordnungen', element.pairs || [], [{key: 'item', label: 'Begriff'}, {key: 'correct', label: 'Richtige Kategorie', type: 'select', options: options}], {item: 'Neuer Begriff', correct: ''}));
}
if (['multiple_choice', 'fill_blank', 'matching'].includes(element.type)) {
modalBody.append(field('Feedback bei korrekter Lösung', 'feedback_correct', element.feedback_correct, {multiline: true}), field('Feedback bei falscher Lösung', 'feedback_incorrect', element.feedback_incorrect, {multiline: true}), checkbox('Für die Abschlussauswertung werten', 'score', element.score));
}
if (element.type === 'image') renderImageFields(element);
}
function renderImageFields(element) {
modalBody.append(field('Alternativtext', 'alt', element.alt, {required: true, hint: 'Beschreibt den Bildinhalt für Screenreader.'}), field('Bildunterschrift (optional)', 'caption', element.caption));
var upload = el('div', 'bs-image-upload');
var input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp,image/gif';
var uploadButton = button('Bild hochladen', function () { uploadImage(input); }, 'bs-editor-button bs-editor-button--primary');
upload.append(input, uploadButton);
modalBody.append(upload, el('div', 'bs-media-grid'));
renderMediaGrid(element);
}
function renderMediaGrid(element) {
var grid = modalBody.querySelector('.bs-media-grid');
if (!grid) return;
grid.replaceChildren();
var none = button('Kein Bild', function () { element.file = ''; renderMediaGrid(element); }, 'bs-media-item' + (!element.file ? ' selected' : ''));
grid.append(none);
images.forEach(function (image) {
var item = el('div', 'bs-media-item' + (element.file === image.file ? ' selected' : ''));
var choose = el('button'); choose.type = 'button';
var preview = document.createElement('img'); preview.src = image.url; preview.alt = image.file;
choose.append(preview); choose.addEventListener('click', function () { element.file = image.file; renderMediaGrid(element); });
var remove = button('×', function () { deleteImage(image.file); }, 'bs-media-delete', 'Bild dauerhaft löschen');
item.append(choose, remove); grid.append(item);
});
}
async function uploadImage(input) {
if (!input.files || !input.files[0]) { showStatus('Bitte zuerst eine Bilddatei auswählen.', 'error'); return; }
var form = new FormData(); form.append('action', 'upload_image'); form.append('csrf_token', editorData.csrfToken); form.append('image', input.files[0]);
try {
var response = await fetch('admin_brandschutz.php', {method: 'POST', body: form});
var result = await response.json();
if (!response.ok || !result.ok) throw new Error(result.error || 'Upload fehlgeschlagen.');
images.unshift(result.image); modalState.element.file = result.image.file; renderMediaGrid(modalState.element); showStatus('Bild wurde hochgeladen.', 'success');
} catch (error) { showStatus(error.message || 'Bild konnte nicht hochgeladen werden.', 'error'); }
}
async function deleteImage(file) {
if (!confirm('Dieses Bild dauerhaft aus der Medienbibliothek löschen?')) return;
var form = new FormData(); form.append('action', 'delete_image'); form.append('csrf_token', editorData.csrfToken); form.append('file', file);
try {
var response = await fetch('admin_brandschutz.php', {method: 'POST', body: form}); var result = await response.json();
if (!response.ok || !result.ok) throw new Error(result.error || 'Löschen fehlgeschlagen.');
images = images.filter(function (image) { return image.file !== file; });
if (modalState.element.file === file) modalState.element.file = '';
renderMediaGrid(modalState.element); showStatus('Bild wurde gelöscht.', 'success');
} catch (error) { showStatus(error.message || 'Bild konnte nicht gelöscht werden.', 'error'); }
}
function openElementEditor(index, isNew) {
var element = isNew ? defaultElement('text') : JSON.parse(JSON.stringify(selectedPage().elements[index]));
openModal(isNew ? 'Baustein hinzufügen' : 'Baustein bearbeiten', {kind: 'element', index: index, isNew: isNew, element: element});
renderElementFields(element, isNew);
}
function readElementForm(element) {
modalBody.querySelectorAll(':scope > .bs-editor-field input, :scope > .bs-editor-field textarea, :scope > .bs-editor-field .bs-richtext-editor').forEach(function (input) {
element[input.name || input.dataset.fieldName] = controlValue(input);
});
modalBody.querySelectorAll(':scope > .bs-editor-check input').forEach(function (input) { element[input.name] = input.checked; });
var repeaters = Array.from(modalBody.querySelectorAll('.bs-repeat-list'));
if (['list', 'checklist'].includes(element.type)) element.items = readRepeater(repeaters[0]).map(function (item) { return item.text; }).filter(Boolean);
if (element.type === 'accordion') element.items = readRepeater(repeaters[0]);
if (element.type === 'multiple_choice') element.answers = readRepeater(repeaters[0]);
if (element.type === 'fill_blank') element.blanks = readRepeater(repeaters[0]).map(function (blank) { return {answers: blank.answers.split('|').map(function (answer) { return answer.trim(); }).filter(Boolean)}; });
if (element.type === 'matching') {
element.choices = readRepeater(repeaters[0]);
element.pairs = readRepeater(repeaters[1]);
}
return element;
}
function saveModal() {
if (!modalState) return;
if (modalState.kind === 'chapter') {
var chapter = modalState.entity;
chapter.title = modalBody.querySelector('[name="title"]').value.trim() || 'Unbenanntes Kapitel';
chapter.short_title = modalBody.querySelector('[name="short_title"]').value.trim() || 'Kapitel';
if (modalState.isNew) {
config.chapters.push(chapter);
currentChapter = config.chapters.length - 1;
currentPage = 0;
} else {
config.chapters[modalState.index] = chapter;
}
} else if (modalState.kind === 'page') {
var page = modalState.entity;
page.title = modalBody.querySelector('[name="title"]').value.trim() || 'Unbenannte Seite';
if (modalState.isNew) {
selectedChapter().pages.push(page);
currentPage = selectedChapter().pages.length - 1;
} else {
selectedChapter().pages[modalState.index] = page;
}
} else if (modalState.kind === 'settings') {
config.title = modalBody.querySelector('[name="course_title"]').value.trim() || 'Brandschutzunterweisung';
config.settings.passing_score = Number(modalBody.querySelector('[name="passing_score"]').value) || 70;
config.settings.max_attempts = Number(modalBody.querySelector('[name="max_attempts"]').value) || 3;
['success_title', 'success_text', 'failure_title', 'failure_text', 'confirmation_text'].forEach(function (name) {
config.settings[name] = controlValue(fieldControl(modalBody, name));
});
} else if (modalState.kind === 'element') {
var element = readElementForm(modalState.element);
if (modalState.isNew) selectedPage().elements.push(element);
else selectedPage().elements[modalState.index] = element;
}
markDirty(); closeModal(); render();
}
function openSettings() {
modalBody.replaceChildren();
var settings = config.settings || (config.settings = {});
modalBody.append(field('Titel der Schulung', 'course_title', config.title), field('Bestehensgrenze in Prozent', 'passing_score', settings.passing_score || 70, {type: 'number'}), field('Maximale Versuche pro Frage', 'max_attempts', settings.max_attempts || 3, {type: 'number'}), field('Erfolgstitel', 'success_title', settings.success_title), field('Erfolgstext', 'success_text', settings.success_text, {richText: true}), field('Titel bei Nichtbestehen', 'failure_title', settings.failure_title), field('Text bei Nichtbestehen', 'failure_text', settings.failure_text, {richText: true}), field('Bestätigungstext', 'confirmation_text', settings.confirmation_text, {richText: true}));
openModal('Schulungseinstellungen', {kind: 'settings'});
}
document.getElementById('bsAddChapter').addEventListener('click', function () {
openEntityEditor('chapter', -1, true);
});
document.getElementById('bsAddPage').addEventListener('click', function () {
var chapter = selectedChapter(); if (!chapter) return;
openEntityEditor('page', -1, true);
});
document.getElementById('bsAddElement').addEventListener('click', function () { if (selectedPage()) openElementEditor(-1, true); });
document.getElementById('bsEditSettings').addEventListener('click', openSettings);
document.getElementById('bsOpenCourseEditor').addEventListener('click', openCourseEditor);
document.getElementById('bsCloseCourseEditor').addEventListener('click', closeCourseEditor);
document.getElementById('bsEditorModalSave').addEventListener('click', saveModal);
document.getElementById('bsEditorModalCancel').addEventListener('click', closeModal);
document.getElementById('bsEditorModalClose').addEventListener('click', closeModal);
modal.addEventListener('click', function (event) { if (event.target === modal) closeModal(); });
courseEditorOverlay.addEventListener('click', function (event) { if (event.target === courseEditorOverlay) closeCourseEditor(); });
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
if (modal.style.display === 'flex') closeModal();
else if (courseEditorOverlay.style.display === 'flex') closeCourseEditor();
else document.querySelectorAll('.standard_overlay[id^="bsModal-"]').forEach(function (overlay) { overlay.style.display = 'none'; });
}
});
window.addEventListener('beforeunload', function (event) { if (dirty || saveInFlight) { event.preventDefault(); event.returnValue = ''; } });
render();
}());
@@ -1,601 +1,327 @@
/* ══════════════════════════════════════
NAVIGATION
══════════════════════════════════════ */
var bsCurrentSection = 0;
var bsTotalSections = 6; // 0=intro, 1-5=pillars
var bsCurrentSub = {}; // track sub-steps per section
(function () {
'use strict';
// Init sub-step tracking
for (var i = 1; i <= 5; i++) bsCurrentSub[i] = 1;
var settingsElement = document.getElementById('bsCourseSettings');
var settings = settingsElement ? JSON.parse(settingsElement.textContent) : {
passingScore: 70,
maxAttempts: 3,
successTitle: 'Schulung bestanden!',
successText: '',
failureTitle: 'Schulung nicht bestanden',
failureText: '',
confirmationText: ''
};
var chapters = Array.from(document.querySelectorAll('.bs-section[data-chapter]'));
var scoreSection = document.getElementById('bsSectionScore');
var finalSection = document.getElementById('bsFinalResult');
var currentChapter = 0;
var currentPage = 0;
var attempts = {};
var results = {};
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);
function escapeHtml(value) {
return String(value).replace(/[&<>'"]/g, function (character) {
return {'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'}[character];
});
}
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 scrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
var headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
var menu = document.getElementById('header_menue');
var title = document.getElementById('header_title');
if (menu) menu.style.top = '0';
if (title) title.style.top = headerHeight + 'px';
}
}
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';
}
function pagesFor(chapterIndex) {
return chapters[chapterIndex] ? Array.from(chapters[chapterIndex].querySelectorAll(':scope > .bs-substep')) : [];
}
/* ── 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');
function updateProgress() {
document.querySelectorAll('#bsMainProgress .bs-progress-step').forEach(function (step, index) {
step.classList.toggle('active', index === currentChapter);
step.classList.toggle('done', index < currentChapter);
});
document.querySelectorAll('#bsMainProgress .bs-progress-line').forEach(function (line, index) {
line.classList.toggle('done', index < currentChapter);
});
}
function currentPageElement() {
return pagesFor(currentChapter)[currentPage] || null;
}
function pageComplete(page) {
if (!page) return true;
var assessments = Array.from(page.querySelectorAll('.bs-assessment'));
var assessmentsDone = assessments.every(function (assessment) { return assessment.dataset.complete === 'true'; });
var checklists = Array.from(page.querySelectorAll('[data-checklist="true"]'));
var checklistsDone = checklists.every(function (checklist) {
var items = Array.from(checklist.querySelectorAll('li'));
return items.length === 0 || items.every(function (item) { return item.classList.contains('checked'); });
});
return assessmentsDone && checklistsDone;
}
function updateNavigation() {
var page = currentPageElement();
if (!page) return;
var previous = page.querySelector('.bs-prev-page');
var next = page.querySelector('.bs-next-page');
if (previous) {
previous.hidden = currentChapter === 0 && currentPage === 0;
previous.textContent = currentPage > 0 ? '< Zurück' : '< Voriges Kapitel';
}
});
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';
if (next) {
var lastPage = currentPage === pagesFor(currentChapter).length - 1;
var lastChapter = currentChapter === chapters.length - 1;
next.textContent = lastPage ? (lastChapter ? 'Zum Ergebnis >' : 'Weiter zum nächsten Kapitel >') : 'Weiter >';
next.classList.toggle('bs-btn-locked', !pageComplete(page));
next.setAttribute('aria-disabled', pageComplete(page) ? 'false' : 'true');
}
}
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();
function showPage(chapterIndex, pageIndex) {
if (!chapters[chapterIndex]) return;
currentChapter = chapterIndex;
var pages = pagesFor(chapterIndex);
currentPage = Math.max(0, Math.min(pageIndex, Math.max(0, pages.length - 1)));
chapters.forEach(function (chapter, index) { chapter.hidden = index !== currentChapter; });
pages.forEach(function (page, index) { page.hidden = index !== currentPage; });
if (scoreSection) scoreSection.hidden = true;
if (finalSection) finalSection.hidden = true;
updateProgress();
updateNavigation();
scrollToTop();
}
var messages = bsQuizMessages[quizId] || ['Richtig!', 'Falsch: Leider falsch. Die richtige Antwort ist markiert.'];
return bsCheckMultiGeneric(quizId, messages[0], messages[1]);
}
function scoredAssessments() {
return Array.from(document.querySelectorAll('.bs-assessment[data-score="true"]'));
}
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 scoreStats() {
var assessments = scoredAssessments();
var correct = 0;
var wrong = 0;
assessments.forEach(function (assessment) {
if (results[assessment.id] === true) correct++;
if (results[assessment.id] === false) wrong++;
});
var total = assessments.length;
return {
correct: correct,
wrong: wrong,
open: total - correct - wrong,
total: total,
percent: total > 0 ? Math.round((correct / total) * 100) : 100
};
}
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">' +
function scoreCards(stats) {
return '<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 class="bs-score-card"><div class="bs-score-card-num">' + (stats.correct + stats.wrong) + '/' + 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>';
function continueAfterChapter() {
if (currentChapter < chapters.length - 1) showPage(currentChapter + 1, 0);
else showFinalResult();
}
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.');
function finishChapter() {
var chapterAssessments = chapters[currentChapter].querySelectorAll('.bs-assessment[data-score="true"]');
if (chapterAssessments.length === 0) {
continueAfterChapter();
return;
}
var stats = scoreStats();
var maxReachable = stats.total > 0 ? 100 - Math.round((stats.wrong / stats.total) * 100) : 100;
var body = scoreSection.querySelector('.bs-body');
body.innerHTML = '<div class="bs-chapter-title">Zwischenstand nach Kapitel ' + (currentChapter + 1) + '</div>' +
'<div class="bs-score-bar-wrap"><div class="bs-score-bar"><div class="bs-score-bar-fill" style="width:' + maxReachable + '%;background:' + (maxReachable >= settings.passingScore ? 'var(--bs-green)' : 'var(--bs-red)') + '"></div></div>' +
'<div class="bs-score-bar-label">Max. erreichbar: ' + maxReachable + '% (mindestens ' + settings.passingScore + '% erforderlich)</div></div>' + scoreCards(stats);
scoreSection.querySelector('.bs-actions-left').innerHTML = '<button type="button" class="bs-btn bs-score-back">&lt; Zurück</button>';
scoreSection.querySelector('.bs-actions-right').innerHTML = '<button type="button" class="bs-btn bs-btn-primary bs-score-next">Weiter &gt;</button>';
scoreSection.querySelector('.bs-score-back').addEventListener('click', function () { showPage(currentChapter, currentPage); });
scoreSection.querySelector('.bs-score-next').addEventListener('click', continueAfterChapter);
chapters.forEach(function (chapter) { chapter.hidden = true; });
scoreSection.hidden = false;
scrollToTop();
}
bsUpdatePageGate(gate.nextBtnId, gate.quizIds);
}
function showFinalResult() {
var stats = scoreStats();
var passed = stats.percent >= Number(settings.passingScore || 70);
var body = finalSection.querySelector('.bs-body');
body.innerHTML = '<div class="bs-chapter-title">' + escapeHtml(passed ? settings.successTitle : settings.failureTitle) + '</div>' +
'<div class="bs-score-bar-wrap"><div class="bs-score-bar"><div class="bs-score-bar-fill" style="width:' + stats.percent + '%;background:' + (passed ? 'var(--bs-green)' : 'var(--bs-red)') + '"></div></div>' +
'<div class="bs-score-bar-label">' + stats.percent + '% richtig (' + stats.correct + ' von ' + stats.total + ' Fragen)</div></div>' + scoreCards(stats);
if (passed) {
body.innerHTML += '<div class="bs-successbox bs-result-message">' + settings.successText + '</div>' +
'<div class="bs-confirmation"><label><input type="checkbox" id="bsConfirmCheckbox"><span>' + settings.confirmationText + '</span></label></div>' +
'<button type="button" class="bs-btn bs-btn-primary bs-btn-locked" id="bsFinishBtn">Schulung abschließen</button>';
var confirm = document.getElementById('bsConfirmCheckbox');
var finish = document.getElementById('bsFinishBtn');
confirm.addEventListener('change', function () { finish.classList.toggle('bs-btn-locked', !confirm.checked); });
finish.addEventListener('click', function () {
if (confirm.checked) document.getElementById('bsCompleteForm').submit();
});
} else {
body.innerHTML += '<div class="bs-warnbox bs-result-message"><div class="bs-warnbox-title">Nicht bestanden</div>' + settings.failureText + '</div>' +
'<button type="button" class="bs-btn bs-btn-primary" id="bsRestartBtn">Schulung neu starten</button>';
document.getElementById('bsRestartBtn').addEventListener('click', restartTraining);
}
chapters.forEach(function (chapter) { chapter.hidden = true; });
if (scoreSection) scoreSection.hidden = true;
finalSection.hidden = false;
scrollToTop();
}
function normalizeAnswer(value) {
return String(value).trim().toLocaleLowerCase('de-DE').replace(/\s+/g, ' ');
}
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 validateAssessment(assessment, mark) {
var kind = assessment.dataset.assessmentKind;
var correct = true;
if (kind === 'multiple_choice') {
assessment.querySelectorAll('.bs-quiz-option').forEach(function (option) {
var input = option.querySelector('input');
var expected = input.dataset.correct === 'true';
if (input.checked !== expected) correct = false;
if (mark) {
option.classList.remove('correct', 'wrong');
if (expected) option.classList.add('correct');
else if (input.checked) option.classList.add('wrong');
}
});
} else if (kind === 'fill_blank') {
assessment.querySelectorAll('.bs-fill-input').forEach(function (input) {
var answers = JSON.parse(input.dataset.answers || '[]').map(normalizeAnswer);
var matches = answers.includes(normalizeAnswer(input.value));
if (!matches) correct = false;
if (mark) {
input.classList.remove('correct', 'wrong');
input.classList.add(matches ? 'correct' : 'wrong');
}
});
} else if (kind === 'matching') {
assessment.querySelectorAll('.bs-dropdown-match-row').forEach(function (row) {
var select = row.querySelector('select');
var matches = select.value === select.dataset.correct;
if (!matches) correct = false;
if (mark) {
row.classList.remove('correct', 'wrong');
row.classList.add(matches ? 'correct' : 'wrong');
}
});
}
return correct;
}
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;
function checkAssessment(assessment) {
if (!assessment || assessment.dataset.complete === 'true') return;
var id = assessment.id;
attempts[id] = (attempts[id] || 0) + 1;
var correct = validateAssessment(assessment, false);
var exhausted = attempts[id] >= Number(settings.maxAttempts || 3);
var feedback = assessment.querySelector('.bs-quiz-feedback');
if (correct || exhausted) {
validateAssessment(assessment, true);
assessment.dataset.complete = 'true';
assessment.classList.add('bs-quiz-locked');
assessment.querySelectorAll('input, select').forEach(function (control) { control.disabled = true; });
if (assessment.dataset.score === 'true') results[id] = correct;
feedback.textContent = correct ? assessment.dataset.feedbackCorrect : assessment.dataset.feedbackIncorrect;
feedback.className = 'bs-quiz-feedback show ' + (correct ? 'correct' : 'wrong');
var button = assessment.querySelector('.bs-check-assessment');
button.textContent = correct ? 'Richtig' : 'Falsch';
button.classList.add(correct ? 'bs-btn-success' : 'bs-btn-failed', 'bs-btn-check-disabled');
button.disabled = true;
} else {
var remaining = Number(settings.maxAttempts || 3) - attempts[id];
feedback.textContent = 'Leider falsch. Du hast noch ' + remaining + (remaining === 1 ? ' Versuch.' : ' Versuche.');
feedback.className = 'bs-quiz-feedback show wrong';
}
updateNavigation();
}
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');
function restartTraining() {
attempts = {};
results = {};
document.querySelectorAll('.bs-assessment').forEach(function (assessment) {
delete assessment.dataset.complete;
assessment.classList.remove('bs-quiz-locked');
assessment.querySelectorAll('input').forEach(function (input) { input.checked = false; input.value = input.type === 'text' ? '' : input.value; input.disabled = false; input.classList.remove('correct', 'wrong'); });
assessment.querySelectorAll('select').forEach(function (select) { select.selectedIndex = 0; select.disabled = false; });
assessment.querySelectorAll('.correct, .wrong, .selected').forEach(function (element) { element.classList.remove('correct', 'wrong', 'selected'); });
var feedback = assessment.querySelector('.bs-quiz-feedback');
feedback.className = 'bs-quiz-feedback';
feedback.textContent = '';
var button = assessment.querySelector('.bs-check-assessment');
button.textContent = 'Antwort prüfen';
button.disabled = false;
button.classList.remove('bs-btn-success', 'bs-btn-failed', 'bs-btn-check-disabled');
});
document.querySelectorAll('.bs-checklist li').forEach(function (item) { item.classList.remove('checked'); });
showPage(0, 0);
}
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 shuffleAnswerOptions() {
document.querySelectorAll('.bs-quiz-options').forEach(function (container) {
var options = Array.from(container.querySelectorAll(':scope > .bs-quiz-option'));
for (var index = options.length - 1; index > 0; index--) {
var other = Math.floor(Math.random() * (index + 1));
var temporary = options[index];
options[index] = options[other];
options[other] = temporary;
}
options.forEach(function (option) { container.appendChild(option); });
});
}
}
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;
document.addEventListener('click', function (event) {
var accordion = event.target.closest('.bs-accordion-header');
if (accordion) {
var container = accordion.closest('.bs-accordion');
var open = container.classList.toggle('open');
accordion.setAttribute('aria-expanded', open ? 'true' : 'false');
return;
}
var checklistItem = event.target.closest('.bs-checklist-item');
if (checklistItem) {
checklistItem.closest('li').classList.toggle('checked');
updateNavigation();
return;
}
var check = event.target.closest('.bs-check-assessment');
if (check) {
checkAssessment(document.getElementById(check.dataset.assessmentId));
return;
}
if (event.target.closest('.bs-next-page')) {
var page = currentPageElement();
if (!pageComplete(page)) return;
if (currentPage < pagesFor(currentChapter).length - 1) showPage(currentChapter, currentPage + 1);
else finishChapter();
return;
}
if (event.target.closest('.bs-prev-page')) {
if (currentPage > 0) showPage(currentChapter, currentPage - 1);
else if (currentChapter > 0) showPage(currentChapter - 1, pagesFor(currentChapter - 1).length - 1);
}
options.forEach(function(opt) { container.appendChild(opt); });
});
})();
document.addEventListener('change', function (event) {
if (event.target.matches('.bs-quiz-option input[type="checkbox"]')) {
event.target.closest('.bs-quiz-option').classList.toggle('selected', event.target.checked);
}
});
shuffleAnswerOptions();
if (chapters.length > 0) showPage(0, 0);
}());