/* global React, Icon, LangSwitch */
const { useState: useS } = React;

/* ============================ helpers ============================ */
/* EMAIL_RE, emailValid, formatPhone en phoneValid staan in js/api.js — dat
 * script wordt eerder ingeladen en zet ze als globale functies klaar, zodat
 * formulier en payload exact dezelfde regels gebruiken. */
/**
 * Belgische ondernemingsnummers zijn 9 cijfers, met (bij oudere nummers) een
 * historische leidende 0 als 10de cijfer. Sinds het "0"-bereik opraakte
 * geeft de KBO ook nummers uit die met "1" beginnen — dus tellen, niet op
 * dat ene cijfer regex-matchen (vgl. vatToKboNumber() server-side in _lib.js).
 */
function vatDigitsValid(v) {
  const digits = String(v || "").replace(/\D/g, "");
  return digits.length === 9 || digits.length === 10;
}

/**
 * "Straat + nummer" moet echt een huisnummer bevatten (Bart 2026-08-20): een
 * pas zonder nummer komt niet aan. Een cijfer alléén is niet genoeg, want
 * straatnamen kunnen ermee beginnen — "4 Windenlaan" is een straat zonder
 * nummer. Daarom eerst een leidend cijfer dat aan een letter vastzit
 * wegdenken, en dan kijken of er nog een cijfer overblijft. "Lijsterstraat 16",
 * "Lijsterstraat 16A" en "Lijsterstraat 16 bus 3" komen door.
 */
function huisnummerAanwezig(straat) {
  const s = String(straat || "").trim();
  if (!/\d/.test(s)) return false;
  return /\d/.test(s.replace(/^\s*\d+\s*(?=[A-Za-zÀ-ÿ])/, ""));
}

/**
 * Het bus-veld is optioneel en kort: "3", "B", "bus 3" of "bte 2.1" kan, een
 * tweede adres niet. De payload plakt het genormaliseerd achter het nummer in
 * het straatveld (straatMetBus in js/api.js) — zo staat het busnummer altijd
 * op de plek waar Odoo het verwacht.
 */
function busGeldig(bus) {
  return /^[A-Za-zÀ-ÿ0-9][A-Za-zÀ-ÿ0-9 .\-\/]{0,11}$/.test(String(bus || "").trim());
}

/** Belgische postcode = 4 cijfers. Buiten België laten we het vrij. */
function postcodeValid(pc, land) {
  const s = String(pc || "").trim();
  const be = !land || /belg|belgi/i.test(String(land));
  return be ? /^[1-9]\d{3}$/.test(s) : s.length > 0;
}

/* Per pasgebruiker: naam, e-mail, telefoon en zijn eigen adres. Dat adres is
 * NIET het leveradres (Bart 2026-08-20) — alle passen van een bedrijf gaan naar
 * het officiële KBO-adres. Het dient als contactadres van de gebruiker in Odoo.
 * Land blijft België. */
function emptyUser() {
  return { lastName: "", firstName: "", language: "nl", email: "", phone: "", street: "", bus: "", postcode: "", city: "" };
}
/* Bedrijf (Bart 2026-08-19): geen "ter attentie van", geen algemene of
 * facturatie-e-mail en geen bedrijfstelefoon meer — de contactpersoon is het
 * aanspreekpunt, met e-mail én telefoon. Het KBO-adres blijft het
 * bedrijfs-/facturatieadres, niet langer het leveradres van de passen. */
function emptyCompany() {
  return { name: "", vat: "", kbo: "", legalForm: "", street: "", postcode: "", city: "", country: "België", contactFirstName: "", contactLastName: "", contactEmail: "", contactPhone: "" };
}
function emptyParticulier() {
  return { lastName: "", firstName: "", language: "nl", email: "", phone: "", street: "", bus: "", postcode: "", city: "", country: "België" };
}

/* ============================ field components ============================ */
function Field({ label, required, value, onChange, error, type = "text", colSpan, placeholder, autoComplete, readOnly, onBlur, inputMode, hint }) {
  return (
    <div className={"field" + (colSpan === 2 ? " col-2" : "")}>
      <label>{label}{required && <span className="req">*</span>}</label>
      <input type={type} value={value} placeholder={placeholder || ""} autoComplete={autoComplete} inputMode={inputMode}
        readOnly={!!readOnly}
        className={(error ? "invalid" : "") + (readOnly ? " readonly" : "")}
        style={readOnly ? { background: "var(--bg-2, #f3f4f6)", color: "var(--fg-2, #555)", cursor: "not-allowed" } : undefined}
        onChange={e => !readOnly && onChange(e.target.value)}
        onBlur={onBlur} />
      {hint && !error && <span className="order-section-sub" style={{ display: "block", marginTop: 4, marginBottom: 0, fontSize: 12.5 }}>{hint}</span>}
      {error && <span className="err">{error}</span>}
    </div>
  );
}

/**
 * Telefoonveld met één vast formaat (+32 473 849 816). Tijdens het typen blijft
 * staan wat je intypt — formatteren bij elke toetsaanslag vecht met backspace —
 * en bij het verlaten van het veld wordt het genormaliseerd. De payload
 * normaliseert nog een keer, dus ook wie nooit uit het veld klikt levert
 * uniforme data.
 */
function PhoneField(props) {
  return (
    <Field
      {...props}
      type="tel"
      inputMode="tel"
      autoComplete="tel"
      placeholder="+32 473 849 816"
      onBlur={() => { const g = formatPhone(props.value); if (g && g !== props.value) props.onChange(g); }}
    />
  );
}
/* Geen taalvraag in het formulier (Bart 2026-08-18): de taal van de klant en
 * van elke pas-gebruiker is altijd de taal waarin de site bezocht wordt; die
 * wordt bij het samenstellen van de bestelling stil meegegeven. */

/**
 * Het veld "Straat + nummer" doet zelf de suggesties (Bart 2026-08-20, tweede
 * ronde). Een apart zoekveld zette postcode en gemeente twee keer op het
 * scherm; nu zijn er gewoon drie velden waarvan het laatste slim is.
 *
 * Wat je al intypte in postcode en gemeente gaat als context mee in de zoekvraag,
 * dus "8810" + "spinnekens" vindt meteen de juiste straat. Kies je een suggestie,
 * dan vullen postcode en gemeente zich (of worden ze bijgestuurd) en blijven ze
 * losse velden — de Odoo-Excels hebben postcode en gemeente apart nodig.
 *
 * Suggesties komen van onze eigen route /api/adres-suggest (bron Photon), pas
 * vanaf drie tekens en 300 ms na de laatste toetsaanslag. Toetsenbord: pijltjes,
 * Enter, Escape. Komt er niets, dan typ je gewoon verder — het veld is een
 * normaal tekstveld dat toevallig helpt.
 */
