form.js 60 KB

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