// Boekingswizard — 4-stap, gewired aan API function Boeking({ onNav }) { const [step, setStep] = React.useState(1); const [service, setService] = React.useState(null); // Pre-vul kenteken + voertuig vanuit home (sessionStorage) const [plate, setPlate] = React.useState(() => { try { return sessionStorage.getItem('gd_plate') || ""; } catch (_) { return ""; } }); const [vehicle, setVehicle] = React.useState(() => { try { const v = sessionStorage.getItem('gd_vehicle'); return v ? JSON.parse(v) : null; } catch (_) { return null; } }); const [mileage, setMileage] = React.useState(""); const [userVehicles, setUserVehicles] = React.useState([]); // opgeslagen voertuigen van ingelogde klant const [selectedVehicleId, setSelectedVehicleId] = React.useState(null); // gekozen opgeslagen voertuig const [selDay, setSelDay] = React.useState(null); const [selTime, setSelTime] = React.useState(null); const [form, setForm] = React.useState({ name: "", email: "", phone: "", address: "", notes: "", haalbreng: false, vervangend: false, terms: false, }); const [confirmed, setConfirmed] = React.useState(false); const [submitError, setSubmitError] = React.useState(null); const [submitting, setSubmitting] = React.useState(false); const [services, setServices] = React.useState([]); const [slots, setSlots] = React.useState([]); // Fallback iconen per service slug const ICON_BY_SLUG = { apk: I.Apk, "apk-combi": I.Apk, herkeuring: I.Apk, "beurt-klein": I.Wrench, "beurt-groot": I.Cog, "remmen-voor": I.Cog, "remmen-set": I.Cog, distributie: I.Cog, diagnose: I.Diag, "diag-oem": I.Diag, "diag-mech": I.Diag, reparatie: I.Cog, "banden-wis": I.Tire, "banden-mont": I.Tire, "banden-opsl": I.Tire, airco: I.Snow, "airco-lek": I.Snow, uitlaat: I.Cog, accu: I.Battery, ruit: I.ShieldCheck, }; // Diensten ophalen bij mount React.useEffect(() => { if (!window.api) return; window.api.services() .then(d => { const top = d.services.filter(s => s.duration_min > 0).slice(0, 8).map(s => ({ id: s.id, slug: s.slug, name: s.name, duration: s.duration_min, price: s.price, desc: s.description, icon: ICON_BY_SLUG[s.slug] || I.Wrench, })); setServices(top); }) .catch(() => {/* fallback to mock below */}); // Opgeslagen voertuigen van ingelogde klant ophalen (faalt stil als niet ingelogd) window.api.auth.me() .then(d => { const vs = d.vehicles || []; setUserVehicles(vs); if (vs.length && !plate) { // Auto-select het primaire (eerste) voertuig setSelectedVehicleId(vs[0].id); setPlate(vs[0].license_plate); setVehicle({ make: vs[0].make, model: vs[0].model, year: vs[0].year, fuel_type: vs[0].fuel_type, apk_expiry: vs[0].apk_expiry, }); // Naam/email/telefoon ook prefillen voor stap 4 setForm(f => ({ ...f, name: d.user.name || f.name, email: d.user.email || f.email, phone: d.user.phone || f.phone })); } }) .catch(() => {/* niet ingelogd, dat mag */}); }, []); // Mock fallback als API niet werkt const fallbackServices = [ { id: 1, slug: "apk", icon: I.Apk, name: "APK keuring", duration: 45, price: 55, desc: "RDW erkend. Inclusief uitgebreid rapport." }, { id: 4, slug: "beurt-klein", icon: I.Wrench, name: "Kleine beurt", duration: 60, price: 89, desc: "Olie + filter + 30-punts inspectie." }, { id: 5, slug: "beurt-groot", icon: I.Cog, name: "Grote beurt", duration: 180, price: 249, desc: "Volledig onderhoud." }, { id: 16, slug: "airco", icon: I.Snow, name: "Airco service", duration: 30, price: 69, desc: "F-gassen — R134a + R1234yf." }, { id: 13, slug: "banden-wis", icon: I.Tire, name: "Banden wisselen (4)", duration: 30, price: 40, desc: "Op velg." }, { id: 9, slug: "diagnose", icon: I.Diag, name: "Diagnose / uitleescode", duration: 30, price: 45, desc: "Universeel." }, { id: 12, slug: "reparatie", icon: I.Cog, name: "Reparatie / anders", duration: null, price: null, desc: "Op maat." }, ]; const displayServices = services.length ? services : fallbackServices; // Vehicle lookup wanneer plate >= 6 React.useEffect(() => { const clean = plate.replace(/[^A-Z0-9]/gi, ""); if (clean.length < 6 || !window.api) { return; } const t = setTimeout(async () => { try { const v = await window.api.kenteken.lookup(plate); setVehicle(v); } catch (e) { setVehicle(null); } }, 400); return () => clearTimeout(t); }, [plate]); // Vrije slots ophalen wanneer dag + service gekozen React.useEffect(() => { if (!service || !selDay || !window.api) { setSlots([]); return; } const dateStr = selDay instanceof Date ? selDay.toISOString().slice(0, 10) : selDay; window.api.availability(service.id, dateStr) .then(d => setSlots(d.slots || [])) .catch(() => setSlots([])); }, [service, selDay]); const goNext = () => setStep(s => Math.min(s + 1, 4)); const goBack = () => setStep(s => Math.max(s - 1, 1)); // Validation per step const canNext = ( (step === 1 && service) || (step === 2 && plate.length >= 6) || (step === 3 && selDay && selTime) || (step === 4 && form.name && form.email && form.phone && form.terms) ); return (
{/* Heading band */}
Afspraak maken