function StreetField({ t, value, error, onChange, onPick, context }) {
  const [lijst, setLijst] = useS([]);
  const [open, setOpen] = useS(false);
  const [actief, setActief] = useS(-1);
  const [bezig, setBezig] = useS(false);
  const [gezocht, setGezocht] = useS(false);
  const timer = React.useRef(null);
  const laatste = React.useRef("");
  const invoer = React.useRef(null);

  function typ(waarde) {
    onChange(waarde);
    setActief(-1);
    if (timer.current) clearTimeout(timer.current);
    if (waarde.trim().length < 3) { setLijst([]); setOpen(false); setGezocht(false); return; }
    setBezig(true);
    timer.current = setTimeout(async () => {
      // postcode en gemeente als context: dat maakt de zoekvraag veel scherper
      const vraag = [context.postcode, context.city, waarde].map(x => String(x || "").trim()).filter(Boolean).join(" ");
      laatste.current = vraag;
      const res = await window.LaadpasAPI.suggestAddress(vraag);
      if (laatste.current !== vraag) return; // antwoord van een oudere toetsaanslag
      setLijst(res);
      setOpen(true);
      setGezocht(true);
      setBezig(false);
    }, 300);
  }

  function kies(sug) {
    onPick(sug);
    setOpen(false);
    setActief(-1);
    // geen huisnummer in de suggestie? dan staat de cursor klaar om het te typen
    if (invoer.current) invoer.current.focus();
  }

  function toets(e) {
    if (!open || lijst.length === 0) return;
    if (e.key === "ArrowDown") { e.preventDefault(); setActief(i => (i + 1) % lijst.length); }
    else if (e.key === "ArrowUp") { e.preventDefault(); setActief(i => (i <= 0 ? lijst.length : i) - 1); }
    else if (e.key === "Enter") { e.preventDefault(); kies(lijst[actief >= 0 ? actief : 0]); }
    else if (e.key === "Escape") { setOpen(false); }
  }

  return (
    <div className="field col-2" style={{ position: "relative" }}>
      <label>{t.order.street}<span className="req">*</span></label>
      <input ref={invoer} type="text" value={value} autoComplete="street-address"
        className={error ? "invalid" : ""}
        onChange={e => typ(e.target.value)} onKeyDown={toets}
        onFocus={() => { if (lijst.length) setOpen(true); }} />
      {!error && (
        <span className="order-section-sub" style={{ display: "block", marginTop: 4, marginBottom: 0, fontSize: 12.5 }}>
          {bezig ? t.order.addressSearchBusy : t.order.addressSearchHint}
        </span>
      )}
      {error && <span className="err">{error}</span>}
      {open && lijst.length > 0 && (
        <ul style={{
          position: "absolute", top: "100%", left: 0, right: 0, zIndex: 5, margin: "4px 0 0", padding: 4,
          listStyle: "none", background: "#fff", border: "1.5px solid #DCE3ED", borderRadius: 10,
          boxShadow: "0 8px 24px rgba(11,45,90,0.12)", maxHeight: 240, overflowY: "auto",
        }}>
          {lijst.map((sug, i) => (
            <li key={sug.label + i}>
              <button type="button" onMouseDown={e => { e.preventDefault(); kies(sug); }}
                onMouseEnter={() => setActief(i)}
                style={{
                  display: "block", width: "100%", textAlign: "left", padding: "8px 10px", border: "none",
                  borderRadius: 7, cursor: "pointer", fontFamily: "inherit", fontSize: "0.88rem",
                  background: actief === i ? "#F1F6FC" : "transparent", color: "#12233F",
                }}>
                {sug.label}
              </button>
            </li>
          ))}
        </ul>
      )}
      {open && gezocht && lijst.length === 0 && !bezig && (
        <span className="order-section-sub" style={{ display: "block", marginTop: 2, marginBottom: 0, fontSize: 12.5 }}>
          {t.order.addressSearchNone}
        </span>
      )}
    </div>
  );
}

/* Adresvelden in invulvolgorde postcode → gemeente → straat (vraag Bart
 * 2026-08-20): zo staat het smalste veld met de meeste zekerheid vooraan en
 * kan de gemeente later automatisch volgen uit de postcode. */
function AddressFields({ data, set, errors, t, prefix }) {
  const kies = (sug) => {
    if (sug.postcode) set("postcode", sug.postcode);
    if (sug.gemeente) set("city", sug.gemeente);
    set("street", sug.straat || "");
  };
  return (
    <div className="field-grid">
      <Field label={t.order.postcode} required value={data.postcode} error={errors[prefix + "postcode"]} onChange={v => set("postcode", v)} autoComplete="postal-code" inputMode="numeric" />
      <Field label={t.order.city} required value={data.city} error={errors[prefix + "city"]} onChange={v => set("city", v)} autoComplete="address-level2" />
      <StreetField t={t} value={data.street} error={errors[prefix + "street"]}
        onChange={v => set("street", v)} onPick={kies}
        context={{ postcode: data.postcode, city: data.city }} />
      <Field label={t.order.bus} value={data.bus} error={errors[prefix + "bus"]} onChange={v => set("bus", v)} hint={t.order.busHint} />
      {/* levering enkel in België (Bart 2026-09-02): het land ligt vast */}
      <Field label={t.order.country} value={t.order.countryBe} readOnly onChange={() => {}} />
    </div>
  );
}

