|
|
@@ -1,24 +1,33 @@
|
|
|
(function () {
|
|
|
const EMAIL_STORAGE_KEY = 'ff_member_form_email_v1';
|
|
|
- const DISCLAIMER_ACCEPTED_KEY = 'ff_member_form_disclaimer_accepted_v1';
|
|
|
const boot = window.APP_BOOT || { steps: [], csrf: '', contactEmail: '' };
|
|
|
const baseUrl = String(boot.baseUrl || '').replace(/\/+$/, '');
|
|
|
+ const schemaSteps = Array.isArray(boot.steps) ? boot.steps : [];
|
|
|
|
|
|
const state = {
|
|
|
email: '',
|
|
|
currentStep: 1,
|
|
|
- totalSteps: boot.steps.length,
|
|
|
+ totalSteps: schemaSteps.length,
|
|
|
+ summaryStep: schemaSteps.length + 1,
|
|
|
autosaveId: null,
|
|
|
+ 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 resetDataBtn = document.getElementById('resetDataBtn');
|
|
|
const compactStatusBox = document.getElementById('compactStatusBox');
|
|
|
const statusEmailValue = document.getElementById('statusEmailValue');
|
|
|
@@ -32,7 +41,35 @@
|
|
|
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();
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
function appUrl(path) {
|
|
|
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
|
@@ -49,6 +86,29 @@
|
|
|
draftStatusValue.classList.toggle('error-text', Boolean(isError));
|
|
|
}
|
|
|
|
|
|
+ function setStartEmailError(text) {
|
|
|
+ if (!startEmailError) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ startEmailError.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 '';
|
|
|
@@ -94,19 +154,15 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function hasAcceptedDisclaimer() {
|
|
|
- try {
|
|
|
- return localStorage.getItem(DISCLAIMER_ACCEPTED_KEY) === '1';
|
|
|
- } catch (_err) {
|
|
|
- return false;
|
|
|
+ function updateDisclaimerAcceptanceState() {
|
|
|
+ if (!acceptDisclaimerBtn || !disclaimerReadCheckbox) {
|
|
|
+ return;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- function setDisclaimerAccepted() {
|
|
|
- try {
|
|
|
- localStorage.setItem(DISCLAIMER_ACCEPTED_KEY, '1');
|
|
|
- } catch (_err) {
|
|
|
- // ignore localStorage errors
|
|
|
+ const accepted = disclaimerReadCheckbox.checked;
|
|
|
+ acceptDisclaimerBtn.disabled = !accepted;
|
|
|
+ if (accepted) {
|
|
|
+ setDisclaimerError('');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -135,6 +191,7 @@
|
|
|
startEmailInput.value = email;
|
|
|
startEmailInput.readOnly = true;
|
|
|
startEmailInput.setAttribute('aria-readonly', 'true');
|
|
|
+ updateStartEmailRequiredMarker();
|
|
|
rememberEmail(email);
|
|
|
enterCompactStatus(email);
|
|
|
}
|
|
|
@@ -146,7 +203,9 @@
|
|
|
startEmailInput.removeAttribute('aria-readonly');
|
|
|
if (clearInput) {
|
|
|
startEmailInput.value = '';
|
|
|
+ setStartEmailError('');
|
|
|
}
|
|
|
+ updateStartEmailRequiredMarker();
|
|
|
forgetRememberedEmail();
|
|
|
leaveCompactStatus();
|
|
|
}
|
|
|
@@ -193,9 +252,16 @@
|
|
|
clearErrors();
|
|
|
renderUploadInfo({});
|
|
|
state.currentStep = 1;
|
|
|
- submitBtn.disabled = false;
|
|
|
- nextBtn.disabled = false;
|
|
|
- prevBtn.disabled = false;
|
|
|
+ state.summaryMissingCount = 0;
|
|
|
+ state.isSubmitting = false;
|
|
|
+ if (submitLabel) {
|
|
|
+ submitLabel.textContent = 'Verbindlich absenden';
|
|
|
+ }
|
|
|
+ if (submitSpinner) {
|
|
|
+ submitSpinner.classList.add('hidden');
|
|
|
+ }
|
|
|
+ submitBtn.classList.remove('is-loading');
|
|
|
+ refreshRequiredMarkers();
|
|
|
updateProgress();
|
|
|
Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => {
|
|
|
const fieldKey = control.getAttribute('data-upload-key');
|
|
|
@@ -205,22 +271,354 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ 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 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 data;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return String(formData[key] || '').trim() !== '';
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 requiredNow = isFieldRequired(field, formData);
|
|
|
+ const completed = isFieldCompleted(field, formData);
|
|
|
+ const hideMarker = !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', refreshRequiredMarkers);
|
|
|
+ el.addEventListener('change', refreshRequiredMarkers);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function isFieldRequired(field, formData) {
|
|
|
+ if (!field || typeof field !== 'object') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (field.required === true) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!field.required_if || typeof field.required_if !== 'object') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const depField = String(field.required_if.field || '').trim();
|
|
|
+ const depValue = String(field.required_if.equals || '');
|
|
|
+ if (!depField) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return String(formData[depField] || '') === depValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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]);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = 'summary-item';
|
|
|
+ const emailLabel = document.createElement('div');
|
|
|
+ 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 fields = Array.isArray(step.fields) ? step.fields.filter((field) => field && typeof field === 'object') : [];
|
|
|
+ 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 = 'summary-item';
|
|
|
+ if (required) {
|
|
|
+ row.classList.add('summary-item-required');
|
|
|
+ }
|
|
|
+ if (missing) {
|
|
|
+ row.classList.add('summary-item-missing');
|
|
|
+ }
|
|
|
+
|
|
|
+ const labelEl = document.createElement('div');
|
|
|
+ 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;
|
|
|
+ } 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() {
|
|
|
- progress.textContent = 'Schritt ' + state.currentStep + ' von ' + state.totalSteps;
|
|
|
+ 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);
|
|
|
});
|
|
|
|
|
|
- prevBtn.disabled = state.currentStep === 1;
|
|
|
- nextBtn.classList.toggle('hidden', state.currentStep === state.totalSteps);
|
|
|
- submitBtn.classList.toggle('hidden', state.currentStep !== state.totalSteps);
|
|
|
+ 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) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
if (field.type === 'checkbox') {
|
|
|
field.checked = ['1', 'on', 'true', true].includes(data[key]);
|
|
|
@@ -228,24 +626,30 @@
|
|
|
field.value = data[key] || '';
|
|
|
}
|
|
|
});
|
|
|
+ refreshRequiredMarkers();
|
|
|
}
|
|
|
|
|
|
function renderUploadInfo(uploads) {
|
|
|
+ state.uploads = uploads && typeof uploads === 'object' ? uploads : {};
|
|
|
+
|
|
|
document.querySelectorAll('[data-upload-list]').forEach((el) => {
|
|
|
el.innerHTML = '';
|
|
|
});
|
|
|
|
|
|
- Object.keys(uploads || {}).forEach((field) => {
|
|
|
+ Object.keys(state.uploads).forEach((field) => {
|
|
|
const target = document.querySelector('[data-upload-list="' + field + '"]');
|
|
|
- if (!target || !Array.isArray(uploads[field])) return;
|
|
|
+ if (!target || !Array.isArray(state.uploads[field])) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- uploads[field].forEach((item) => {
|
|
|
+ state.uploads[field].forEach((item) => {
|
|
|
const div = document.createElement('div');
|
|
|
div.className = 'upload-item';
|
|
|
div.textContent = item.original_filename + ' (' + item.uploaded_at + ')';
|
|
|
target.appendChild(div);
|
|
|
});
|
|
|
});
|
|
|
+ refreshRequiredMarkers();
|
|
|
}
|
|
|
|
|
|
function updateUploadSelectionText(fieldKey) {
|
|
|
@@ -266,6 +670,7 @@
|
|
|
}
|
|
|
|
|
|
target.textContent = label;
|
|
|
+ refreshRequiredMarkers();
|
|
|
}
|
|
|
|
|
|
async function triggerInstantUpload() {
|
|
|
@@ -277,6 +682,10 @@
|
|
|
try {
|
|
|
await saveDraft(true);
|
|
|
setFeedback('', false);
|
|
|
+ refreshRequiredMarkers();
|
|
|
+ if (state.currentStep === state.summaryStep) {
|
|
|
+ renderSummary();
|
|
|
+ }
|
|
|
} catch (err) {
|
|
|
const msg = (err.payload && err.payload.message) || err.message || 'Upload fehlgeschlagen.';
|
|
|
setDraftStatus('Upload fehlgeschlagen', true);
|
|
|
@@ -346,12 +755,16 @@
|
|
|
const fd = new FormData();
|
|
|
fd.append('csrf', boot.csrf);
|
|
|
fd.append('email', state.email);
|
|
|
- fd.append('step', String(state.currentStep));
|
|
|
+ fd.append('step', String(Math.min(state.currentStep, state.totalSteps)));
|
|
|
fd.append('website', '');
|
|
|
|
|
|
Array.from(applicationForm.elements).forEach((el) => {
|
|
|
- if (!el.name) return;
|
|
|
- if (!el.name.startsWith('form_data[')) return;
|
|
|
+ if (!el.name) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!el.name.startsWith('form_data[')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
if (el.type === 'checkbox') {
|
|
|
fd.append(el.name, el.checked ? '1' : '0');
|
|
|
@@ -419,26 +832,84 @@
|
|
|
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() {
|
|
|
- const payload = collectPayload(true);
|
|
|
- const response = await postForm(appUrl('api/submit.php'), payload);
|
|
|
- clearErrors();
|
|
|
- setDraftStatus('Abgeschlossen', false);
|
|
|
- setFeedback('Antrag erfolgreich abgeschlossen. Vielen Dank.', false);
|
|
|
- submitBtn.disabled = true;
|
|
|
- nextBtn.disabled = true;
|
|
|
- prevBtn.disabled = true;
|
|
|
- return response;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
async function startProcess(rawEmail) {
|
|
|
const email = normalizeEmail(rawEmail);
|
|
|
if (!isValidEmail(email)) {
|
|
|
- setFeedback('Bitte eine gültige E-Mail-Adresse eingeben.', true);
|
|
|
+ const message = 'Bitte eine gültige E-Mail-Adresse eingeben.';
|
|
|
+ setStartEmailError(message);
|
|
|
+ setFeedback(message, true);
|
|
|
startEmailInput.focus();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ startEmailInput.value = email;
|
|
|
+ setStartEmailError('');
|
|
|
+ startEmailInput.setCustomValidity('');
|
|
|
+ updateStartEmailRequiredMarker();
|
|
|
+
|
|
|
+ if (startSubmitBtn) {
|
|
|
+ startSubmitBtn.disabled = true;
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
const result = await loadDraft(email);
|
|
|
lockEmail(email);
|
|
|
@@ -469,14 +940,46 @@
|
|
|
} catch (err) {
|
|
|
const msg = (err.payload && err.payload.message) || err.message || 'Laden fehlgeschlagen.';
|
|
|
setFeedback(msg, true);
|
|
|
+ } finally {
|
|
|
+ if (startSubmitBtn) {
|
|
|
+ startSubmitBtn.disabled = false;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
startForm.addEventListener('submit', async (event) => {
|
|
|
event.preventDefault();
|
|
|
+ if (!validateStartEmail(true)) {
|
|
|
+ startEmailInput.focus();
|
|
|
+ return;
|
|
|
+ }
|
|
|
await startProcess(startEmailInput.value || '');
|
|
|
});
|
|
|
|
|
|
+ startEmailInput.addEventListener('input', () => {
|
|
|
+ if (startEmailInput.readOnly) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (startEmailInput.value.trim() === '') {
|
|
|
+ setStartEmailError('');
|
|
|
+ startEmailInput.setCustomValidity('');
|
|
|
+ updateStartEmailRequiredMarker();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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);
|
|
|
@@ -503,10 +1006,14 @@
|
|
|
});
|
|
|
|
|
|
prevBtn.addEventListener('click', async () => {
|
|
|
- if (state.currentStep <= 1) return;
|
|
|
+ if (state.currentStep <= 1 || state.isSubmitting) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
try {
|
|
|
- await saveDraft(false);
|
|
|
+ if (state.currentStep <= state.totalSteps) {
|
|
|
+ await saveDraft(false);
|
|
|
+ }
|
|
|
state.currentStep -= 1;
|
|
|
updateProgress();
|
|
|
} catch (err) {
|
|
|
@@ -516,7 +1023,9 @@
|
|
|
});
|
|
|
|
|
|
nextBtn.addEventListener('click', async () => {
|
|
|
- if (state.currentStep >= state.totalSteps) return;
|
|
|
+ if (state.currentStep >= state.summaryStep || state.isSubmitting) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
try {
|
|
|
await saveDraft(false);
|
|
|
@@ -529,6 +1038,16 @@
|
|
|
});
|
|
|
|
|
|
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 {
|
|
|
await submitApplication();
|
|
|
} catch (err) {
|
|
|
@@ -550,31 +1069,35 @@
|
|
|
startSection.classList.remove('hidden');
|
|
|
|
|
|
const rememberedEmail = normalizeEmail(getRememberedEmail());
|
|
|
- if (rememberedEmail !== '') {
|
|
|
- if (isValidEmail(rememberedEmail)) {
|
|
|
- startEmailInput.value = rememberedEmail;
|
|
|
- setFeedback('', false);
|
|
|
- startProcess(rememberedEmail);
|
|
|
- } else {
|
|
|
- forgetRememberedEmail();
|
|
|
- }
|
|
|
+ if (rememberedEmail !== '' && isValidEmail(rememberedEmail)) {
|
|
|
+ startEmailInput.value = rememberedEmail;
|
|
|
+ setFeedback('', false);
|
|
|
+ startProcess(rememberedEmail);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if (disclaimerReadCheckbox) {
|
|
|
+ disclaimerReadCheckbox.addEventListener('change', updateDisclaimerAcceptanceState);
|
|
|
+ }
|
|
|
+
|
|
|
if (acceptDisclaimerBtn) {
|
|
|
acceptDisclaimerBtn.addEventListener('click', () => {
|
|
|
- setDisclaimerAccepted();
|
|
|
+ if (!disclaimerReadCheckbox || !disclaimerReadCheckbox.checked) {
|
|
|
+ setDisclaimerError('Bitte lesen und bestätigen Sie den Hinweis.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setDisclaimerError('');
|
|
|
initializeAfterDisclaimer();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- if (hasAcceptedDisclaimer()) {
|
|
|
- initializeAfterDisclaimer();
|
|
|
- } else {
|
|
|
- disclaimerSection.classList.remove('hidden');
|
|
|
- startSection.classList.add('hidden');
|
|
|
- }
|
|
|
+ updateDisclaimerAcceptanceState();
|
|
|
+ disclaimerSection.classList.remove('hidden');
|
|
|
+ startSection.classList.add('hidden');
|
|
|
|
|
|
initUploadControls();
|
|
|
+ initRequiredMarkerTracking();
|
|
|
+ refreshRequiredMarkers();
|
|
|
+ updateStartEmailRequiredMarker();
|
|
|
updateProgress();
|
|
|
})();
|