{confirmed ? "Bevestigd, tot ziens." : "Plan uw bezoek in 4 stappen."}

{!confirmed && (
Duurt < 60 seconden
)}
{/* Stepper */} {!confirmed && }
{confirmed ? ( ) : (
{/* Left: step content */}
{step === 1 && ( )} {step === 2 && ( )} {step === 3 && ( )} {step === 4 && ( )} {/* Nav buttons */}
{submitError && (
{submitError}
)}
{/* Right: live summary */}
)}
); } function Stepper({ step }) { const labels = ["Dienst", "Voertuig", "Tijdstip", "Gegevens"]; return (
{labels.map((l, i) => { const n = i + 1; const done = n < step; const cur = n === step; return (
{done ? : n}
Stap {n}
{l}
{n < 4 && (
)}
); })}
); } // ─── STEP 1 ───────────────────────────────────────────── function Step1({ services, value, onChange }) { return (

Welke service heeft u nodig?

Niet zeker? Kies "Reparatie / anders" en beschrijf het probleem.

{services.map(s => { const sel = value?.id === s.id; return ( ); })}
); } // ─── STEP 2 ───────────────────────────────────────────── function Step2({ plate, setPlate, vehicle, setVehicle, mileage, setMileage, userVehicles = [], selectedVehicleId, setSelectedVehicleId }) { // "Ander voertuig" mode — toont kenteken-input ook al heeft de klant opgeslagen voertuigen const [otherMode, setOtherMode] = React.useState(false); const showSavedPicker = userVehicles.length > 0 && !otherMode; // Een opgeslagen voertuig kiezen const pickSaved = (v) => { setSelectedVehicleId(v.id); setPlate(v.license_plate); setVehicle({ make: v.make, model: v.model, year: v.year, fuel_type: v.fuel_type, apk_expiry: v.apk_expiry }); setMileage(v.mileage ? String(v.mileage) : ""); }; // Schakelen naar "ander voertuig" — wis selectie zodat kenteken-input opent const switchToOther = () => { setOtherMode(true); setSelectedVehicleId(null); setPlate(""); setVehicle(null); setMileage(""); }; // RDW lookup wanneer in "ander voertuig" mode of niet ingelogd React.useEffect(() => { if (selectedVehicleId) return; // niet looken-up als opgeslagen voertuig actief is const clean = plate.replace(/[^A-Z0-9]/gi, ""); if (clean.length < 6) { setVehicle(null); return; } if (!window.api) return; const t = setTimeout(async () => { try { const v = await window.api.kenteken.lookup(plate); setVehicle(v); } catch (e) { setVehicle(null); } }, 400); return () => clearTimeout(t); }, [plate, selectedVehicleId]); return (