/* ============================ KBO lookup (bedrijf) ============================ */
function KboLookup({ t, company, setComp, status, setStatus, errors }) {
  async function lookup() {
    const vat = company.vat.trim();
    if (!vatDigitsValid(vat)) { setStatus("invalid"); return; }
    setStatus("busy");
    try {
      const c = await window.LaadpasAPI.lookupCompany(vat);
      setComp("name", c.name);
      setComp("kbo", c.kbo);
      setComp("vat", c.vat);
      setComp("legalForm", c.legal_form || "");
      setComp("street", c.street);
      setComp("postcode", c.postcode);
      setComp("city", c.city);
      setComp("country", c.country);
      setStatus("done");
    } catch (e) {
      setStatus(e.code === "vat_not_found" || e.code === "not_found" ? "notfound" : "error");
    }
  }

  return (
    <React.Fragment>
      <div className="field-grid">
        <div className="field">
          <label>{t.order.vatNumber}<span className="req">*</span></label>
          <div style={{ display: "flex", gap: 8 }}>
            <input type="text" value={company.vat} placeholder="BE0837865709"
              className={errors.vat || status === "invalid" || status === "notfound" ? "invalid" : ""}
              style={{ flex: 1 }}
              onChange={e => { setComp("vat", e.target.value.toUpperCase()); if (status !== "idle") setStatus("idle"); }}
              onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); lookup(); } }} />
            <button type="button" className="btn btn-primary" onClick={lookup} disabled={status === "busy"}
              style={{ whiteSpace: "nowrap" }}>
              {status === "busy" ? t.order.kbo.busy : t.order.kbo.lookupBtn}
            </button>
          </div>
          {errors.vat && <span className="err">{errors.vat}</span>}
          {status === "invalid" && <span className="err">{t.order.kbo.invalid}</span>}
          {status === "notfound" && <span className="err">{t.order.kbo.notFound}</span>}
          {status === "error" && <span className="err">{t.order.kbo.error}</span>}
        </div>
      </div>

      {status === "done" && (
        <div className="review-block" style={{ marginTop: 4 }}>
          <h4>✓ {t.order.kbo.foundTitle}</h4>
          <div className="review-rows">
            <div className="review-row"><span className="rk">{t.order.companyName}</span><span className="rv">{company.name}{company.legalForm ? " (" + company.legalForm + ")" : ""}</span></div>
            <div className="review-row"><span className="rk">{t.order.kboNumber}</span><span className="rv">{company.kbo}</span></div>
            <div className="review-row"><span className="rk">{t.order.companyAddress}</span><span className="rv">{company.street}, {company.postcode} {company.city}, {company.country}</span></div>
          </div>
          <p className="order-section-sub" style={{ marginTop: 10, marginBottom: 0 }}>{t.order.kbo.addressNote}</p>
        </div>
      )}
    </React.Fragment>
  );
}

/* ============================ payment info (particulier) ============================ */
function PaymentInfo({ t }) {
  return (
    <div className="info-alert" style={{ marginTop: 18 }}>
      <span className="info-alert-icon">💳</span>
      <div className="info-alert-body">
        <span className="info-alert-title">{t.order.pay.title}</span>
        <span>{t.order.pay.body}</span>
      </div>
    </div>
  );
}

