const registryUrl = "formulare/formulare.json"; const schoolLogoUrl = "logoUndpics/WbK%20Logo%20-%201080%20x%201080%20px.png"; const formList = document.querySelector("#formList"); const masterDataForm = document.querySelector("#masterDataForm"); const formElement = document.querySelector("#dynamicForm"); const outputPreview = document.querySelector("#outputPreview"); const statusElement = document.querySelector("#status"); const clearMasterDataButton = document.querySelector("#clearMasterDataButton"); const copyButton = document.querySelector("#copyButton"); const blankA4Button = document.querySelector("#blankA4Button"); const pdfButton = document.querySelector("#pdfButton"); const printButton = document.querySelector("#printButton"); const exportButton = document.querySelector("#exportButton"); const masterDataStorageKey = "schulformular:stammdaten"; const masterDataFields = { vorname: ["vorname"], name: ["name", "nachname"], klasse: ["klasse"], semester: ["semester"], schuljahr: ["schuljahr"], klassenleitung: ["klassenleitung"] }; let registry = []; let optionLists = {}; let activeForm = null; let outputPreviewCollapsed = true; init(); async function init() { try { registry = await fetchJson(registryUrl); optionLists.faecher = await fetchJson("daten/faecher.json"); optionLists.klausurlaengen = await fetchJson("daten/klausurlaengen.json"); renderFormList(); const firstForm = new URLSearchParams(window.location.search).get("formular") || registry[0]?.id; if (firstForm) { await loadForm(firstForm); } } catch (error) { setStatus(`Formulare konnten nicht geladen werden: ${error.message}`, true); } fillMasterDataForm(); masterDataForm.addEventListener("input", handleMasterDataInput); masterDataForm.addEventListener("change", handleMasterDataInput); clearMasterDataButton.addEventListener("click", clearMasterData); copyButton.addEventListener("click", copyCompactOutputToClipboard); blankA4Button.addEventListener("click", printBlankA4Version); pdfButton.addEventListener("click", exportAsPdf); printButton.addEventListener("click", printCurrentOutput); exportButton.addEventListener("click", exportCurrentData); outputPreview.addEventListener("click", handleOutputPreviewClick); } function exportAsPdf() { updateFormPrintTimestamp(); updateOutputPreview(); setStatus("PDF-Export: Im Druckdialog als Ziel \"Als PDF speichern\" wählen."); window.print(); } function printCurrentOutput() { updateFormPrintTimestamp(); updateOutputPreview(); window.print(); } function printBlankA4Version() { if (!activeForm) { setStatus("Es ist kein Formular geladen.", true); return; } const currentData = readFormData(); const currentMasterData = readMasterData(); const currentCollapsed = outputPreviewCollapsed; formElement.reset(); applyBlankA4Defaults(); writeMasterData({}); fillMasterDataForm(); updateConditionalVisibility(); outputPreviewCollapsed = false; updateFormPrintTimestamp(); updateOutputPreview(); setStatus("Leere A4-Druckversion wurde vorbereitet."); window.print(); applyFormData(currentData); writeMasterData(currentMasterData); fillMasterDataForm(); outputPreviewCollapsed = currentCollapsed; updateConditionalVisibility(); updateOutputPreview(); setStatus("Leere A4-Druckversion wurde erzeugt; Eingaben wurden wiederhergestellt."); } function applyBlankA4Defaults() { if (activeForm?.id !== "kurswahl") return; const selection = formElement.elements.aenderung; if (selection) { selection.value = "anwahl"; } } function handleOutputPreviewClick(event) { const button = event.target.closest("[data-toggle-output-preview]"); if (!button) return; outputPreviewCollapsed = !outputPreviewCollapsed; applyOutputPreviewToggle(); } async function copyCompactOutputToClipboard() { if (!activeForm) { setStatus("Es ist kein Formular geladen.", true); return; } updateOutputPreview(); const text = activeForm.id === "nachschreibeantrag" ? buildNachschreibeText(readFormData()) : activeForm.id === "kurswahl" ? buildKurswahlText(readFormDataWithMasterData()) : activeForm.id === "beurlaubung" ? buildBeurlaubungText(readFormDataWithMasterData()) : activeForm.id === "schulbuch-ausleihe" ? buildSchulbuchAusleiheText(readFormDataWithMasterData()) : activeForm.id === "einwilligung-veroeffentlichung" ? buildEinwilligungText(readFormDataWithMasterData()) : activeForm.id === "unterrichtsversaeumnisse" ? buildUnterrichtsversaeumnisseText(readFormDataWithMasterData()) : outputPreview.textContent.trim(); if (!text) { setStatus("Für dieses Formular gibt es keine kompakte Textausgabe.", true); return; } try { await writeClipboardText(text); setStatus("Textversion wurde in die Zwischenablage kopiert."); } catch (error) { setStatus(`Textversion konnte nicht kopiert werden: ${error.message}`, true); } } async function writeClipboardText(text) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return; } catch (error) { // Fall back to the older copy command below. } } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; document.body.append(textarea); textarea.select(); const copied = document.execCommand("copy"); textarea.remove(); if (!copied) { throw new Error("Browser hat den Zugriff auf die Zwischenablage blockiert."); } } async function fetchJson(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`${url} (${response.status})`); } return response.json(); } function renderFormList() { formList.innerHTML = ""; registry.forEach((entry) => { const button = document.createElement("button"); button.type = "button"; button.className = "form-link"; button.textContent = entry.title; button.dataset.formId = entry.id; button.addEventListener("click", () => loadForm(entry.id)); formList.append(button); }); } async function loadForm(formId) { const entry = registry.find((item) => item.id === formId); if (!entry) { setStatus("Dieses Formular ist nicht registriert.", true); return; } activeForm = await fetchJson(entry.path); activeForm.registryEntry = entry; history.replaceState(null, "", `?formular=${encodeURIComponent(formId)}`); renderActiveState(formId); renderForm(activeForm); fillDefaultData(); fillSavedData(); resetKurswahlSelection(); if (hasMasterData()) { applyMasterDataToActiveForm(); } else { syncMasterDataFromActiveForm(); } updateConditionalVisibility(); updateOutputPreview(); setStatus("Bereit. Eingaben werden lokal im Browser zwischengespeichert."); } function resetKurswahlSelection() { if (activeForm?.id !== "kurswahl") return; const selection = formElement.elements.aenderung; if (selection) { selection.value = ""; } } function renderActiveState(formId) { document.querySelectorAll(".form-link").forEach((button) => { button.classList.toggle("active", button.dataset.formId === formId); }); } function renderForm(formConfig) { formElement.innerHTML = ""; formElement.className = formConfig.id ? `form-${formConfig.id}` : ""; const printHeader = document.createElement("div"); printHeader.className = "a4-print-header form-print-header"; printHeader.innerHTML = renderA4Header(formConfig.title); formElement.append(printHeader); const head = document.createElement("div"); head.className = "form-head"; head.innerHTML = `

${escapeHtml(formConfig.title)}

${escapeHtml(formConfig.description || "")}

`; formElement.append(head); const grid = document.createElement("div"); grid.className = `field-grid ${formConfig.layout === "tiles" ? "tile-grid" : ""}`; if (formConfig.layout === "tiles") { renderTileFields(formConfig.fields, grid); } else { formConfig.fields.forEach((field) => grid.append(createField(field))); } formElement.append(grid); const printTimestamp = document.createElement("div"); printTimestamp.className = "form-print-timestamp"; formElement.append(printTimestamp); updateFormPrintTimestamp(); const actions = document.createElement("div"); actions.className = "form-actions"; actions.innerHTML = ` `; formElement.append(actions); formElement.onsubmit = (event) => { event.preventDefault(); saveCurrentData(); }; formElement.querySelector("#resetButton").addEventListener("click", resetCurrentData); formElement.oninput = (event) => { handleDynamicFormInput(event); updateConditionalVisibility(); updateOutputPreview(); }; formElement.onchange = (event) => { handleDynamicFormInput(event); updateConditionalVisibility(); updateOutputPreview(); }; formElement.onclick = (event) => { handleSpecialFieldClick(event); }; } function renderTileFields(fields, grid) { let currentTile = null; let currentTileGrid = null; fields.forEach((field) => { if (field.type === "section" && field.tile) { currentTile = document.createElement("section"); currentTile.className = "form-tile full"; setVisibleWhen(currentTile, field.visibleWhen); currentTile.innerHTML = `

${escapeHtml(field.label)}

`; appendHint(currentTile, field.hint); currentTileGrid = document.createElement("div"); currentTileGrid.className = "tile-fields"; currentTile.append(currentTileGrid); grid.append(currentTile); return; } const target = currentTileGrid || grid; target.append(createField(field)); }); } function createField(field) { const wrapper = document.createElement("div"); wrapper.className = `field ${field.fullWidth ? "full" : ""}`; setVisibleWhen(wrapper, field.visibleWhen); if (field.type === "section") { wrapper.className = "form-section full"; wrapper.innerHTML = `

${escapeHtml(field.label)}

`; appendHint(wrapper, field.hint); return wrapper; } if (field.type === "notice") { wrapper.className = `notice full ${field.variant ? `notice-${field.variant}` : ""}`; wrapper.textContent = field.label; return wrapper; } if (field.type === "klausurlaenge-table") { wrapper.className = "table-field full"; wrapper.innerHTML = renderKlausurlaengenTable(); appendHint(wrapper, field.hint); return wrapper; } if (field.type === "absence-list") { wrapper.className = "absence-field full"; wrapper.innerHTML = renderAbsenceListField(field); appendHint(wrapper, field.hint); return wrapper; } if (field.type === "signature") { wrapper.classList.add("signature-field"); wrapper.innerHTML = ` `; appendHint(wrapper, field.hint); return wrapper; } if (field.type === "checkbox") { wrapper.classList.add("checkbox-field"); wrapper.innerHTML = ` `; appendHint(wrapper, field.hint); return wrapper; } if (field.type === "select") { const options = getFieldOptions(field); wrapper.innerHTML = ` `; appendHint(wrapper, field.hint); return wrapper; } if (field.type === "textarea") { wrapper.innerHTML = ` `; appendHint(wrapper, field.hint); return wrapper; } wrapper.innerHTML = ` `; appendHint(wrapper, field.hint); return wrapper; } function setVisibleWhen(element, visibleWhen) { if (!visibleWhen) return; element.dataset.visibleWhen = JSON.stringify(visibleWhen); } function updateConditionalVisibility() { const data = readFormValues(formElement); formElement.querySelectorAll("[data-visible-when]").forEach((element) => { const condition = JSON.parse(element.dataset.visibleWhen); element.hidden = !matchesVisibleWhen(condition, data); }); } function matchesVisibleWhen(condition, data) { if (condition.any) { return condition.any.some((nestedCondition) => matchesVisibleWhen(nestedCondition, data)); } if (condition.all) { return condition.all.every((nestedCondition) => matchesVisibleWhen(nestedCondition, data)); } return Object.entries(condition).every(([fieldName, expected]) => { const actual = data[fieldName]; return Array.isArray(expected) ? expected.includes(actual) : actual === expected; }); } function appendHint(wrapper, hint) { if (!hint) return; const hintElement = document.createElement("span"); hintElement.className = "hint"; hintElement.textContent = hint; wrapper.append(hintElement); } function getFieldOptions(field) { const options = field.optionsSource ? optionLists[field.optionsSource] : field.options; return (options || []).map((option) => { if (typeof option === "string") { return { value: option, label: option }; } return option; }); } function renderKlausurlaengenTable() { const tables = optionLists.klausurlaengen || []; return tables.map((table) => `

${escapeHtml(table.title)}

${table.columns.map((column) => ``).join("")} ${table.rows.map((row) => ` ${row.map((cell) => ``).join("")} `).join("")}
${escapeHtml(column)}
${escapeHtml(cell)}
`).join(""); } function renderAbsenceListField(field) { const subjects = getFieldOptions({ optionsSource: "faecher" }); return `

Noch keine Fehlzeiten eingetragen.

`; } function handleSpecialFieldClick(event) { const addButton = event.target.closest("[data-add-absence]"); if (addButton) { addAbsenceEntry(addButton.dataset.addAbsence); return; } const removeButton = event.target.closest("[data-remove-absence]"); if (removeButton) { removeAbsenceEntry(removeButton.dataset.removeAbsence, Number(removeButton.dataset.index)); } } function addAbsenceEntry(fieldId) { const entry = formElement.querySelector(`[data-absence-entry="${fieldId}"]`); const input = formElement.elements[fieldId]; if (!entry || !input) return; const date = entry.querySelector("[data-absence-date]").value; const lesson = entry.querySelector("[data-absence-lesson]").value; const subject = entry.querySelector("[data-absence-subject]").value; const teacher = entry.querySelector("[data-absence-teacher]").value.trim(); const note = entry.querySelector("[data-absence-note]").value.trim(); if (!date || !subject) { setStatus("Bitte mindestens Datum und Fach für die Fehlzeit eintragen.", true); return; } const entries = readAbsenceEntries(fieldId); entries.push({ date, lesson, subject, teacher, note }); writeAbsenceEntries(fieldId, entries); entry.querySelector("[data-absence-date]").value = ""; entry.querySelector("[data-absence-subject]").value = ""; entry.querySelector("[data-absence-teacher]").value = ""; entry.querySelector("[data-absence-note]").value = ""; updateOutputPreview(); setStatus("Fehlzeit wurde hinzugefügt."); } function removeAbsenceEntry(fieldId, index) { const entries = readAbsenceEntries(fieldId); entries.splice(index, 1); writeAbsenceEntries(fieldId, entries); updateOutputPreview(); } function readAbsenceEntries(fieldId) { const input = formElement.elements[fieldId]; if (!input?.value) return []; try { return JSON.parse(input.value); } catch (error) { return []; } } function writeAbsenceEntries(fieldId, entries) { const input = formElement.elements[fieldId]; if (!input) return; const sortedEntries = sortAbsences(entries); input.value = JSON.stringify(sortedEntries); renderAbsenceList(fieldId, sortedEntries); } function renderAbsenceList(fieldId, entries = readAbsenceEntries(fieldId)) { const list = formElement.querySelector(`[data-absence-list="${fieldId}"]`); if (!list) return; if (!entries.length) { list.innerHTML = `

Noch keine Fehlzeiten eingetragen.

`; return; } list.innerHTML = ` ${entries.map((entry, index) => ` `).join("")}
Datum Std. Fach Kürzel
${escapeHtml(formatDate(entry.date))} ${escapeHtml(entry.lesson)}. ${escapeHtml(entry.subject)} ${escapeHtml(entry.teacher || "")}
`; } function sortAbsences(entries) { return [...entries].sort((a, b) => `${a.date}-${a.lesson}`.localeCompare(`${b.date}-${b.lesson}`)); } function saveCurrentData() { if (!activeForm) return; syncMasterDataFromActiveForm(); localStorage.setItem(storageKey(), JSON.stringify(readFormData())); updateOutputPreview(); setStatus("Gespeichert."); } function fillSavedData() { const saved = localStorage.getItem(storageKey()); if (!saved) return; const data = JSON.parse(saved); applyFormData(data); } function fillDefaultData() { if (!activeForm) return; const defaults = {}; activeForm.fields.forEach((field) => { if (field.defaultValue !== undefined) { defaults[field.id] = resolveDefaultValue(field); } if (field.defaultChecked !== undefined) { defaults[field.id] = field.defaultChecked; } }); applyFormData(defaults); } function resolveDefaultValue(field) { if (field.defaultValue === "today" && field.type === "date") { return formatDateInputValue(new Date()); } return field.defaultValue; } function applyFormData(data) { Object.entries(data).forEach(([name, value]) => { const input = formElement.elements[name]; if (!input) return; if (input.type === "checkbox") { input.checked = Boolean(value); } else { input.value = value; } if (input.type === "hidden" && input.closest(".absence-field")) { renderAbsenceList(name); } }); } function fillMasterDataForm() { applyDataToForm(masterDataForm, readMasterData()); } function handleMasterDataInput() { const masterData = readMasterDataFromForm(); writeMasterData(masterData); applyMasterDataToActiveForm(); updateOutputPreview(); } function handleDynamicFormInput(event) { const canonicalName = getMasterDataKey(event.target.name); if (!canonicalName) return; const masterData = readMasterData(); masterData[canonicalName] = event.target.value; writeMasterData(masterData); fillMasterDataForm(); } function clearMasterData() { localStorage.removeItem(masterDataStorageKey); masterDataForm.reset(); applyMasterDataToActiveForm(); updateOutputPreview(); setStatus("Stammdaten wurden geleert."); } function applyMasterDataToActiveForm() { if (!activeForm) return; const masterData = readMasterData(); const formData = {}; Object.entries(masterDataFields).forEach(([key, aliases]) => { aliases.forEach((alias) => { if (formElement.elements[alias]) { formData[alias] = masterData[key] || ""; } }); }); applyFormData(formData); } function syncMasterDataFromActiveForm() { const masterData = readMasterData(); Object.entries(masterDataFields).forEach(([key, aliases]) => { const alias = aliases.find((name) => formElement.elements[name]); if (alias) { masterData[key] = formElement.elements[alias].value; } }); writeMasterData(masterData); fillMasterDataForm(); } function readMasterDataFromForm() { return readFormValues(masterDataForm); } function readMasterData() { const saved = localStorage.getItem(masterDataStorageKey); return saved ? JSON.parse(saved) : {}; } function hasMasterData() { return Object.values(readMasterData()).some((value) => value); } function writeMasterData(data) { localStorage.setItem(masterDataStorageKey, JSON.stringify(data)); } function getMasterDataKey(fieldName) { return Object.entries(masterDataFields).find(([, aliases]) => aliases.includes(fieldName))?.[0]; } function applyDataToForm(form, data) { Object.entries(data).forEach(([name, value]) => { const input = form.elements[name]; if (!input) return; input.value = value; }); } function resetCurrentData() { if (!activeForm) return; formElement.reset(); localStorage.removeItem(storageKey()); fillDefaultData(); applyMasterDataToActiveForm(); refreshSpecialFields(); updateOutputPreview(); setStatus("Eingaben wurden geleert."); } function exportCurrentData() { if (!activeForm) return; const payload = { formId: activeForm.id, formTitle: activeForm.title, exportedAt: new Date().toISOString(), masterData: readMasterData(), data: readFormData() }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${activeForm.id}-daten.json`; link.click(); URL.revokeObjectURL(url); } function readFormData() { return readFormValues(formElement, activeForm.fields); } function readFormDataWithMasterData() { return { ...readMasterData(), ...readFormData() }; } function refreshSpecialFields() { formElement.querySelectorAll(".absence-field input[type='hidden']").forEach((input) => { renderAbsenceList(input.name); }); } function readFormValues(form, fields = null) { const data = {}; if (!fields) { Array.from(form.elements).forEach((input) => { if (!input.name) return; data[input.name] = input.type === "checkbox" ? input.checked : input.value; }); return data; } fields.forEach((field) => { if (field.type === "section" || field.type === "notice") return; const input = form.elements[field.id]; if (!input) return; data[field.id] = input.type === "checkbox" ? input.checked : input.value; }); return data; } function updateOutputPreview() { outputPreview.innerHTML = ""; document.body.classList.remove("has-output-preview"); if (!activeForm) return; if (activeForm.id === "nachschreibeantrag") { outputPreview.innerHTML = renderNachschreibeOutput(readFormData()); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); return; } if (activeForm.id === "kurswahl") { const data = readFormDataWithMasterData(); if (data.aenderung) { outputPreview.innerHTML = renderKurswahlOutput(data); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); } return; } if (activeForm.id === "beurlaubung") { outputPreview.innerHTML = renderBeurlaubungOutput(readFormDataWithMasterData()); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); return; } if (activeForm.id === "schulbuch-ausleihe") { outputPreview.innerHTML = renderSchulbuchAusleiheOutput(readFormDataWithMasterData()); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); return; } if (activeForm.id === "einwilligung-veroeffentlichung") { outputPreview.innerHTML = renderEinwilligungOutput(readFormDataWithMasterData()); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); return; } if (activeForm.id === "unterrichtsversaeumnisse") { outputPreview.innerHTML = renderUnterrichtsversaeumnisseOutput(readFormDataWithMasterData()); prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); return; } if (activeForm.id !== "abwahl-schriftlichkeit") return; const data = readFormData(); const advice = buildAbwahlAdvice(data); const timestampRow = outputTimestampRow(); const copies = [ { title: "Fachlehrer*in", rows: [ ["Studierende*r", `${fullName(data)} (${value(data.semester)})`], ["Fach", value(data.fach)], ["Lehrkraft", value(data.lehrkraft)], ["Gültig ab", formatDate(data.gueltig_ab)], ["Hinweis", "Abwahl bei der Leistungsbewertung berücksichtigen."], timestampRow ], signature: "Information erhalten: Fachlehrer*in" }, { title: "Schild-Koordination", rows: [ ["Studierende*r", `${fullName(data)} (${value(data.semester)})`], ["Fach", value(data.fach)], ["Eintragung", `Abwahl Schriftlichkeit ab ${formatDate(data.gueltig_ab)}`], ["Rückgabe", "Nach Eintragung an die Stufenleitung zurück."], timestampRow ], signature: "In Schild übernommen: Paraphe" }, { title: "Schulakte", rows: [ ["Studierende*r", `${fullName(data)} (${value(data.semester)})`], ["Fach / Lehrkraft", `${value(data.fach)} bei ${value(data.lehrkraft)}`], ["Besprochen / gültig", `${formatDate(data.antragsdatum)} / ${formatDate(data.gueltig_ab)}`], ["Beratung", checkedText(data.konsequenzen_informiert, "Konsequenzen für Abiturfächer erläutert.")], ["Prüfung", advice.summary], timestampRow ], signature: "Zugestimmt: Stufenleitung" }, { title: "Studierende*r", rows: [ ["Nachweis für", `${fullName(data)} (${value(data.semester)})`], ["Fach / Lehrkraft", `${value(data.fach)} bei ${value(data.lehrkraft)}`], ["Gültig ab", formatDate(data.gueltig_ab)], ["Hinweis", "Persönlicher Nachweis der Abwahl."], timestampRow ], signature: "Unterschrift der Stufenleitung" } ]; outputPreview.innerHTML = `

Druckausgabe

Aus den einmal erfassten Daten werden vier Versionen vorbereitet.

${renderAdvice(advice)} ${copies.map(renderCopy).join("")}
`; prepareOutputPreviewToggle(); document.body.classList.add("has-output-preview"); } function prepareOutputPreviewToggle() { const head = outputPreview.querySelector(".output-head"); if (!head) return; const button = document.createElement("button"); button.type = "button"; button.className = "output-toggle"; button.dataset.toggleOutputPreview = ""; head.append(button); applyOutputPreviewToggle(); } function applyOutputPreviewToggle() { outputPreview.classList.toggle("collapsed", outputPreviewCollapsed); const button = outputPreview.querySelector("[data-toggle-output-preview]"); if (!button) return; button.textContent = outputPreviewCollapsed ? "Druckausgabe einblenden" : "Druckausgabe ausblenden"; button.setAttribute("aria-expanded", String(!outputPreviewCollapsed)); } function renderNachschreibeOutput(data) { const rows = getNachschreibeRows(data); const notes = data.terminhinweise?.trim(); return `

Druckausgabe

Kompakte Ausgabe für PDF oder Ausdruck. Die Klausurlänge wird nicht ausgegeben.

${renderA4Header("Nachschreibeantrag")}

Nachschreibeantrag

${rows.map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join("")}
${notes ? `

Hinweise zu Nachschreibterminen

${escapeHtml(notes)}

` : ""}
Unterschrift Studierende*r: ____________________ Kenntnisnahme Fachlehrkraft: ____________________

Bitte innerhalb von 3 Arbeitstagen nach der versäumten Klausur bei der Fachlehrkraft vorlegen.

${renderA4Footer()}
`; } function buildNachschreibeText(data) { const rows = getNachschreibeRows(data); const notes = data.terminhinweise?.trim(); const lines = [ "Nachschreibeantrag", "", ...rows.map(([label, value]) => `${label}: ${value}`) ]; if (notes) { lines.push("", "Hinweise zu Nachschreibterminen:", notes); } lines.push( "", "Unterschrift Studierende*r: ____________________", "Kenntnisnahme Fachlehrkraft: ____________________", "", "Bitte innerhalb von 3 Arbeitstagen nach der versäumten Klausur bei der Fachlehrkraft vorlegen." ); return lines.join("\n"); } function getNachschreibeRows(data) { return [ ["Studierende*r", fullName(data)], ["Klasse/Kurs", value(data.klasse)], ["Klassen-/Stufenleitung", value(data.klassenleitung)], ["Fach / Kursart", `${value(data.fach)} / ${value(data.kursart)}`], ["Fachlehrer*in", value(data.fachlehrer)], ["Versäumte Klausur", `${value(data.klausurnummer)} am ${formatDate(data.klausurdatum)}`], ["Antragsdatum", formatDate(data.antragsdatum)], outputTimestampRow() ]; } function renderKurswahlOutput(data) { const copies = getKurswahlCopies(data); return `

Druckausgabe

Kompakte Ausgabe für Antrag, Fachlehrer-Info, Schild und Studierende*n.

${copies.map(renderCopy).join("")}
`; } function buildKurswahlText(data) { return getKurswahlCopies(data) .map((copy) => { const rows = copy.rows.map(([label, value]) => `${label}: ${value}`).join("\n"); return `${copy.title}\n${rows}\n${copy.signature}: ____________________`; }) .join("\n\n---\n\n"); } function getKurswahlCopies(data) { const action = getKurswahlAction(data); const courseLine = getKurswahlCourseLine(data); const timestampRow = outputTimestampRow(); const baseRows = [ ["Name, Vorname", fullNameLastFirst(data)], ["Semester / Schuljahr", `${value(data.semester)} / ${value(data.schuljahr)}`], ["Anwahl / Abwahl", action], ["Fach / Fachlehrerkürzel", courseLine], ["Köln, den", formatDate(data.antragsdatum)], timestampRow ]; const approvalRows = [ ...baseRows, ["Antrag", checkedText(data.antrag_genehmigt, "Antrag genehmigt.")], ["Abiturzulassung", checkedText(data.abiturbedingungen_geprueft, "Trotz eventueller Abwahl geprüft.")] ]; if (data.bemerkung?.trim()) { approvalRows.push(["Bemerkung", data.bemerkung.trim()]); } const copies = [ { title: "Kurs-An-, Ab- und Umwahl - Abendgymnasium Köln", rows: approvalRows, signature: "Unterschrift Stufenleitung" } ]; if (data.aenderung === "abwahl" || data.aenderung === "umwahl") { copies.push({ title: `Info betr. Fachlehrer - ${data.aenderung === "umwahl" ? "bisheriger Kurs" : "Abwahl"}`, rows: [ ["Name, Vorname", fullNameLastFirst(data)], ["Semester / Schuljahr", `${value(data.semester)} / ${value(data.schuljahr)}`], ["Anwahl / Abwahl", data.aenderung === "umwahl" ? "Umwahl von Kurs" : "Abwahl"], ["Fach / Fachlehrerkürzel", getKurswahlFromCourse(data)], timestampRow ], signature: "Kenntnisnahme Fachlehrer*in" }); } if (data.aenderung === "anwahl" || data.aenderung === "umwahl") { copies.push({ title: `Info betr. Fachlehrer - ${data.aenderung === "umwahl" ? "neuer Kurs" : "Anwahl"}`, rows: [ ["Name, Vorname", fullNameLastFirst(data)], ["Semester / Schuljahr", `${value(data.semester)} / ${value(data.schuljahr)}`], ["Anwahl / Abwahl", data.aenderung === "umwahl" ? "Umwahl nach Kurs" : "Anwahl"], ["Fach / Fachlehrerkürzel", getKurswahlToCourse(data)], timestampRow ], signature: "Kenntnisnahme Fachlehrer*in" }); } copies.push( { title: "Info Schild", rows: [ ["Name, Vorname", fullNameLastFirst(data)], ["Anwahl / Abwahl", action], ["Fach / Fachlehrerkürzel", courseLine], timestampRow ], signature: "Bearbeitet" }, { title: "Info Studierende*r", rows: [ ["Name, Vorname", fullNameLastFirst(data)], ["Anwahl / Abwahl", action], ["Fach / Fachlehrerkürzel", courseLine], timestampRow ], signature: "Ausgegeben durch Stufenleitung" } ); return copies; } function getKurswahlAction(data) { if (data.aenderung === "umwahl") return "Umwahl"; if (data.aenderung === "abwahl") return "Abwahl"; if (data.aenderung === "anwahl") return "Anwahl"; return "____________________"; } function getKurswahlCourseLine(data) { if (data.aenderung === "abwahl") return getKurswahlFromCourse(data); if (data.aenderung === "anwahl") return getKurswahlToCourse(data); if (data.aenderung === "umwahl") { return `Von: ${getKurswahlFromCourse(data)}; nach: ${getKurswahlToCourse(data)}`; } return "____________________"; } function getKurswahlFromCourse(data) { const fach = data.aenderung === "umwahl" ? data.umwahl_von_fach : data.abwahl_fach; const lehrkraft = data.aenderung === "umwahl" ? data.umwahl_von_lehrkraft : data.abwahl_lehrkraft; return `${value(fach)} / ${value(lehrkraft)}`; } function getKurswahlToCourse(data) { const fach = data.aenderung === "umwahl" ? data.umwahl_nach_fach : data.anwahl_fach; const lehrkraft = data.aenderung === "umwahl" ? data.umwahl_nach_lehrkraft : data.anwahl_lehrkraft; return `${value(fach)} / ${value(lehrkraft)}`; } function fullNameLastFirst(data) { const lastFirst = `${value(data.name)}, ${value(data.vorname)}`; return lastFirst.includes("____________________") ? fullName(data) : lastFirst; } function renderBeurlaubungOutput(data) { const copies = getBeurlaubungCopies(data); return `

Druckausgabe

Antrag und Mitteilung an die Klassen-/Stufenleitung.

${copies.map(renderCopy).join("")}
`; } function buildBeurlaubungText(data) { return getBeurlaubungCopies(data) .map((copy) => { const rows = copy.rows.map(([label, value]) => `${label}: ${value}`).join("\n"); return `${copy.title}\n${rows}\n${copy.signature}: ____________________`; }) .join("\n\n---\n\n"); } function getBeurlaubungCopies(data) { const timestampRow = outputTimestampRow(); const period = `${formatDate(data.beurlaubung_von)} bis ${formatDate(data.beurlaubung_bis)}`; const returnDeadline = data.rueckmeldung_bis ? formatDate(data.rueckmeldung_bis) : "01.12. bzw. _____________ 20___"; const studentRows = [ ["Name", value(data.name)], ["Vorname", value(data.vorname)], ["Schuljahr", value(data.schuljahr)], ["Klasse / Semester", classSemester(data)], ["Klassen-/Stufenleitung", value(data.klassenleitung)] ]; return [ { title: "Antrag auf Beurlaubung - Abendgymnasium Köln", rows: [ ...studentRows, ["Antrag", `Hiermit beantrage ich gemäß APO-WBK § 9 Abs. 6 die Beurlaubung für ${period}.`], ["Rückmeldung", checkedText(data.rueckmeldung_informiert, `Schriftliche Rückmeldung erforderlich bis ${returnDeadline} (Posteingang); sonst verfällt das Anrecht auf einen Studienplatz am Abendgymnasium Köln WbK.`)], ["Schulbücher", checkedText(data.buecher_abgeben, "Leihweise erhaltene Schulbücher werden für die Zeit der Beurlaubung abgegeben.")], ["Köln, den", formatDate(data.antragsdatum)], timestampRow ], signature: "Unterschrift d. Studierenden / Unterschrift d. Schulleitung" }, { title: "Mitteilung an die Klassen-/Stufenleitung", rows: [ ["Name", value(data.name)], ["Vorname", value(data.vorname)], ["Semester", value(data.semester)], ["Beurlaubung", `hat sich am ${formatDate(data.antragsdatum)} bis zum ${formatDate(data.beurlaubung_bis)} beurlauben lassen.`], ["Hinweis", "Bitte informieren Sie die anderen Fachlehrer/innen und streichen Sie sie von den Kurslisten."], ["Version", "1"], timestampRow ], signature: "Kenntnisnahme Klassen-/Stufenleitung" }, { title: "Rechtsgrundlage", rows: [ ["APO-WBK § 9 Abs. 6", "Die Schulleiterin oder der Schulleiter kann eine Studierende oder einen Studierenden auf Antrag für längstens zwei Semester beurlauben (§ 43 Abs. 3 SchulG - jetzt: Absatz 4). Die Zeit der Beurlaubung wird auf die Höchstverweildauer nicht angerechnet."], timestampRow ], signature: "Zur Kenntnis genommen" } ]; } function classSemester(data) { return uniqueValues([data.klasse, data.semester]).join(" / ") || "____________________"; } function renderSchulbuchAusleiheOutput(data) { return `

Druckausgabe

Kompakter Antrag auf Ausleihe aus dem Eigenanteil.

${renderCopy(getSchulbuchAusleiheCopy(data))}
`; } function buildSchulbuchAusleiheText(data) { const copy = getSchulbuchAusleiheCopy(data); const rows = copy.rows.map(([label, value]) => `${label}: ${value}`).join("\n"); return `${copy.title}\n${rows}\n${copy.signature}: ____________________`; } function getSchulbuchAusleiheCopy(data) { return { title: "Antrag auf Ausleihe von Schulbüchern aus dem Eigenanteil", rows: [ ["Name, Vorname", fullNameLastFirst(data)], ["Anschrift", value(data.anschrift)], ["Ich beziehe", `${value(data.leistungsbezug)} (Beleg bitte beifügen)`], ["Antrag", "Deshalb beantrage ich die Ausleihe des folgenden Schulbuches, das ich normalerweise aus dem Eigenanteil beschaffen müsste."], ["Schulbuch", value(data.buch)], ["Hinweis", checkedText(data.buchpflicht_bestaetigt, "Das Buch ist so zu behandeln, dass es nach der Nutzung weiter ausgeliehen werden kann; andernfalls sind die Kosten nachträglich zu übernehmen.")], ["Datum", formatDate(data.antragsdatum)], outputTimestampRow() ], signature: "Datum und Unterschrift" }; } function renderEinwilligungOutput(data) { const rows = getEinwilligungRows(data); const sections = getEinwilligungSections(data); return `

Druckausgabe

Einwilligung mit Erklärungsteil und Datenschutz-Hinweisen.

${renderA4Header("Einwilligung Veröffentlichung")}

Einwilligung in die Veröffentlichung von personenbezogenen Daten

einschließlich Fotos/Videos

${sections.map(renderPlainSection).join("")}
${rows.map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join("")}
Unterschrift: ____________________
${renderA4Footer()}
`; } function buildEinwilligungText(data) { const sections = getEinwilligungSections(data) .map((section) => `${section.title}\n${section.body}`) .join("\n\n"); const rows = getEinwilligungRows(data) .map(([label, value]) => `${label}: ${value}`) .join("\n"); return `Einwilligung in die Veröffentlichung von personenbezogenen Daten einschließlich Fotos/Videos\n\n${sections}\n\n${rows}\nUnterschrift: ____________________`; } function getEinwilligungRows(data) { return [ ["Name / Klasse", `${fullNameLastFirst(data)} / ${classSemester(data)}`], ["Ort, Datum", `Köln, den ${formatDate(data.antragsdatum)}`], ["Einwilligung", checkedText(data.einwilligung_erteilt, "Veröffentlichung personenbezogener Daten einschließlich Fotos/Videos im schulischen Kontext.")], ["Ausnahmen", getEinwilligungExceptions(data)], outputTimestampRow() ]; } function getEinwilligungSections(data) { return [ { title: "Erklärung", body: "In geeigneten Fällen möchten wir Informationen über Ereignisse aus unserem Schulleben auch mit personenbezogenem Bezug einer größeren Öffentlichkeit zugänglich machen. Dies betrifft insbesondere Texte, Fotos oder Videos, die im Rahmen der pädagogischen Arbeit, von Schulveranstaltungen oder Projekten entstehen. Hierzu bitten wir um Ihre Einwilligung." }, { title: "Nutzungsrechte und Widerruf", body: "Die Einräumung der Nutzungsrechte erfolgt unentgeltlich und umfasst auch das Recht zur Bearbeitung, sofern diese nicht entstellend ist. Die Einwilligung kann jederzeit schriftlich gegenüber der Schulleiterin oder dem Schulleiter widerrufen werden. Bei bereits erstellten, bestellten oder im Umlauf befindlichen Druckwerken ist ein Widerruf unter Umständen nicht mehr vollständig umsetzbar. Sofern die Einwilligung nicht widerrufen wird, gilt sie für den schulischen Kontext auch über das Ende der Schulzugehörigkeit hinaus, solange der Veröffentlichungszweck fortbesteht. Die Einwilligung erfolgt freiwillig; aus Nichterteilung oder Widerruf entstehen keine Nachteile." }, { title: "Zustimmung", body: "Die Schule darf personenbezogene Daten einschließlich Fotos/Videos im Schulkontext digital, zum Beispiel Homepage, digitale oder soziale Medien wie YouTube und Instagram einschließlich vergleichbarer Plattformen, sowie analog nutzen und verbreiten. Die Zustimmung gilt für alle genannten Nutzungsarten, sofern diese nicht ausdrücklich ausgeschlossen werden." }, { title: "Veröffentlichungen im Internet / Datenschutz", body: "Bei Veröffentlichungen im Internet können personenbezogene Daten einschließlich Fotos weltweit abgerufen und gespeichert werden. Sie können über Suchmaschinen aufgefunden, mit weiteren Daten verknüpft, verändert oder zu anderen Zwecken verwendet werden. Bei sozialen Netzwerken kann eine Verarbeitung in Staaten außerhalb der Europäischen Union nicht ausgeschlossen werden." }, { title: "Hinweise zu Fotos und Filmen bei erwachsenen Schüler*innen", body: "Fotos/Videos mit anderen Personen sind nur mit deren Einwilligung oder ausschließlich im privaten Kreis zulässig. Ausnahmsweise ist eine Veröffentlichung ohne Einwilligung möglich, wenn abgebildete Personen lediglich Beiwerk sind. Bei Schulveranstaltungen kann Fotografieren zum Beispiel in ausgewiesenen Fotozonen möglich sein. Fotos/Videos im Unterricht sind nur mit vorheriger freiwilliger Einwilligung aller Betroffenen zulässig. Veröffentlichungen im Internet oder in sozialen Medien erfordern immer eine Einwilligung, sobald kein geschlossener Nutzerkreis besteht." }, { title: "Links", body: "https://www.ldi.nrw.de/Bildaufnahmen-in-der-Schule\nhttps://www.schulministerium.nrw/fragen-und-antworten-zum-datenschutz" } ]; } function getEinwilligungExceptions(data) { const exceptions = []; if (data.ausnahme_fotoaushang) exceptions.push("Fotoaushang in der Schule"); if (data.ausnahme_homepage) exceptions.push("Homepage der Schule"); if (data.ausnahme_blog) exceptions.push("Blog der Schule"); if (data.ausnahme_social_media) exceptions.push("Social Media und vergleichbare Plattformen"); if (data.ausnahme_sonstiges?.trim()) exceptions.push(data.ausnahme_sonstiges.trim()); return exceptions.length ? exceptions.join("; ") : "Keine Ausnahmen angegeben."; } function renderPlainSection(section) { return `

${escapeHtml(section.title)}

${section.body.split("\n").map((paragraph) => `

${escapeHtml(paragraph)}

`).join("")}
`; } function renderUnterrichtsversaeumnisseOutput(data) { return `

Druckausgabe

A4-Matrix für Unterrichtsversäumnisse.

${renderA4Header("Unterrichtsversäumnisse")}

Unterrichtsversäumnisse Semester ${value(data.halbjahr)} ${value(data.jahr)}

${getUnterrichtsversaeumnisseRows(data).map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join("")}
${renderAbsenceMatrix(data)} ${renderAbsenceProcedure(data)}
Köln, den ${formatDate(data.antragsdatum)} Unterschrift d. Studierenden: ____________________
${renderA4Footer()}
`; } function buildUnterrichtsversaeumnisseText(data) { const rows = getUnterrichtsversaeumnisseRows(data) .map(([label, value]) => `${label}: ${value}`) .join("\n"); const entries = parseAbsenceData(data.abwesenheiten) .map((entry) => `${formatDate(entry.date)}, ${entry.lesson}. Std., ${entry.subject}, Kürzel: ${entry.teacher || "____"}`) .join("\n") || "Keine Fehlzeiten eingetragen."; return `Unterrichtsversäumnisse\n${rows}\n\nFehlzeiten:\n${entries}`; } function getUnterrichtsversaeumnisseRows(data) { return [ ["Name, Vorname", fullNameLastFirst(data)], ["Klasse/Stufe und -Leitung", `${classSemester(data)} / ${value(data.klassenleitung)}`], ["BAföG", data.bafoeg ? "Ja - Kopie zum Quartal und Semesterende an die Stufenleitung geben." : "Nein - Formular selbst aufbewahren."], outputTimestampRow() ]; } function renderAbsenceMatrix(data) { const entries = parseAbsenceData(data.abwesenheiten); const columns = buildAbsenceColumns(entries, 20); const lessons = ["0", "1", "2", "3", "4", "5"]; return ` ${columns.map((column) => ``).join("")} ${lessons.map((lesson) => ` ${columns.map((column) => ``).join("")} `).join("")}
Std.${renderAbsenceColumnHead(column)}
${lesson}. Std.${renderAbsenceCell(column, lesson)}
`; } function renderAbsenceColumnHead(column) { if (!column.date) { return `Mo Di Mi Do Fr
__ . __`; } return `${escapeHtml(weekdayShort(column.date))}
${escapeHtml(formatDate(column.date))}`; } function renderAbsenceCell(column, lesson) { const entry = column.entries.find((item) => item.lesson === lesson); if (!entry) { return `  `; } return ` ${escapeHtml(entry.subject)} ${escapeHtml(entry.teacher || "")} `; } function buildAbsenceColumns(entries, minColumns) { const dates = uniqueValues(entries.map((entry) => entry.date)); const columns = dates.map((date) => ({ date, entries: entries.filter((entry) => entry.date === date) })); while (columns.length < minColumns) { columns.push({ date: "", entries: [] }); } return columns; } function parseAbsenceData(input) { if (!input) return []; if (Array.isArray(input)) return sortAbsences(input); try { return sortAbsences(JSON.parse(input)); } catch (error) { return []; } } function weekdayShort(input) { const date = new Date(`${input}T00:00:00`); return ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"][date.getDay()]; } function renderAbsenceProcedure(data) { const bafoegText = data.bafoeg ? "BAföG: Bitte spätestens zum Quartal und zum Semesterende eine Kopie dieses Blattes an die Stufenleitung geben und selbst eine Kopie/ein Foto als Beleg aufbewahren." : "Kein BAföG: Bitte das Formular selbst aufbewahren; es kann bei Notenbesprechungen relevant werden."; return `

Vorgehen

Tragen Sie versäumte Tage mit Datum und Fächern ein. Die betroffenen Lehrer*innen zeichnen nach Kenntnisnahme im kleinen Kästchen der jeweiligen Stunde ab. Von der Fachlehrerin/vom Fachlehrer unterschriebene Stunden gelten als entschuldigt.

${escapeHtml(bafoegText)}

Bitte verwenden Sie ausschließlich dieses Formular, um Unterrichtsversäumnisse zu entschuldigen. Bei mehr als einer Woche, vorhersehbaren oder regelmäßigen Fehlzeiten nehmen Sie bitte Kontakt mit Fachlehrer*innen und Stufenleitung auf.

Bei verpassten Klausuren bitte das Nachschreibformular unter formular.agkoeln.de nutzen; es gilt eine Frist von 3 Tagen.

`; } function buildAbwahlAdvice(data) { const subject = getSubjectMeta(data.fach); const abiSubjects = uniqueValues([data.lk1_fach, data.lk2_fach]); const writtenSubjects = getWrittenSubjectsAfterAbwahl(data); const writtenAreas = uniqueValues(writtenSubjects.map((name) => getSubjectMeta(name)?.bereich).filter(Boolean)); const requiredAreas = ["Sprache", "Gesellschaftswissenschaft", "Mathe/Naturwissenschaft"]; const missingAreas = requiredAreas.filter((area) => !writtenAreas.includes(area)); const coreAbiSubjects = uniqueValues(writtenSubjects.filter((name) => getSubjectMeta(name)?.kernfach)); const isAbiSubject = Boolean(data.fach) && abiSubjects.includes(data.fach); const checks = [ { ok: !isAbiSubject, text: isAbiSubject ? `Das Fach ${data.fach} ist Leistungskurs/Abiturfach und darf nicht abgewählt werden.` : "Das abzuwählende Fach ist kein LK-Abiturfach." }, { ok: coreAbiSubjects.length >= 2, text: `${coreAbiSubjects.length} von 3 Kernfächern (Deutsch, Englisch, Mathematik) bleiben schriftlich belegt.` }, { ok: missingAreas.length === 0, text: missingAreas.length === 0 ? `Alle drei Bereiche bleiben schriftlich abgedeckt: ${writtenAreas.join(", ")}.` : `Nicht schriftlich abgedeckt: ${missingAreas.join(", ")}.` } ]; if (subject?.kernfach) { checks.push({ ok: true, text: "Kernfach-Hinweis: In H3/H4 kann bei zwei Klausuren nur die erste Klausur abgewählt werden; ab H5 muss keine Klausur mehr geschrieben werden." }); } else if (subject) { checks.push({ ok: true, text: `Kein Kernfach. Bereich des Fachs: ${subject.bereich}.` }); } const blockers = checks.filter((check) => !check.ok); return { checks, summary: blockers.length === 0 ? "Beratungsregeln erfüllt." : `Nicht erfüllt: ${blockers.map((check) => check.text).join(" ")}` }; } function getWrittenSubjectsAfterAbwahl(data) { const courses = [ { name: data.lk1_fach, written: true }, { name: data.lk2_fach, written: true }, { name: data.gk1_fach, written: data.gk1_status === "schriftlich" }, { name: data.gk2_fach, written: data.gk2_status === "schriftlich" }, { name: data.gk3_fach, written: data.gk3_status === "schriftlich" }, { name: data.gk4_fach, written: data.gk4_status === "schriftlich" } ]; return uniqueValues( courses .filter((course) => course.name && course.written && course.name !== data.fach) .map((course) => course.name) ); } function uniqueValues(values) { return [...new Set(values.filter(Boolean))]; } function renderAdvice(advice) { return `

Beratungsprüfung

`; } function getSubjectMeta(subjectName) { return (optionLists.faecher || []).find((subject) => subject.value === subjectName || subject.label === subjectName); } function renderCopy(copy) { return `
${renderA4Header(copy.title)}

${escapeHtml(copy.title)}

${copy.rows.map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join("")}
Köln, den ____________________ ${escapeHtml(copy.signature)}: ____________________
${renderA4Footer()}
`; } function renderA4Header(title) { return `

Abendgymnasium Köln

${escapeHtml(title)}

Dieses Formular liegt digital auf der Formularseite formular.agkoeln.de vor.

Es liegt in der Regel auch vor dem Sekretariat aus.

Dieses Formular lässt sich in der PDF-Version bearbeiten, abspeichern und ausdrucken.

Bitte eine Kopie/einen Screenshot/ein Foto o.ä. als eigene Kopie aufbewahren.

Abendgymnasium Köln Logo
`; } function renderA4Footer() { return ` `; } function currentFormUrl() { const url = new URL(window.location.href); if (activeForm?.id) { url.searchParams.set("formular", activeForm.id); } return url.href; } function fullName(data) { return `${value(data.vorname)} ${value(data.name)}`.trim() || "____________________"; } function value(input) { return input || "____________________"; } function checkedText(isChecked, text) { return isChecked ? text : `Noch zu bestätigen: ${text}`; } function formatDate(input) { if (!input) return "____________________"; const [year, month, day] = input.split("-"); return `${day}.${month}.${year}`; } function outputTimestampRow() { return ["Zeitstempel", formatDateTime(new Date())]; } function updateFormPrintTimestamp() { const timestamp = formElement.querySelector(".form-print-timestamp"); if (!timestamp) return; timestamp.textContent = `Zeitstempel: ${formatDateTime(new Date())}`; } function formatDateTime(date) { return `${formatDateInputValue(date).split("-").reverse().join(".")}, ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")} Uhr`; } function formatDateInputValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function storageKey() { return `schulformular:${activeForm.id}`; } function setStatus(message, isError = false) { statusElement.textContent = message; statusElement.style.color = isError ? "var(--danger)" : "var(--muted)"; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }