form.js 44 KB

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