form.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  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 feedbackMessage = document.getElementById('feedbackMessage');
  33. const wizardSection = document.getElementById('wizardSection');
  34. const applicationForm = document.getElementById('applicationForm');
  35. const applicationEmail = document.getElementById('applicationEmail');
  36. const progress = document.getElementById('progress');
  37. const prevBtn = document.getElementById('prevBtn');
  38. const nextBtn = document.getElementById('nextBtn');
  39. const submitBtn = document.getElementById('submitBtn');
  40. const submitSpinner = document.getElementById('submitSpinner');
  41. const submitLabel = submitBtn ? submitBtn.querySelector('[data-submit-label]') : null;
  42. const summarySection = document.getElementById('summarySection');
  43. const summaryContent = document.getElementById('summaryContent');
  44. const summaryMissingNotice = document.getElementById('summaryMissingNotice');
  45. const stepElements = Array.from(document.querySelectorAll('.step'));
  46. const startEmailRequiredMark = document.querySelector('#startEmailField .required-mark-field-start');
  47. const fieldContainersByKey = new Map();
  48. const fieldsByKey = new Map();
  49. document.querySelectorAll('.field[data-field]').forEach((container) => {
  50. const key = String(container.getAttribute('data-field') || '').trim();
  51. if (key !== '') {
  52. fieldContainersByKey.set(key, container);
  53. }
  54. });
  55. schemaSteps.forEach((step) => {
  56. const fields = Array.isArray(step.fields) ? step.fields : [];
  57. fields.forEach((field) => {
  58. if (!field || typeof field !== 'object') {
  59. return;
  60. }
  61. const key = String(field.key || '').trim();
  62. if (key !== '') {
  63. fieldsByKey.set(key, field);
  64. }
  65. });
  66. });
  67. function appUrl(path) {
  68. const normalizedPath = String(path || '').replace(/^\/+/, '');
  69. return (baseUrl ? baseUrl + '/' : '/') + normalizedPath;
  70. }
  71. function setFeedback(text, isError) {
  72. feedbackMessage.textContent = text || '';
  73. feedbackMessage.classList.toggle('error-text', Boolean(isError));
  74. }
  75. function setDraftStatus(text, isError) {
  76. draftStatusValue.textContent = text || '';
  77. draftStatusValue.classList.toggle('error-text', Boolean(isError));
  78. }
  79. function setStartEmailError(text) {
  80. if (!startEmailError) {
  81. return;
  82. }
  83. startEmailError.textContent = text || '';
  84. }
  85. function setDisclaimerError(text) {
  86. if (!disclaimerReadError) {
  87. return;
  88. }
  89. disclaimerReadError.textContent = text || '';
  90. }
  91. function updateStartEmailRequiredMarker() {
  92. if (!startEmailRequiredMark) {
  93. return;
  94. }
  95. const email = normalizeEmail(startEmailInput.value || '');
  96. startEmailRequiredMark.classList.toggle('hidden', isValidEmail(email));
  97. }
  98. function formatTimestamp(isoDate) {
  99. if (!isoDate) {
  100. return '';
  101. }
  102. const date = new Date(isoDate);
  103. if (Number.isNaN(date.getTime())) {
  104. return String(isoDate);
  105. }
  106. return date.toLocaleString('de-DE');
  107. }
  108. function normalizeEmail(email) {
  109. return (email || '').trim().toLowerCase();
  110. }
  111. function isValidEmail(email) {
  112. return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  113. }
  114. function rememberEmail(email) {
  115. try {
  116. localStorage.setItem(EMAIL_STORAGE_KEY, email);
  117. } catch (_err) {
  118. // ignore localStorage errors
  119. }
  120. }
  121. function getRememberedEmail() {
  122. try {
  123. return localStorage.getItem(EMAIL_STORAGE_KEY) || '';
  124. } catch (_err) {
  125. return '';
  126. }
  127. }
  128. function forgetRememberedEmail() {
  129. try {
  130. localStorage.removeItem(EMAIL_STORAGE_KEY);
  131. } catch (_err) {
  132. // ignore localStorage errors
  133. }
  134. }
  135. function updateDisclaimerAcceptanceState() {
  136. if (!acceptDisclaimerBtn || !disclaimerReadCheckbox) {
  137. return;
  138. }
  139. const accepted = disclaimerReadCheckbox.checked;
  140. acceptDisclaimerBtn.disabled = !accepted;
  141. if (accepted) {
  142. setDisclaimerError('');
  143. }
  144. }
  145. function enterCompactStatus(email) {
  146. statusEmailValue.textContent = email;
  147. startSection.classList.add('compact-mode');
  148. compactStatusBox.classList.remove('hidden');
  149. startIntroText.classList.add('hidden');
  150. startEmailField.classList.add('hidden');
  151. startActions.classList.add('hidden');
  152. }
  153. function leaveCompactStatus() {
  154. startSection.classList.remove('compact-mode');
  155. compactStatusBox.classList.add('hidden');
  156. startIntroText.classList.remove('hidden');
  157. startEmailField.classList.remove('hidden');
  158. startActions.classList.remove('hidden');
  159. statusEmailValue.textContent = '-';
  160. setDraftStatus('Noch nicht gespeichert', false);
  161. }
  162. function lockEmail(email) {
  163. state.email = email;
  164. applicationEmail.value = email;
  165. startEmailInput.value = email;
  166. startEmailInput.readOnly = true;
  167. startEmailInput.setAttribute('aria-readonly', 'true');
  168. updateStartEmailRequiredMarker();
  169. rememberEmail(email);
  170. enterCompactStatus(email);
  171. }
  172. function unlockEmail(clearInput) {
  173. state.email = '';
  174. applicationEmail.value = '';
  175. startEmailInput.readOnly = false;
  176. startEmailInput.removeAttribute('aria-readonly');
  177. if (clearInput) {
  178. startEmailInput.value = '';
  179. setStartEmailError('');
  180. }
  181. updateStartEmailRequiredMarker();
  182. forgetRememberedEmail();
  183. leaveCompactStatus();
  184. }
  185. function stopAutosave() {
  186. if (state.autosaveId) {
  187. clearInterval(state.autosaveId);
  188. state.autosaveId = null;
  189. }
  190. }
  191. function startAutosave() {
  192. stopAutosave();
  193. state.autosaveId = setInterval(async () => {
  194. if (!state.email || wizardSection.classList.contains('hidden')) {
  195. return;
  196. }
  197. try {
  198. await saveDraft(false);
  199. } catch (_err) {
  200. // visible on next manual action
  201. }
  202. }, 15000);
  203. }
  204. function clearErrors() {
  205. document.querySelectorAll('[data-error-for]').forEach((el) => {
  206. el.textContent = '';
  207. });
  208. }
  209. function showErrors(errors) {
  210. clearErrors();
  211. Object.keys(errors || {}).forEach((key) => {
  212. const el = document.querySelector('[data-error-for="' + key + '"]');
  213. if (el) {
  214. el.textContent = errors[key];
  215. }
  216. });
  217. }
  218. function clearWizardData() {
  219. applicationForm.reset();
  220. clearErrors();
  221. renderUploadInfo({});
  222. state.currentStep = 1;
  223. state.summaryMissingCount = 0;
  224. state.isSubmitting = false;
  225. if (submitLabel) {
  226. submitLabel.textContent = 'Verbindlich absenden';
  227. }
  228. if (submitSpinner) {
  229. submitSpinner.classList.add('hidden');
  230. }
  231. submitBtn.classList.remove('is-loading');
  232. refreshRequiredMarkers();
  233. updateProgress();
  234. Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => {
  235. const fieldKey = control.getAttribute('data-upload-key');
  236. if (fieldKey) {
  237. updateUploadSelectionText(fieldKey);
  238. }
  239. });
  240. }
  241. function parseFieldKey(name) {
  242. const match = /^form_data\[(.+)\]$/.exec(String(name || ''));
  243. return match ? match[1] : '';
  244. }
  245. function isCheckboxTrue(value) {
  246. return ['1', 'on', 'true', true, 1].includes(value);
  247. }
  248. function collectCurrentFormData() {
  249. const data = {};
  250. Array.from(applicationForm.elements).forEach((el) => {
  251. if (!el.name) {
  252. return;
  253. }
  254. const key = parseFieldKey(el.name);
  255. if (!key) {
  256. return;
  257. }
  258. if (el.type === 'checkbox') {
  259. data[key] = el.checked ? '1' : '0';
  260. } else {
  261. data[key] = el.value || '';
  262. }
  263. });
  264. return data;
  265. }
  266. function hasFileValue(fieldKey) {
  267. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  268. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  269. const hasLocalSelection =
  270. (fileInput && fileInput.files && fileInput.files.length > 0) ||
  271. (cameraInput && cameraInput.files && cameraInput.files.length > 0);
  272. if (hasLocalSelection) {
  273. return true;
  274. }
  275. const storedUploads = state.uploads[fieldKey];
  276. return Array.isArray(storedUploads) && storedUploads.length > 0;
  277. }
  278. function isFieldCompleted(field, formData) {
  279. const key = String(field.key || '').trim();
  280. const type = String(field.type || 'text');
  281. if (key === '') {
  282. return false;
  283. }
  284. if (type === 'file') {
  285. return hasFileValue(key);
  286. }
  287. if (type === 'checkbox') {
  288. return isCheckboxTrue(formData[key]);
  289. }
  290. return String(formData[key] || '').trim() !== '';
  291. }
  292. function refreshRequiredMarkers() {
  293. const formData = collectCurrentFormData();
  294. fieldsByKey.forEach((field, key) => {
  295. const container = fieldContainersByKey.get(key);
  296. if (!container) {
  297. return;
  298. }
  299. const markers = container.querySelectorAll('.required-mark-field');
  300. if (!markers.length) {
  301. return;
  302. }
  303. const requiredNow = isFieldRequired(field, formData);
  304. const completed = isFieldCompleted(field, formData);
  305. const hideMarker = !requiredNow || completed;
  306. markers.forEach((marker) => {
  307. marker.classList.toggle('hidden', hideMarker);
  308. });
  309. });
  310. }
  311. function initRequiredMarkerTracking() {
  312. Array.from(applicationForm.elements).forEach((el) => {
  313. if (!el.name || !el.name.startsWith('form_data[')) {
  314. return;
  315. }
  316. el.addEventListener('blur', refreshRequiredMarkers);
  317. el.addEventListener('change', refreshRequiredMarkers);
  318. });
  319. }
  320. function isFieldRequired(field, formData) {
  321. if (!field || typeof field !== 'object') {
  322. return false;
  323. }
  324. if (field.required === true) {
  325. return true;
  326. }
  327. if (!field.required_if || typeof field.required_if !== 'object') {
  328. return false;
  329. }
  330. const depField = String(field.required_if.field || '').trim();
  331. const depValue = String(field.required_if.equals || '');
  332. if (!depField) {
  333. return false;
  334. }
  335. return String(formData[depField] || '') === depValue;
  336. }
  337. function isFieldMissing(field, formData) {
  338. if (!isFieldRequired(field, formData)) {
  339. return false;
  340. }
  341. const key = String(field.key || '').trim();
  342. if (!key) {
  343. return false;
  344. }
  345. const type = String(field.type || 'text');
  346. if (type === 'file') {
  347. const uploadItems = state.uploads[key];
  348. return !Array.isArray(uploadItems) || uploadItems.length === 0;
  349. }
  350. if (type === 'checkbox') {
  351. return !isCheckboxTrue(formData[key]);
  352. }
  353. return String(formData[key] || '').trim() === '';
  354. }
  355. function resolveSelectLabel(field, value) {
  356. if (!Array.isArray(field.options)) {
  357. return value;
  358. }
  359. const matched = field.options.find((option) => {
  360. return option && String(option.value || '') === String(value);
  361. });
  362. if (!matched) {
  363. return value;
  364. }
  365. return String(matched.label || value);
  366. }
  367. function fieldDisplayValue(field, formData) {
  368. const key = String(field.key || '').trim();
  369. const type = String(field.type || 'text');
  370. if (!key) {
  371. return '';
  372. }
  373. if (type === 'file') {
  374. const uploadItems = state.uploads[key];
  375. if (!Array.isArray(uploadItems) || uploadItems.length === 0) {
  376. return '';
  377. }
  378. return uploadItems
  379. .map((item) => {
  380. if (!item || typeof item !== 'object') {
  381. return '';
  382. }
  383. return String(item.original_filename || item.stored_filename || '').trim();
  384. })
  385. .filter(Boolean)
  386. .join(', ');
  387. }
  388. if (type === 'checkbox') {
  389. return isCheckboxTrue(formData[key]) ? 'Ja' : 'Nein';
  390. }
  391. const rawValue = String(formData[key] || '').trim();
  392. if (rawValue === '') {
  393. return '';
  394. }
  395. if (type === 'select') {
  396. return resolveSelectLabel(field, rawValue);
  397. }
  398. return rawValue;
  399. }
  400. function renderSummary() {
  401. if (!summarySection || !summaryContent || !summaryMissingNotice) {
  402. return;
  403. }
  404. const formData = collectCurrentFormData();
  405. const fragment = document.createDocumentFragment();
  406. let missingCount = 0;
  407. const introCard = document.createElement('div');
  408. introCard.className = 'summary-step-card';
  409. const introTitle = document.createElement('h4');
  410. introTitle.textContent = 'Startdaten';
  411. introCard.appendChild(introTitle);
  412. const emailRow = document.createElement('div');
  413. emailRow.className = 'summary-item';
  414. const emailLabel = document.createElement('div');
  415. emailLabel.className = 'summary-item-label';
  416. emailLabel.textContent = 'E-Mail';
  417. const emailValue = document.createElement('div');
  418. emailValue.className = 'summary-item-value';
  419. emailValue.textContent = state.email || 'Nicht gesetzt';
  420. emailRow.appendChild(emailLabel);
  421. emailRow.appendChild(emailValue);
  422. introCard.appendChild(emailRow);
  423. fragment.appendChild(introCard);
  424. schemaSteps.forEach((step, stepIndex) => {
  425. const fields = Array.isArray(step.fields) ? step.fields.filter((field) => field && typeof field === 'object') : [];
  426. if (fields.length === 0) {
  427. return;
  428. }
  429. const card = document.createElement('div');
  430. card.className = 'summary-step-card';
  431. const title = document.createElement('h4');
  432. title.textContent = 'Schritt ' + String(stepIndex + 1) + ': ' + String(step.title || '');
  433. card.appendChild(title);
  434. fields.forEach((field) => {
  435. const required = isFieldRequired(field, formData);
  436. const missing = isFieldMissing(field, formData);
  437. if (missing) {
  438. missingCount += 1;
  439. }
  440. const row = document.createElement('div');
  441. row.className = 'summary-item';
  442. if (required) {
  443. row.classList.add('summary-item-required');
  444. }
  445. if (missing) {
  446. row.classList.add('summary-item-missing');
  447. }
  448. const labelEl = document.createElement('div');
  449. labelEl.className = 'summary-item-label';
  450. labelEl.textContent = String(field.label || field.key || 'Feld');
  451. if (required) {
  452. const requiredBadge = document.createElement('span');
  453. requiredBadge.className = 'summary-badge summary-badge-required';
  454. requiredBadge.textContent = 'Pflichtfeld';
  455. labelEl.appendChild(requiredBadge);
  456. }
  457. if (missing) {
  458. const missingBadge = document.createElement('span');
  459. missingBadge.className = 'summary-badge summary-badge-missing';
  460. missingBadge.textContent = '! Pflichtfeld fehlt';
  461. labelEl.appendChild(missingBadge);
  462. }
  463. const valueEl = document.createElement('div');
  464. valueEl.className = 'summary-item-value';
  465. const value = fieldDisplayValue(field, formData);
  466. if (value !== '') {
  467. valueEl.textContent = value;
  468. } else {
  469. valueEl.textContent = String(field.type || '') === 'file' ? 'Keine Datei hochgeladen' : 'Nicht ausgefüllt';
  470. valueEl.classList.add('summary-item-value-empty');
  471. if (missing) {
  472. valueEl.classList.add('summary-item-value-missing');
  473. }
  474. }
  475. row.appendChild(labelEl);
  476. row.appendChild(valueEl);
  477. card.appendChild(row);
  478. });
  479. fragment.appendChild(card);
  480. });
  481. summaryContent.innerHTML = '';
  482. summaryContent.appendChild(fragment);
  483. state.summaryMissingCount = missingCount;
  484. if (missingCount > 0) {
  485. summaryMissingNotice.classList.remove('hidden');
  486. summaryMissingNotice.classList.add('summary-missing-warning');
  487. summaryMissingNotice.textContent = '! Es fehlen noch ' + String(missingCount) + ' Pflichtfelder. Bitte korrigieren Sie die rot markierten Einträge.';
  488. } else {
  489. summaryMissingNotice.classList.remove('hidden');
  490. summaryMissingNotice.classList.remove('summary-missing-warning');
  491. summaryMissingNotice.textContent = 'Alle Pflichtfelder sind ausgefüllt.';
  492. }
  493. }
  494. function updateProgress() {
  495. const isSummary = state.currentStep === state.summaryStep;
  496. if (isSummary) {
  497. progress.textContent = 'Zusammenfassung (Schritt ' + String(state.summaryStep) + ' von ' + String(state.summaryStep) + ')';
  498. } else {
  499. progress.textContent = 'Schritt ' + String(state.currentStep) + ' von ' + String(state.totalSteps);
  500. }
  501. stepElements.forEach((el) => {
  502. const step = Number(el.getAttribute('data-step'));
  503. el.classList.toggle('hidden', step !== state.currentStep);
  504. });
  505. if (summarySection) {
  506. summarySection.classList.toggle('hidden', !isSummary);
  507. if (isSummary) {
  508. renderSummary();
  509. }
  510. }
  511. prevBtn.disabled = state.isSubmitting || state.currentStep === 1;
  512. nextBtn.textContent = state.currentStep === state.totalSteps ? 'Zur Zusammenfassung' : 'Weiter';
  513. nextBtn.classList.toggle('hidden', state.currentStep >= state.summaryStep);
  514. nextBtn.disabled = state.isSubmitting;
  515. submitBtn.classList.toggle('hidden', !isSummary);
  516. submitBtn.disabled = state.isSubmitting;
  517. }
  518. function fillFormData(data) {
  519. Object.keys(data || {}).forEach((key) => {
  520. const field = applicationForm.querySelector('[name="form_data[' + key + ']"]');
  521. if (!field) {
  522. return;
  523. }
  524. if (field.type === 'checkbox') {
  525. field.checked = ['1', 'on', 'true', true].includes(data[key]);
  526. } else {
  527. field.value = data[key] || '';
  528. }
  529. });
  530. refreshRequiredMarkers();
  531. }
  532. function renderUploadInfo(uploads) {
  533. state.uploads = uploads && typeof uploads === 'object' ? uploads : {};
  534. document.querySelectorAll('[data-upload-list]').forEach((el) => {
  535. el.innerHTML = '';
  536. });
  537. Object.keys(state.uploads).forEach((field) => {
  538. const target = document.querySelector('[data-upload-list="' + field + '"]');
  539. if (!target || !Array.isArray(state.uploads[field])) {
  540. return;
  541. }
  542. state.uploads[field].forEach((item) => {
  543. const div = document.createElement('div');
  544. div.className = 'upload-item';
  545. div.textContent = item.original_filename + ' (' + item.uploaded_at + ')';
  546. target.appendChild(div);
  547. });
  548. });
  549. refreshRequiredMarkers();
  550. }
  551. function updateUploadSelectionText(fieldKey) {
  552. const target = document.querySelector('[data-upload-selected="' + fieldKey + '"]');
  553. if (!target) {
  554. return;
  555. }
  556. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  557. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  558. let label = 'Keine Datei gewählt';
  559. if (fileInput && fileInput.files && fileInput.files[0]) {
  560. label = 'Ausgewählt: ' + fileInput.files[0].name;
  561. }
  562. if (cameraInput && cameraInput.files && cameraInput.files[0]) {
  563. label = 'Ausgewählt: ' + cameraInput.files[0].name + ' (Foto)';
  564. }
  565. target.textContent = label;
  566. refreshRequiredMarkers();
  567. }
  568. async function triggerInstantUpload() {
  569. if (!state.email || wizardSection.classList.contains('hidden')) {
  570. return;
  571. }
  572. setDraftStatus('Datei wird hochgeladen ...', false);
  573. try {
  574. await saveDraft(true);
  575. setFeedback('', false);
  576. refreshRequiredMarkers();
  577. if (state.currentStep === state.summaryStep) {
  578. renderSummary();
  579. }
  580. } catch (err) {
  581. const msg = (err.payload && err.payload.message) || err.message || 'Upload fehlgeschlagen.';
  582. setDraftStatus('Upload fehlgeschlagen', true);
  583. setFeedback(msg, true);
  584. }
  585. }
  586. function initUploadControls() {
  587. const controls = Array.from(document.querySelectorAll('[data-upload-key]'));
  588. controls.forEach((control) => {
  589. const fieldKey = control.getAttribute('data-upload-key') || '';
  590. if (!fieldKey) {
  591. return;
  592. }
  593. const fileInput = applicationForm.querySelector('[name="' + fieldKey + '"]');
  594. const cameraInput = applicationForm.querySelector('[name="' + fieldKey + '__camera"]');
  595. if (fileInput) {
  596. fileInput.addEventListener('change', async () => {
  597. if (fileInput.files && fileInput.files[0] && cameraInput) {
  598. cameraInput.value = '';
  599. }
  600. updateUploadSelectionText(fieldKey);
  601. if (fileInput.files && fileInput.files[0]) {
  602. await triggerInstantUpload();
  603. }
  604. });
  605. }
  606. if (cameraInput) {
  607. cameraInput.addEventListener('change', async () => {
  608. if (cameraInput.files && cameraInput.files[0] && fileInput) {
  609. fileInput.value = '';
  610. }
  611. updateUploadSelectionText(fieldKey);
  612. if (cameraInput.files && cameraInput.files[0]) {
  613. await triggerInstantUpload();
  614. }
  615. });
  616. }
  617. updateUploadSelectionText(fieldKey);
  618. });
  619. }
  620. async function postForm(url, formData) {
  621. const response = await fetch(url, {
  622. method: 'POST',
  623. body: formData,
  624. credentials: 'same-origin',
  625. headers: { 'X-Requested-With': 'XMLHttpRequest' },
  626. });
  627. const payload = await response.json();
  628. if (!response.ok || payload.ok === false) {
  629. const err = new Error(payload.message || 'Anfrage fehlgeschlagen');
  630. err.payload = payload;
  631. throw err;
  632. }
  633. return payload;
  634. }
  635. function collectPayload(includeFiles) {
  636. const fd = new FormData();
  637. fd.append('csrf', boot.csrf);
  638. fd.append('email', state.email);
  639. fd.append('step', String(Math.min(state.currentStep, state.totalSteps)));
  640. fd.append('website', '');
  641. Array.from(applicationForm.elements).forEach((el) => {
  642. if (!el.name) {
  643. return;
  644. }
  645. if (!el.name.startsWith('form_data[')) {
  646. return;
  647. }
  648. if (el.type === 'checkbox') {
  649. fd.append(el.name, el.checked ? '1' : '0');
  650. } else {
  651. fd.append(el.name, el.value || '');
  652. }
  653. });
  654. if (includeFiles) {
  655. Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => {
  656. if (input.files && input.files[0]) {
  657. fd.append(input.name, input.files[0]);
  658. }
  659. });
  660. }
  661. return fd;
  662. }
  663. async function loadDraft(email) {
  664. const fd = new FormData();
  665. fd.append('csrf', boot.csrf);
  666. fd.append('email', email);
  667. fd.append('website', '');
  668. return postForm(appUrl('api/load-draft.php'), fd);
  669. }
  670. async function resetSavedData(email) {
  671. const fd = new FormData();
  672. fd.append('csrf', boot.csrf);
  673. fd.append('email', email);
  674. fd.append('website', '');
  675. return postForm(appUrl('api/reset.php'), fd);
  676. }
  677. async function saveDraft(includeFiles) {
  678. const payload = collectPayload(includeFiles);
  679. const response = await postForm(appUrl('api/save-draft.php'), payload);
  680. if (response.upload_errors && Object.keys(response.upload_errors).length > 0) {
  681. showErrors(response.upload_errors);
  682. setDraftStatus('Uploadfehler', true);
  683. setFeedback('Einige Dateien konnten nicht gespeichert werden.', true);
  684. } else {
  685. const ts = formatTimestamp(response.updated_at);
  686. setDraftStatus('Gespeichert: ' + (ts || 'gerade eben'), false);
  687. }
  688. if (response.uploads) {
  689. renderUploadInfo(response.uploads);
  690. }
  691. if (includeFiles) {
  692. Array.from(applicationForm.querySelectorAll('input[type="file"]')).forEach((input) => {
  693. input.value = '';
  694. });
  695. Array.from(document.querySelectorAll('[data-upload-key]')).forEach((control) => {
  696. const fieldKey = control.getAttribute('data-upload-key');
  697. if (fieldKey) {
  698. updateUploadSelectionText(fieldKey);
  699. }
  700. });
  701. }
  702. return response;
  703. }
  704. function setSubmitting(isSubmitting) {
  705. state.isSubmitting = isSubmitting;
  706. submitBtn.classList.toggle('is-loading', isSubmitting);
  707. if (submitSpinner) {
  708. submitSpinner.classList.toggle('hidden', !isSubmitting);
  709. }
  710. if (submitLabel) {
  711. submitLabel.textContent = isSubmitting ? 'Wird gesendet ...' : 'Verbindlich absenden';
  712. }
  713. updateProgress();
  714. }
  715. async function submitApplication() {
  716. setSubmitting(true);
  717. setFeedback('Absenden gestartet ...', false);
  718. try {
  719. const payload = collectPayload(true);
  720. const response = await postForm(appUrl('api/submit.php'), payload);
  721. clearErrors();
  722. setDraftStatus('Abgeschlossen', false);
  723. setFeedback('Antrag erfolgreich abgeschlossen. Vielen Dank.', false);
  724. setSubmitting(false);
  725. submitBtn.disabled = true;
  726. nextBtn.disabled = true;
  727. prevBtn.disabled = true;
  728. if (submitLabel) {
  729. submitLabel.textContent = 'Abgesendet';
  730. }
  731. return response;
  732. } catch (err) {
  733. setSubmitting(false);
  734. throw err;
  735. }
  736. }
  737. function validateStartEmail(showError) {
  738. const email = normalizeEmail(startEmailInput.value || '');
  739. const valid = isValidEmail(email);
  740. startEmailInput.value = email;
  741. if (valid) {
  742. setStartEmailError('');
  743. startEmailInput.setCustomValidity('');
  744. updateStartEmailRequiredMarker();
  745. return true;
  746. }
  747. const message = 'Bitte eine gültige E-Mail-Adresse eingeben.';
  748. startEmailInput.setCustomValidity(message);
  749. if (showError) {
  750. setStartEmailError(message);
  751. setFeedback(message, true);
  752. }
  753. updateStartEmailRequiredMarker();
  754. return false;
  755. }
  756. async function startProcess(rawEmail) {
  757. const email = normalizeEmail(rawEmail);
  758. if (!isValidEmail(email)) {
  759. const message = 'Bitte eine gültige E-Mail-Adresse eingeben.';
  760. setStartEmailError(message);
  761. setFeedback(message, true);
  762. startEmailInput.focus();
  763. return;
  764. }
  765. startEmailInput.value = email;
  766. setStartEmailError('');
  767. startEmailInput.setCustomValidity('');
  768. updateStartEmailRequiredMarker();
  769. if (startSubmitBtn) {
  770. startSubmitBtn.disabled = true;
  771. }
  772. try {
  773. const result = await loadDraft(email);
  774. lockEmail(email);
  775. if (result.already_submitted) {
  776. wizardSection.classList.add('hidden');
  777. setDraftStatus('Antrag bereits abgeschlossen', false);
  778. setFeedback(boot.contactEmail ? 'Kontakt: ' + boot.contactEmail : '', false);
  779. stopAutosave();
  780. return;
  781. }
  782. wizardSection.classList.remove('hidden');
  783. fillFormData(result.data || {});
  784. renderUploadInfo(result.uploads || {});
  785. state.currentStep = Math.min(Math.max(Number(result.step || 1), 1), state.totalSteps);
  786. updateProgress();
  787. startAutosave();
  788. const loadedAt = formatTimestamp(result.updated_at);
  789. if (loadedAt) {
  790. setDraftStatus('Entwurf geladen: ' + loadedAt, false);
  791. } else {
  792. setDraftStatus('Neuer Entwurf gestartet', false);
  793. }
  794. setFeedback('', false);
  795. } catch (err) {
  796. const msg = (err.payload && err.payload.message) || err.message || 'Laden fehlgeschlagen.';
  797. setFeedback(msg, true);
  798. } finally {
  799. if (startSubmitBtn) {
  800. startSubmitBtn.disabled = false;
  801. }
  802. }
  803. }
  804. startForm.addEventListener('submit', async (event) => {
  805. event.preventDefault();
  806. if (!validateStartEmail(true)) {
  807. startEmailInput.focus();
  808. return;
  809. }
  810. await startProcess(startEmailInput.value || '');
  811. });
  812. startEmailInput.addEventListener('input', () => {
  813. if (startEmailInput.readOnly) {
  814. return;
  815. }
  816. if (startEmailInput.value.trim() === '') {
  817. setStartEmailError('');
  818. startEmailInput.setCustomValidity('');
  819. updateStartEmailRequiredMarker();
  820. return;
  821. }
  822. validateStartEmail(false);
  823. });
  824. startEmailInput.addEventListener('blur', () => {
  825. if (startEmailInput.readOnly) {
  826. return;
  827. }
  828. if (startEmailInput.value.trim() === '') {
  829. updateStartEmailRequiredMarker();
  830. return;
  831. }
  832. validateStartEmail(true);
  833. });
  834. resetDataBtn.addEventListener('click', async () => {
  835. if (!state.email) {
  836. setFeedback('Keine aktive E-Mail vorhanden.', true);
  837. return;
  838. }
  839. const confirmed = window.confirm('Alle gespeicherten Daten zu dieser E-Mail endgültig löschen und neu starten?');
  840. if (!confirmed) {
  841. return;
  842. }
  843. try {
  844. await resetSavedData(state.email);
  845. stopAutosave();
  846. wizardSection.classList.add('hidden');
  847. clearWizardData();
  848. unlockEmail(true);
  849. setFeedback('Alle gespeicherten Daten wurden gelöscht. Sie können neu starten.', false);
  850. startEmailInput.focus();
  851. } catch (err) {
  852. const msg = (err.payload && err.payload.message) || err.message || 'Löschen fehlgeschlagen.';
  853. setFeedback(msg, true);
  854. }
  855. });
  856. prevBtn.addEventListener('click', async () => {
  857. if (state.currentStep <= 1 || state.isSubmitting) {
  858. return;
  859. }
  860. try {
  861. if (state.currentStep <= state.totalSteps) {
  862. await saveDraft(false);
  863. }
  864. state.currentStep -= 1;
  865. updateProgress();
  866. } catch (err) {
  867. const msg = (err.payload && err.payload.message) || err.message;
  868. setFeedback(msg, true);
  869. }
  870. });
  871. nextBtn.addEventListener('click', async () => {
  872. if (state.currentStep >= state.summaryStep || state.isSubmitting) {
  873. return;
  874. }
  875. try {
  876. await saveDraft(false);
  877. state.currentStep += 1;
  878. updateProgress();
  879. } catch (err) {
  880. const msg = (err.payload && err.payload.message) || err.message;
  881. setFeedback(msg, true);
  882. }
  883. });
  884. submitBtn.addEventListener('click', async () => {
  885. if (state.isSubmitting) {
  886. return;
  887. }
  888. renderSummary();
  889. if (state.summaryMissingCount > 0) {
  890. setFeedback('Bitte zuerst alle rot markierten Pflichtfelder ausfüllen.', true);
  891. return;
  892. }
  893. try {
  894. await submitApplication();
  895. } catch (err) {
  896. const payload = err.payload || {};
  897. if (payload.errors) {
  898. showErrors(payload.errors);
  899. }
  900. const msg = payload.message || err.message || 'Absenden fehlgeschlagen.';
  901. setFeedback(msg, true);
  902. if (payload.already_submitted) {
  903. wizardSection.classList.add('hidden');
  904. setDraftStatus('Antrag bereits abgeschlossen', false);
  905. }
  906. }
  907. });
  908. function initializeAfterDisclaimer() {
  909. disclaimerSection.classList.add('hidden');
  910. startSection.classList.remove('hidden');
  911. const rememberedEmail = normalizeEmail(getRememberedEmail());
  912. if (rememberedEmail !== '' && isValidEmail(rememberedEmail)) {
  913. startEmailInput.value = rememberedEmail;
  914. setFeedback('', false);
  915. startProcess(rememberedEmail);
  916. }
  917. }
  918. if (disclaimerReadCheckbox) {
  919. disclaimerReadCheckbox.addEventListener('change', updateDisclaimerAcceptanceState);
  920. }
  921. if (acceptDisclaimerBtn) {
  922. acceptDisclaimerBtn.addEventListener('click', () => {
  923. if (!disclaimerReadCheckbox || !disclaimerReadCheckbox.checked) {
  924. setDisclaimerError('Bitte lesen und bestätigen Sie den Hinweis.');
  925. return;
  926. }
  927. setDisclaimerError('');
  928. initializeAfterDisclaimer();
  929. });
  930. }
  931. updateDisclaimerAcceptanceState();
  932. disclaimerSection.classList.remove('hidden');
  933. startSection.classList.add('hidden');
  934. initUploadControls();
  935. initRequiredMarkerTracking();
  936. refreshRequiredMarkers();
  937. updateStartEmailRequiredMarker();
  938. updateProgress();
  939. })();