(function () { const EMAIL_STORAGE_KEY = 'ff_member_form_email_v1'; const AUTO_OTP_SESSION_PREFIX = 'ff_member_form_auto_otp_sent_v1:'; const boot = window.APP_BOOT || { steps: [], csrf: '', contactEmail: '' }; const baseUrl = String(boot.baseUrl || '').replace(/\/+$/, ''); const schemaSteps = Array.isArray(boot.steps) ? boot.steps : []; const verificationConfig = boot.verification && typeof boot.verification === 'object' ? boot.verification : {}; const state = { email: '', otpEmail: '', isVerified: false, lastUserActivityAt: Math.floor(Date.now() / 1000), currentStep: 1, totalSteps: schemaSteps.length, summaryStep: schemaSteps.length + 1, autosaveId: null, otpCooldownId: null, otpCooldownRemaining: 0, uploads: {}, isSubmitting: false, summaryMissingCount: 0, }; const disclaimerSection = document.getElementById('disclaimerSection'); const disclaimerReadCheckbox = document.getElementById('disclaimerReadCheckbox'); const disclaimerReadError = document.getElementById('disclaimerReadError'); const acceptDisclaimerBtn = document.getElementById('acceptDisclaimerBtn'); const startSection = document.getElementById('startSection'); const startForm = document.getElementById('startForm'); const startIntroText = document.getElementById('startIntroText'); const startEmailField = document.getElementById('startEmailField'); const startActions = document.getElementById('startActions'); const startEmailInput = document.getElementById('startEmail'); const startEmailError = document.getElementById('startEmailError'); const startSubmitBtn = document.getElementById('startSubmitBtn'); const otpSection = document.getElementById('otpSection'); const otpInfoText = document.getElementById('otpInfoText'); const startOtpInput = document.getElementById('startOtp'); const startOtpError = document.getElementById('startOtpError'); const verifyOtpBtn = document.getElementById('verifyOtpBtn'); const resendOtpBtn = document.getElementById('resendOtpBtn'); const otpCooldownMessage = document.getElementById('otpCooldownMessage'); const resetDataBtn = document.getElementById('resetDataBtn'); const compactStatusBox = document.getElementById('compactStatusBox'); const statusEmailValue = document.getElementById('statusEmailValue'); const draftStatusValue = document.getElementById('draftStatusValue'); const startFeedbackMessage = document.getElementById('startFeedbackMessage'); const feedbackMessage = document.getElementById('feedbackMessage'); const wizardSection = document.getElementById('wizardSection'); const applicationForm = document.getElementById('applicationForm'); const applicationEmail = document.getElementById('applicationEmail'); const applicationWebsiteInput = document.getElementById('applicationWebsite'); const startWebsiteInput = document.getElementById('website'); const progress = document.getElementById('progress'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); const submitBtn = document.getElementById('submitBtn'); const submitSpinner = document.getElementById('submitSpinner'); const submitLabel = submitBtn ? submitBtn.querySelector('[data-submit-label]') : null; const summarySection = document.getElementById('summarySection'); const summaryContent = document.getElementById('summaryContent'); const summaryMissingNotice = document.getElementById('summaryMissingNotice'); const stepElements = Array.from(document.querySelectorAll('.step')); const startEmailRequiredMark = document.querySelector('#startEmailField .required-mark-field-start'); const fieldContainersByKey = new Map(); const fieldsByKey = new Map(); const tableFieldsByKey = new Map(); document.querySelectorAll('.field[data-field]').forEach((container) => { const key = String(container.getAttribute('data-field') || '').trim(); if (key !== '') { fieldContainersByKey.set(key, container); } }); schemaSteps.forEach((step) => { const fields = Array.isArray(step.fields) ? step.fields : []; fields.forEach((field) => { if (!field || typeof field !== 'object') { return; } const key = String(field.key || '').trim(); if (key !== '') { fieldsByKey.set(key, field); } }); }); document.querySelectorAll('[data-table-field="1"][data-table-key]').forEach((tableEl) => { const key = String(tableEl.getAttribute('data-table-key') || '').trim(); if (key === '') { return; } const rows = Math.max(1, Number(tableEl.getAttribute('data-table-rows') || 0)); const headers = Array.from(tableEl.querySelectorAll('thead th')).map((th) => String(th.textContent || '').trim()); tableFieldsByKey.set(key, { key, tableEl, rows, headers }); }); function appUrl(path) { const normalizedPath = String(path || '').replace(/^\/+/, ''); return (baseUrl ? baseUrl + '/' : '/') + normalizedPath; } function scrollWizardToTop() { if (!wizardSection || wizardSection.classList.contains('hidden')) { return; } wizardSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function setFeedbackText(target, text, isError) { if (!target) { return; } target.textContent = text || ''; target.classList.toggle('error-text', Boolean(isError)); } function setFeedback(text, isError, scope) { const targetScope = scope || (wizardSection && !wizardSection.classList.contains('hidden') ? 'wizard' : 'start'); if (targetScope === 'wizard') { setFeedbackText(feedbackMessage, text, isError); setFeedbackText(startFeedbackMessage, '', false); return; } if (targetScope === 'start') { setFeedbackText(startFeedbackMessage, text, isError); setFeedbackText(feedbackMessage, '', false); return; } setFeedbackText(feedbackMessage, text, isError); setFeedbackText(startFeedbackMessage, text, isError); } function setDraftStatus(text, isError) { draftStatusValue.textContent = text || ''; draftStatusValue.classList.toggle('error-text', Boolean(isError)); } function setStartEmailError(text) { if (!startEmailError) { return; } startEmailError.textContent = text || ''; } function setStartOtpError(text) { if (!startOtpError) { return; } startOtpError.textContent = text || ''; } function setDisclaimerError(text) { if (!disclaimerReadError) { return; } disclaimerReadError.textContent = text || ''; } function updateStartEmailRequiredMarker() { if (!startEmailRequiredMark) { return; } const email = normalizeEmail(startEmailInput.value || ''); startEmailRequiredMark.classList.toggle('hidden', isValidEmail(email)); } function formatTimestamp(isoDate) { if (!isoDate) { return ''; } const date = new Date(isoDate); if (Number.isNaN(date.getTime())) { return String(isoDate); } return date.toLocaleString('de-DE'); } function normalizeEmail(email) { return (email || '').trim().toLowerCase(); } function normalizeOtpCode(code) { return String(code || '').replace(/[^\d]/g, '').slice(0, 6); } function honeypotValue() { const applicationValue = applicationWebsiteInput ? String(applicationWebsiteInput.value || '').trim() : ''; if (applicationValue !== '') { return applicationValue; } return startWebsiteInput ? String(startWebsiteInput.value || '').trim() : ''; } function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } function rememberEmail(email) { try { localStorage.setItem(EMAIL_STORAGE_KEY, email); } catch (_err) { // ignore localStorage errors } } function getRememberedEmail() { try { return localStorage.getItem(EMAIL_STORAGE_KEY) || ''; } catch (_err) { return ''; } } function forgetRememberedEmail() { try { localStorage.removeItem(EMAIL_STORAGE_KEY); } catch (_err) { // ignore localStorage errors } } function autoOtpSessionKey(email) { return AUTO_OTP_SESSION_PREFIX + normalizeEmail(email); } function hasAutoOtpSessionFlag(email) { try { return sessionStorage.getItem(autoOtpSessionKey(email)) === '1'; } catch (_err) { return false; } } function markAutoOtpSessionFlag(email) { try { sessionStorage.setItem(autoOtpSessionKey(email), '1'); } catch (_err) { // ignore } } function updateDisclaimerAcceptanceState() { if (!acceptDisclaimerBtn || !disclaimerReadCheckbox) { return; } const accepted = disclaimerReadCheckbox.checked; acceptDisclaimerBtn.disabled = !accepted; if (accepted) { setDisclaimerError(''); } } function setResetActionVisible(isVisible) { if (!resetDataBtn) { return; } resetDataBtn.classList.toggle('hidden', !isVisible); resetDataBtn.disabled = !isVisible; } function enterCompactStatus(email) { statusEmailValue.textContent = email; startSection.classList.add('compact-mode'); compactStatusBox.classList.remove('hidden'); startIntroText.classList.add('hidden'); startEmailField.classList.add('hidden'); startActions.classList.add('hidden'); if (otpSection) { otpSection.classList.add('hidden'); } } function leaveCompactStatus() { startSection.classList.remove('compact-mode'); compactStatusBox.classList.add('hidden'); startIntroText.classList.remove('hidden'); startEmailField.classList.remove('hidden'); startActions.classList.remove('hidden'); statusEmailValue.textContent = '-'; setDraftStatus('Noch nicht gespeichert', false); setResetActionVisible(false); } function showOtpSection(email, message) { state.otpEmail = normalizeEmail(email); if (otpSection) { otpSection.classList.remove('hidden'); } if (otpInfoText) { otpInfoText.textContent = message || ('Code wurde an ' + state.otpEmail + ' gesendet.'); otpInfoText.classList.remove('error-text'); } if (startOtpInput) { startOtpInput.value = ''; startOtpInput.focus(); } setStartOtpError(''); } function hideOtpSection() { if (otpSection) { otpSection.classList.add('hidden'); } if (otpInfoText) { otpInfoText.textContent = ''; } setStartOtpError(''); clearOtpCooldown(); state.otpEmail = ''; } function clearOtpCooldown() { if (state.otpCooldownId) { clearInterval(state.otpCooldownId); state.otpCooldownId = null; } state.otpCooldownRemaining = 0; if (resendOtpBtn) { resendOtpBtn.disabled = false; } if (otpCooldownMessage) { otpCooldownMessage.textContent = ''; } } function setOtpCooldown(seconds) { clearOtpCooldown(); const total = Math.max(0, Number(seconds || 0)); state.otpCooldownRemaining = total; if (total <= 0) { return; } if (resendOtpBtn) { resendOtpBtn.disabled = true; } const update = () => { if (!otpCooldownMessage) { return; } if (state.otpCooldownRemaining <= 0) { otpCooldownMessage.textContent = ''; if (resendOtpBtn) { resendOtpBtn.disabled = false; } if (state.otpCooldownId) { clearInterval(state.otpCooldownId); state.otpCooldownId = null; } return; } otpCooldownMessage.textContent = 'Neuer Code in ' + String(state.otpCooldownRemaining) + 's verfügbar.'; state.otpCooldownRemaining -= 1; }; update(); state.otpCooldownId = setInterval(update, 1000); } function lockEmail(email) { state.email = email; state.isVerified = true; applicationEmail.value = email; startEmailInput.value = email; startEmailInput.readOnly = true; startEmailInput.setAttribute('aria-readonly', 'true'); updateStartEmailRequiredMarker(); rememberEmail(email); enterCompactStatus(email); } function unlockEmail(clearInput, forgetEmail) { const shouldForget = forgetEmail !== false; state.email = ''; state.isVerified = false; applicationEmail.value = ''; startEmailInput.readOnly = false; startEmailInput.removeAttribute('aria-readonly'); if (clearInput) { startEmailInput.value = ''; setStartEmailError(''); } updateStartEmailRequiredMarker(); if (shouldForget) { forgetRememberedEmail(); } leaveCompactStatus(); hideOtpSection(); setDisclaimerError(''); } function stopAutosave() { if (state.autosaveId) { clearInterval(state.autosaveId); state.autosaveId = null; } } function markUserActivity() { state.lastUserActivityAt = Math.floor(Date.now() / 1000); } function startAutosave() { stopAutosave(); state.autosaveId = setInterval(async () => { if (!state.email || !state.isVerified || wizardSection.classList.contains('hidden')) { return; } try { await saveDraft(false); } catch (err) { const payload = (err && err.payload) || {}; if (isAuthFailurePayload(payload)) { handleProtectedAuthFailure(payload); } } }, 15000); } function clearErrors() { document.querySelectorAll('[data-error-for]').forEach((el) => { el.textContent = ''; }); } function showErrors(errors) { clearErrors(); Object.keys(errors || {}).forEach((key) => { const el = document.querySelector('[data-error-for="' + key + '"]'); if (el) { el.textContent = errors[key]; } }); } function clearWizardData() { applicationForm.reset(); populateAllTablesFromHiddenValues(); syncAllTableHiddenValues(); clearErrors(); renderUploadInfo({}); state.currentStep = 1; state.summaryMissingCount = 0; state.isSubmitting = false; if (submitLabel) { submitLabel.textContent = 'Verbindlich absenden'; } if (submitSpinner) { submitSpinner.classList.add('hidden'); } submitBtn.classList.remove('is-loading'); applyFieldVisibility(); refreshRequiredMarkers(); updateProgress(); Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => { const fieldKey = control.getAttribute('data-upload-key'); if (fieldKey) { updateUploadSelectionText(fieldKey); } }); } function parseFieldKey(name) { const match = /^form_data\[(.+)\]$/.exec(String(name || '')); return match ? match[1] : ''; } function isCheckboxTrue(value) { return ['1', 'on', 'true', true, 1].includes(value); } function parseBirthdateParts(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim()); if (!match) { return null; } const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const candidate = new Date(Date.UTC(year, month - 1, day)); if ( Number.isNaN(candidate.getTime()) || candidate.getUTCFullYear() !== year || candidate.getUTCMonth() !== month - 1 || candidate.getUTCDate() !== day ) { return null; } return { year, month, day }; } function deriveAdultFlagFromBirthdate(birthdateValue) { const parts = parseBirthdateParts(birthdateValue); if (!parts) { return ''; } const now = new Date(); const yearNow = now.getFullYear(); const monthNow = now.getMonth() + 1; const dayNow = now.getDate(); const isFutureBirthdate = parts.year > yearNow || (parts.year === yearNow && (parts.month > monthNow || (parts.month === monthNow && parts.day > dayNow))); if (isFutureBirthdate) { return ''; } let age = yearNow - parts.year; const hadBirthdayThisYear = monthNow > parts.month || (monthNow === parts.month && dayNow >= parts.day); if (!hadBirthdayThisYear) { age -= 1; } return age >= 18 ? '1' : '0'; } function addComputedAgeFlags(data) { const adultFlag = deriveAdultFlagFromBirthdate(data.geburtsdatum || ''); data.is_minor = adultFlag === '' ? '' : adultFlag === '1' ? '0' : '1'; return data; } function csvEscapeValue(value) { const normalized = String(value || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); if (normalized.includes('"') || normalized.includes(',') || normalized.includes('\n')) { return '"' + normalized.replace(/"/g, '""') + '"'; } return normalized; } function parseCsvLine(line) { const cells = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i += 1) { const char = line[i]; if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i += 1; continue; } inQuotes = !inQuotes; continue; } if (char === ',' && !inQuotes) { cells.push(current); current = ''; continue; } current += char; } cells.push(current); return cells; } function csvHeaderMatches(headers, row) { if (!Array.isArray(headers) || headers.length === 0 || !Array.isArray(row)) { return false; } if (row.length !== headers.length) { return false; } return headers.every((header, index) => String(header || '').trim().toLowerCase() === String(row[index] || '').trim().toLowerCase()); } function isTableCsvEmpty(value, field) { const raw = String(value || '').trim(); if (raw === '') { return true; } const lines = raw .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line !== ''); if (lines.length === 0) { return true; } const parsedRows = lines.map((line) => parseCsvLine(line)); const headers = Array.isArray(field && field.columns) ? field.columns .filter((column) => column && typeof column === 'object') .map((column) => String(column.label || '').trim()) .filter((label) => label !== '') : []; const dataRows = csvHeaderMatches(headers, parsedRows[0]) ? parsedRows.slice(1) : parsedRows; if (dataRows.length === 0) { return true; } return !dataRows.some((row) => row.some((cell) => String(cell || '').trim() !== '')); } function syncTableHiddenValue(tableKey) { const tableMeta = tableFieldsByKey.get(tableKey); if (!tableMeta) { return; } const hiddenField = applicationForm.querySelector('[name="form_data[' + tableKey + ']"]'); if (!hiddenField) { return; } const dataRows = []; for (let rowIndex = 0; rowIndex < tableMeta.rows; rowIndex += 1) { const rowValues = []; for (let colIndex = 0; colIndex < tableMeta.headers.length; colIndex += 1) { const input = tableMeta.tableEl.querySelector('[data-table-cell="1"][data-row-index="' + rowIndex + '"][data-col-index="' + colIndex + '"]'); rowValues.push(String((input && input.value) || '').trim()); } dataRows.push(rowValues); } const hasAnyValue = dataRows.some((row) => row.some((cell) => cell !== '')); if (!hasAnyValue) { hiddenField.value = ''; return; } const lines = []; lines.push(tableMeta.headers.map((header) => csvEscapeValue(header)).join(',')); dataRows.forEach((row) => { lines.push(row.map((cell) => csvEscapeValue(cell)).join(',')); }); hiddenField.value = lines.join('\n'); } function syncAllTableHiddenValues() { tableFieldsByKey.forEach((tableMeta) => { syncTableHiddenValue(tableMeta.key); }); } function populateTableFromHiddenValue(tableKey) { const tableMeta = tableFieldsByKey.get(tableKey); if (!tableMeta) { return; } const hiddenField = applicationForm.querySelector('[name="form_data[' + tableKey + ']"]'); if (!hiddenField) { return; } const raw = String(hiddenField.value || '').trim(); const lines = raw === '' ? [] : raw.split(/\r?\n/); const parsedRows = lines.map((line) => parseCsvLine(line)); const dataRows = parsedRows.length > 0 && csvHeaderMatches(tableMeta.headers, parsedRows[0]) ? parsedRows.slice(1) : parsedRows; for (let rowIndex = 0; rowIndex < tableMeta.rows; rowIndex += 1) { const sourceRow = Array.isArray(dataRows[rowIndex]) ? dataRows[rowIndex] : []; for (let colIndex = 0; colIndex < tableMeta.headers.length; colIndex += 1) { const input = tableMeta.tableEl.querySelector('[data-table-cell="1"][data-row-index="' + rowIndex + '"][data-col-index="' + colIndex + '"]'); if (!input) { continue; } input.value = String(sourceRow[colIndex] || ''); } } } function populateAllTablesFromHiddenValues() { tableFieldsByKey.forEach((tableMeta) => { populateTableFromHiddenValue(tableMeta.key); }); } function initTableFields() { tableFieldsByKey.forEach((tableMeta) => { const cells = Array.from(tableMeta.tableEl.querySelectorAll('[data-table-cell="1"]')); cells.forEach((cell) => { const syncAndRefresh = () => { syncTableHiddenValue(tableMeta.key); applyFieldVisibility(); refreshRequiredMarkers(); if (state.currentStep === state.summaryStep) { renderSummary(); } }; cell.addEventListener('input', syncAndRefresh); cell.addEventListener('change', syncAndRefresh); }); }); populateAllTablesFromHiddenValues(); syncAllTableHiddenValues(); } function collectCurrentFormData() { const data = {}; Array.from(applicationForm.elements).forEach((el) => { if (!el.name) { return; } const key = parseFieldKey(el.name); if (!key) { return; } if (el.type === 'checkbox') { data[key] = el.checked ? '1' : '0'; } else { data[key] = el.value || ''; } }); return addComputedAgeFlags(data); } function evaluateFieldRule(rule, formData) { if (!rule || typeof rule !== 'object') { return false; } const depField = String(rule.field || '').trim(); const depValue = String(rule.equals || ''); if (depField === '') { return false; } return String(formData[depField] || '') === depValue; } function isFieldVisible(field, formData) { if (!field || typeof field !== 'object') { return true; } if (!field.visible_if || typeof field.visible_if !== 'object') { return true; } return evaluateFieldRule(field.visible_if, formData); } function isOptionVisible(option, formData) { if (!option || typeof option !== 'object') { return true; } if (option.visible_if && typeof option.visible_if === 'object' && !evaluateFieldRule(option.visible_if, formData)) { return false; } if (option.hidden_if && typeof option.hidden_if === 'object' && evaluateFieldRule(option.hidden_if, formData)) { return false; } return true; } function applySelectOptionVisibility(formData) { let changedSelection = false; fieldsByKey.forEach((field, key) => { if (!field || typeof field !== 'object') { return; } if (String(field.type || '') !== 'select' || !Array.isArray(field.options)) { return; } const select = applicationForm.querySelector('[name="form_data[' + key + ']"]'); if (!select) { return; } const optionRules = new Map(); field.options.forEach((option) => { if (!option || typeof option !== 'object') { return; } optionRules.set(String(option.value || ''), option); }); let selectedHidden = false; Array.from(select.options).forEach((optionEl) => { const optionValue = String(optionEl.value || ''); if (optionValue === '') { optionEl.hidden = false; optionEl.disabled = false; return; } const optionRule = optionRules.get(optionValue); const visible = isOptionVisible(optionRule, formData); optionEl.hidden = !visible; optionEl.disabled = !visible; if (!visible && optionEl.selected) { selectedHidden = true; } }); if (selectedHidden) { select.value = ''; changedSelection = true; } }); return changedSelection; } function hasFileValue(fieldKey) { const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]'); const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]'); const hasLocalSelection = (fileInput && fileInput.files && fileInput.files.length > 0) || (cameraInput && cameraInput.files && cameraInput.files.length > 0); if (hasLocalSelection) { return true; } const storedUploads = state.uploads[fieldKey]; return Array.isArray(storedUploads) && storedUploads.length > 0; } function isFieldCompleted(field, formData) { const key = String(field.key || '').trim(); const type = String(field.type || 'text'); if (key === '') { return false; } if (type === 'file') { return hasFileValue(key); } if (type === 'checkbox') { return isCheckboxTrue(formData[key]); } if (type === 'table') { return !isTableCsvEmpty(formData[key], field); } return String(formData[key] || '').trim() !== ''; } function applyFieldVisibility() { const formData = collectCurrentFormData(); const changedSelection = applySelectOptionVisibility(formData); const effectiveFormData = changedSelection ? collectCurrentFormData() : formData; fieldsByKey.forEach((field, key) => { const container = fieldContainersByKey.get(key); if (!container) { return; } const visible = isFieldVisible(field, effectiveFormData); container.classList.toggle('field-hidden-by-rule', !visible); const controlElements = container.querySelectorAll('input, select, textarea'); controlElements.forEach((el) => { el.disabled = !visible; }); if (!visible) { const errorEl = container.querySelector('[data-error-for="' + key + '"]'); if (errorEl) { errorEl.textContent = ''; } } }); } function refreshRequiredMarkers() { const formData = collectCurrentFormData(); fieldsByKey.forEach((field, key) => { const container = fieldContainersByKey.get(key); if (!container) { return; } const markers = container.querySelectorAll('.required-mark-field'); if (!markers.length) { return; } const visibleNow = isFieldVisible(field, formData); const requiredNow = isFieldRequired(field, formData); const completed = isFieldCompleted(field, formData); const hideMarker = !visibleNow || !requiredNow || completed; markers.forEach((marker) => { marker.classList.toggle('hidden', hideMarker); }); }); } function initRequiredMarkerTracking() { Array.from(applicationForm.elements).forEach((el) => { if (!el.name || !el.name.startsWith('form_data[')) { return; } el.addEventListener('blur', () => { applyFieldVisibility(); refreshRequiredMarkers(); }); el.addEventListener('change', () => { applyFieldVisibility(); refreshRequiredMarkers(); }); }); } function isFieldRequired(field, formData) { if (!field || typeof field !== 'object') { return false; } if (!isFieldVisible(field, formData)) { return false; } if (field.required === true) { return true; } if (!field.required_if || typeof field.required_if !== 'object') { return false; } return evaluateFieldRule(field.required_if, formData); } function isFieldMissing(field, formData) { if (!isFieldRequired(field, formData)) { return false; } const key = String(field.key || '').trim(); if (!key) { return false; } const type = String(field.type || 'text'); if (type === 'file') { const uploadItems = state.uploads[key]; return !Array.isArray(uploadItems) || uploadItems.length === 0; } if (type === 'checkbox') { return !isCheckboxTrue(formData[key]); } if (type === 'table') { return isTableCsvEmpty(formData[key], field); } return String(formData[key] || '').trim() === ''; } function resolveSelectLabel(field, value) { if (!Array.isArray(field.options)) { return value; } const matched = field.options.find((option) => { return option && String(option.value || '') === String(value); }); if (!matched) { return value; } return String(matched.label || value); } function fieldDisplayValue(field, formData) { const key = String(field.key || '').trim(); const type = String(field.type || 'text'); if (!key) { return ''; } if (type === 'file') { const uploadItems = state.uploads[key]; if (!Array.isArray(uploadItems) || uploadItems.length === 0) { return ''; } return uploadItems .map((item) => { if (!item || typeof item !== 'object') { return ''; } return String(item.original_filename || item.stored_filename || '').trim(); }) .filter(Boolean) .join(', '); } if (type === 'checkbox') { return isCheckboxTrue(formData[key]) ? 'Ja' : 'Nein'; } const rawValue = String(formData[key] || '').trim(); if (rawValue === '') { return ''; } if (type === 'select') { return resolveSelectLabel(field, rawValue); } if (type === 'table') { return rawValue; } return rawValue; } function renderSummary() { if (!summarySection || !summaryContent || !summaryMissingNotice) { return; } const formData = collectCurrentFormData(); const fragment = document.createDocumentFragment(); let missingCount = 0; const introCard = document.createElement('div'); introCard.className = 'summary-step-card'; const introTitle = document.createElement('h4'); introTitle.textContent = 'Startdaten'; introCard.appendChild(introTitle); const emailRow = document.createElement('div'); emailRow.className = 'field summary-item'; const emailLabel = document.createElement('label'); emailLabel.className = 'summary-item-label'; emailLabel.textContent = 'E-Mail'; const emailValue = document.createElement('div'); emailValue.className = 'summary-item-value'; emailValue.textContent = state.email || 'Nicht gesetzt'; emailRow.appendChild(emailLabel); emailRow.appendChild(emailValue); introCard.appendChild(emailRow); fragment.appendChild(introCard); schemaSteps.forEach((step, stepIndex) => { const allFields = Array.isArray(step.fields) ? step.fields.filter((field) => field && typeof field === 'object') : []; const fields = allFields.filter((field) => isFieldVisible(field, formData)); if (fields.length === 0) { return; } const card = document.createElement('div'); card.className = 'summary-step-card'; const title = document.createElement('h4'); title.textContent = 'Schritt ' + String(stepIndex + 1) + ': ' + String(step.title || ''); card.appendChild(title); fields.forEach((field) => { const required = isFieldRequired(field, formData); const missing = isFieldMissing(field, formData); if (missing) { missingCount += 1; } const row = document.createElement('div'); row.className = 'field summary-item'; if (required) { row.classList.add('summary-item-required'); } if (missing) { row.classList.add('summary-item-missing'); } const labelEl = document.createElement('label'); labelEl.className = 'summary-item-label'; labelEl.textContent = String(field.label || field.key || 'Feld'); if (required) { const requiredBadge = document.createElement('span'); requiredBadge.className = 'summary-badge summary-badge-required'; requiredBadge.textContent = 'Pflichtfeld'; labelEl.appendChild(requiredBadge); } if (missing) { const missingBadge = document.createElement('span'); missingBadge.className = 'summary-badge summary-badge-missing'; missingBadge.textContent = '! Pflichtfeld fehlt'; labelEl.appendChild(missingBadge); } const valueEl = document.createElement('div'); valueEl.className = 'summary-item-value'; const value = fieldDisplayValue(field, formData); if (value !== '') { valueEl.textContent = value; if (String(field.type || '') === 'table') { valueEl.classList.add('summary-item-value-multiline'); } } else { valueEl.textContent = String(field.type || '') === 'file' ? 'Keine Datei hochgeladen' : 'Nicht ausgefüllt'; valueEl.classList.add('summary-item-value-empty'); if (missing) { valueEl.classList.add('summary-item-value-missing'); } } row.appendChild(labelEl); row.appendChild(valueEl); card.appendChild(row); }); fragment.appendChild(card); }); summaryContent.innerHTML = ''; summaryContent.appendChild(fragment); state.summaryMissingCount = missingCount; if (missingCount > 0) { summaryMissingNotice.classList.remove('hidden'); summaryMissingNotice.classList.add('summary-missing-warning'); summaryMissingNotice.textContent = '! Es fehlen noch ' + String(missingCount) + ' Pflichtfelder. Bitte korrigieren Sie die rot markierten Einträge.'; } else { summaryMissingNotice.classList.remove('hidden'); summaryMissingNotice.classList.remove('summary-missing-warning'); summaryMissingNotice.textContent = 'Alle Pflichtfelder sind ausgefüllt.'; } } function updateProgress() { const isSummary = state.currentStep === state.summaryStep; if (isSummary) { progress.textContent = 'Zusammenfassung (Schritt ' + String(state.summaryStep) + ' von ' + String(state.summaryStep) + ')'; } else { progress.textContent = 'Schritt ' + String(state.currentStep) + ' von ' + String(state.totalSteps); } stepElements.forEach((el) => { const step = Number(el.getAttribute('data-step')); el.classList.toggle('hidden', step !== state.currentStep); }); if (summarySection) { summarySection.classList.toggle('hidden', !isSummary); if (isSummary) { renderSummary(); } } prevBtn.disabled = state.isSubmitting || state.currentStep === 1; nextBtn.textContent = state.currentStep === state.totalSteps ? 'Zur Zusammenfassung' : 'Weiter'; nextBtn.classList.toggle('hidden', state.currentStep >= state.summaryStep); nextBtn.disabled = state.isSubmitting; submitBtn.classList.toggle('hidden', !isSummary); submitBtn.disabled = state.isSubmitting; } function fillFormData(data) { Object.keys(data || {}).forEach((key) => { const field = applicationForm.querySelector('[name="form_data[' + key + ']"]'); if (!field) { return; } if (field.type === 'checkbox') { field.checked = ['1', 'on', 'true', true].includes(data[key]); } else { field.value = data[key] || ''; } }); populateAllTablesFromHiddenValues(); syncAllTableHiddenValues(); applyFieldVisibility(); refreshRequiredMarkers(); } function formatUploadTimestamp(isoDate) { const formatted = formatTimestamp(String(isoDate || '')); if (formatted !== '') { return formatted; } return String(isoDate || ''); } function buildUploadPreviewUrl(fieldKey, index) { const params = new URLSearchParams(); params.set('csrf', boot.csrf); params.set('email', state.email); params.set('field', fieldKey); params.set('index', String(index)); params.set('last_user_activity_at', String(state.lastUserActivityAt)); return appUrl('api/upload-preview.php') + '?' + params.toString(); } async function deleteUploadedFile(fieldKey, index) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('email', state.email); fd.append('website', honeypotValue()); fd.append('field', fieldKey); fd.append('index', String(index)); fd.append('last_user_activity_at', String(state.lastUserActivityAt)); const response = await postForm(appUrl('api/delete-upload.php'), fd); renderUploadInfo(response.uploads || {}); const ts = formatTimestamp(response.updated_at); setDraftStatus('Gespeichert: ' + (ts || 'gerade eben'), false); if (state.currentStep === state.summaryStep) { renderSummary(); } } function renderUploadInfo(uploads) { state.uploads = uploads && typeof uploads === 'object' ? uploads : {}; document.querySelectorAll('[data-upload-list]').forEach((el) => { el.innerHTML = ''; }); Object.keys(state.uploads).forEach((field) => { const target = document.querySelector('[data-upload-list="' + field + '"]'); if (!target || !Array.isArray(state.uploads[field])) { return; } state.uploads[field].forEach((item, index) => { const div = document.createElement('div'); div.className = 'upload-item'; const info = document.createElement('div'); info.className = 'upload-item-info'; const filename = String((item && item.original_filename) || (item && item.stored_filename) || 'Datei'); const uploadedAt = formatUploadTimestamp(item && item.uploaded_at); info.textContent = uploadedAt !== '' ? filename + ' (' + uploadedAt + ')' : filename; div.appendChild(info); const actions = document.createElement('div'); actions.className = 'upload-item-actions'; const previewLink = document.createElement('a'); previewLink.className = 'upload-item-btn'; previewLink.href = buildUploadPreviewUrl(field, index); previewLink.target = '_blank'; previewLink.rel = 'noopener noreferrer'; previewLink.textContent = 'Vorschau'; previewLink.addEventListener('click', () => { markUserActivity(); previewLink.href = buildUploadPreviewUrl(field, index); }); actions.appendChild(previewLink); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'upload-item-btn upload-item-btn-danger'; deleteBtn.textContent = 'Löschen'; deleteBtn.addEventListener('click', async () => { markUserActivity(); const confirmed = window.confirm('Diesen Upload wirklich löschen?'); if (!confirmed) { return; } deleteBtn.disabled = true; try { await deleteUploadedFile(field, index); setFeedback('Upload gelöscht.', false); } catch (err) { handleProtectedError(err, 'Löschen fehlgeschlagen.', 'wizard'); } finally { deleteBtn.disabled = false; } }); actions.appendChild(deleteBtn); div.appendChild(actions); target.appendChild(div); }); }); applyFieldVisibility(); refreshRequiredMarkers(); } function updateUploadSelectionText(fieldKey) { const target = document.querySelector('[data-upload-selected="' + fieldKey + '"]'); if (!target) { return; } const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]'); const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]'); let label = 'Noch keine Datei hochgeladen'; if (fileInput && fileInput.files && fileInput.files[0]) { label = 'Datei ausgewählt: ' + fileInput.files[0].name; } if (cameraInput && cameraInput.files && cameraInput.files[0]) { label = 'Foto ausgewählt: ' + cameraInput.files[0].name; } target.textContent = label; refreshRequiredMarkers(); } async function triggerInstantUpload() { if (!state.email || wizardSection.classList.contains('hidden')) { return; } setDraftStatus('Datei wird hochgeladen ...', false); try { await saveDraft(true); setFeedback('', false); refreshRequiredMarkers(); if (state.currentStep === state.summaryStep) { renderSummary(); } } catch (err) { const wasAuth = handleProtectedError(err, 'Upload fehlgeschlagen.', 'wizard'); if (wasAuth) { return; } const payload = (err && err.payload) || {}; const msg = payload.message || err.message || 'Upload fehlgeschlagen.'; setDraftStatus('Upload fehlgeschlagen', true); setFeedback(msg, true); } } function initUploadControls() { const controls = Array.from(document.querySelectorAll('[data-upload-key]')); controls.forEach((control) => { const fieldKey = control.getAttribute('data-upload-key') || ''; if (!fieldKey) { return; } const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]'); const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]'); if (fileInput) { fileInput.addEventListener('change', async () => { if (fileInput.files && fileInput.files[0] && cameraInput) { cameraInput.value = ''; } updateUploadSelectionText(fieldKey); if (fileInput.files && fileInput.files[0]) { await triggerInstantUpload(); } }); } if (cameraInput) { cameraInput.addEventListener('change', async () => { if (cameraInput.files && cameraInput.files[0] && fileInput) { fileInput.value = ''; } updateUploadSelectionText(fieldKey); if (cameraInput.files && cameraInput.files[0]) { await triggerInstantUpload(); } }); } updateUploadSelectionText(fieldKey); }); } async function postForm(url, formData) { const response = await fetch(url, { method: 'POST', body: formData, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' }, }); let payload = {}; try { payload = await response.json(); } catch (_err) { payload = {}; } if (!response.ok || payload.ok === false) { const err = new Error(payload.message || 'Anfrage fehlgeschlagen'); err.payload = payload; throw err; } return payload; } function isAuthFailurePayload(payload) { return Boolean(payload && (payload.auth_required || payload.auth_expired)); } function inactivityInfoText() { const inactivitySeconds = Math.max(60, Number(verificationConfig.inactivity_seconds || 3600)); const inactivityMinutes = Math.round(inactivitySeconds / 60); if (inactivityMinutes % 60 === 0) { const hours = inactivityMinutes / 60; return 'nach ' + String(hours) + ' Stunde' + (hours === 1 ? '' : 'n') + ' Inaktivität'; } return 'nach ' + String(inactivityMinutes) + ' Minuten Inaktivität'; } function handleProtectedAuthFailure(payload) { const email = normalizeEmail(state.email || startEmailInput.value || ''); stopAutosave(); wizardSection.classList.add('hidden'); disclaimerSection.classList.add('hidden'); clearWizardData(); unlockEmail(false, false); setResetActionVisible(false); if (email !== '') { startEmailInput.value = email; updateStartEmailRequiredMarker(); } const isExpired = Boolean(payload && payload.auth_expired); const defaultMessage = isExpired ? 'Ihre Sitzung ist ' + inactivityInfoText() + ' abgelaufen. Bitte erneut verifizieren.' : 'Bitte zuerst E-Mail und Sicherheitscode bestätigen.'; setFeedback((payload && payload.message) || defaultMessage, true, 'start'); setDraftStatus(isExpired ? 'Sitzung abgelaufen' : 'Verifizierung erforderlich', true); hideOtpSection(); startEmailInput.focus(); } function handleProtectedError(err, fallbackMessage, scope) { const payload = err && err.payload ? err.payload : {}; if (isAuthFailurePayload(payload)) { handleProtectedAuthFailure(payload); return true; } const msg = payload.message || err.message || fallbackMessage; setFeedback(msg, true, scope || 'wizard'); return false; } async function requestOtpCode(email, autoStart) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('website', honeypotValue()); fd.append('email', email); fd.append('auto_start', autoStart ? '1' : '0'); return postForm(appUrl('api/request-otp.php'), fd); } async function verifyOtpCode(email, code) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('website', honeypotValue()); fd.append('email', email); fd.append('otp_code', code); return postForm(appUrl('api/verify-otp.php'), fd); } function collectPayload(includeFiles) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('email', state.email); fd.append('step', String(Math.min(state.currentStep, state.totalSteps))); fd.append('website', honeypotValue()); fd.append('last_user_activity_at', String(state.lastUserActivityAt)); Array.from(applicationForm.elements).forEach((el) => { if (!el.name) { return; } if (el.disabled) { return; } if (!el.name.startsWith('form_data[')) { return; } if (el.type === 'checkbox') { fd.append(el.name, el.checked ? '1' : '0'); } else { fd.append(el.name, el.value || ''); } }); if (includeFiles) { Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => { if (input.disabled) { return; } if (input.files && input.files[0]) { fd.append(input.name, input.files[0]); } }); } return fd; } async function loadDraft(email) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('email', email); fd.append('website', honeypotValue()); fd.append('last_user_activity_at', String(state.lastUserActivityAt)); return postForm(appUrl('api/load-draft.php'), fd); } async function resetSavedData(email) { const fd = new FormData(); fd.append('csrf', boot.csrf); fd.append('email', email); fd.append('website', honeypotValue()); fd.append('last_user_activity_at', String(state.lastUserActivityAt)); return postForm(appUrl('api/reset.php'), fd); } async function saveDraft(includeFiles) { const payload = collectPayload(includeFiles); const response = await postForm(appUrl('api/save-draft.php'), payload); if (response.upload_errors && Object.keys(response.upload_errors).length > 0) { showErrors(response.upload_errors); setDraftStatus('Uploadfehler', true); setFeedback('Einige Dateien konnten nicht gespeichert werden.', true); } else { const ts = formatTimestamp(response.updated_at); setDraftStatus('Gespeichert: ' + (ts || 'gerade eben'), false); } if (response.uploads) { renderUploadInfo(response.uploads); } if (includeFiles) { Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => { input.value = ''; }); Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => { const fieldKey = control.getAttribute('data-upload-key'); if (fieldKey) { updateUploadSelectionText(fieldKey); } }); } return response; } function setSubmitting(isSubmitting) { state.isSubmitting = isSubmitting; submitBtn.classList.toggle('is-loading', isSubmitting); if (submitSpinner) { submitSpinner.classList.toggle('hidden', !isSubmitting); } if (submitLabel) { submitLabel.textContent = isSubmitting ? 'Wird gesendet ...' : 'Verbindlich absenden'; } updateProgress(); } async function submitApplication() { setSubmitting(true); setFeedback('Absenden gestartet ...', false); try { const payload = collectPayload(true); const response = await postForm(appUrl('api/submit.php'), payload); clearErrors(); setDraftStatus('Abgeschlossen', false); setFeedback('Antrag erfolgreich abgeschlossen. Vielen Dank.', false); setSubmitting(false); submitBtn.disabled = true; nextBtn.disabled = true; prevBtn.disabled = true; setResetActionVisible(false); if (submitLabel) { submitLabel.textContent = 'Abgesendet'; } return response; } catch (err) { setSubmitting(false); throw err; } } function validateStartEmail(showError) { const email = normalizeEmail(startEmailInput.value || ''); const valid = isValidEmail(email); startEmailInput.value = email; if (valid) { setStartEmailError(''); startEmailInput.setCustomValidity(''); updateStartEmailRequiredMarker(); return true; } const message = 'Bitte eine gültige E-Mail-Adresse eingeben.'; startEmailInput.setCustomValidity(message); if (showError) { setStartEmailError(message); setFeedback(message, true); } updateStartEmailRequiredMarker(); return false; } function validateOtpCode(showError) { const code = normalizeOtpCode(startOtpInput.value || ''); startOtpInput.value = code; if (/^\d{6}$/.test(code)) { setStartOtpError(''); return true; } if (showError) { setStartOtpError('Bitte einen 6-stelligen Code eingeben.'); } return false; } async function requestOtpFlow(rawEmail, options) { const opts = options || {}; const autoStart = Boolean(opts.autoStart); const email = normalizeEmail(rawEmail); if (!isValidEmail(email)) { const message = 'Bitte eine gültige E-Mail-Adresse eingeben.'; setStartEmailError(message); setFeedback(message, true, 'start'); startEmailInput.focus(); return; } startEmailInput.value = email; setStartEmailError(''); startEmailInput.setCustomValidity(''); updateStartEmailRequiredMarker(); if (startSubmitBtn) { startSubmitBtn.disabled = true; } if (resendOtpBtn) { resendOtpBtn.disabled = true; } try { const result = await requestOtpCode(email, autoStart); if (autoStart) { markAutoOtpSessionFlag(email); } if (result.auto_skipped) { showOtpSection(email, 'Bitte Sicherheitscode eingeben oder einen neuen Code anfordern.'); setFeedback('', false, 'start'); } else { showOtpSection(email, result.message || ('Sicherheitscode wurde an ' + email + ' gesendet.')); setFeedback(result.message || 'Sicherheitscode wurde versendet.', false, 'start'); setOtpCooldown(Number(result.resend_available_in || 0)); } } catch (err) { const payload = (err && err.payload) || {}; const retryAfter = Number(payload.retry_after || 0); if (retryAfter > 0) { setOtpCooldown(retryAfter); } setFeedback(payload.message || err.message || 'Code konnte nicht gesendet werden.', true, 'start'); } finally { if (startSubmitBtn) { startSubmitBtn.disabled = false; } if (resendOtpBtn && state.otpCooldownRemaining <= 0) { resendOtpBtn.disabled = false; } } } async function verifyOtpFlow() { const email = normalizeEmail(startEmailInput.value || ''); if (!isValidEmail(email)) { setStartEmailError('Bitte eine gültige E-Mail-Adresse eingeben.'); startEmailInput.focus(); return; } if (!validateOtpCode(true)) { startOtpInput.focus(); return; } if (verifyOtpBtn) { verifyOtpBtn.disabled = true; } if (resendOtpBtn) { resendOtpBtn.disabled = true; } try { const result = await verifyOtpCode(email, normalizeOtpCode(startOtpInput.value || '')); lockEmail(email); setResetActionVisible(true); hideOtpSection(); setFeedback(result.message || 'E-Mail erfolgreich bestätigt.', false, 'start'); markUserActivity(); disclaimerSection.classList.remove('hidden'); wizardSection.classList.add('hidden'); if (disclaimerReadCheckbox) { disclaimerReadCheckbox.checked = false; } updateDisclaimerAcceptanceState(); disclaimerSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (err) { const payload = (err && err.payload) || {}; const attemptsLeft = Number(payload.attempts_left); if (Number.isFinite(attemptsLeft) && attemptsLeft >= 0 && attemptsLeft < 5) { setStartOtpError((payload.message || 'Code ungültig.') + ' Verbleibende Versuche: ' + String(attemptsLeft)); } else { setStartOtpError(payload.message || err.message || 'Code konnte nicht bestätigt werden.'); } setFeedback(payload.message || err.message || 'Code konnte nicht bestätigt werden.', true, 'start'); startOtpInput.focus(); } finally { if (verifyOtpBtn) { verifyOtpBtn.disabled = false; } if (resendOtpBtn && state.otpCooldownRemaining <= 0) { resendOtpBtn.disabled = false; } } } async function openWizardForVerifiedEmail() { if (!state.email || !state.isVerified) { setFeedback('Bitte zuerst E-Mail und Sicherheitscode bestätigen.', true, 'start'); return false; } try { markUserActivity(); const result = await loadDraft(state.email); setResetActionVisible(true); if (result.already_submitted) { wizardSection.classList.add('hidden'); disclaimerSection.classList.add('hidden'); setDraftStatus('Antrag bereits abgeschlossen', false); setFeedback( result.message || 'Für diese E-Mail liegt bereits ein abgeschlossener Antrag vor.', false, 'start' ); setResetActionVisible(false); stopAutosave(); return true; } disclaimerSection.classList.add('hidden'); wizardSection.classList.remove('hidden'); fillFormData(result.data || {}); renderUploadInfo(result.uploads || {}); state.currentStep = 1; updateProgress(); scrollWizardToTop(); startAutosave(); const loadedAt = formatTimestamp(result.updated_at); if (loadedAt) { setDraftStatus('Entwurf geladen: ' + loadedAt, false); } else { setDraftStatus('Neuer Entwurf gestartet', false); } setFeedback('', false, 'wizard'); return true; } catch (err) { handleProtectedError(err, 'Laden fehlgeschlagen.', 'start'); return false; } } async function resumeVerifiedSession(email) { const normalized = normalizeEmail(email); if (!isValidEmail(normalized)) { return false; } lockEmail(normalized); setResetActionVisible(true); return openWizardForVerifiedEmail(); } async function initAutoOtpForRememberedEmail() { const rememberedEmail = normalizeEmail(getRememberedEmail()); if (rememberedEmail === '' || !isValidEmail(rememberedEmail)) { return; } startEmailInput.value = rememberedEmail; updateStartEmailRequiredMarker(); const resumed = await resumeVerifiedSession(rememberedEmail); if (resumed) { return; } if (state.isVerified) { unlockEmail(false, false); startEmailInput.value = rememberedEmail; updateStartEmailRequiredMarker(); } if (!state.isVerified && !hasAutoOtpSessionFlag(rememberedEmail)) { await requestOtpFlow(rememberedEmail, { autoStart: true }); } } startForm.addEventListener('submit', async (event) => { event.preventDefault(); if (!validateStartEmail(true)) { startEmailInput.focus(); return; } await requestOtpFlow(startEmailInput.value || '', { autoStart: false }); }); startEmailInput.addEventListener('input', () => { if (startEmailInput.readOnly) { return; } if (startEmailInput.value.trim() === '') { setStartEmailError(''); startEmailInput.setCustomValidity(''); updateStartEmailRequiredMarker(); return; } if (state.otpEmail !== '' && state.otpEmail !== normalizeEmail(startEmailInput.value || '')) { hideOtpSection(); } validateStartEmail(false); }); startEmailInput.addEventListener('blur', () => { if (startEmailInput.readOnly) { return; } if (startEmailInput.value.trim() === '') { updateStartEmailRequiredMarker(); return; } validateStartEmail(true); }); resetDataBtn.addEventListener('click', async () => { if (!state.email) { setFeedback('Keine aktive E-Mail vorhanden.', true); return; } const confirmed = window.confirm('Alle gespeicherten Daten zu dieser E-Mail endgültig löschen und neu starten?'); if (!confirmed) { return; } try { markUserActivity(); await resetSavedData(state.email); stopAutosave(); wizardSection.classList.add('hidden'); disclaimerSection.classList.add('hidden'); clearWizardData(); unlockEmail(true); setFeedback('Alle gespeicherten Daten wurden gelöscht. Sie können neu starten.', false); startEmailInput.focus(); } catch (err) { handleProtectedError(err, 'Löschen fehlgeschlagen.', 'start'); } }); prevBtn.addEventListener('click', async () => { if (state.currentStep <= 1 || state.isSubmitting) { return; } try { markUserActivity(); if (state.currentStep <= state.totalSteps) { await saveDraft(false); } state.currentStep -= 1; updateProgress(); scrollWizardToTop(); } catch (err) { handleProtectedError(err, 'Speichern fehlgeschlagen.', 'wizard'); } }); nextBtn.addEventListener('click', async () => { if (state.currentStep >= state.summaryStep || state.isSubmitting) { return; } try { markUserActivity(); await saveDraft(false); state.currentStep += 1; updateProgress(); scrollWizardToTop(); } catch (err) { handleProtectedError(err, 'Speichern fehlgeschlagen.', 'wizard'); } }); submitBtn.addEventListener('click', async () => { if (state.isSubmitting) { return; } renderSummary(); if (state.summaryMissingCount > 0) { setFeedback('Bitte zuerst alle rot markierten Pflichtfelder ausfüllen.', true); return; } try { markUserActivity(); await submitApplication(); } catch (err) { const payload = err.payload || {}; if (isAuthFailurePayload(payload)) { handleProtectedAuthFailure(payload); return; } if (payload.errors) { showErrors(payload.errors); } const msg = payload.message || err.message || 'Absenden fehlgeschlagen.'; setFeedback(msg, true); if (payload.already_submitted) { wizardSection.classList.add('hidden'); setDraftStatus('Antrag bereits abgeschlossen', false); setResetActionVisible(false); } } }); if (disclaimerReadCheckbox) { disclaimerReadCheckbox.addEventListener('change', updateDisclaimerAcceptanceState); } if (acceptDisclaimerBtn) { acceptDisclaimerBtn.addEventListener('click', async () => { if (!disclaimerReadCheckbox || !disclaimerReadCheckbox.checked) { setDisclaimerError('Bitte lesen und bestätigen Sie den Hinweis.'); return; } setDisclaimerError(''); markUserActivity(); await openWizardForVerifiedEmail(); }); } if (verifyOtpBtn) { verifyOtpBtn.addEventListener('click', async () => { await verifyOtpFlow(); }); } if (resendOtpBtn) { resendOtpBtn.addEventListener('click', async () => { await requestOtpFlow(startEmailInput.value || '', { autoStart: false }); }); } if (startOtpInput) { startOtpInput.addEventListener('input', () => { startOtpInput.value = normalizeOtpCode(startOtpInput.value || ''); setStartOtpError(''); }); startOtpInput.addEventListener('keydown', async (event) => { if (event.key !== 'Enter') { return; } event.preventDefault(); await verifyOtpFlow(); }); } ['input', 'change', 'click', 'keydown'].forEach((eventName) => { applicationForm.addEventListener(eventName, () => { if (wizardSection.classList.contains('hidden')) { return; } markUserActivity(); }); }); updateDisclaimerAcceptanceState(); disclaimerSection.classList.add('hidden'); startSection.classList.remove('hidden'); wizardSection.classList.add('hidden'); initTableFields(); initUploadControls(); initRequiredMarkerTracking(); applyFieldVisibility(); refreshRequiredMarkers(); updateStartEmailRequiredMarker(); updateProgress(); initAutoOtpForRememberedEmail(); })();