form.js 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636
  1. (function () {
  2. const EMAIL_STORAGE_KEY = 'ff_member_form_email_v1';
  3. const boot = window.APP_BOOT || { steps: [], csrf: '', contactEmail: '' };
  4. const baseUrl = String(boot.baseUrl || '').replace(/\/+$/, '');
  5. const schemaSteps = Array.isArray(boot.steps) ? boot.steps : [];
  6. const state = {
  7. email: '',
  8. currentStep: 1,
  9. totalSteps: schemaSteps.length,
  10. summaryStep: schemaSteps.length + 1,
  11. autosaveId: null,
  12. uploads: {},
  13. isSubmitting: false,
  14. summaryMissingCount: 0,
  15. };
  16. const disclaimerSection = document.getElementById('disclaimerSection');
  17. const disclaimerReadCheckbox = document.getElementById('disclaimerReadCheckbox');
  18. const disclaimerReadError = document.getElementById('disclaimerReadError');
  19. const acceptDisclaimerBtn = document.getElementById('acceptDisclaimerBtn');
  20. const startSection = document.getElementById('startSection');
  21. const startForm = document.getElementById('startForm');
  22. const startIntroText = document.getElementById('startIntroText');
  23. const startEmailField = document.getElementById('startEmailField');
  24. const startActions = document.getElementById('startActions');
  25. const startEmailInput = document.getElementById('startEmail');
  26. const startEmailError = document.getElementById('startEmailError');
  27. const startSubmitBtn = document.getElementById('startSubmitBtn');
  28. const resetDataBtn = document.getElementById('resetDataBtn');
  29. const compactStatusBox = document.getElementById('compactStatusBox');
  30. const statusEmailValue = document.getElementById('statusEmailValue');
  31. const draftStatusValue = document.getElementById('draftStatusValue');
  32. const startFeedbackMessage = document.getElementById('startFeedbackMessage');
  33. const feedbackMessage = document.getElementById('feedbackMessage');
  34. const wizardSection = document.getElementById('wizardSection');
  35. const applicationForm = document.getElementById('applicationForm');
  36. const applicationEmail = document.getElementById('applicationEmail');
  37. const progress = document.getElementById('progress');
  38. const prevBtn = document.getElementById('prevBtn');
  39. const nextBtn = document.getElementById('nextBtn');
  40. const submitBtn = document.getElementById('submitBtn');
  41. const submitSpinner = document.getElementById('submitSpinner');
  42. const submitLabel = submitBtn ? submitBtn.querySelector('[data-submit-label]') : null;
  43. const summarySection = document.getElementById('summarySection');
  44. const summaryContent = document.getElementById('summaryContent');
  45. const summaryMissingNotice = document.getElementById('summaryMissingNotice');
  46. const stepElements = Array.from(document.querySelectorAll('.step'));
  47. const startEmailRequiredMark = document.querySelector('#startEmailField .required-mark-field-start');
  48. const fieldContainersByKey = new Map();
  49. const fieldsByKey = new Map();
  50. const tableFieldsByKey = new Map();
  51. document.querySelectorAll('.field[data-field]').forEach((container) => {
  52. const key = String(container.getAttribute('data-field') || '').trim();
  53. if (key !== '') {
  54. fieldContainersByKey.set(key, container);
  55. }
  56. });
  57. schemaSteps.forEach((step) => {
  58. const fields = Array.isArray(step.fields) ? step.fields : [];
  59. fields.forEach((field) => {
  60. if (!field || typeof field !== 'object') {
  61. return;
  62. }
  63. const key = String(field.key || '').trim();
  64. if (key !== '') {
  65. fieldsByKey.set(key, field);
  66. }
  67. });
  68. });
  69. document.querySelectorAll('[data-table-field="1"][data-table-key]').forEach((tableEl) => {
  70. const key = String(tableEl.getAttribute('data-table-key') || '').trim();
  71. if (key === '') {
  72. return;
  73. }
  74. const rows = Math.max(1, Number(tableEl.getAttribute('data-table-rows') || 0));
  75. const headers = Array.from(tableEl.querySelectorAll('thead th')).map((th) => String(th.textContent || '').trim());
  76. tableFieldsByKey.set(key, { key, tableEl, rows, headers });
  77. });
  78. function appUrl(path) {
  79. const normalizedPath = String(path || '').replace(/^\/+/, '');
  80. return (baseUrl ? baseUrl + '/' : '/') + normalizedPath;
  81. }
  82. function scrollWizardToTop() {
  83. if (!wizardSection || wizardSection.classList.contains('hidden')) {
  84. return;
  85. }
  86. wizardSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
  87. }
  88. function setFeedbackText(target, text, isError) {
  89. if (!target) {
  90. return;
  91. }
  92. target.textContent = text || '';
  93. target.classList.toggle('error-text', Boolean(isError));
  94. }
  95. function setFeedback(text, isError, scope) {
  96. const targetScope = scope || (wizardSection && !wizardSection.classList.contains('hidden') ? 'wizard' : 'start');
  97. if (targetScope === 'wizard') {
  98. setFeedbackText(feedbackMessage, text, isError);
  99. setFeedbackText(startFeedbackMessage, '', false);
  100. return;
  101. }
  102. if (targetScope === 'start') {
  103. setFeedbackText(startFeedbackMessage, text, isError);
  104. setFeedbackText(feedbackMessage, '', false);
  105. return;
  106. }
  107. setFeedbackText(feedbackMessage, text, isError);
  108. setFeedbackText(startFeedbackMessage, text, isError);
  109. }
  110. function setDraftStatus(text, isError) {
  111. draftStatusValue.textContent = text || '';
  112. draftStatusValue.classList.toggle('error-text', Boolean(isError));
  113. }
  114. function setStartEmailError(text) {
  115. if (!startEmailError) {
  116. return;
  117. }
  118. startEmailError.textContent = text || '';
  119. }
  120. function setDisclaimerError(text) {
  121. if (!disclaimerReadError) {
  122. return;
  123. }
  124. disclaimerReadError.textContent = text || '';
  125. }
  126. function updateStartEmailRequiredMarker() {
  127. if (!startEmailRequiredMark) {
  128. return;
  129. }
  130. const email = normalizeEmail(startEmailInput.value || '');
  131. startEmailRequiredMark.classList.toggle('hidden', isValidEmail(email));
  132. }
  133. function formatTimestamp(isoDate) {
  134. if (!isoDate) {
  135. return '';
  136. }
  137. const date = new Date(isoDate);
  138. if (Number.isNaN(date.getTime())) {
  139. return String(isoDate);
  140. }
  141. return date.toLocaleString('de-DE');
  142. }
  143. function normalizeEmail(email) {
  144. return (email || '').trim().toLowerCase();
  145. }
  146. function isValidEmail(email) {
  147. return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  148. }
  149. function rememberEmail(email) {
  150. try {
  151. localStorage.setItem(EMAIL_STORAGE_KEY, email);
  152. } catch (_err) {
  153. // ignore localStorage errors
  154. }
  155. }
  156. function getRememberedEmail() {
  157. try {
  158. return localStorage.getItem(EMAIL_STORAGE_KEY) || '';
  159. } catch (_err) {
  160. return '';
  161. }
  162. }
  163. function forgetRememberedEmail() {
  164. try {
  165. localStorage.removeItem(EMAIL_STORAGE_KEY);
  166. } catch (_err) {
  167. // ignore localStorage errors
  168. }
  169. }
  170. function updateDisclaimerAcceptanceState() {
  171. if (!acceptDisclaimerBtn || !disclaimerReadCheckbox) {
  172. return;
  173. }
  174. const accepted = disclaimerReadCheckbox.checked;
  175. acceptDisclaimerBtn.disabled = !accepted;
  176. if (accepted) {
  177. setDisclaimerError('');
  178. }
  179. }
  180. function setResetActionVisible(isVisible) {
  181. if (!resetDataBtn) {
  182. return;
  183. }
  184. resetDataBtn.classList.toggle('hidden', !isVisible);
  185. resetDataBtn.disabled = !isVisible;
  186. }
  187. function enterCompactStatus(email) {
  188. statusEmailValue.textContent = email;
  189. startSection.classList.add('compact-mode');
  190. compactStatusBox.classList.remove('hidden');
  191. startIntroText.classList.add('hidden');
  192. startEmailField.classList.add('hidden');
  193. startActions.classList.add('hidden');
  194. }
  195. function leaveCompactStatus() {
  196. startSection.classList.remove('compact-mode');
  197. compactStatusBox.classList.add('hidden');
  198. startIntroText.classList.remove('hidden');
  199. startEmailField.classList.remove('hidden');
  200. startActions.classList.remove('hidden');
  201. statusEmailValue.textContent = '-';
  202. setDraftStatus('Noch nicht gespeichert', false);
  203. setResetActionVisible(true);
  204. }
  205. function lockEmail(email) {
  206. state.email = email;
  207. applicationEmail.value = email;
  208. startEmailInput.value = email;
  209. startEmailInput.readOnly = true;
  210. startEmailInput.setAttribute('aria-readonly', 'true');
  211. updateStartEmailRequiredMarker();
  212. rememberEmail(email);
  213. enterCompactStatus(email);
  214. }
  215. function unlockEmail(clearInput) {
  216. state.email = '';
  217. applicationEmail.value = '';
  218. startEmailInput.readOnly = false;
  219. startEmailInput.removeAttribute('aria-readonly');
  220. if (clearInput) {
  221. startEmailInput.value = '';
  222. setStartEmailError('');
  223. }
  224. updateStartEmailRequiredMarker();
  225. forgetRememberedEmail();
  226. leaveCompactStatus();
  227. }
  228. function stopAutosave() {
  229. if (state.autosaveId) {
  230. clearInterval(state.autosaveId);
  231. state.autosaveId = null;
  232. }
  233. }
  234. function startAutosave() {
  235. stopAutosave();
  236. state.autosaveId = setInterval(async () => {
  237. if (!state.email || wizardSection.classList.contains('hidden')) {
  238. return;
  239. }
  240. try {
  241. await saveDraft(false);
  242. } catch (_err) {
  243. // visible on next manual action
  244. }
  245. }, 15000);
  246. }
  247. function clearErrors() {
  248. document.querySelectorAll('[data-error-for]').forEach((el) => {
  249. el.textContent = '';
  250. });
  251. }
  252. function showErrors(errors) {
  253. clearErrors();
  254. Object.keys(errors || {}).forEach((key) => {
  255. const el = document.querySelector('[data-error-for="' + key + '"]');
  256. if (el) {
  257. el.textContent = errors[key];
  258. }
  259. });
  260. }
  261. function clearWizardData() {
  262. applicationForm.reset();
  263. populateAllTablesFromHiddenValues();
  264. syncAllTableHiddenValues();
  265. clearErrors();
  266. renderUploadInfo({});
  267. state.currentStep = 1;
  268. state.summaryMissingCount = 0;
  269. state.isSubmitting = false;
  270. if (submitLabel) {
  271. submitLabel.textContent = 'Verbindlich absenden';
  272. }
  273. if (submitSpinner) {
  274. submitSpinner.classList.add('hidden');
  275. }
  276. submitBtn.classList.remove('is-loading');
  277. applyFieldVisibility();
  278. refreshRequiredMarkers();
  279. updateProgress();
  280. Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => {
  281. const fieldKey = control.getAttribute('data-upload-key');
  282. if (fieldKey) {
  283. updateUploadSelectionText(fieldKey);
  284. }
  285. });
  286. }
  287. function parseFieldKey(name) {
  288. const match = /^form_data\[(.+)\]$/.exec(String(name || ''));
  289. return match ? match[1] : '';
  290. }
  291. function isCheckboxTrue(value) {
  292. return ['1', 'on', 'true', true, 1].includes(value);
  293. }
  294. function parseBirthdateParts(value) {
  295. const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim());
  296. if (!match) {
  297. return null;
  298. }
  299. const year = Number(match[1]);
  300. const month = Number(match[2]);
  301. const day = Number(match[3]);
  302. const candidate = new Date(Date.UTC(year, month - 1, day));
  303. if (
  304. Number.isNaN(candidate.getTime()) ||
  305. candidate.getUTCFullYear() !== year ||
  306. candidate.getUTCMonth() !== month - 1 ||
  307. candidate.getUTCDate() !== day
  308. ) {
  309. return null;
  310. }
  311. return { year, month, day };
  312. }
  313. function deriveAdultFlagFromBirthdate(birthdateValue) {
  314. const parts = parseBirthdateParts(birthdateValue);
  315. if (!parts) {
  316. return '';
  317. }
  318. const now = new Date();
  319. const yearNow = now.getFullYear();
  320. const monthNow = now.getMonth() + 1;
  321. const dayNow = now.getDate();
  322. const isFutureBirthdate =
  323. parts.year > yearNow ||
  324. (parts.year === yearNow && (parts.month > monthNow || (parts.month === monthNow && parts.day > dayNow)));
  325. if (isFutureBirthdate) {
  326. return '';
  327. }
  328. let age = yearNow - parts.year;
  329. const hadBirthdayThisYear = monthNow > parts.month || (monthNow === parts.month && dayNow >= parts.day);
  330. if (!hadBirthdayThisYear) {
  331. age -= 1;
  332. }
  333. return age >= 18 ? '1' : '0';
  334. }
  335. function addComputedAgeFlags(data) {
  336. const adultFlag = deriveAdultFlagFromBirthdate(data.geburtsdatum || '');
  337. data.is_minor = adultFlag === '' ? '' : adultFlag === '1' ? '0' : '1';
  338. return data;
  339. }
  340. function csvEscapeValue(value) {
  341. const normalized = String(value || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
  342. if (normalized.includes('"') || normalized.includes(',') || normalized.includes('\n')) {
  343. return '"' + normalized.replace(/"/g, '""') + '"';
  344. }
  345. return normalized;
  346. }
  347. function parseCsvLine(line) {
  348. const cells = [];
  349. let current = '';
  350. let inQuotes = false;
  351. for (let i = 0; i < line.length; i += 1) {
  352. const char = line[i];
  353. if (char === '"') {
  354. if (inQuotes && line[i + 1] === '"') {
  355. current += '"';
  356. i += 1;
  357. continue;
  358. }
  359. inQuotes = !inQuotes;
  360. continue;
  361. }
  362. if (char === ',' && !inQuotes) {
  363. cells.push(current);
  364. current = '';
  365. continue;
  366. }
  367. current += char;
  368. }
  369. cells.push(current);
  370. return cells;
  371. }
  372. function csvHeaderMatches(headers, row) {
  373. if (!Array.isArray(headers) || headers.length === 0 || !Array.isArray(row)) {
  374. return false;
  375. }
  376. if (row.length !== headers.length) {
  377. return false;
  378. }
  379. return headers.every((header, index) => String(header || '').trim().toLowerCase() === String(row[index] || '').trim().toLowerCase());
  380. }
  381. function isTableCsvEmpty(value, field) {
  382. const raw = String(value || '').trim();
  383. if (raw === '') {
  384. return true;
  385. }
  386. const lines = raw
  387. .split(/\r?\n/)
  388. .map((line) => line.trim())
  389. .filter((line) => line !== '');
  390. if (lines.length === 0) {
  391. return true;
  392. }
  393. const parsedRows = lines.map((line) => parseCsvLine(line));
  394. const headers = Array.isArray(field && field.columns)
  395. ? field.columns
  396. .filter((column) => column && typeof column === 'object')
  397. .map((column) => String(column.label || '').trim())
  398. .filter((label) => label !== '')
  399. : [];
  400. const dataRows = csvHeaderMatches(headers, parsedRows[0]) ? parsedRows.slice(1) : parsedRows;
  401. if (dataRows.length === 0) {
  402. return true;
  403. }
  404. return !dataRows.some((row) => row.some((cell) => String(cell || '').trim() !== ''));
  405. }
  406. function syncTableHiddenValue(tableKey) {
  407. const tableMeta = tableFieldsByKey.get(tableKey);
  408. if (!tableMeta) {
  409. return;
  410. }
  411. const hiddenField = applicationForm.querySelector('[name="form_data[' + tableKey + ']"]');
  412. if (!hiddenField) {
  413. return;
  414. }
  415. const dataRows = [];
  416. for (let rowIndex = 0; rowIndex < tableMeta.rows; rowIndex += 1) {
  417. const rowValues = [];
  418. for (let colIndex = 0; colIndex < tableMeta.headers.length; colIndex += 1) {
  419. const input = tableMeta.tableEl.querySelector('[data-table-cell="1"][data-row-index="' + rowIndex + '"][data-col-index="' + colIndex + '"]');
  420. rowValues.push(String((input && input.value) || '').trim());
  421. }
  422. dataRows.push(rowValues);
  423. }
  424. const hasAnyValue = dataRows.some((row) => row.some((cell) => cell !== ''));
  425. if (!hasAnyValue) {
  426. hiddenField.value = '';
  427. return;
  428. }
  429. const lines = [];
  430. lines.push(tableMeta.headers.map((header) => csvEscapeValue(header)).join(','));
  431. dataRows.forEach((row) => {
  432. lines.push(row.map((cell) => csvEscapeValue(cell)).join(','));
  433. });
  434. hiddenField.value = lines.join('\n');
  435. }
  436. function syncAllTableHiddenValues() {
  437. tableFieldsByKey.forEach((tableMeta) => {
  438. syncTableHiddenValue(tableMeta.key);
  439. });
  440. }
  441. function populateTableFromHiddenValue(tableKey) {
  442. const tableMeta = tableFieldsByKey.get(tableKey);
  443. if (!tableMeta) {
  444. return;
  445. }
  446. const hiddenField = applicationForm.querySelector('[name="form_data[' + tableKey + ']"]');
  447. if (!hiddenField) {
  448. return;
  449. }
  450. const raw = String(hiddenField.value || '').trim();
  451. const lines = raw === '' ? [] : raw.split(/\r?\n/);
  452. const parsedRows = lines.map((line) => parseCsvLine(line));
  453. const dataRows = parsedRows.length > 0 && csvHeaderMatches(tableMeta.headers, parsedRows[0]) ? parsedRows.slice(1) : parsedRows;
  454. for (let rowIndex = 0; rowIndex < tableMeta.rows; rowIndex += 1) {
  455. const sourceRow = Array.isArray(dataRows[rowIndex]) ? dataRows[rowIndex] : [];
  456. for (let colIndex = 0; colIndex < tableMeta.headers.length; colIndex += 1) {
  457. const input = tableMeta.tableEl.querySelector('[data-table-cell="1"][data-row-index="' + rowIndex + '"][data-col-index="' + colIndex + '"]');
  458. if (!input) {
  459. continue;
  460. }
  461. input.value = String(sourceRow[colIndex] || '');
  462. }
  463. }
  464. }
  465. function populateAllTablesFromHiddenValues() {
  466. tableFieldsByKey.forEach((tableMeta) => {
  467. populateTableFromHiddenValue(tableMeta.key);
  468. });
  469. }
  470. function initTableFields() {
  471. tableFieldsByKey.forEach((tableMeta) => {
  472. const cells = Array.from(tableMeta.tableEl.querySelectorAll('[data-table-cell="1"]'));
  473. cells.forEach((cell) => {
  474. const syncAndRefresh = () => {
  475. syncTableHiddenValue(tableMeta.key);
  476. applyFieldVisibility();
  477. refreshRequiredMarkers();
  478. if (state.currentStep === state.summaryStep) {
  479. renderSummary();
  480. }
  481. };
  482. cell.addEventListener('input', syncAndRefresh);
  483. cell.addEventListener('change', syncAndRefresh);
  484. });
  485. });
  486. populateAllTablesFromHiddenValues();
  487. syncAllTableHiddenValues();
  488. }
  489. function collectCurrentFormData() {
  490. const data = {};
  491. Array.from(applicationForm.elements).forEach((el) => {
  492. if (!el.name) {
  493. return;
  494. }
  495. const key = parseFieldKey(el.name);
  496. if (!key) {
  497. return;
  498. }
  499. if (el.type === 'checkbox') {
  500. data[key] = el.checked ? '1' : '0';
  501. } else {
  502. data[key] = el.value || '';
  503. }
  504. });
  505. return addComputedAgeFlags(data);
  506. }
  507. function evaluateFieldRule(rule, formData) {
  508. if (!rule || typeof rule !== 'object') {
  509. return false;
  510. }
  511. const depField = String(rule.field || '').trim();
  512. const depValue = String(rule.equals || '');
  513. if (depField === '') {
  514. return false;
  515. }
  516. return String(formData[depField] || '') === depValue;
  517. }
  518. function isFieldVisible(field, formData) {
  519. if (!field || typeof field !== 'object') {
  520. return true;
  521. }
  522. if (!field.visible_if || typeof field.visible_if !== 'object') {
  523. return true;
  524. }
  525. return evaluateFieldRule(field.visible_if, formData);
  526. }
  527. function isOptionVisible(option, formData) {
  528. if (!option || typeof option !== 'object') {
  529. return true;
  530. }
  531. if (option.visible_if && typeof option.visible_if === 'object' && !evaluateFieldRule(option.visible_if, formData)) {
  532. return false;
  533. }
  534. if (option.hidden_if && typeof option.hidden_if === 'object' && evaluateFieldRule(option.hidden_if, formData)) {
  535. return false;
  536. }
  537. return true;
  538. }
  539. function applySelectOptionVisibility(formData) {
  540. let changedSelection = false;
  541. fieldsByKey.forEach((field, key) => {
  542. if (!field || typeof field !== 'object') {
  543. return;
  544. }
  545. if (String(field.type || '') !== 'select' || !Array.isArray(field.options)) {
  546. return;
  547. }
  548. const select = applicationForm.querySelector('[name="form_data[' + key + ']"]');
  549. if (!select) {
  550. return;
  551. }
  552. const optionRules = new Map();
  553. field.options.forEach((option) => {
  554. if (!option || typeof option !== 'object') {
  555. return;
  556. }
  557. optionRules.set(String(option.value || ''), option);
  558. });
  559. let selectedHidden = false;
  560. Array.from(select.options).forEach((optionEl) => {
  561. const optionValue = String(optionEl.value || '');
  562. if (optionValue === '') {
  563. optionEl.hidden = false;
  564. optionEl.disabled = false;
  565. return;
  566. }
  567. const optionRule = optionRules.get(optionValue);
  568. const visible = isOptionVisible(optionRule, formData);
  569. optionEl.hidden = !visible;
  570. optionEl.disabled = !visible;
  571. if (!visible && optionEl.selected) {
  572. selectedHidden = true;
  573. }
  574. });
  575. if (selectedHidden) {
  576. select.value = '';
  577. changedSelection = true;
  578. }
  579. });
  580. return changedSelection;
  581. }
  582. function hasFileValue(fieldKey) {
  583. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  584. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  585. const hasLocalSelection =
  586. (fileInput && fileInput.files && fileInput.files.length > 0) ||
  587. (cameraInput && cameraInput.files && cameraInput.files.length > 0);
  588. if (hasLocalSelection) {
  589. return true;
  590. }
  591. const storedUploads = state.uploads[fieldKey];
  592. return Array.isArray(storedUploads) && storedUploads.length > 0;
  593. }
  594. function isFieldCompleted(field, formData) {
  595. const key = String(field.key || '').trim();
  596. const type = String(field.type || 'text');
  597. if (key === '') {
  598. return false;
  599. }
  600. if (type === 'file') {
  601. return hasFileValue(key);
  602. }
  603. if (type === 'checkbox') {
  604. return isCheckboxTrue(formData[key]);
  605. }
  606. if (type === 'table') {
  607. return !isTableCsvEmpty(formData[key], field);
  608. }
  609. return String(formData[key] || '').trim() !== '';
  610. }
  611. function applyFieldVisibility() {
  612. const formData = collectCurrentFormData();
  613. const changedSelection = applySelectOptionVisibility(formData);
  614. const effectiveFormData = changedSelection ? collectCurrentFormData() : formData;
  615. fieldsByKey.forEach((field, key) => {
  616. const container = fieldContainersByKey.get(key);
  617. if (!container) {
  618. return;
  619. }
  620. const visible = isFieldVisible(field, effectiveFormData);
  621. container.classList.toggle('field-hidden-by-rule', !visible);
  622. const controlElements = container.querySelectorAll('input, select, textarea');
  623. controlElements.forEach((el) => {
  624. el.disabled = !visible;
  625. });
  626. if (!visible) {
  627. const errorEl = container.querySelector('[data-error-for="' + key + '"]');
  628. if (errorEl) {
  629. errorEl.textContent = '';
  630. }
  631. }
  632. });
  633. }
  634. function refreshRequiredMarkers() {
  635. const formData = collectCurrentFormData();
  636. fieldsByKey.forEach((field, key) => {
  637. const container = fieldContainersByKey.get(key);
  638. if (!container) {
  639. return;
  640. }
  641. const markers = container.querySelectorAll('.required-mark-field');
  642. if (!markers.length) {
  643. return;
  644. }
  645. const visibleNow = isFieldVisible(field, formData);
  646. const requiredNow = isFieldRequired(field, formData);
  647. const completed = isFieldCompleted(field, formData);
  648. const hideMarker = !visibleNow || !requiredNow || completed;
  649. markers.forEach((marker) => {
  650. marker.classList.toggle('hidden', hideMarker);
  651. });
  652. });
  653. }
  654. function initRequiredMarkerTracking() {
  655. Array.from(applicationForm.elements).forEach((el) => {
  656. if (!el.name || !el.name.startsWith('form_data[')) {
  657. return;
  658. }
  659. el.addEventListener('blur', () => {
  660. applyFieldVisibility();
  661. refreshRequiredMarkers();
  662. });
  663. el.addEventListener('change', () => {
  664. applyFieldVisibility();
  665. refreshRequiredMarkers();
  666. });
  667. });
  668. }
  669. function isFieldRequired(field, formData) {
  670. if (!field || typeof field !== 'object') {
  671. return false;
  672. }
  673. if (!isFieldVisible(field, formData)) {
  674. return false;
  675. }
  676. if (field.required === true) {
  677. return true;
  678. }
  679. if (!field.required_if || typeof field.required_if !== 'object') {
  680. return false;
  681. }
  682. return evaluateFieldRule(field.required_if, formData);
  683. }
  684. function isFieldMissing(field, formData) {
  685. if (!isFieldRequired(field, formData)) {
  686. return false;
  687. }
  688. const key = String(field.key || '').trim();
  689. if (!key) {
  690. return false;
  691. }
  692. const type = String(field.type || 'text');
  693. if (type === 'file') {
  694. const uploadItems = state.uploads[key];
  695. return !Array.isArray(uploadItems) || uploadItems.length === 0;
  696. }
  697. if (type === 'checkbox') {
  698. return !isCheckboxTrue(formData[key]);
  699. }
  700. if (type === 'table') {
  701. return isTableCsvEmpty(formData[key], field);
  702. }
  703. return String(formData[key] || '').trim() === '';
  704. }
  705. function resolveSelectLabel(field, value) {
  706. if (!Array.isArray(field.options)) {
  707. return value;
  708. }
  709. const matched = field.options.find((option) => {
  710. return option && String(option.value || '') === String(value);
  711. });
  712. if (!matched) {
  713. return value;
  714. }
  715. return String(matched.label || value);
  716. }
  717. function fieldDisplayValue(field, formData) {
  718. const key = String(field.key || '').trim();
  719. const type = String(field.type || 'text');
  720. if (!key) {
  721. return '';
  722. }
  723. if (type === 'file') {
  724. const uploadItems = state.uploads[key];
  725. if (!Array.isArray(uploadItems) || uploadItems.length === 0) {
  726. return '';
  727. }
  728. return uploadItems
  729. .map((item) => {
  730. if (!item || typeof item !== 'object') {
  731. return '';
  732. }
  733. return String(item.original_filename || item.stored_filename || '').trim();
  734. })
  735. .filter(Boolean)
  736. .join(', ');
  737. }
  738. if (type === 'checkbox') {
  739. return isCheckboxTrue(formData[key]) ? 'Ja' : 'Nein';
  740. }
  741. const rawValue = String(formData[key] || '').trim();
  742. if (rawValue === '') {
  743. return '';
  744. }
  745. if (type === 'select') {
  746. return resolveSelectLabel(field, rawValue);
  747. }
  748. if (type === 'table') {
  749. return rawValue;
  750. }
  751. return rawValue;
  752. }
  753. function renderSummary() {
  754. if (!summarySection || !summaryContent || !summaryMissingNotice) {
  755. return;
  756. }
  757. const formData = collectCurrentFormData();
  758. const fragment = document.createDocumentFragment();
  759. let missingCount = 0;
  760. const introCard = document.createElement('div');
  761. introCard.className = 'summary-step-card';
  762. const introTitle = document.createElement('h4');
  763. introTitle.textContent = 'Startdaten';
  764. introCard.appendChild(introTitle);
  765. const emailRow = document.createElement('div');
  766. emailRow.className = 'summary-item';
  767. const emailLabel = document.createElement('div');
  768. emailLabel.className = 'summary-item-label';
  769. emailLabel.textContent = 'E-Mail';
  770. const emailValue = document.createElement('div');
  771. emailValue.className = 'summary-item-value';
  772. emailValue.textContent = state.email || 'Nicht gesetzt';
  773. emailRow.appendChild(emailLabel);
  774. emailRow.appendChild(emailValue);
  775. introCard.appendChild(emailRow);
  776. fragment.appendChild(introCard);
  777. schemaSteps.forEach((step, stepIndex) => {
  778. const allFields = Array.isArray(step.fields) ? step.fields.filter((field) => field && typeof field === 'object') : [];
  779. const fields = allFields.filter((field) => isFieldVisible(field, formData));
  780. if (fields.length === 0) {
  781. return;
  782. }
  783. const card = document.createElement('div');
  784. card.className = 'summary-step-card';
  785. const title = document.createElement('h4');
  786. title.textContent = 'Schritt ' + String(stepIndex + 1) + ': ' + String(step.title || '');
  787. card.appendChild(title);
  788. fields.forEach((field) => {
  789. const required = isFieldRequired(field, formData);
  790. const missing = isFieldMissing(field, formData);
  791. if (missing) {
  792. missingCount += 1;
  793. }
  794. const row = document.createElement('div');
  795. row.className = 'summary-item';
  796. if (required) {
  797. row.classList.add('summary-item-required');
  798. }
  799. if (missing) {
  800. row.classList.add('summary-item-missing');
  801. }
  802. const labelEl = document.createElement('div');
  803. labelEl.className = 'summary-item-label';
  804. labelEl.textContent = String(field.label || field.key || 'Feld');
  805. if (required) {
  806. const requiredBadge = document.createElement('span');
  807. requiredBadge.className = 'summary-badge summary-badge-required';
  808. requiredBadge.textContent = 'Pflichtfeld';
  809. labelEl.appendChild(requiredBadge);
  810. }
  811. if (missing) {
  812. const missingBadge = document.createElement('span');
  813. missingBadge.className = 'summary-badge summary-badge-missing';
  814. missingBadge.textContent = '! Pflichtfeld fehlt';
  815. labelEl.appendChild(missingBadge);
  816. }
  817. const valueEl = document.createElement('div');
  818. valueEl.className = 'summary-item-value';
  819. const value = fieldDisplayValue(field, formData);
  820. if (value !== '') {
  821. valueEl.textContent = value;
  822. if (String(field.type || '') === 'table') {
  823. valueEl.classList.add('summary-item-value-multiline');
  824. }
  825. } else {
  826. valueEl.textContent = String(field.type || '') === 'file' ? 'Keine Datei hochgeladen' : 'Nicht ausgefüllt';
  827. valueEl.classList.add('summary-item-value-empty');
  828. if (missing) {
  829. valueEl.classList.add('summary-item-value-missing');
  830. }
  831. }
  832. row.appendChild(labelEl);
  833. row.appendChild(valueEl);
  834. card.appendChild(row);
  835. });
  836. fragment.appendChild(card);
  837. });
  838. summaryContent.innerHTML = '';
  839. summaryContent.appendChild(fragment);
  840. state.summaryMissingCount = missingCount;
  841. if (missingCount > 0) {
  842. summaryMissingNotice.classList.remove('hidden');
  843. summaryMissingNotice.classList.add('summary-missing-warning');
  844. summaryMissingNotice.textContent = '! Es fehlen noch ' + String(missingCount) + ' Pflichtfelder. Bitte korrigieren Sie die rot markierten Einträge.';
  845. } else {
  846. summaryMissingNotice.classList.remove('hidden');
  847. summaryMissingNotice.classList.remove('summary-missing-warning');
  848. summaryMissingNotice.textContent = 'Alle Pflichtfelder sind ausgefüllt.';
  849. }
  850. }
  851. function updateProgress() {
  852. const isSummary = state.currentStep === state.summaryStep;
  853. if (isSummary) {
  854. progress.textContent = 'Zusammenfassung (Schritt ' + String(state.summaryStep) + ' von ' + String(state.summaryStep) + ')';
  855. } else {
  856. progress.textContent = 'Schritt ' + String(state.currentStep) + ' von ' + String(state.totalSteps);
  857. }
  858. stepElements.forEach((el) => {
  859. const step = Number(el.getAttribute('data-step'));
  860. el.classList.toggle('hidden', step !== state.currentStep);
  861. });
  862. if (summarySection) {
  863. summarySection.classList.toggle('hidden', !isSummary);
  864. if (isSummary) {
  865. renderSummary();
  866. }
  867. }
  868. prevBtn.disabled = state.isSubmitting || state.currentStep === 1;
  869. nextBtn.textContent = state.currentStep === state.totalSteps ? 'Zur Zusammenfassung' : 'Weiter';
  870. nextBtn.classList.toggle('hidden', state.currentStep >= state.summaryStep);
  871. nextBtn.disabled = state.isSubmitting;
  872. submitBtn.classList.toggle('hidden', !isSummary);
  873. submitBtn.disabled = state.isSubmitting;
  874. }
  875. function fillFormData(data) {
  876. Object.keys(data || {}).forEach((key) => {
  877. const field = applicationForm.querySelector('[name="form_data[' + key + ']"]');
  878. if (!field) {
  879. return;
  880. }
  881. if (field.type === 'checkbox') {
  882. field.checked = ['1', 'on', 'true', true].includes(data[key]);
  883. } else {
  884. field.value = data[key] || '';
  885. }
  886. });
  887. populateAllTablesFromHiddenValues();
  888. syncAllTableHiddenValues();
  889. applyFieldVisibility();
  890. refreshRequiredMarkers();
  891. }
  892. function formatUploadTimestamp(isoDate) {
  893. const formatted = formatTimestamp(String(isoDate || ''));
  894. if (formatted !== '') {
  895. return formatted;
  896. }
  897. return String(isoDate || '');
  898. }
  899. function buildUploadPreviewUrl(fieldKey, index) {
  900. const params = new URLSearchParams();
  901. params.set('csrf', boot.csrf);
  902. params.set('email', state.email);
  903. params.set('field', fieldKey);
  904. params.set('index', String(index));
  905. return appUrl('api/upload-preview.php') + '?' + params.toString();
  906. }
  907. async function deleteUploadedFile(fieldKey, index) {
  908. const fd = new FormData();
  909. fd.append('csrf', boot.csrf);
  910. fd.append('email', state.email);
  911. fd.append('website', '');
  912. fd.append('field', fieldKey);
  913. fd.append('index', String(index));
  914. const response = await postForm(appUrl('api/delete-upload.php'), fd);
  915. renderUploadInfo(response.uploads || {});
  916. const ts = formatTimestamp(response.updated_at);
  917. setDraftStatus('Gespeichert: ' + (ts || 'gerade eben'), false);
  918. if (state.currentStep === state.summaryStep) {
  919. renderSummary();
  920. }
  921. }
  922. function renderUploadInfo(uploads) {
  923. state.uploads = uploads && typeof uploads === 'object' ? uploads : {};
  924. document.querySelectorAll('[data-upload-list]').forEach((el) => {
  925. el.innerHTML = '';
  926. });
  927. Object.keys(state.uploads).forEach((field) => {
  928. const target = document.querySelector('[data-upload-list="' + field + '"]');
  929. if (!target || !Array.isArray(state.uploads[field])) {
  930. return;
  931. }
  932. state.uploads[field].forEach((item, index) => {
  933. const div = document.createElement('div');
  934. div.className = 'upload-item';
  935. const info = document.createElement('div');
  936. info.className = 'upload-item-info';
  937. const filename = String((item && item.original_filename) || (item && item.stored_filename) || 'Datei');
  938. const uploadedAt = formatUploadTimestamp(item && item.uploaded_at);
  939. info.textContent = uploadedAt !== '' ? filename + ' (' + uploadedAt + ')' : filename;
  940. div.appendChild(info);
  941. const actions = document.createElement('div');
  942. actions.className = 'upload-item-actions';
  943. const previewLink = document.createElement('a');
  944. previewLink.className = 'upload-item-btn';
  945. previewLink.href = buildUploadPreviewUrl(field, index);
  946. previewLink.target = '_blank';
  947. previewLink.rel = 'noopener noreferrer';
  948. previewLink.textContent = 'Vorschau';
  949. actions.appendChild(previewLink);
  950. const deleteBtn = document.createElement('button');
  951. deleteBtn.type = 'button';
  952. deleteBtn.className = 'upload-item-btn upload-item-btn-danger';
  953. deleteBtn.textContent = 'Löschen';
  954. deleteBtn.addEventListener('click', async () => {
  955. const confirmed = window.confirm('Diesen Upload wirklich löschen?');
  956. if (!confirmed) {
  957. return;
  958. }
  959. deleteBtn.disabled = true;
  960. try {
  961. await deleteUploadedFile(field, index);
  962. setFeedback('Upload gelöscht.', false);
  963. } catch (err) {
  964. const msg = (err.payload && err.payload.message) || err.message || 'Löschen fehlgeschlagen.';
  965. setFeedback(msg, true);
  966. } finally {
  967. deleteBtn.disabled = false;
  968. }
  969. });
  970. actions.appendChild(deleteBtn);
  971. div.appendChild(actions);
  972. target.appendChild(div);
  973. });
  974. });
  975. applyFieldVisibility();
  976. refreshRequiredMarkers();
  977. }
  978. function updateUploadSelectionText(fieldKey) {
  979. const target = document.querySelector('[data-upload-selected="' + fieldKey + '"]');
  980. if (!target) {
  981. return;
  982. }
  983. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  984. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  985. let label = 'Noch keine Datei hochgeladen';
  986. if (fileInput && fileInput.files && fileInput.files[0]) {
  987. label = 'Datei ausgewählt: ' + fileInput.files[0].name;
  988. }
  989. if (cameraInput && cameraInput.files && cameraInput.files[0]) {
  990. label = 'Foto ausgewählt: ' + cameraInput.files[0].name;
  991. }
  992. target.textContent = label;
  993. refreshRequiredMarkers();
  994. }
  995. async function triggerInstantUpload() {
  996. if (!state.email || wizardSection.classList.contains('hidden')) {
  997. return;
  998. }
  999. setDraftStatus('Datei wird hochgeladen ...', false);
  1000. try {
  1001. await saveDraft(true);
  1002. setFeedback('', false);
  1003. refreshRequiredMarkers();
  1004. if (state.currentStep === state.summaryStep) {
  1005. renderSummary();
  1006. }
  1007. } catch (err) {
  1008. const msg = (err.payload && err.payload.message) || err.message || 'Upload fehlgeschlagen.';
  1009. setDraftStatus('Upload fehlgeschlagen', true);
  1010. setFeedback(msg, true);
  1011. }
  1012. }
  1013. function initUploadControls() {
  1014. const controls = Array.from(document.querySelectorAll('[data-upload-key]'));
  1015. controls.forEach((control) => {
  1016. const fieldKey = control.getAttribute('data-upload-key') || '';
  1017. if (!fieldKey) {
  1018. return;
  1019. }
  1020. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  1021. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  1022. if (fileInput) {
  1023. fileInput.addEventListener('change', async () => {
  1024. if (fileInput.files && fileInput.files[0] && cameraInput) {
  1025. cameraInput.value = '';
  1026. }
  1027. updateUploadSelectionText(fieldKey);
  1028. if (fileInput.files && fileInput.files[0]) {
  1029. await triggerInstantUpload();
  1030. }
  1031. });
  1032. }
  1033. if (cameraInput) {
  1034. cameraInput.addEventListener('change', async () => {
  1035. if (cameraInput.files && cameraInput.files[0] && fileInput) {
  1036. fileInput.value = '';
  1037. }
  1038. updateUploadSelectionText(fieldKey);
  1039. if (cameraInput.files && cameraInput.files[0]) {
  1040. await triggerInstantUpload();
  1041. }
  1042. });
  1043. }
  1044. updateUploadSelectionText(fieldKey);
  1045. });
  1046. }
  1047. async function postForm(url, formData) {
  1048. const response = await fetch(url, {
  1049. method: 'POST',
  1050. body: formData,
  1051. credentials: 'same-origin',
  1052. headers: { 'X-Requested-With': 'XMLHttpRequest' },
  1053. });
  1054. const payload = await response.json();
  1055. if (!response.ok || payload.ok === false) {
  1056. const err = new Error(payload.message || 'Anfrage fehlgeschlagen');
  1057. err.payload = payload;
  1058. throw err;
  1059. }
  1060. return payload;
  1061. }
  1062. function collectPayload(includeFiles) {
  1063. const fd = new FormData();
  1064. fd.append('csrf', boot.csrf);
  1065. fd.append('email', state.email);
  1066. fd.append('step', String(Math.min(state.currentStep, state.totalSteps)));
  1067. fd.append('website', '');
  1068. Array.from(applicationForm.elements).forEach((el) => {
  1069. if (!el.name) {
  1070. return;
  1071. }
  1072. if (el.disabled) {
  1073. return;
  1074. }
  1075. if (!el.name.startsWith('form_data[')) {
  1076. return;
  1077. }
  1078. if (el.type === 'checkbox') {
  1079. fd.append(el.name, el.checked ? '1' : '0');
  1080. } else {
  1081. fd.append(el.name, el.value || '');
  1082. }
  1083. });
  1084. if (includeFiles) {
  1085. Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => {
  1086. if (input.disabled) {
  1087. return;
  1088. }
  1089. if (input.files && input.files[0]) {
  1090. fd.append(input.name, input.files[0]);
  1091. }
  1092. });
  1093. }
  1094. return fd;
  1095. }
  1096. async function loadDraft(email) {
  1097. const fd = new FormData();
  1098. fd.append('csrf', boot.csrf);
  1099. fd.append('email', email);
  1100. fd.append('website', '');
  1101. return postForm(appUrl('api/load-draft.php'), fd);
  1102. }
  1103. async function resetSavedData(email) {
  1104. const fd = new FormData();
  1105. fd.append('csrf', boot.csrf);
  1106. fd.append('email', email);
  1107. fd.append('website', '');
  1108. return postForm(appUrl('api/reset.php'), fd);
  1109. }
  1110. async function saveDraft(includeFiles) {
  1111. const payload = collectPayload(includeFiles);
  1112. const response = await postForm(appUrl('api/save-draft.php'), payload);
  1113. if (response.upload_errors && Object.keys(response.upload_errors).length > 0) {
  1114. showErrors(response.upload_errors);
  1115. setDraftStatus('Uploadfehler', true);
  1116. setFeedback('Einige Dateien konnten nicht gespeichert werden.', true);
  1117. } else {
  1118. const ts = formatTimestamp(response.updated_at);
  1119. setDraftStatus('Gespeichert: ' + (ts || 'gerade eben'), false);
  1120. }
  1121. if (response.uploads) {
  1122. renderUploadInfo(response.uploads);
  1123. }
  1124. if (includeFiles) {
  1125. Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => {
  1126. input.value = '';
  1127. });
  1128. Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => {
  1129. const fieldKey = control.getAttribute('data-upload-key');
  1130. if (fieldKey) {
  1131. updateUploadSelectionText(fieldKey);
  1132. }
  1133. });
  1134. }
  1135. return response;
  1136. }
  1137. function setSubmitting(isSubmitting) {
  1138. state.isSubmitting = isSubmitting;
  1139. submitBtn.classList.toggle('is-loading', isSubmitting);
  1140. if (submitSpinner) {
  1141. submitSpinner.classList.toggle('hidden', !isSubmitting);
  1142. }
  1143. if (submitLabel) {
  1144. submitLabel.textContent = isSubmitting ? 'Wird gesendet ...' : 'Verbindlich absenden';
  1145. }
  1146. updateProgress();
  1147. }
  1148. async function submitApplication() {
  1149. setSubmitting(true);
  1150. setFeedback('Absenden gestartet ...', false);
  1151. try {
  1152. const payload = collectPayload(true);
  1153. const response = await postForm(appUrl('api/submit.php'), payload);
  1154. clearErrors();
  1155. setDraftStatus('Abgeschlossen', false);
  1156. setFeedback('Antrag erfolgreich abgeschlossen. Vielen Dank.', false);
  1157. setSubmitting(false);
  1158. submitBtn.disabled = true;
  1159. nextBtn.disabled = true;
  1160. prevBtn.disabled = true;
  1161. setResetActionVisible(false);
  1162. if (submitLabel) {
  1163. submitLabel.textContent = 'Abgesendet';
  1164. }
  1165. return response;
  1166. } catch (err) {
  1167. setSubmitting(false);
  1168. throw err;
  1169. }
  1170. }
  1171. function validateStartEmail(showError) {
  1172. const email = normalizeEmail(startEmailInput.value || '');
  1173. const valid = isValidEmail(email);
  1174. startEmailInput.value = email;
  1175. if (valid) {
  1176. setStartEmailError('');
  1177. startEmailInput.setCustomValidity('');
  1178. updateStartEmailRequiredMarker();
  1179. return true;
  1180. }
  1181. const message = 'Bitte eine gültige E-Mail-Adresse eingeben.';
  1182. startEmailInput.setCustomValidity(message);
  1183. if (showError) {
  1184. setStartEmailError(message);
  1185. setFeedback(message, true);
  1186. }
  1187. updateStartEmailRequiredMarker();
  1188. return false;
  1189. }
  1190. async function startProcess(rawEmail) {
  1191. const email = normalizeEmail(rawEmail);
  1192. if (!isValidEmail(email)) {
  1193. const message = 'Bitte eine gültige E-Mail-Adresse eingeben.';
  1194. setStartEmailError(message);
  1195. setFeedback(message, true);
  1196. startEmailInput.focus();
  1197. return;
  1198. }
  1199. startEmailInput.value = email;
  1200. setStartEmailError('');
  1201. startEmailInput.setCustomValidity('');
  1202. updateStartEmailRequiredMarker();
  1203. if (startSubmitBtn) {
  1204. startSubmitBtn.disabled = true;
  1205. }
  1206. try {
  1207. const result = await loadDraft(email);
  1208. lockEmail(email);
  1209. setResetActionVisible(true);
  1210. if (result.already_submitted) {
  1211. wizardSection.classList.add('hidden');
  1212. setDraftStatus('Antrag bereits abgeschlossen', false);
  1213. setFeedback(
  1214. result.message || 'Für diese E-Mail liegt bereits ein abgeschlossener Antrag vor.',
  1215. false,
  1216. 'start'
  1217. );
  1218. setResetActionVisible(false);
  1219. stopAutosave();
  1220. return;
  1221. }
  1222. wizardSection.classList.remove('hidden');
  1223. fillFormData(result.data || {});
  1224. renderUploadInfo(result.uploads || {});
  1225. state.currentStep = 1;
  1226. updateProgress();
  1227. scrollWizardToTop();
  1228. startAutosave();
  1229. const loadedAt = formatTimestamp(result.updated_at);
  1230. if (loadedAt) {
  1231. setDraftStatus('Entwurf geladen: ' + loadedAt, false);
  1232. } else {
  1233. setDraftStatus('Neuer Entwurf gestartet', false);
  1234. }
  1235. setFeedback('', false);
  1236. } catch (err) {
  1237. const msg = (err.payload && err.payload.message) || err.message || 'Laden fehlgeschlagen.';
  1238. setFeedback(msg, true);
  1239. } finally {
  1240. if (startSubmitBtn) {
  1241. startSubmitBtn.disabled = false;
  1242. }
  1243. }
  1244. }
  1245. startForm.addEventListener('submit', async (event) => {
  1246. event.preventDefault();
  1247. if (!validateStartEmail(true)) {
  1248. startEmailInput.focus();
  1249. return;
  1250. }
  1251. await startProcess(startEmailInput.value || '');
  1252. });
  1253. startEmailInput.addEventListener('input', () => {
  1254. if (startEmailInput.readOnly) {
  1255. return;
  1256. }
  1257. if (startEmailInput.value.trim() === '') {
  1258. setStartEmailError('');
  1259. startEmailInput.setCustomValidity('');
  1260. updateStartEmailRequiredMarker();
  1261. return;
  1262. }
  1263. validateStartEmail(false);
  1264. });
  1265. startEmailInput.addEventListener('blur', () => {
  1266. if (startEmailInput.readOnly) {
  1267. return;
  1268. }
  1269. if (startEmailInput.value.trim() === '') {
  1270. updateStartEmailRequiredMarker();
  1271. return;
  1272. }
  1273. validateStartEmail(true);
  1274. });
  1275. resetDataBtn.addEventListener('click', async () => {
  1276. if (!state.email) {
  1277. setFeedback('Keine aktive E-Mail vorhanden.', true);
  1278. return;
  1279. }
  1280. const confirmed = window.confirm('Alle gespeicherten Daten zu dieser E-Mail endgültig löschen und neu starten?');
  1281. if (!confirmed) {
  1282. return;
  1283. }
  1284. try {
  1285. await resetSavedData(state.email);
  1286. stopAutosave();
  1287. wizardSection.classList.add('hidden');
  1288. clearWizardData();
  1289. unlockEmail(true);
  1290. setFeedback('Alle gespeicherten Daten wurden gelöscht. Sie können neu starten.', false);
  1291. startEmailInput.focus();
  1292. } catch (err) {
  1293. const msg = (err.payload && err.payload.message) || err.message || 'Löschen fehlgeschlagen.';
  1294. setFeedback(msg, true);
  1295. }
  1296. });
  1297. prevBtn.addEventListener('click', async () => {
  1298. if (state.currentStep <= 1 || state.isSubmitting) {
  1299. return;
  1300. }
  1301. try {
  1302. if (state.currentStep <= state.totalSteps) {
  1303. await saveDraft(false);
  1304. }
  1305. state.currentStep -= 1;
  1306. updateProgress();
  1307. scrollWizardToTop();
  1308. } catch (err) {
  1309. const msg = (err.payload && err.payload.message) || err.message;
  1310. setFeedback(msg, true);
  1311. }
  1312. });
  1313. nextBtn.addEventListener('click', async () => {
  1314. if (state.currentStep >= state.summaryStep || state.isSubmitting) {
  1315. return;
  1316. }
  1317. try {
  1318. await saveDraft(false);
  1319. state.currentStep += 1;
  1320. updateProgress();
  1321. scrollWizardToTop();
  1322. } catch (err) {
  1323. const msg = (err.payload && err.payload.message) || err.message;
  1324. setFeedback(msg, true);
  1325. }
  1326. });
  1327. submitBtn.addEventListener('click', async () => {
  1328. if (state.isSubmitting) {
  1329. return;
  1330. }
  1331. renderSummary();
  1332. if (state.summaryMissingCount > 0) {
  1333. setFeedback('Bitte zuerst alle rot markierten Pflichtfelder ausfüllen.', true);
  1334. return;
  1335. }
  1336. try {
  1337. await submitApplication();
  1338. } catch (err) {
  1339. const payload = err.payload || {};
  1340. if (payload.errors) {
  1341. showErrors(payload.errors);
  1342. }
  1343. const msg = payload.message || err.message || 'Absenden fehlgeschlagen.';
  1344. setFeedback(msg, true);
  1345. if (payload.already_submitted) {
  1346. wizardSection.classList.add('hidden');
  1347. setDraftStatus('Antrag bereits abgeschlossen', false);
  1348. setResetActionVisible(false);
  1349. }
  1350. }
  1351. });
  1352. function initializeAfterDisclaimer() {
  1353. disclaimerSection.classList.add('hidden');
  1354. startSection.classList.remove('hidden');
  1355. const rememberedEmail = normalizeEmail(getRememberedEmail());
  1356. if (rememberedEmail !== '' && isValidEmail(rememberedEmail)) {
  1357. startEmailInput.value = rememberedEmail;
  1358. setFeedback('', false);
  1359. startProcess(rememberedEmail);
  1360. }
  1361. }
  1362. if (disclaimerReadCheckbox) {
  1363. disclaimerReadCheckbox.addEventListener('change', updateDisclaimerAcceptanceState);
  1364. }
  1365. if (acceptDisclaimerBtn) {
  1366. acceptDisclaimerBtn.addEventListener('click', () => {
  1367. if (!disclaimerReadCheckbox || !disclaimerReadCheckbox.checked) {
  1368. setDisclaimerError('Bitte lesen und bestätigen Sie den Hinweis.');
  1369. return;
  1370. }
  1371. setDisclaimerError('');
  1372. initializeAfterDisclaimer();
  1373. });
  1374. }
  1375. updateDisclaimerAcceptanceState();
  1376. disclaimerSection.classList.remove('hidden');
  1377. startSection.classList.add('hidden');
  1378. initTableFields();
  1379. initUploadControls();
  1380. initRequiredMarkerTracking();
  1381. applyFieldVisibility();
  1382. refreshRequiredMarkers();
  1383. updateStartEmailRequiredMarker();
  1384. updateProgress();
  1385. })();