/* ============================ stepper ============================ */
function Stepper({ step, labels }) {
  return (
    <div className="stepper">
      {labels.map((lab, i) => (
        <React.Fragment key={i}>
          {i > 0 && <div className={"stepper-line" + (step > i ? " done" : "")}></div>}
          <div className={"stepper-item" + (step === i + 1 ? " active" : "") + (step > i + 1 ? " done" : "")}>
            <span className="stepper-dot">{step > i + 1 ? <Icon.check style={{ width: 15, height: 15, stroke: "#fff" }} /> : (i + 1)}</span>
            <span className="stepper-label">{lab}</span>
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}

/* ============================ main order flow ============================ */
function OrderFlow({ t, lang, setLang, onBack }) {
  const [step, setStep] = useS(1);
  const [type, setType] = useS(null); // 'particulier' | 'bedrijf'
  const [particulier, setParticulier] = useS(emptyParticulier);
  const [company, setCompany] = useS(emptyCompany);
  const [users, setUsers] = useS([emptyUser()]);
  const [agree, setAgree] = useS(false);
  const [promo, setPromo] = useS("");
  const [sepa, setSepa] = useS(false);
  const [sepaErr, setSepaErr] = useS(false);
  const [errors, setErrors] = useS({});
  const [termsErr, setTermsErr] = useS(false);
  const [done, setDone] = useS(false);
  const [submitting, setSubmitting] = useS(false);
  const [submitError, setSubmitError] = useS(null);
  // KBO-lookup: 'idle' | 'busy' | 'done' | 'invalid' | 'notfound' | 'error'
  const [kboStatus, setKboStatus] = useS("idle");
  const [lostPass, setLostPass] = useS(false);    // particulier: reeds klant + pas verloren/defect
  // whitelist eigen laadstation (particulier): status idle|busy|ok|invalid|notfound|noteligible|error
  const [whitelist, setWhitelist] = useS({ enabled: false, serial: "", status: "idle" });
  // Cloudflare Turnstile (bot-bescherming). Alleen actief als er een publieke
  // sitekey is ingesteld (window.TURNSTILE_SITEKEY); anders blijft alles zoals
  // vroeger en verifieert de server ook niets.
  const [turnstileToken, setTurnstileToken] = useS("");
  const tsRef = React.useRef(null);
  const tsWidget = React.useRef(null);
  const TS_SITEKEY = (typeof window !== "undefined" && window.TURNSTILE_SITEKEY) || "";

  const labels =[t.order.chooseProfile.split("?")[0], type === "bedrijf" ? t.order.companyDetails : t.order.personalDetails, t.order.reviewTitle];

  function scrollTop() { window.scrollTo({ top: 0, behavior: "smooth" }); }

  /* ---- validation ---- */
  function validateDetails() {
    const e = {};
    if (type === "particulier") {
      const p = particulier;
      if (!p.lastName.trim()) e.lastName = t.order.required;
      if (!p.firstName.trim()) e.firstName = t.order.required;
      if (!p.email.trim()) e.email = t.order.required; else if (!emailValid(p.email)) e.email = t.order.invalidEmail;
      if (p.phone.trim() && !phoneValid(p.phone)) e.phone = t.order.invalidPhone;
      ["street", "postcode", "city"].forEach(k => { if (!p[k].trim()) e[k] = t.order.required; });
      // land ligt vast op België, dus de Belgische postcodecheck geldt altijd
      if (!e.postcode && !postcodeValid(p.postcode, "België")) e.postcode = t.order.invalidPostcode;
      if (!e.street && !huisnummerAanwezig(p.street)) e.street = t.order.missingHouseNumber;
      if (p.bus.trim() && !busGeldig(p.bus)) e.bus = t.order.invalidBus;
      // whitelist aangevinkt = serienummer verplicht én succesvol gecheckt
      if (!lostPass && whitelist.enabled && whitelist.status !== "ok") e.whitelist = t.order.whitelist.requiredCheck;
    } else {
      const c = company;
      if (!c.vat.trim()) e.vat = t.order.required;
      if (kboStatus !== "done") e.vat = e.vat || t.order.kbo.requiredLookup;
      if (!c.contactLastName.trim()) e.contactLastName = t.order.required;
      if (!c.contactFirstName.trim()) e.contactFirstName = t.order.required;
      if (!c.contactEmail.trim()) e.contactEmail = t.order.required; else if (!emailValid(c.contactEmail)) e.contactEmail = t.order.invalidEmail;
      if (!c.contactPhone.trim()) e.contactPhone = t.order.required; else if (!phoneValid(c.contactPhone)) e.contactPhone = t.order.invalidPhone;
      // Eén app-gebruiker per e-mailadres (Bart 2026-08-20): twee passen op
      // hetzelfde adres kunnen niet samen in de app bestaan.
      const gezien = new Map();
      users.forEach((u, i) => {
        if (!u.lastName.trim()) e["u" + i + "lastName"] = t.order.required;
        if (!u.firstName.trim()) e["u" + i + "firstName"] = t.order.required;
        const mail = u.email.trim().toLowerCase();
        if (!mail) e["u" + i + "email"] = t.order.required;
        else if (!emailValid(mail)) e["u" + i + "email"] = t.order.invalidEmail;
        else if (gezien.has(mail)) e["u" + i + "email"] = t.order.duplicateEmail;
        else gezien.set(mail, i);
        if (!u.phone.trim()) e["u" + i + "phone"] = t.order.required;
        else if (!phoneValid(u.phone)) e["u" + i + "phone"] = t.order.invalidPhone;
        // eigen adres van de gebruiker (contactadres, niet het leveradres)
        ["street", "postcode", "city"].forEach(k => { if (!u[k].trim()) e["u" + i + k] = t.order.required; });
        if (!e["u" + i + "postcode"] && !postcodeValid(u.postcode, "België")) e["u" + i + "postcode"] = t.order.invalidPostcode;
        if (!e["u" + i + "street"] && !huisnummerAanwezig(u.street)) e["u" + i + "street"] = t.order.missingHouseNumber;
        if (u.bus.trim() && !busGeldig(u.bus)) e["u" + i + "bus"] = t.order.invalidBus;
      });
    }
    setErrors(e);
    if (type === "particulier" && !lostPass && !sepa) setSepaErr(true);
    return Object.keys(e).length === 0 && (type === "bedrijf" || lostPass || sepa);
  }

  function next() {
    if (step === 1) { if (!type) return; setStep(2); scrollTop(); }
    else if (step === 2) { if (validateDetails()) { setStep(3); scrollTop(); } else { scrollTop(); } }
  }
  function prev() {
    if (step > 1) { setStep(step - 1); scrollTop(); }
    else { onBack(); }
  }

  // taal = de sitetaal, stil meegegeven voor klant en elke pas-gebruiker
  function order() {
    return {
      type,
      particulier: { ...particulier, language: lang },
      company,
      users: users.map(u => ({ ...u, language: lang })),
      promo, sepa, lostPass,
      whitelist: lostPass ? { enabled: false } : whitelist,
      turnstileToken,
    };
  }

  // Turnstile-widget renderen zodra we op de review-stap komen. Wacht tot het
  // Cloudflare-script geladen is; ruimt de widget op bij het verlaten van de stap.
  React.useEffect(() => {
    if (step !== 3 || !TS_SITEKEY) return;
    let cancelled = false;
    (function tryRender() {
      if (cancelled) return;
      if (!window.turnstile || !tsRef.current) { setTimeout(tryRender, 200); return; }
      if (tsWidget.current !== null) return;
      tsWidget.current = window.turnstile.render(tsRef.current, {
        sitekey: TS_SITEKEY,
        callback: (tok) => setTurnstileToken(tok),
        "expired-callback": () => setTurnstileToken(""),
        "error-callback": () => setTurnstileToken(""),
      });
    })();
    return () => {
      cancelled = true;
      if (tsWidget.current !== null && window.turnstile) {
        try { window.turnstile.remove(tsWidget.current); } catch (e) {}
        tsWidget.current = null;
      }
    };
  }, [step]);

  async function submit() {
    if (!agree) { setTermsErr(true); return; }
    setTermsErr(false);
    setSubmitError(null);
    if (TS_SITEKEY && !turnstileToken) {
      setSubmitError({
        nl: "Even wachten op de beveiligingscontrole en probeer opnieuw.",
        fr: "Veuillez patienter pour le contrôle de sécurité, puis réessayez.",
        en: "Please wait for the security check, then try again.",
      }[lang] || "…");
      return;
    }
    setSubmitting(true);

    try {
      const result = await window.LaadpasAPI.submitOrder(order());
      if (type === "particulier") {
        if (result.checkoutUrl) {
          // door naar de Mollie-checkout (€0,10 verificatiebetaling — zet het SEPA-mandaat op)
          window.location.href = result.checkoutUrl;
          return; // niet setSubmitting(false): we verlaten de pagina
        }
        // verloren/defecte pas: geen betaling; melding gaat mee in het dagticket
        if (lostPass) { setDone(true); scrollTop(); return; }
        throw new Error(t.order.submitError);
      }
      setDone(true);
      scrollTop();
    } catch (err) {
      const msg = {
        vat_not_found: t.order.kbo.notFound,
        company_inactive: t.order.kbo.inactive,
        duplicate_card_email: t.order.duplicateEmail,
        invalid_card_email: t.order.invalidEmail,
        email_already_has_pass: t.order.emailHasPass
          + (Array.isArray(err.emails) && err.emails.length ? " (" + err.emails.join(", ") + ")" : ""),
        country_not_belgium: t.order.countryNotBelgium,
        turnstile_failed: {
          nl: "De beveiligingscontrole is mislukt. Probeer opnieuw.",
          fr: "Le contrôle de sécurité a échoué. Veuillez réessayer.",
          en: "The security check failed. Please try again.",
        }[lang],
      }[err.code];
      // Onbekende codes (order_failed, http_500) zijn geen klantentaal: toon
      // dan de nette algemene melding; de echte fout gaat naar de console.
      console.error("[order] submit", err);
      setSubmitError(msg || t.order.submitError);
      // Token is eenmalig: reset de widget zodat een nieuwe poging een vers token krijgt.
      if (TS_SITEKEY && window.turnstile && tsWidget.current !== null) {
        try { window.turnstile.reset(tsWidget.current); } catch (e) {}
        setTurnstileToken("");
      }
    } finally {
      if (type !== "particulier") setSubmitting(false);
      else setSubmitting(false);
    }
  }

  function resetAll() {
    setStep(1); setType(null); setParticulier(emptyParticulier()); setCompany(emptyCompany());
    setUsers([emptyUser()]); setAgree(false); setPromo(""); setSepa(false); setSepaErr(false);
    setErrors({}); setTermsErr(false); setDone(false); setKboStatus("idle");
    setLostPass(false); setReedsKlant(false); setWhitelist({ enabled: false, serial: "", status: "idle" });
    setTurnstileToken("");
    scrollTop();
  }

  /* ---- user mutators ---- */
  const setUser = (i, key, val) => setUsers(us => us.map((u, j) => j === i ? { ...u, [key]: val } : u));
  const addUser = () => setUsers(us => [...us, emptyUser()]);
  const removeUser = (i) => setUsers(us => us.length > 1 ? us.filter((_, j) => j !== i) : us);
  const setComp = (key, val) => setCompany(c => ({ ...c, [key]: val }));
  const setPart = (key, val) => setParticulier(p => ({ ...p, [key]: val }));

  return (
    <div className="order-page">
      <div className="order-topbar">
        <div className="container order-topbar-inner">
          <button className="order-back" onClick={onBack}><Icon.arrowLeft />{t.order.back}</button>
          <img className="header-logo" src="assets/mobilityplus-logo.png" alt="MobilityPlus" />
          <div className="header-right"><LangSwitch lang={lang} setLang={setLang} /></div>
        </div>
      </div>

      <div className="order-body">
        {done ? (
          <Success t={t} type={type} onNew={resetAll} />
        ) : (
          <React.Fragment>
            <div className="order-head">
              <h1>{t.order.title}</h1>
              <p>{t.order.lead}</p>
            </div>
            <Stepper step={step} labels={labels} />

            {/* STEP 1 — profile */}
            {step === 1 && (
              <div className="order-card">
                <h2 className="order-section-title">{t.order.chooseProfile}</h2>
                <p className="order-section-sub">{t.order.chooseProfileSub}</p>
                <div className="profile-grid">
                  <button className={"profile-option" + (type === "particulier" ? " selected" : "")} onClick={() => setType("particulier")}>
                    <span className="po-check"><Icon.check /></span>
                    <span className="po-icon"><Icon.user /></span>
                    <span className="po-title">{t.order.particulier}</span>
                    <span className="po-sub">{t.order.particulierSub}</span>
                  </button>
                  <button className={"profile-option" + (type === "bedrijf" ? " selected" : "")} onClick={() => setType("bedrijf")}>
                    <span className="po-check"><Icon.check /></span>
                    <span className="po-icon"><Icon.building /></span>
                    <span className="po-title">{t.order.bedrijf}</span>
                    <span className="po-sub">{t.order.bedrijfSub}</span>
                  </button>
                </div>
                <div className="order-nav">
                  <button className="btn btn-ghost" onClick={onBack}><Icon.arrowLeft />{t.order.back}</button>
                  <div className="spacer"></div>
                  <button className="btn btn-primary" disabled={!type} style={{ opacity: type ? 1 : .45, pointerEvents: type ? "auto" : "none" }} onClick={next}>{t.order.next} <Icon.arrowRight /></button>
                </div>
              </div>
            )}

            {/* STEP 2 — particulier */}
            {step === 2 && type === "particulier" && (
              <div className="order-card">
                <h2 className="order-section-title">{t.order.personalDetails}</h2>
                <p className="order-section-sub">{t.order.particulierSub}</p>

                <div className="terms-row" style={{ marginBottom: lostPass ? 4 : 16 }}>
                  <input type="checkbox" id="lostpass" checked={lostPass} onChange={e => setLostPass(e.target.checked)} />
                  <label htmlFor="lostpass">{t.order.lostPassLabel}</label>
                </div>
                {lostPass && <p className="order-section-sub" style={{ marginTop: 0, marginBottom: 16 }}>{t.order.lostPassNote}</p>}

                <div className="field-grid">
                  <Field label={t.order.lastName} required value={particulier.lastName} error={errors.lastName} onChange={v => setPart("lastName", v)} autoComplete="family-name" />
                  <Field label={t.order.firstName} required value={particulier.firstName} error={errors.firstName} onChange={v => setPart("firstName", v)} autoComplete="given-name" />
                  <Field label={t.order.email} required type="email" value={particulier.email} error={errors.email} onChange={v => setPart("email", v)} autoComplete="email" />
                  <PhoneField label={t.order.phone} value={particulier.phone} error={errors.phone} onChange={v => setPart("phone", v)} />
                </div>
                <div className="field-group-title">{t.order.deliveryTitle}</div>
                <AddressFields data={particulier} set={setPart} errors={errors} t={t} prefix="" />

                {/* SEPA via de €0,10-verificatiebetaling — niet bij verloren/defecte pas (reeds klant met bestaand mandaat) */}
                {!lostPass && (
                  <React.Fragment>
                    <div className="field-group-title">{t.order.sepaTitle}</div>
                    <p className="order-section-sub" style={{ marginTop: -8 }}>{t.order.sepaSub}</p>
                    <div className={"terms-row" + (sepaErr ? " sepa-err" : "")} style={{ marginTop: 8 }}>
                      <input type="checkbox" id="sepa-part" checked={sepa} onChange={e => { setSepa(e.target.checked); if (e.target.checked) setSepaErr(false); }} />
                      <label htmlFor="sepa-part">{t.order.sepaConsent}</label>
                    </div>
                    {sepaErr && <div className="terms-error">{t.order.sepaRequired}</div>}
                    <PaymentInfo t={t} />
                  </React.Fragment>
                )}
                {!lostPass && <WhitelistField t={t} wl={whitelist} setWl={setWhitelist} error={errors.whitelist} />}
                {!lostPass && <PromoField t={t} value={promo} onChange={setPromo} />}
                <NavButtons t={t} onPrev={prev} onNext={next} />
              </div>
            )}

            {/* STEP 2 — bedrijf */}
            {step === 2 && type === "bedrijf" && (
              <React.Fragment>
                <div className="order-card">
                  <h2 className="order-section-title">{t.order.companyDetails}</h2>
                  <p className="order-section-sub">{t.order.kbo.intro}</p>

                  <KboLookup t={t} company={company} setComp={setComp} status={kboStatus} setStatus={setKboStatus} errors={errors} />

                  {kboStatus === "done" && (
                    <React.Fragment>
                      <div className="field-group-title" style={{ marginTop: 18 }}>{t.order.contactPerson}</div>
                      <p className="order-section-sub" style={{ marginTop: -10, marginBottom: 16 }}>{t.order.contactPersonSub}</p>
                      <div className="field-grid">
                        <Field label={t.order.contactLastName} required value={company.contactLastName} error={errors.contactLastName} onChange={v => setComp("contactLastName", v)} autoComplete="family-name" />
                        <Field label={t.order.contactFirstName} required value={company.contactFirstName} error={errors.contactFirstName} onChange={v => setComp("contactFirstName", v)} autoComplete="given-name" />
                        <Field label={t.order.contactEmail} required type="email" value={company.contactEmail} error={errors.contactEmail} onChange={v => setComp("contactEmail", v)} autoComplete="email" />
                        <PhoneField label={t.order.contactPhone} required value={company.contactPhone} error={errors.contactPhone} onChange={v => setComp("contactPhone", v)} />
                      </div>
                    </React.Fragment>
                  )}
                </div>

                {kboStatus === "done" && (
                  <React.Fragment>
                    <div className="order-card">
                      <h2 className="order-section-title">{t.order.passUsers}</h2>
                      <p className="order-section-sub">{t.order.passUsersSub} {t.order.kbo.usersDeliveryNote}</p>
                      {users.map((u, i) => (
                        <div className="user-block" key={i}>
                          <div className="user-block-head">
                            <span className="ub-title"><span className="ub-num">{i + 1}</span>{t.order.user} {i + 1}</span>
                            {users.length > 1 && <button className="user-remove" onClick={() => removeUser(i)}><Icon.trash />{t.order.removeUser}</button>}
                          </div>
                          <div className="field-grid">
                            <Field label={t.order.lastName} required value={u.lastName} error={errors["u" + i + "lastName"]} onChange={v => setUser(i, "lastName", v)} />
                            <Field label={t.order.firstName} required value={u.firstName} error={errors["u" + i + "firstName"]} onChange={v => setUser(i, "firstName", v)} />
                            <Field label={t.order.email} required type="email" value={u.email} error={errors["u" + i + "email"]} onChange={v => setUser(i, "email", v)} />
                            <PhoneField label={t.order.phone} required value={u.phone} error={errors["u" + i + "phone"]} onChange={v => setUser(i, "phone", v)} />
                          </div>
                          <div className="field-group-title">{t.order.addressTitle}</div>
                          <div className="field-grid">
                            <Field label={t.order.postcode} required value={u.postcode} error={errors["u" + i + "postcode"]} onChange={v => setUser(i, "postcode", v)} inputMode="numeric" />
                            <Field label={t.order.city} required value={u.city} error={errors["u" + i + "city"]} onChange={v => setUser(i, "city", v)} />
                            <StreetField t={t} value={u.street} error={errors["u" + i + "street"]}
                              onChange={v => setUser(i, "street", v)}
                              onPick={sug => {
                                if (sug.postcode) setUser(i, "postcode", sug.postcode);
                                if (sug.gemeente) setUser(i, "city", sug.gemeente);
                                setUser(i, "street", sug.straat || "");
                              }}
                              context={{ postcode: u.postcode, city: u.city }} />
                            <Field label={t.order.bus} value={u.bus} error={errors["u" + i + "bus"]} onChange={v => setUser(i, "bus", v)} hint={t.order.busHint} />
                          </div>
                        </div>
                      ))}
                      <button className="add-user-btn" onClick={addUser}><Icon.plus />{t.order.addUser}</button>
                    </div>
                    <div className="order-card">
                      <PromoField t={t} value={promo} onChange={setPromo} inline />
                    </div>
                  </React.Fragment>
                )}
                <NavButtons t={t} onPrev={prev} onNext={next} />
              </React.Fragment>
            )}

            {/* STEP 3 — review */}
            {step === 3 && (
              <div className="order-card">
                <h2 className="order-section-title">{t.order.reviewTitle}</h2>
                <p className="order-section-sub">{t.order.reviewSub}</p>
                <Review t={t} order={order()} />
                <div className="terms-row">
                  <input type="checkbox" id="agree" checked={agree} onChange={e => { setAgree(e.target.checked); if (e.target.checked) setTermsErr(false); }} />
                  <label htmlFor="agree" dangerouslySetInnerHTML={{ __html: t.order.termsLabel }} />
                </div>
                {termsErr && <div className="terms-error">{t.order.mustAgree}</div>}
                {TS_SITEKEY && <div ref={tsRef} style={{ marginTop: 12 }} />}
                {submitError && <div className="terms-error" style={{ marginTop: 8 }}>⚠ {submitError}</div>}
                <div className="order-nav">
                  <button className="btn btn-ghost" onClick={prev} disabled={submitting}><Icon.arrowLeft />{t.order.prev}</button>
                  <div className="spacer"></div>
                  <button className="btn btn-lime btn-lg" onClick={submit} disabled={submitting}
                    style={{ opacity: submitting ? 0.6 : 1, pointerEvents: submitting ? "none" : "auto" }}>
                    {submitting ? (t.order.submitting || "Bezig…")
                      : <React.Fragment>{type === "particulier" && !lostPass ? t.order.submitPay : t.order.submit} <Icon.arrowRight /></React.Fragment>}
                  </button>
                </div>
              </div>
            )}
          </React.Fragment>
        )}
      </div>
    </div>
  );
}

/* ============================ whitelist eigen laadstation (particulier) ============================
 * Vinkje + serienummer + live check tegen Optimile (/api/station-lookup).
 * De check zegt alleen gevonden/niet-gevonden; eigenaarsgegevens blijven
 * server-side. De echte fraudecontrole (adres, identiteit, handshake) draait
 * pas na het betaalde mandaat. */
function WhitelistField({ t, wl, setWl, error }) {
  const W = t.order.whitelist;

  async function check() {
    const serial = wl.serial.trim();
    if (serial.length < 3) { setWl({ ...wl, status: "invalid" }); return; }
    setWl({ ...wl, status: "busy" });
    try {
      await window.LaadpasAPI.lookupStation(serial);
      setWl({ ...wl, serial, status: "ok" });
    } catch (e) {
      setWl({ ...wl, status: e.code === "not_eligible" ? "noteligible" : e.code === "lookup_failed" ? "error" : "notfound" });
    }
  }

  return (
    <React.Fragment>
      <div className="field-group-title">{W.title} · {t.order.optional}</div>
      <p className="order-section-sub" style={{ marginTop: -8 }}>{W.sub}</p>
      {W.disclaimer && <p className="order-section-sub" style={{ marginTop: -6, fontSize: 13.5 }}>{W.disclaimer}</p>}
      <div className="terms-row" style={{ marginTop: 8 }}>
        <input type="checkbox" id="wl-enable" checked={wl.enabled}
          onChange={e => setWl({ ...wl, enabled: e.target.checked, status: e.target.checked ? wl.status : "idle" })} />
        <label htmlFor="wl-enable">{W.label}</label>
      </div>
      {wl.enabled && (
        <React.Fragment>
          <div className="field-grid" style={{ marginTop: 12 }}>
            <div className="field col-2">
              <label>{W.serialLabel}<span className="req">*</span></label>
              <div style={{ display: "flex", gap: 8 }}>
                <input type="text" value={wl.serial} placeholder={W.serialPlaceholder}
                  className={error || ["invalid", "notfound", "noteligible"].includes(wl.status) ? "invalid" : ""}
                  style={{ flex: 1, maxWidth: 320 }}
                  onChange={e => setWl({ ...wl, serial: e.target.value, status: "idle" })}
                  onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); check(); } }} />
                <button type="button" className="btn btn-primary" onClick={check} disabled={wl.status === "busy"}
                  style={{ whiteSpace: "nowrap" }}>
                  {wl.status === "busy" ? W.busy : W.checkBtn}
                </button>
              </div>
              <span className="order-section-sub" style={{ display: "block", marginTop: 6, marginBottom: 0 }}>{W.serialHint}</span>
              {wl.status === "ok" && <span style={{ display: "block", marginTop: 6, fontWeight: 600, color: "var(--ok, #3B6D11)" }}>✓ {W.found}</span>}
              {wl.status === "invalid" && <span className="err">{W.invalid}</span>}
              {wl.status === "notfound" && <span className="err">{W.notFound}</span>}
              {wl.status === "noteligible" && <span className="err">{W.notEligible}</span>}
              {wl.status === "error" && <span className="err">{W.error}</span>}
              {error && <span className="err">{error}</span>}
            </div>
          </div>
          <div className="info-alert" style={{ marginTop: 10 }}>
            <span className="info-alert-icon">🏠</span>
            <div className="info-alert-body">
              <span className="info-alert-title">{W.noteTitle}</span>
              <span>{W.note}</span>
            </div>
          </div>
        </React.Fragment>
      )}
    </React.Fragment>
  );
}

