import { useState, useRef, useEffect } from "react"; import { useIsMobile } from "@/spa/hooks/useIsMobile"; // ─── CONSTANTS ──────────────────────────────────────────────────────────────── const NAVY = "#111D55"; const GOLD = "#EFBF04"; const CREAM = "#EFE5E3"; const BLUE = "#4A8FC9"; const WHITE = "#fff"; const TIERS = [ { max: 10, label: "bis 10", annual: 480 }, { max: 20, label: "bis 20", annual: 600 }, { max: 50, label: "bis 50", annual: 900 }, { max: 100, label: "bis 100", annual: 1200 }, { max: 250, label: "bis 250", annual: 2400 }, { max: 1000, label: "bis 1.000", annual: 3600 }, ]; const VAT = 0.19; const ADMISSION = 500; const STEPS = [ { id: 1, label: "Unternehmen" }, { id: 2, label: "Kontakt" }, { id: 3, label: "Details" }, { id: 4, label: "Zahlung" }, { id: 5, label: "Abschluss" }, ]; // ─── TYPES ──────────────────────────────────────────────────────────────────── interface FormData { firmenname: string; rechtsform: string; strasse: string; plz: string; ort: string; mitarbeiter: string; website: string; anrede: string; vorname: string; nachname: string; position: string; email: string; telefon: string; eintrittsdatum: string; aufmerksam: string; motivation: string; kontoinhaber: string; iban: string; bic: string; bank: string; sepaMandat: boolean; datenschutz: boolean; satzung: boolean; widerruf: boolean; newsletter: boolean; websitePub: boolean; } type Errors = Partial>; // ─── HELPERS ────────────────────────────────────────────────────────────────── function getFirstOfNextMonth(): string { const d = new Date(); d.setMonth(d.getMonth() + 1, 1); return d.toISOString().split("T")[0]; } function formatEur(n: number): string { return n.toLocaleString("de-DE", { style: "currency", currency: "EUR" }); } function calcBeitrag(mitarbeiter: string) { const tier = TIERS.find((t) => t.label === mitarbeiter) ?? TIERS[0]; const annualNet = tier.annual; const annualGross = annualNet * (1 + VAT); const admissionGross = ADMISSION * (1 + VAT); const today = new Date(); const monthsLeft = 12 - today.getMonth(); const anteilig = (annualGross / 12) * monthsLeft + admissionGross; return { annualNet, annualGross, admissionGross, anteilig }; } // Basic IBAN mod-97 check (DE format, 22 chars) function validateIBAN(raw: string): boolean { const iban = raw.replace(/\s/g, "").toUpperCase(); if (!/^DE\d{20}$/.test(iban)) return false; const rearranged = iban.slice(4) + iban.slice(0, 4); const numeric = rearranged.split("").map((c) => { const code = c.charCodeAt(0); return code >= 65 ? (code - 55).toString() : c; }).join(""); let remainder = 0; for (let i = 0; i < numeric.length; i++) { remainder = (remainder * 10 + parseInt(numeric[i])) % 97; } return remainder === 1; } function maskIBAN(iban: string): string { const clean = iban.replace(/\s/g, ""); if (clean.length < 8) return clean; return clean.slice(0, 4) + " **** **** **** " + clean.slice(-4); } function validateStep(step: number, data: FormData): Errors { const e: Errors = {}; if (step === 1) { if (!data.firmenname.trim()) e.firmenname = "Pflichtfeld"; if (!data.rechtsform) e.rechtsform = "Bitte wählen"; if (!data.strasse.trim()) e.strasse = "Pflichtfeld"; if (!/^\d{5}$/.test(data.plz)) e.plz = "5-stellige PLZ erforderlich"; if (!data.ort.trim()) e.ort = "Pflichtfeld"; if (!data.mitarbeiter) e.mitarbeiter = "Bitte wählen"; } if (step === 2) { if (!data.anrede) e.anrede = "Bitte wählen"; if (!data.vorname.trim()) e.vorname = "Pflichtfeld"; if (!data.nachname.trim()) e.nachname = "Pflichtfeld"; if (!data.position.trim()) e.position = "Pflichtfeld"; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) e.email = "Gültige E-Mail erforderlich"; if (!data.telefon.trim()) e.telefon = "Pflichtfeld"; } if (step === 3) { if (!data.eintrittsdatum) e.eintrittsdatum = "Pflichtfeld"; if (!data.aufmerksam) e.aufmerksam = "Bitte wählen"; } if (step === 4) { if (!data.kontoinhaber.trim()) e.kontoinhaber = "Pflichtfeld"; if (!validateIBAN(data.iban)) e.iban = "Ungültige IBAN (DE, 22 Zeichen)"; if (!data.bic.trim()) e.bic = "Pflichtfeld"; if (!data.bank.trim()) e.bank = "Pflichtfeld"; if (!data.sepaMandat) e.sepaMandat = "Bitte SEPA-Mandat bestätigen"; } if (step === 5) { if (!data.datenschutz) e.datenschutz = "Pflichtfeld"; if (!data.satzung) e.satzung = "Pflichtfeld"; if (!data.widerruf) e.widerruf = "Pflichtfeld"; } return e; } // ─── SUB-COMPONENTS ─────────────────────────────────────────────────────────── function FieldError({ msg }: { msg?: string }) { if (!msg) return null; return ( ⚡ {msg} ); } const fieldBase: React.CSSProperties = { width: "100%", background: "rgba(255,255,255,0.07)", border: "1px solid rgba(255,255,255,0.15)", borderRadius: 8, color: WHITE, fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 14, padding: "11px 14px", outline: "none", transition: "border-color 0.2s, background 0.2s", boxSizing: "border-box", }; function Field({ label, required, error, children, }: { label: string; required?: boolean; error?: string; children: React.ReactNode; }) { return (
{children}
); } function Input({ value, onChange, placeholder, type = "text", error, onFocus, onBlur, maxLength, }: { value: string; onChange: (v: string) => void; placeholder?: string; type?: string; error?: string; onFocus?: () => void; onBlur?: () => void; maxLength?: number; }) { const [focused, setFocused] = useState(false); const isMobile = useIsMobile(); return ( onChange(e.target.value)} onFocus={() => { setFocused(true); onFocus?.(); }} onBlur={() => { setFocused(false); onBlur?.(); }} style={{ ...fieldBase, // ≥16px on mobile prevents iOS auto-zoom on focus fontSize: isMobile ? 16 : fieldBase.fontSize, borderColor: error ? GOLD : focused ? "rgba(239,191,4,0.6)" : "rgba(255,255,255,0.15)", background: focused ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.07)", }} /> ); } function Select({ value, onChange, options, placeholder, error, }: { value: string; onChange: (v: string) => void; options: { value: string; label: string }[]; placeholder?: string; error?: string; }) { const [focused, setFocused] = useState(false); const isMobile = useIsMobile(); return ( ); } function Textarea({ value, onChange, placeholder, maxLength, error, }: { value: string; onChange: (v: string) => void; placeholder?: string; maxLength?: number; error?: string; }) { const [focused, setFocused] = useState(false); const isMobile = useIsMobile(); return (