|
|
@@ -20,6 +20,8 @@
|
|
|
uploads: {},
|
|
|
isSubmitting: false,
|
|
|
summaryMissingCount: 0,
|
|
|
+ summaryInvalidCount: 0,
|
|
|
+ summaryValidationErrors: {},
|
|
|
};
|
|
|
|
|
|
const disclaimerSection = document.getElementById('disclaimerSection');
|
|
|
@@ -460,6 +462,8 @@
|
|
|
renderUploadInfo({});
|
|
|
state.currentStep = 1;
|
|
|
state.summaryMissingCount = 0;
|
|
|
+ state.summaryInvalidCount = 0;
|
|
|
+ state.summaryValidationErrors = {};
|
|
|
state.isSubmitting = false;
|
|
|
if (submitLabel) {
|
|
|
submitLabel.textContent = 'Verbindlich absenden';
|
|
|
@@ -976,19 +980,92 @@
|
|
|
|
|
|
const type = String(field.type || 'text');
|
|
|
if (type === 'file') {
|
|
|
- const uploadItems = state.uploads[key];
|
|
|
- return !Array.isArray(uploadItems) || uploadItems.length === 0;
|
|
|
+ return !hasFileValue(key);
|
|
|
}
|
|
|
|
|
|
+ return isFormValueEmpty(formData[key], type, field);
|
|
|
+ }
|
|
|
+
|
|
|
+ function isFormValueEmpty(value, type, field) {
|
|
|
if (type === 'checkbox') {
|
|
|
- return !isCheckboxTrue(formData[key]);
|
|
|
+ return !isCheckboxTrue(value);
|
|
|
}
|
|
|
|
|
|
if (type === 'table') {
|
|
|
- return isTableCsvEmpty(formData[key], field);
|
|
|
+ return isTableCsvEmpty(value, field);
|
|
|
}
|
|
|
|
|
|
- return String(formData[key] || '').trim() === '';
|
|
|
+ return String(value || '').trim() === '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function validationStringLength(value) {
|
|
|
+ return Array.from(String(value || '')).length;
|
|
|
+ }
|
|
|
+
|
|
|
+ function validateFormData(formData) {
|
|
|
+ const errors = {};
|
|
|
+
|
|
|
+ fieldsByKey.forEach((field, key) => {
|
|
|
+ const type = String((field && field.type) || 'text');
|
|
|
+ if (!isFieldVisible(field, formData)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const required = isFieldRequired(field, formData);
|
|
|
+ if (type === 'file') {
|
|
|
+ if (required && !hasFileValue(key)) {
|
|
|
+ errors[key] = 'Dieses Upload-Feld ist erforderlich.';
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const value = formData[key];
|
|
|
+ const emptyValue = isFormValueEmpty(value, type, field);
|
|
|
+ if (required && emptyValue) {
|
|
|
+ errors[key] = 'Dieses Feld ist erforderlich.';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (emptyValue) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const scalarValue = String(value || '').trim();
|
|
|
+
|
|
|
+ if (type === 'email' && !isValidEmail(scalarValue)) {
|
|
|
+ errors[key] = 'Bitte eine gültige E-Mail-Adresse eingeben.';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'select' && Array.isArray(field.options)) {
|
|
|
+ const selected = scalarValue;
|
|
|
+ let validSelection = false;
|
|
|
+
|
|
|
+ field.options.forEach((option) => {
|
|
|
+ if (validSelection || !option || typeof option !== 'object') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (String(option.value || '') !== selected) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ validSelection = isOptionVisible(option, formData);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!validSelection) {
|
|
|
+ errors[key] = 'Ungültige Auswahl.';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const maxLength = Number(field.max_length);
|
|
|
+ if (Number.isInteger(maxLength) && maxLength > 0 && validationStringLength(scalarValue) > maxLength) {
|
|
|
+ errors[key] = 'Eingabe ist zu lang.';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return errors;
|
|
|
}
|
|
|
|
|
|
function resolveSelectLabel(field, value) {
|
|
|
@@ -1051,14 +1128,27 @@
|
|
|
return rawValue;
|
|
|
}
|
|
|
|
|
|
- function renderSummary() {
|
|
|
+ function renderSummary(serverErrors) {
|
|
|
if (!summarySection || !summaryContent || !summaryMissingNotice) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const formData = collectCurrentFormData();
|
|
|
+ const clientErrors = validateFormData(formData);
|
|
|
+ const mergedErrors = { ...clientErrors };
|
|
|
+ if (serverErrors && typeof serverErrors === 'object') {
|
|
|
+ Object.keys(serverErrors).forEach((key) => {
|
|
|
+ const message = String(serverErrors[key] || '').trim();
|
|
|
+ if (message !== '') {
|
|
|
+ mergedErrors[key] = message;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ state.summaryValidationErrors = mergedErrors;
|
|
|
+
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
let missingCount = 0;
|
|
|
+ let invalidCount = 0;
|
|
|
|
|
|
const introCard = document.createElement('div');
|
|
|
introCard.className = 'summary-step-card';
|
|
|
@@ -1096,9 +1186,15 @@
|
|
|
fields.forEach((field) => {
|
|
|
const required = isFieldRequired(field, formData);
|
|
|
const missing = isFieldMissing(field, formData);
|
|
|
+ const fieldKey = String(field.key || '').trim();
|
|
|
+ const fieldError = String(mergedErrors[fieldKey] || '').trim();
|
|
|
+ const invalid = fieldError !== '';
|
|
|
if (missing) {
|
|
|
missingCount += 1;
|
|
|
}
|
|
|
+ if (invalid) {
|
|
|
+ invalidCount += 1;
|
|
|
+ }
|
|
|
|
|
|
const row = document.createElement('div');
|
|
|
row.className = 'field summary-item';
|
|
|
@@ -1108,6 +1204,9 @@
|
|
|
if (missing) {
|
|
|
row.classList.add('summary-item-missing');
|
|
|
}
|
|
|
+ if (invalid) {
|
|
|
+ row.classList.add('summary-item-invalid');
|
|
|
+ }
|
|
|
|
|
|
const labelEl = document.createElement('label');
|
|
|
labelEl.className = 'summary-item-label';
|
|
|
@@ -1125,6 +1224,11 @@
|
|
|
missingBadge.className = 'summary-badge summary-badge-missing';
|
|
|
missingBadge.textContent = '! Pflichtfeld fehlt';
|
|
|
labelEl.appendChild(missingBadge);
|
|
|
+ } else if (invalid) {
|
|
|
+ const invalidBadge = document.createElement('span');
|
|
|
+ invalidBadge.className = 'summary-badge summary-badge-invalid';
|
|
|
+ invalidBadge.textContent = '! Ungültiger Wert';
|
|
|
+ labelEl.appendChild(invalidBadge);
|
|
|
}
|
|
|
|
|
|
const valueEl = document.createElement('div');
|
|
|
@@ -1145,6 +1249,12 @@
|
|
|
|
|
|
row.appendChild(labelEl);
|
|
|
row.appendChild(valueEl);
|
|
|
+ if (invalid) {
|
|
|
+ const errorEl = document.createElement('div');
|
|
|
+ errorEl.className = 'error';
|
|
|
+ errorEl.textContent = fieldError;
|
|
|
+ row.appendChild(errorEl);
|
|
|
+ }
|
|
|
card.appendChild(row);
|
|
|
});
|
|
|
|
|
|
@@ -1155,14 +1265,15 @@
|
|
|
summaryContent.appendChild(fragment);
|
|
|
|
|
|
state.summaryMissingCount = missingCount;
|
|
|
- if (missingCount > 0) {
|
|
|
+ state.summaryInvalidCount = invalidCount;
|
|
|
+ if (invalidCount > 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.';
|
|
|
+ summaryMissingNotice.textContent = '! Es gibt noch ' + String(invalidCount) + ' ungültige oder fehlende Felder. 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.';
|
|
|
+ summaryMissingNotice.textContent = 'Alle Pflichtfelder sind ausgefüllt und die Angaben sind gültig.';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1963,9 +2074,11 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ clearErrors();
|
|
|
renderSummary();
|
|
|
- if (state.summaryMissingCount > 0) {
|
|
|
- setFeedback('Bitte zuerst alle rot markierten Pflichtfelder ausfüllen.', true);
|
|
|
+ if (state.summaryInvalidCount > 0) {
|
|
|
+ showErrors(state.summaryValidationErrors);
|
|
|
+ setFeedback('Bitte zuerst alle rot markierten Pflichtfelder und ungültigen Angaben korrigieren.', true);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -1980,6 +2093,7 @@
|
|
|
}
|
|
|
if (payload.errors) {
|
|
|
showErrors(payload.errors);
|
|
|
+ renderSummary(payload.errors);
|
|
|
}
|
|
|
const msg = payload.message || err.message || 'Absenden fehlgeschlagen.';
|
|
|
setFeedback(msg, true);
|