Welk voertuig?

{showSavedPicker ? "Kies een opgeslagen voertuig of voeg er een toe via kenteken." : "We halen de auto-gegevens automatisch op via de RDW."}

{showSavedPicker && (
{userVehicles.map(v => { const sel = selectedVehicleId === v.id; const apkDate = v.apk_expiry ? new Date(v.apk_expiry) : null; return ( ); })}
)} {!showSavedPicker && userVehicles.length > 0 && ( )}
NL
setPlate(e.target.value.toUpperCase())} placeholder="X-123-YZ" className="plate-input" style={{ paddingLeft: 60 }} maxLength={10} />
{/* RDW result */}
{plate.length < 6 ? (
voer kenteken in voor auto-detectie
) : !vehicle ? (
RDW raadplegen…
) : (
{[vehicle.make || vehicle.merk, vehicle.model].filter(Boolean).join(' ')}
{vehicle.color || vehicle.kleur || ''}
Gevonden
)}
{vehicle && (
setMileage(e.target.value)}/>
)}
); } function Spec({ label, value, accent }) { return (
{label}
{value}
); } // ─── STEP 3 ───────────────────────────────────────────── function Step3({ service, selDay, setSelDay, selTime, setSelTime }) { const today = new Date(2026, 4, 4); // 4 mei 2026 (maandag) const [monthOffset, setMonthOffset] = React.useState(0); // Build month grid const month = new Date(today.getFullYear(), today.getMonth() + monthOffset, 1); const monthName = month.toLocaleDateString("nl-NL", { month: "long", year: "numeric" }); const firstDow = (month.getDay() + 6) % 7; // Mon=0 const daysInMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate(); // Random availability stable per date const availabilityFor = (d) => { if (!d) return "none"; const date = new Date(month.getFullYear(), month.getMonth(), d); if (date < today) return "past"; const dow = date.getDay(); if (dow === 0) return "closed"; // Sunday if (date.toDateString() === new Date(2026, 4, 5).toDateString()) return "closed"; // Bevrijdingsdag const seed = (d * 7 + month.getMonth() * 13) % 10; if (seed < 2) return "full"; if (seed < 5) return "limited"; return "free"; }; // Slots for selected day const slotsFor = (date) => { if (!date) return []; const all = ["08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "13:00", "13:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00"]; const seed = (date * 31) % 10; return all.map((s, i) => ({ time: s, free: ((i + seed) % 4) !== 0, })); }; const cells = []; for (let i = 0; i < firstDow; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); while (cells.length < 42) cells.push(null); return (

Wanneer komt het uit?

Geblokte tijden zijn al volgeboekt. Slot duurt {service?.duration || 60} min — buffer inclusief.

{/* Calendar */}
{monthName}
{["Ma","Di","Wo","Do","Vr","Za","Zo"].map(d => (
{d}
))} {cells.map((d, i) => { if (!d) return
; const av = availabilityFor(d); const isSel = selDay === d && monthOffset === 0; const disabled = av === "past" || av === "full" || av === "closed" || av === "none"; const colorMap = { free: "var(--success)", limited: "var(--warning)", full: "var(--text-muted)", closed: "var(--text-muted)", past: "var(--text-muted)", }; return ( ); })}
{/* Time slots */}
{selDay ? `${selDay} ${monthName}` : "Kies eerst een dag"}
{selDay ? (
{slotsFor(selDay).map((s, i) => ( ))}
) : (
kies een dag
)}
); } function Legend({ color, border, label }) { return ( {label} ); } // ─── STEP 4 ───────────────────────────────────────────── function Step4({ form, setForm }) { const set = (k, v) => setForm(f => ({ ...f, [k]: v })); return (

Uw gegevens

We sturen een bevestiging per e-mail en SMS-reminder 24u vooraf.

set("name", e.target.value)}/>
set("email", e.target.value)}/>
set("phone", e.target.value)}/>
set("address", e.target.value)}/>