/** * Bestellungh Form Validation with Zod * For phone + tariff orders with ID card verification * Extends bestellung-validation.js with handy-specific fields */ console.log('📦 bestellungh-validation.js loading...'); let z; // Zod module // Load Zod from CDN async function initZod() { if (typeof z !== 'undefined') return; try { const zodModule = await import('https://cdn.jsdelivr.net/npm/zod@3.22.4/+esm'); z = zodModule.z; console.log('✓ Zod loaded'); return true; } catch (error) { console.error('✗ Failed to load Zod:', error); throw error; } } // Helper function for onlyLetterNumber validation function validateOnlyLetterNumber(str) { return /^[0-9a-zA-ZäüöÄÜÖßs-]+$/.test(str); } // Helper function for onlyNumber validation function validateOnlyNumber(str) { return /^[0-9]+$/.test(str); } // Define validation schema for phone+tariff orders function getBestellunghSchema() { return z.object({ anrede: z.string().min(1, '* Bitte wählen Sie eine Anrede'), vorname: z.string().min(1, '* Vorname erforderlich'), nachname: z.string().min(1, '* Nachname erforderlich'), strasse: z.string().min(1, '* Straße erforderlich'), haus_nr: z.string().min(1, '* Hausnummer erforderlich'), plz: z.string().min(1, '* PLZ erforderlich').refine(v => validateOnlyNumber(v) && v.length >= 5, '* PLZ muss mindestens 5 Ziffern sein'), ort: z.string().min(1, '* Ort erforderlich'), geburtstag: z.string().refine(v => v !== '-1' && v !== '', '* Geburtstag erforderlich'), geburtsmonat: z.string().refine(v => v !== '-1' && v !== '', '* Geburtsmonat erforderlich'), geburtsjahr: z.string().refine(v => v !== '-1' && v !== '', '* Geburtsjahr erforderlich'), telnr: z.string().min(1, '* Telefonnummer erforderlich').regex(/^([\+][0-9]{1,3}[ \.\-])?([\(]{1}[0-9]{1,6}[\)])?([0-9 \.\-\/]{3,20})((x|ext|extension)[ ]?[0-9]{1,4})?$/, '* Ungültige Telefonnummer'), mail: z.string().min(1, '* E-Mail-Adresse erforderlich'), bankname: z.string().min(1, '* Bankname erforderlich'), kontonr: z.string().min(1, '* Kontonummer erforderlich'), blz: z.string().min(1, '* Bankleitzahl erforderlich').refine(v => validateOnlyLetterNumber(v), '* Nur Buchstaben und Zahlen erlaubt'), // ===== PHONE-SPECIFIC FIELDS ===== ausweisart: z.string().refine(v => v !== '' && v !== '-1', '* Ausweis-Typ erforderlich'), nationality: z.string().refine(v => v !== '' && v !== '-1', '* Nationalität erforderlich'), idcardnr: z.string().min(1, '* Ausweis-Nummer erforderlich'), pbistag: z.string().refine(v => v !== '-1' && v !== '', '* Gültig bis: Tag erforderlich'), pbismonat: z.string().refine(v => v !== '-1' && v !== '', '* Gültig bis: Monat erforderlich'), pbisjahr: z.string().refine(v => v !== '-1' && v !== '', '* Gültig bis: Jahr erforderlich'), // ===== MNP & PASSWORD ===== p_art: z.string().min(1, '* Bitte wählen Sie eine Aktivierungsmöglichkeit'), passwort: z.string().optional(), // Optional - only required if NOT MNP mnp: z.string().optional(), p_msisdn: z.string().default(''), p_provider: z.string().default(''), p_kundennr: z.string().optional(), // ===== CHECKBOXES ===== agb: z.string().refine(v => v === '1', '* Bitte akzeptieren Sie die AGB'), sepa: z.string().refine(v => v === '1', '* Bitte akzeptieren Sie das SEPA-Mandat'), datenschutzklausel: z.string().refine(v => v === '1', '* Bitte akzeptieren Sie die Datenschutzerklärung'), widerrufsbelehrung: z.string().refine(v => v === '1', '* Bitte akzeptieren Sie die Widerrufsbelehrung'), }) // Password REQUIRED only if NOT MNP (new contract needs password) .refine((data) => { const isMNP = data.mnp === 'ja'; if (!isMNP && (!data.passwort || data.passwort.trim() === '')) { return false; } return true; }, { message: '* Hotline-Passwort erforderlich (8-20 Zeichen)', path: ['passwort'] }) // Password length validation .refine((data) => { if (!data.passwort || data.passwort.trim() === '') return true; // Empty is ok if MNP const pwd = data.passwort.trim(); return pwd.length >= 8 && pwd.length <= 20; }, { message: '* Passwort muss 8-20 Zeichen lang sein', path: ['passwort'] }) // MNP Mobilfunknummer - REQUIRED wenn MNP aktiviert .refine((data) => { const isMNP = data.mnp === 'ja'; if (isMNP && (!data.p_msisdn || data.p_msisdn.trim() === '')) { return false; } return true; }, { message: '* Mobilfunknummer erforderlich (Rufnummernmitnahme)', path: ['p_msisdn'] }) // MNP Provider - REQUIRED wenn MNP aktiviert .refine((data) => { const isMNP = data.mnp === 'ja'; if (isMNP && (!data.p_provider || data.p_provider === '' || data.p_provider === '-1')) { return false; } return true; }, { message: '* Mobilfunkanbieter erforderlich (Rufnummernmitnahme)', path: ['p_provider'] }); } // Get form data function getFormData() { const form = document.getElementById('order') || document.querySelector('form[name="order"]'); if (!form) return {}; const formData = new FormData(form); const data = Object.fromEntries(formData); // Fix checkbox values data.agb = document.getElementById('agb')?.checked ? '1' : ''; data.sepa = document.getElementById('sepa')?.checked ? '1' : ''; data.datenschutzklausel = document.getElementById('datenschutzklausel')?.checked ? '1' : ''; data.widerrufsbelehrung = document.getElementById('widerrufsbelehrung')?.checked ? '1' : ''; data.mnp = document.getElementById('rufnummernportierung')?.checked ? 'ja' : ''; // Ensure MNP fields are set (even if empty) data.p_msisdn = document.getElementById('p_msisdn')?.value || ''; data.p_provider = document.getElementById('p_provider')?.value || ''; data.p_kundennr = document.getElementById('p_kundennr')?.value || ''; data.passwort = document.getElementById('passwort')?.value || ''; // ID Card fields data.ausweisart = document.getElementById('ausweisart')?.value || ''; data.nationality = document.getElementById('nationality')?.value || ''; data.idcardnr = document.getElementById('idcardnr')?.value || ''; data.pbistag = document.getElementById('pbistag')?.value || '-1'; data.pbismonat = document.getElementById('pbismonat')?.value || '-1'; data.pbisjahr = document.getElementById('pbisjahr')?.value || '-1'; return data; } // Validate form async function validateBestellungh(formData) { const schema = getBestellunghSchema(); const result = schema.safeParse(formData); const errors = {}; if (!result.success) { result.error.issues.forEach(issue => { const path = issue.path[0] || 'unknown'; errors[path] = issue.message; }); } // Manual MNP validation (not handled by refine) const isMNP = formData.mnp === 'ja'; if (isMNP) { if (!formData.p_msisdn || formData.p_msisdn.trim() === '') { errors['p_msisdn'] = '* Mobilfunknummer erforderlich (Rufnummernmitnahme)'; } if (!formData.p_provider || formData.p_provider === '' || formData.p_provider === '-1') { errors['p_provider'] = '* Mobilfunkanbieter erforderlich (Rufnummernmitnahme)'; } } if (Object.keys(errors).length === 0) { return { valid: true, errors: {} }; } return { valid: false, errors }; } // Show error in container (pure text, no DOM manipulation) function showError(containerId, message, fieldId) { const container = document.getElementById(containerId); const field = document.getElementById(fieldId); if (container) { container.textContent = message; container.classList.add('show'); } if (field) { field.setAttribute('aria-invalid', 'true'); field.classList.remove('is-valid'); } } // Clear error function clearError(containerId, fieldId) { const container = document.getElementById(containerId); const field = document.getElementById(fieldId); if (container) { container.textContent = ''; container.classList.remove('show'); } if (field) { field.setAttribute('aria-invalid', 'false'); field.classList.add('is-valid'); } } // Clear all errors function clearAllErrors() { document.querySelectorAll('.error-container').forEach(el => { el.textContent = ''; el.classList.remove('show'); }); document.querySelectorAll('[aria-invalid="true"]').forEach(field => { field.setAttribute('aria-invalid', 'false'); field.classList.remove('is-valid'); }); } // Initialize validation async function initFormValidation() { console.log('🚀 Initializing Zod validation for bestellungh...'); await initZod(); const form = document.getElementById('order') || document.querySelector('form[name="order"]'); if (!form) { console.error('❌ Form not found'); return; } console.log('✓ Form found'); // Find submit button const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); if (!submitButton) { console.error('❌ Submit button not found'); return; } // Submit button click handler submitButton.addEventListener('click', async (e) => { console.log('📝 Submit button clicked - validating...'); e.preventDefault(); e.stopPropagation(); clearAllErrors(); const formData = getFormData(); const { valid, errors } = await validateBestellungh(formData); if (valid) { console.log('✅ Validation PASSED!'); // Submit form natively (bypasses validation loop) setTimeout(() => { HTMLFormElement.prototype.submit.call(form); }, 0); } else { console.log('❌ Validation FAILED:', Object.keys(errors).length, 'errors'); console.log('Errors:', errors); // Show errors Object.entries(errors).forEach(([fieldName, message]) => { const containerId = `${fieldName}-error`; console.log(`Showing error for ${fieldName}: "${message}" in container ${containerId}`); showError(containerId, message, fieldName); }); // Scroll to first error const firstError = Object.keys(errors)[0]; const firstField = document.getElementById(firstError); if (firstField) { firstField.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstField.focus(); } } }); console.log('✓ Validation handler attached'); // Live validation on blur/change const validationFields = [ 'vorname', 'nachname', 'strasse', 'haus_nr', 'plz', 'ort', 'geburtstag', 'geburtsmonat', 'geburtsjahr', 'telnr', 'mail', 'bankname', 'kontonr', 'blz', 'p_msisdn', 'p_provider', 'passwort', 'ausweisart', 'nationality', 'idcardnr', 'pbistag', 'pbismonat', 'pbisjahr' ]; validationFields.forEach(fieldName => { const field = document.getElementById(fieldName); if (!field) return; field.addEventListener('blur', async () => { const formData = getFormData(); const { valid, errors } = await validateBestellungh(formData); const containerId = `${fieldName}-error`; if (errors[fieldName]) { showError(containerId, errors[fieldName], fieldName); } else { clearError(containerId, fieldName); } }); field.addEventListener('change', async () => { const formData = getFormData(); const { valid, errors } = await validateBestellungh(formData); const containerId = `${fieldName}-error`; if (errors[fieldName]) { showError(containerId, errors[fieldName], fieldName); } else { clearError(containerId, fieldName); } }); }); console.log('✓ Live validation attached to', validationFields.length, 'fields'); } // Start on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFormValidation); } else { initFormValidation(); }