function PromoField({ t, value, onChange, inline }) {
  return (
    <React.Fragment>
      {inline
        ? <h2 className="order-section-title" style={{ marginBottom: 18 }}>{t.order.promoCode} <span style={{ fontWeight: 400, fontSize: 15, color: "var(--fg-3)" }}>· {t.order.optional}</span></h2>
        : <div className="field-group-title">{t.order.promoCode} · {t.order.optional}</div>}
      <p className="order-section-sub" style={{ marginTop: inline ? -8 : -6, marginBottom: 14 }}>{t.order.promoHint}</p>
      <div className="field-grid">
        <div className="field col-2">
          <input type="text" value={value} placeholder={t.order.promoCode} style={{ maxWidth: 320, textTransform: "uppercase", letterSpacing: ".04em" }}
            onChange={e => onChange(e.target.value.toUpperCase())} />
        </div>
      </div>
    </React.Fragment>
  );
}

function NavButtons({ t, onPrev, onNext }) {
  return (
    <div className="order-nav">
      <button className="btn btn-ghost" onClick={onPrev}><Icon.arrowLeft />{t.order.prev}</button>
      <div className="spacer"></div>
      <button className="btn btn-primary" onClick={onNext}>{t.order.next} <Icon.arrowRight /></button>
    </div>
  );
}

