|
@@ -51,6 +51,7 @@
|
|
|
const startEmailRequiredMark = document.querySelector('#startEmailField .required-mark-field-start');
|
|
const startEmailRequiredMark = document.querySelector('#startEmailField .required-mark-field-start');
|
|
|
const fieldContainersByKey = new Map();
|
|
const fieldContainersByKey = new Map();
|
|
|
const fieldsByKey = new Map();
|
|
const fieldsByKey = new Map();
|
|
|
|
|
+ const tableFieldsByKey = new Map();
|
|
|
|
|
|
|
|
document.querySelectorAll('.field[data-field]').forEach((container) => {
|
|
document.querySelectorAll('.field[data-field]').forEach((container) => {
|
|
|
const key = String(container.getAttribute('data-field') || '').trim();
|
|
const key = String(container.getAttribute('data-field') || '').trim();
|
|
@@ -72,6 +73,17 @@
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ 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) {
|
|
function appUrl(path) {
|
|
|
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
|
return (baseUrl ? baseUrl + '/' : '/') + normalizedPath;
|
|
return (baseUrl ? baseUrl + '/' : '/') + normalizedPath;
|
|
@@ -272,6 +284,8 @@
|
|
|
|
|
|
|
|
function clearWizardData() {
|
|
function clearWizardData() {
|
|
|
applicationForm.reset();
|
|
applicationForm.reset();
|
|
|
|
|
+ populateAllTablesFromHiddenValues();
|
|
|
|
|
+ syncAllTableHiddenValues();
|
|
|
clearErrors();
|
|
clearErrors();
|
|
|
renderUploadInfo({});
|
|
renderUploadInfo({});
|
|
|
state.currentStep = 1;
|
|
state.currentStep = 1;
|
|
@@ -358,6 +372,183 @@
|
|
|
return data;
|
|
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() {
|
|
function collectCurrentFormData() {
|
|
|
const data = {};
|
|
const data = {};
|
|
|
|
|
|
|
@@ -507,6 +698,10 @@
|
|
|
return isCheckboxTrue(formData[key]);
|
|
return isCheckboxTrue(formData[key]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (type === 'table') {
|
|
|
|
|
+ return !isTableCsvEmpty(formData[key], field);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return String(formData[key] || '').trim() !== '';
|
|
return String(formData[key] || '').trim() !== '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -619,6 +814,10 @@
|
|
|
return !isCheckboxTrue(formData[key]);
|
|
return !isCheckboxTrue(formData[key]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (type === 'table') {
|
|
|
|
|
+ return isTableCsvEmpty(formData[key], field);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return String(formData[key] || '').trim() === '';
|
|
return String(formData[key] || '').trim() === '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -675,6 +874,10 @@
|
|
|
return resolveSelectLabel(field, rawValue);
|
|
return resolveSelectLabel(field, rawValue);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (type === 'table') {
|
|
|
|
|
+ return rawValue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return rawValue;
|
|
return rawValue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -759,6 +962,9 @@
|
|
|
const value = fieldDisplayValue(field, formData);
|
|
const value = fieldDisplayValue(field, formData);
|
|
|
if (value !== '') {
|
|
if (value !== '') {
|
|
|
valueEl.textContent = value;
|
|
valueEl.textContent = value;
|
|
|
|
|
+ if (String(field.type || '') === 'table') {
|
|
|
|
|
+ valueEl.classList.add('summary-item-value-multiline');
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
valueEl.textContent = String(field.type || '') === 'file' ? 'Keine Datei hochgeladen' : 'Nicht ausgefüllt';
|
|
valueEl.textContent = String(field.type || '') === 'file' ? 'Keine Datei hochgeladen' : 'Nicht ausgefüllt';
|
|
|
valueEl.classList.add('summary-item-value-empty');
|
|
valueEl.classList.add('summary-item-value-empty');
|
|
@@ -834,6 +1040,8 @@
|
|
|
field.value = data[key] || '';
|
|
field.value = data[key] || '';
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
+ populateAllTablesFromHiddenValues();
|
|
|
|
|
+ syncAllTableHiddenValues();
|
|
|
applyFieldVisibility();
|
|
applyFieldVisibility();
|
|
|
refreshRequiredMarkers();
|
|
refreshRequiredMarkers();
|
|
|
}
|
|
}
|
|
@@ -1311,6 +1519,7 @@
|
|
|
disclaimerSection.classList.remove('hidden');
|
|
disclaimerSection.classList.remove('hidden');
|
|
|
startSection.classList.add('hidden');
|
|
startSection.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
+ initTableFields();
|
|
|
initUploadControls();
|
|
initUploadControls();
|
|
|
initRequiredMarkerTracking();
|
|
initRequiredMarkerTracking();
|
|
|
applyFieldVisibility();
|
|
applyFieldVisibility();
|