/* ============================ review ============================ */
function Review({ t, order }) {
  if (order.type === "particulier") {
    const p = order.particulier;
    return (
      <div>
        <div className="review-count-pill"><Icon.check style={{ width: 15, height: 15 }} />{t.order.particulier} · 1 {t.order.user}</div>
        {order.promo && order.promo.trim() && <PromoReview t={t} promo={order.promo} />}
        <div className="review-block">
          <h4>{t.order.personalDetails}</h4>
          <div className="review-rows">
            <RRow k={t.order.lastName} v={p.lastName} /><RRow k={t.order.firstName} v={p.firstName} />
            <RRow k={t.order.email} v={p.email} /><RRow k={t.order.phone} v={p.phone || "-"} />
          </div>
        </div>
        <div className="review-block">
          <h4>{t.order.deliveryTitle}</h4>
          <div className="review-rows">
            <RRow k={t.order.street} v={window.LaadpasAPI.straatMetBus(p.street, p.bus)} /><RRow k={t.order.postcode} v={p.postcode} />
            <RRow k={t.order.city} v={p.city} /><RRow k={t.order.country} v={t.order.countryBe} />
          </div>
        </div>
        {!order.lostPass && (
          <div className="review-block">
            <h4>{t.order.sepaTitle}</h4>
            <div className="review-rows">
              <RRow k={t.order.sepaMandate} v={order.sepa ? "✓ " + t.order.sepaConfirmed : "n.v.t."} />
              <RRow k={t.order.pay.reviewLabel} v={t.order.pay.reviewValue} />
            </div>
          </div>
        )}
        {order.lostPass && (
          <div className="review-block">
            <h4>{t.order.lostPassLabel}</h4>
            <p className="order-section-sub" style={{ marginBottom: 0 }}>{t.order.lostPassNote}</p>
          </div>
        )}
        {order.whitelist && order.whitelist.enabled && (
          <div className="review-block">
            <h4>{t.order.whitelist.title}</h4>
            <div className="review-rows">
              <RRow k={t.order.whitelist.serialLabel} v={order.whitelist.serial} />
              <RRow k={t.order.whitelist.reviewLabel} v={"✓ " + t.order.whitelist.reviewValue} />
            </div>
            <p className="order-section-sub" style={{ marginTop: 10, marginBottom: 0 }}>{t.order.whitelist.reviewNote}</p>
          </div>
        )}
      </div>
    );
  }
  const c = order.company;
  return (
    <div>
      <div className="review-count-pill"><Icon.check style={{ width: 15, height: 15 }} />{t.order.bedrijf} · {order.users.length} {t.order.passes}</div>
      {order.promo && order.promo.trim() && <PromoReview t={t} promo={order.promo} />}
      <div className="review-block">
        <h4>{t.order.companyDetails}</h4>
        <div className="review-rows">
          <RRow k={t.order.companyName} v={c.name} /><RRow k={t.order.vatNumber} v={c.vat} />
          <RRow k={t.order.kboNumber} v={c.kbo || "-"} />
        </div>
      </div>
      <div className="review-block">
        <h4>{t.order.kbo.deliveryReviewTitle}</h4>
        <div className="review-rows">
          <RRow k={t.order.street} v={c.street} /><RRow k={t.order.postcode + " · " + t.order.city} v={c.postcode + " " + c.city} />
          <RRow k={t.order.country} v={c.country} />
        </div>
        <p className="order-section-sub" style={{ marginTop: 10, marginBottom: 0 }}>{t.order.kbo.addressNote}</p>
      </div>
      <div className="review-block">
        <h4>{t.order.contactPerson}</h4>
        <div className="review-rows">
          <RRow k={t.order.contactLastName} v={c.contactLastName} />
          <RRow k={t.order.contactFirstName} v={c.contactFirstName} />
          <RRow k={t.order.contactEmail} v={c.contactEmail} />
          <RRow k={t.order.contactPhone} v={c.contactPhone} />
        </div>
      </div>
      <div className="review-block">
        <h4>{t.order.passUsers} ({order.users.length})</h4>
        {order.users.map((u, i) => (
          <div className="review-user" key={i}>
            <div className="ru-head">{t.order.user} {i + 1}: {u.firstName} {u.lastName}</div>
            <div className="review-rows">
              <RRow k={t.order.email} v={u.email} />
              <RRow k={t.order.phone} v={u.phone} />
              <RRow k={t.order.addressTitle} v={[window.LaadpasAPI.straatMetBus(u.street, u.bus), [u.postcode, u.city].filter(Boolean).join(" ")].filter(Boolean).join(", ")} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
function RRow({ k, v }) { return (<div className="review-row"><span className="rk">{k}</span><span className="rv">{v || "-"}</span></div>); }

function PromoReview({ t, promo }) {
  return (
    <div className="review-block">
      <h4>{t.order.promoCode}</h4>
      <div className="review-rows single">
        <RRow k={t.order.promoCode} v={promo} />
      </div>
    </div>
  );
}

/* ============================ success (bedrijf) ============================ */
function Success({ t, type, onNew }) {
  return (
    <div className="order-success">
      <div className="success-icon"><Icon.check /></div>
      <h2>{t.order.successTitle}</h2>
      <p>{type === "bedrijf" ? t.order.successBodyCompany : t.order.successBody}</p>
      <div className="success-actions">
        <button className="btn btn-ghost" onClick={onNew}>{t.order.successNew}</button>
      </div>
    </div>
  );
}

/* ============================ payment return (particulier) ============================ */
function PaymentReturn({ t, lang, setLang, orderId, onBack }) {
  const [state, setState] = React.useState("checking"); // checking | verified | failed | timeout

  React.useEffect(() => {
    let stopped = false;
    let tries = 0;
    async function poll() {
      while (!stopped && tries < 15) {
        tries++;
        try {
          const status = await window.LaadpasAPI.getOrderStatus(orderId);
          if (status === "verified" || status === "exported" || status === "pushed_to_odoo") { setState("verified"); return; }
          if (status === "payment_failed" || status === "payment_expired") { setState("failed"); return; }
        } catch (e) { /* tijdelijk; opnieuw proberen */ }
        await new Promise(r => setTimeout(r, 2000));
      }
      if (!stopped) setState("timeout");
    }
    poll();
    return () => { stopped = true; };
  }, [orderId]);

  return (
    <div className="order-page">
      <div className="order-topbar">
        <div className="container order-topbar-inner">
          <button className="order-back" onClick={onBack}><Icon.arrowLeft />{t.order.back}</button>
          <img className="header-logo" src="assets/mobilityplus-logo.png" alt="MobilityPlus" />
          <div className="header-right"><LangSwitch lang={lang} setLang={setLang} /></div>
        </div>
      </div>
      <div className="order-body">
        {state === "checking" && (
          <div className="order-success">
            <h2>{t.order.pay.checking}</h2>
            <p>{t.order.pay.checkingSub}</p>
          </div>
        )}
        {state === "verified" && (
          <div className="order-success">
            <div className="success-icon"><Icon.check /></div>
            <h2>{t.order.pay.paidTitle}</h2>
            <p>{t.order.pay.paidBody}</p>
            <div className="success-actions">
              <button className="btn btn-primary" onClick={onBack}>{t.order.back}</button>
            </div>
          </div>
        )}
        {state === "failed" && (
          <div className="order-success">
            <h2>{t.order.pay.failedTitle}</h2>
            <p>{t.order.pay.failedBody}</p>
            <div className="success-actions">
              <button className="btn btn-primary" onClick={onBack}>{t.order.pay.retry}</button>
            </div>
          </div>
        )}
        {state === "timeout" && (
          <div className="order-success">
            <h2>{t.order.pay.pendingTitle}</h2>
            <p>{t.order.pay.pendingBody}</p>
            <div className="success-actions">
              <button className="btn btn-primary" onClick={onBack}>{t.order.back}</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { OrderFlow, PaymentReturn });
