function Login({ onSluit }){
  const [email,setEmail]=useState(""); const [gestuurd,setGestuurd]=useState(false);
  const [f,setF]=useState(null); const [bezig,setBezig]=useState(false);
  async function stuur(){ setBezig(true); setF(null);
    const r=await db.auth.signInWithOtp({email:email.trim(),options:{emailRedirectTo:window.location.origin}});
    if(r.error){ setF(r.error.message); setBezig(false); return; } setGestuurd(true); setBezig(false); }
  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        {gestuurd
          ? <div><h3>Kijk in je mail</h3><p className="bij">We hebben een inloglink gestuurd naar {email}. Open die op dit apparaat, anders logt hij op de verkeerde plek in.</p>
              <button className="knop vol stil" onClick={onSluit}>Sluiten</button></div>
          : <div><h3>Inloggen</h3><p className="bij">Voor seizoens- en strippenkaarthouders. Vul je e-mail in, dan sturen we een inloglink.</p>
              <label>E-mailadres</label>
              <input type="email" value={email} onChange={e=>setEmail(e.target.value)} placeholder="jij@voorbeeld.nl" onKeyDown={e=>e.key==="Enter"&&geldigEmail(email)&&stuur()}/>
              {/* Dezelfde controle en dezelfde zin als in het boekingsblad. Zonder dit ging een tikfout
                  gewoon naar de inlogdienst en kreeg de gast daar een Engelse foutmelding terug.
                  Gemeten op 12 augustus 2026: met alleen het woord "roy" in het veld werd de knop
                  actief. */}
              {email.trim() && !geldigEmail(email) && <div className="fout">Dit lijkt geen e-mailadres. Controleer of er een apenstaartje in staat en een punt erachter.</div>}
              {f && <div className="fout">{f}</div>}
              <button className="knop vol" onClick={stuur} disabled={bezig||!geldigEmail(email)}>{bezig?"Versturen…":"Stuur inloglink"}</button>
              <button className="terug" onClick={onSluit}>Annuleren</button></div>}
      </div>
    </div>
  );
}

// Bevestigingsstap vlak voor de betaalknop. Aanleiding, letterlijk uit het gesprek van 21 augustus
// 2026: "Je moet bij een boeking in een pop-up komen. Of een bericht. Weet je het zeker. Dit kan
// niet geannuleerd worden." De gast moet dit venster actief wegklikken, het staat dus niet als losse
// waarschuwing op de pagina maar blokkeert de weg naar de betaalknop (of naar "bevestigen" als er
// niets te betalen valt). Toont alleen waarden die op dit punt in de code al zeker zijn: datum,
// starttijd, aantal spelers en het bedrag staan er al in stap 3, dus die gaan hier gewoon mee.
function BevestigStap({ datum, slot, aantal, totaal, mollie, paar18, onDoorgaan, onTerug }){
  // Punt 6 (17 september 2026): akkoord met de voorwaarden is verplicht voordat er geboekt
  // of betaald kan worden. Het vinkje staat hier, in de laatste stap voor de betaling, en
  // niet eerder: dit is het moment waarop de gast zich vastlegt.
  const [akkoord,setAkkoord]=useState(false);
  const venster = React.useRef(null);
  useEffect(()=>{
    if(venster.current) venster.current.focus();
    function opToets(e){ if(e.key==="Escape") onTerug(); }
    window.addEventListener("keydown", opToets);
    return ()=>window.removeEventListener("keydown", opToets);
  },[]);
  const heeftBedrag = mollie && totaal>0;
  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onTerug()}>
      <div className="blad" role="dialog" aria-modal="true" aria-labelledby="bevestig-titel" ref={venster} tabIndex={-1}>
        <div className="greep"/>
        <h3 id="bevestig-titel">Weet je het zeker?</h3>
        <div className="details">
          <div><span>Datum</span><span>{lang(datum)}</span></div>
          <div><span>{paar18?"Eerste starttijd":"Starttijd"}</span><span>{slot.starttijd.slice(0,5)}</span></div>
          {paar18 && <div><span>Tweede starttijd</span><span>{String(slot.tweede_starttijd).slice(0,5)}</span></div>}
          <div><span>Spelers</span><span>{aantal}</span></div>
          <div><span>Bedrag</span><span>{euro(totaal)}</span></div>
        </div>
        {/* Het geld-punt en het verplaats-punt staan hier bewust altijd samen: de harde mededeling
            ("geen geld terug") naast de geruststelling ("verplaatsen kan wel, zelf"), zodat het
            tweede niet als kleine lettertjes achter het eerste wegvalt. Bij nul euro blijft de
            tekst over geld kloppen door het simpelweg niet te noemen. */}
        <p className="bij">
          {heeftBedrag
            ? "Als je doorgaat en betaalt, kun je deze reservering niet meer annuleren. Er komt dan geen geld terug. Wil je toch een andere datum of tijd, dan kun je de boeking zelf verplaatsen."
            : "Als je doorgaat, kun je deze reservering niet meer annuleren. Wil je toch een andere datum of tijd, dan kun je de boeking zelf verplaatsen."}
        </p>
        <label className="akkoordregel">
          <input type="checkbox" checked={akkoord} onChange={e=>setAkkoord(e.target.checked)}/>
          <span>Ik ga akkoord met de <VoorwaardenLink/>.</span>
        </label>
        <button className="knop vol" onClick={onDoorgaan} disabled={!akkoord}>{heeftBedrag ? "Ja, naar betalen" : "Ja, reservering bevestigen"}</button>
        <button className="terug" onClick={onTerug}>Terug</button>
      </div>
    </div>
  );
}

// Ronde 9: foutteksten bij een losse greenfee via Recras. De teksten die niet over een kaart gaan komen uit
// recrasFoutTekst; voor de rest een zin zonder "kaart", want de gast heeft er geen.
function losseRecrasFoutTekst(reden, referentie){
  if(["turnstile","invoer","recras_onzeker","te_veel_pogingen"].indexOf(reden)>=0) return recrasFoutTekst(reden, referentie);
  return "Online boeken lukt nu niet. Probeer het zo nog eens, of bel ons op "+TELEFOON+".";
}

function Boeken({ slot, datum, types, regels, producten, lid, mollie, holes, onSluit, onKlaar }){
  // Ronde 6 (18 september 2026): 18 holes is twee keer de negen van Weesp, dus twee starttijden.
  // De tweede starttijd komt uit de view en staat op het slot dat de gast heeft gekozen.
  // Bij 18 holes sturen we het product WEL mee naar de database. Bij 9 holes niet, precies zoals
  // het was: dan kiest boek_tijdslot zelf het greenfeeproduct met een strip.
  const paar18 = holes===18 && !!slot.tweede_starttijd;
  const product = productVoorHoles(producten, slot.sport, paar18 ? 18 : 9);
  const productId = product ? product.id : null;
  // Bij 18 holes bepaalt de krapste van de twee starttijden hoeveel spelers erin passen, en
  // daarboven geldt de grens van vier: meer dan vier gaat via de balie.
  const plekken18 = paar18 ? Math.min(slot.vrij, slot.tweede_vrij) : slot.vrij;
  const baanId = slot.baan_id || null;
  const gast = types.filter(t=>!t.is_lid)[0];
  const lidType = types.filter(t=>t.is_lid)[0];
  // Kaarthouderstarieven gelden per sport. Staan ze voor deze ingang uit (zie SPORTEN in data.js),
  // dan bestaan ze hier niet: niet in de opties, niet in de beginstand van de rijen en niet in de
  // meldingen. Een optie tonen die de database daarna weigert, is erger dan hem weglaten.
  const kaartTarief = !!PROFIEL.kaarttarieven;
  const heeftStrip = kaartTarief && lid && lid.kaarten && lid.kaarten.length>0;
  const heeftSeizoen = kaartTarief && lid && lid.seizoenskaart;
  const strip = heeftStrip ? lid.kaarten[0] : null;
  const [aantal,setAantal]=useState(1);
  // Het ledentarief bestaat alleen als er een prijsregel voor is. Voor golf is die er (nul euro),
  // voor footgolf niet; dan zou `boek_tijdslot` afbreken met "Geen prijsregel gevonden voor Lid".
  const lidPrijs = (kaartTarief && lidType)
    ? prijsVan(regels, lidType.id, datum, slot.starttijd, product?product.id:null, slot.baan_id||null) : null;
  const [rijen,setRijen]=useState([
    (lid && kaartTarief && (heeftSeizoen || heeftStrip || lidPrijs!==null))
      ? { betaal: heeftSeizoen ? "seizoen" : (heeftStrip ? "kaart" : "lid"), type: gast?gast.id:"" }
      : { betaal:"gast", type: gast?gast.id:"" }]);
  const [naam,setNaam]=useState(lid?lid.naam:""); const [email,setEmail]=useState(lid?lid.email:"");
  const [tel,setTel]=useState(""); const [f,setF]=useState(null); const [bezig,setBezig]=useState(false);
  // Ronde 10 (15 september 2026): GSN-nummer (Golf Service Nummer van de NGF) verplicht bij elke online
  // golfreservering, voor minimaal een speler. Alleen golf; footgolf vraagt het niet. De database dwingt het
  // ook af (boek_tijdslot en boek_groep, schakelaar golf_gsn_verplicht_online). Alleen trimmen en een ruime
  // maximumlengte: de NGF zegt alleen dat het nummer met NL begint, en dat controleren we (nog) niet.
  const [gsn,setGsn]=useState("");
  const gsnNodig = false;  // punt 2: niet meer aan de gast vragen; p_gsn blijft bestaan voor de balie
  const gsnSchoon = gsn.trim();
  const gsnTeLang = gsnSchoon.length>20;
  const gsnKlaar = !gsnNodig || (gsnSchoon.length>=1 && !gsnTeLang);
  const gsnDeel = gsnNodig ? { p_gsn: gsnSchoon } : {};
  // Ronde 3 (security-oordeel V2): Turnstile bij een Recras-reservering, alleen als de instelling aan staat.
  const [ts,setTs]=useState({actief:false, sitekey:null});
  const [tsToken,setTsToken]=useState(null);
  const [tsRonde,setTsRonde]=useState(0);
  useEffect(()=>{ turnstileInstelling().then(setTs); },[]);
  // Ronde 9 (15 september 2026, besluit Roy: een online losse greenfee moet in Recras, hoeft niet op naam).
  // Staat de instelling recras_losse_greenfee_actief op "true", dan gaat een greenfee zonder kaart niet
  // meer via boek_tijdslot plus onze eigen Mollie, maar via recras-reserveer (soort losse_greenfee): die
  // maakt onze boeking en de reservering in Recras, en de gast betaalt via de Mollie-koppeling van Recras.
  // Staat de instelling uit of ontbreekt hij, dan verandert er niets aan dit scherm.
  const [losseRecras,setLosseRecras]=useState(false);
  useEffect(()=>{ db.from("instellingen").select("waarde").eq("sleutel","recras_losse_greenfee_actief").maybeSingle()
    .then(r=>setLosseRecras(!!(r && r.data && r.data.waarde==="true")), ()=>setLosseRecras(false)); },[]);
  // Drie stappen in plaats van een lang formulier: eerst spelers en tarief, dan gegevens, dan
  // controleren en betalen. Op een telefoon paste het oude scherm niet in beeld, waardoor de knop
  // onder de vouw viel en de gast moest scrollen om te zien wat hij ging betalen.
  const [stap,setStap]=useState(1);
  // De bevestigingspop-up ("weet je het zeker") staat los van de drie stappen: hij komt in beeld
  // BOVENOP stap 3, pas nadat de gast al op de echte betaal- of bevestigknop heeft gedrukt, en gaat
  // pas naar reserveer() als de gast hem actief wegklikt met "Ja". Terug sluit hem alleen, de gast
  // blijft dan gewoon op stap 3 staan met alles nog ingevuld.
  const [bevestigOpen,setBevestigOpen]=useState(false);
  // Bij 9 holes onveranderd: meer spelers dan er in dit slot passen betekent de groepsroute
  // (boek_groep verdeelt ze over opeenvolgende starttijden). Bij 18 holes bestaat die route niet:
  // boek_groep kent de tweede starttijd niet en weigert het 18-holesproduct. Daarom gaat het daar
  // nooit via boek_groep, en vangt het scherm het af met de melding hieronder.
  const groot = !paar18 && aantal > slot.vrij;
  // Ronde 6: de gast heeft meer dan vier spelers en wil 18 holes. Dan is dit geen fout maar een
  // doorverwijzing: hij kan bellen, of 9 holes kiezen.
  const teGrootVoor18 = paar18 && aantal > HOLES_18_MAX_SPELERS;
  const teVolVoor18 = paar18 && !teGrootVoor18 && aantal > plekken18;

  // De teller blijft tot PROFIEL.maxSpelers lopen, ook bij 18 holes. Zou hij daar op vier
  // vastlopen, dan gebeurt er bij de vijfde tik niets en snapt de gast niet waarom. Nu kan hij
  // wel verder tellen, ziet hij de melding met het nummer, en is alleen de knop uit.
  function zetAantal(n){ if(n<1||n>PROFIEL.maxSpelers) return; setAantal(n);
    setRijen(r=>{ const k=r.slice(0,n); while(k.length<n) k.push({betaal:"gast",type:gast?gast.id:""}); return k; }); }
  function wz(i,patch){ setRijen(rijen.map((r,n)=>n===i?Object.assign({},r,patch):r)); }
  // Een kaartcode wordt gecontroleerd zodra de gast klaar is met typen, niet bij elke toetsaanslag.
  // `kaart_bij_code` spiegelt de wachters van `boek_tijdslot`, dus wat hier groen is gaat er ook
  // doorheen. De uitkomst blijft bij de rij staan, want daar hoort hij ook op het scherm.
  // De setState gaat hier met een functie en niet met de rijen uit de closure. Er zit een
  // netwerkaanroep tussen twee updates, en dan is de lijst uit de closure verouderd: de tweede
  // update draaide de eerste terug en de uitkomst kwam nooit in beeld. Gemeten op 14 augustus.
  function wzFn(i, patch){ setRijen(rs=>rs.map((r,n)=>n===i?Object.assign({},r,patch):r)); }
  async function controleerCode(i, ruw){
    const code = (ruw||"").trim();
    if(!code){ wzFn(i,{kaart:null, kaartBezig:false}); return; }
    wzFn(i,{kaartBezig:true});
    const r = await db.rpc("kaart_bij_code", { p_code: code, p_sport: SPORT, p_datum: datum });
    if(r.error){
      console.warn("[upgolf] code controleren mislukt: " + r.error.message);
      wzFn(i,{kaart:{gevonden:false}, kaartBezig:false, gecontroleerdeCode:code}); return;
    }
    wzFn(i,{kaart:r.data, kaartBezig:false, gecontroleerdeCode:code});
  }
  // Recras-kaart (bouwspec Recras-kaart 4.2, 14 september 2026). De uitkomst komt op de rij als
  // kaart:{bron:"recras", ...antwoord}, via wzFn om de closurefout van 14 augustus niet terug te halen.
  // Het aantal spelers gaat mee, want Recras rekent uit hoeveel spelers de kaart dekt.
  async function controleerRecrasCode(i, ruw, spelers){
    const code = (ruw||"").trim();
    if(!code){ wzFn(i,{kaart:null, kaartBezig:false}); return; }
    wzFn(i,{kaartBezig:true});
    const d = await recrasControle(code, datum, spelers||aantal);
    wzFn(i,{kaart:Object.assign({bron:"recras"}, d), kaartBezig:false, gecontroleerdeCode:code});
  }
  // Ronde 2 (bouwspec 4.2 punt 1): een Recras-kaart die op naam is gevonden. Het codeveld blijft leeg;
  // de controle gaat met kaart_ref plus de naam en het e-mailadres waarmee de gast zocht.
  async function controleerRecrasRef(i, keuze, spelers){
    const ref = {kaart_ref:keuze.kaart_ref, naam:keuze.naam, email:keuze.email};
    wzFn(i,{kaartBezig:true, code:"", recrasRef:Object.assign({}, keuze)});
    const d = await recrasControle(ref, datum, spelers||aantal);
    wzFn(i,{kaart:Object.assign({bron:"recras", viaNaam:true, kaart_ref:keuze.kaart_ref, code_eind:keuze.code_eind}, d, {kaartnaam:d.kaartnaam||keuze.kaartnaam}), kaartBezig:false});
  }
  // Welke weg een ingetypte code gaat. Een eigen code van 9 tekens gaat exact de oude weg.
  function controleerVolgensSoort(i, v, bijVerlaten){
    const soort = codeSoort(v);
    if(soort==="eigen") controleerCode(i, v);
    else if(soort==="recras") controleerRecrasCode(i, v);
    else if(soort==="onbekend" && bijVerlaten) controleerCode(i, v);
  }
  // Staat deze code al nagekeken op de rij? Dan hoeft het niet nog een keer bij het verlaten van
  // het veld. Waarom dat nodig is, gemeten op 20 september 2026: de gast typt zijn code, de kaart
  // wordt goedgekeurd, hij klikt op "Verder naar je gegevens", en er gebeurt niets. De muisknop
  // indrukken haalt de aandacht uit het codeveld, onBlur startte dan dezelfde controle opnieuw,
  // `kaartBezig` werd waar en de knop ging op dat moment uit. Een knop die uit staat krijgt geen
  // klik meer, dus de klik ging verloren en pas de tweede klik werkte. Gemeten: direct na het
  // indrukken stond de knop 0 tot 100 ms uit, en na ongeveer 125 ms weer aan.
  // De bescherming blijft heel: dit slaat alleen een HERHALING over van een controle die al een
  // uitkomst heeft voor exact dezelfde code. Verandert de gast ook maar een teken, dan zet de
  // onChange hierboven `kaart` op null en wordt er gewoon opnieuw gekeken; en zolang er geen
  // bruikbare uitkomst ligt, houdt `codeNietKlaar` de knop nog steeds tegen.
  function alGecontroleerd(r, v){
    return !!r && !!r.kaart && !r.kaartBezig && (r.gecontroleerdeCode||"") === (v||"").trim();
  }
  // Is het aantal spelers veranderd terwijl er een Recras-kaart op een rij staat, dan opnieuw
  // controleren: hoeveel spelers de kaart dekt en wat er bij moet, hangt van dat aantal af.
  useEffect(()=>{
    rijen.forEach((r,i)=>{
      if(r.betaal==="code" && r.kaart && r.kaart.bron==="recras" && r.code) controleerRecrasCode(i, r.code, aantal);
      else if(r.betaal==="code" && r.kaart && r.kaart.bron==="recras" && r.recrasRef) controleerRecrasRef(i, r.recrasRef, aantal);
    });
  },[aantal]);
  // "Code kwijt?" op deze rij: naam plus e-mailadres in plaats van de code. `kaart_bij_naam_email`
  // geeft bij een treffer óók de code terug, en die zetten we in r.code alsof hij was overgetypt:
  // de rest van het scherm (bon, waarschuwingen, boek_tijdslot) hoeft dan niets van dit pad te weten.
  // Bewust GEEN auto-zoeken tijdens het typen (zoals bij de code wel gebeurt): dit veld gaat naar een
  // functie met een rem op mislukte pogingen, en elke halve poging telt daarin mee.
  async function zoekOpNaamEmail(i, naam, email){
    if(!(naam||"").trim() || !(email||"").trim()){
      wzFn(i,{zoekFout:null}); return;
    }
    wzFn(i,{zoekBezig:true, zoekFout:null, recrasKeuzes:null, recrasRef:null});
    // Ronde 2 (bouwspec 4.2 punt 1): ons eigen kaartsysteem en Recras tegelijk. Het filteren op naam
    // en e-mailadres gebeurt in recras-houder, niet hier.
    const [r, h] = await Promise.all([
      db.rpc("kaart_bij_naam_email", { p_naam: naam.trim(), p_email: email.trim(), p_sport: SPORT, p_datum: datum }),
      recrasHouder({ zoek:"naam_email", naam: naam.trim(), email: email.trim(), datum: datum })
    ]);
    const keuzes = (h && h.gevonden && Array.isArray(h.kaarten))
      ? h.kaarten.map(k=>Object.assign({}, k, {naam:naam.trim(), email:email.trim()})) : null;
    if(r.error && !keuzes){
      console.warn("[upgolf] zoeken op naam/e-mail mislukt: " + r.error.message);
      wzFn(i,{zoekBezig:false, zoekFout:"Kan nu niet zoeken. Probeer het zo nog eens, of bel ons op "+TELEFOON+"."}); return;
    }
    const d = r.error ? {gevonden:false} : r.data;
    if(!keuzes && (d.reden==="te_veel_pogingen" || (h && h.reden==="te_veel_pogingen"))){
      wzFn(i,{zoekBezig:false, zoekFout:"Er is net te vaak gezocht. Wacht een paar minuten, of bel ons op "+TELEFOON+".", kaart:null, code:""}); return;
    }
    if(!d.gevonden && keuzes){
      wzFn(i,{zoekBezig:false, zoekFout:null, kaart:null, code:"", recrasKeuzes:keuzes}); return;
    }
    if(!d.gevonden){
      // Zelfde melding ongeacht welk van de twee velden niet klopte: de functie geeft zelf ook
      // geen onderscheid terug, dus er is hier niets te tonen wat dat verraadt.
      wzFn(i,{zoekBezig:false, zoekFout:"We vinden geen kaart bij deze naam en dit e-mailadres. Controleer beide, of bel ons op "+TELEFOON+".", kaart:null, code:""}); return;
    }
    // Gevonden: net als bij een code kan de kaart zelf niet bruikbaar zijn (verlopen, geen ritten
    // meer, geblokkeerd, andere sport). Dat loopt verder via dezelfde codeFout-tekst als bij een
    // ingetypte code, want de reden-codes zijn identiek.
    wzFn(i,{zoekBezig:false, zoekFout:null, kaart:d, code:d.code||"", recrasKeuzes:keuzes});
  }
  // Wat de gast leest als de code niet bruikbaar is. Elke regel zegt wat er aan de hand is en
  // wat hij kan doen; "ongeldige code" staat er nergens.
  function codeFout(k){
    if(!k) return null;
    if(!k.gevonden) return "Deze code kennen we niet. Kijk of je hem goed hebt overgetypt.";
    if(k.bruikbaar) return null;
    if(k.reden==="geblokkeerd") return "Deze kaart is geblokkeerd. Meld je bij de balie, dan lossen we het op.";
    if(k.reden==="geen_ritten") return "Op deze kaart staan geen ritten meer. Koop een nieuwe via <a href=\"kaart.html\">Kaart kopen</a> of aan de balie.";
    if(k.reden==="verlopen") return "Deze kaart is niet meer geldig. Kies een ander tarief of bel ons op "+TELEFOON+".";
    if(k.reden==="geen_kaart") return "Bij deze gegevens staat geen actieve kaart of abonnement. Bel ons op "+TELEFOON+".";
    if(k.reden==="andere_sport") return "Deze kaart geldt voor " + (k.sport||"een andere sport") + ", niet voor " + PROFIEL.naam.toLowerCase() + ".";
    return "Deze kaart kunnen we nu niet gebruiken. Bel ons op "+TELEFOON+".";
  }
  // Eén tarief voor het hele gezelschap. Bij footgolf staan er tot veertig spelers in één
  // starttijd, en dan is een keuzelijst per speler geen keuze meer maar een formulier.
  function zetAlleTypes(typeId){ setRijen(rijen.map(()=>({betaal:"gast", type:typeId}))); }
  // Kiest de gast per speler een tarief, of één tarief voor iedereen? De grens staat per sport
  // in data.js. Bij een gezelschap dat over meerdere starttijden verdeeld wordt (groot) was er
  // al één tarief, want boek_groep kent er ook maar één.
  const apartTarief = !groot && aantal <= PROFIEL.tariefApartTot;
  const samenType = rijen[0] ? (rijen[0].betaal==="gast" ? rijen[0].type : (gast?gast.id:"")) : "";
  const prijsGast=t=>prijsVan(regels,t,datum,slot.starttijd,productId,baanId);
  function prijsRij(r){ if(r.betaal==="seizoen"||r.betaal==="kaart"||r.betaal==="code") return 0;
    if(r.betaal==="lid") return prijsVan(regels, lidType?lidType.id:r.type, datum, slot.starttijd, productId, baanId)||0;
    return prijsGast(r.type)||0; }
  const strepen = rijen.filter(r=>r.betaal==="kaart").length;
  const stripTekort = strip && strepen>strip.saldo;
  // Rijen die op een ingetypte code betalen. Zolang er een code loopt die nog niet gecontroleerd
  // is of die niet bruikbaar bleek, mag de knop niet door: anders weigert de database het pas
  // nadat de gast al twee schermen verder is.
  const codeRijen = rijen.filter(r=>r.betaal==="code");
  const kaartKlaar = k => !!k && (k.bron==="recras" ? k.geldig===true : !!k.bruikbaar);
  const codeNietKlaar = codeRijen.some(r=>!kaartKlaar(r.kaart) || r.kaartBezig);
  // Recras-kaart (bouwspec 4.2 punt 3 en 4). De eerste rij met een geldige Recras-code draagt de hele
  // reservering. Een tweede Recras-code, een eigen code of een eigen kaart ernaast kan online niet.
  const recrasIndex = rijen.findIndex(r=>r.betaal==="code" && r.kaart && r.kaart.bron==="recras" && r.kaart.geldig===true);
  const recras = recrasIndex>=0 ? rijen[recrasIndex].kaart : null;
  const recrasCode = recrasIndex>=0 ? rijen[recrasIndex].code : null;
  // Ronde 2: een op naam gevonden Recras-kaart heeft geen code in de browser, wel een kaart_ref.
  const recrasRef = recrasIndex>=0 && !recrasCode ? rijen[recrasIndex].recrasRef : null;
  const recrasTeGroot = !!recras && (groot || aantal>4);
  function recrasMelding(r, i){
    const k = r.kaart;
    if(!k || k.bron!=="recras") return null;
    if(k.geldig===true && recrasIndex>=0 && i!==recrasIndex)
      return {klasse:"fout", tekst:"Een Recras-kaart geldt voor de hele reservering. Vul hem maar één keer in."};
    if(k.geldig!==true) return {klasse:"fout", tekst:recrasFoutTekst(k.reden)};
    const naam = k.kaartnaam || "Strippenkaart";
    const strip = n => n + " strip" + (n===1?"":"pen");
    const opNaam = k.viaNaam ? "Kaart gevonden op naam: "+naam+", eindigt op "+k.code_eind+". " : "";
    if(k.strippen_over===null || k.strippen_over===undefined)
      return {klasse:"let groen", tekst:opNaam+naam+" is geldig. Dekt "+k.gedekt_spelers+" van je "+aantal+" spelers"+
        (k.bij_te_betalen_cent>0 ? "; de rest ("+euro(k.bij_te_betalen_cent)+") betaal je daarna online." : ".")};
    if(k.bij_te_betalen_cent>0)
      return {klasse:"let groen", tekst:opNaam+naam+", nog "+strip(k.strippen_over)+". Dekt "+k.gedekt_spelers+" van je "+aantal+" spelers; de rest ("+euro(k.bij_te_betalen_cent)+") betaal je daarna online."};
    return {klasse:"let groen", tekst:opNaam+naam+", nog "+strip(k.strippen_over)+". Dekt deze reservering."};
  }
  // Rijen die niet samen met een Recras-kaart kunnen: een tweede Recras-code, een eigen code, of een
  // eigen strippen- of seizoenskaart van een ingelogde houder.
  const recrasDubbel = !!recras && rijen.some((r,i)=>i!==recrasIndex && r.betaal==="code" && r.kaart && r.kaart.bron==="recras" && r.kaart.geldig===true);
  const recrasGemengd = !!recras && rijen.some((r,i)=>i!==recrasIndex &&
    ((r.betaal==="code" && !(r.kaart && r.kaart.bron==="recras")) || r.betaal==="kaart" || r.betaal==="seizoen"));
  const recrasBlokkeert = recrasTeGroot || recrasDubbel || recrasGemengd;
  // Dezelfde rittenkaart mag meerdere plekken betalen, maar niet meer dan er ritten op staan.
  // Dat rekent de database ook na; dit is de vriendelijke variant ervoor.
  const codeTekort = (() => {
    const per = {};
    codeRijen.forEach(r=>{ if(r.kaart && r.kaart.bruikbaar && r.kaart.soort==="strippenkaart"){
      const c=(r.code||"").trim().toUpperCase(); per[c]=per[c]||{n:0,saldo:r.kaart.saldo,houder:r.kaart.houder}; per[c].n++; } });
    return Object.keys(per).map(c=>per[c]).find(x=>x.n>x.saldo) || null;
  })();
  // Een abonnement geldt voor de houder zelf, niet voor het hele gezelschap. De database laat de
  // tweede speler op hetzelfde abonnement gewoon het normale tarief betalen; hier zeggen we dat
  // vooraf, want anders staat er ineens een bedrag dat de gast niet verwacht.
  const aboDubbel = (() => {
    const gezien = {};
    for(const r of codeRijen){
      if(r.kaart && r.kaart.bruikbaar && r.kaart.soort==="abonnement"){
        const c=(r.code||"").trim().toUpperCase();
        if(gezien[c]) return r.kaart.houder || "dit abonnement";
        gezien[c]=true;
      }
    }
    return null;
  })();
  const totaalEigen = groot ? (prijsGast(rijen[0].type)||0)*aantal : rijen.reduce((s,r)=>s+prijsRij(r),0);
  // Met een Recras-kaart is Recras de betaalmeester: het bedrag komt uit recras-kaart-controle, niet uit prijsVan.
  const totaal = recras ? (recras.bij_te_betalen_cent||0) : totaalEigen;
  // Ronde 9: alleen golf, een starttijd, hoogstens 4 spelers, iedereen op het gasttarief Greenfee, geen kaart
  // of code op een rij, en er valt iets te betalen. Alles daarbuiten (jeugd, lid, kaart, footgolf, groepen)
  // gaat precies de oude weg: Recras kent daar in de testomgeving nog geen product voor.
  // Bevinding 1, security-poort (19 september 2026): !paar18 toegevoegd. Zonder deze uitsluiting
  // koos 2x 9 holes zonder kaart ook deze route, terwijl `boek_tijdslot` daar met p_product_id
  // hard null krijgt en zelf het 9-holesproduct kiest (order by strippen, code limit 1). Gevolg:
  // het scherm toonde 36,50, er werd 21,00 afgerekend en de tweede starttijd bleef onvastgelegd.
  // 2x 9 holes zonder kaart gaat nu weer precies de oude weg (recras-reserveer met p_product_id
  // van het 18-holesproduct), die het goede bedrag afrekent; negen holes zonder kaart blijft de
  // nieuwe, starttijdloze Recras-route volgen.
  const losseViaRecras = losseRecras && !!mollie && !recras && !groot && !paar18 && SPORT==="golf" && slot.sport==="golf"
    && aantal<=4 && !!gast && gast.code==="GAST" && totaalEigen>0
    && rijen.every(r=>r.betaal==="gast" && r.type===gast.id);

  async function reserveer(){
    setBezig(true); setF(null);
    // Recras-kaart (bouwspec 4.2 punt 5): NIET boek_tijdslot en probeerBetaling, maar recras-reserveer.
    // Die maakt zelf eerst onze boeking en daarna de reservering in Recras. Bewust geen stuurMail:
    // nog niet vastgesteld dat de bevestigingsmail klopt voor dit pad.
    if(recras){
      if(recrasBlokkeert){ setBezig(false); return; }
      const kaartDeel = recrasRef
        ? { kaart_ref: recrasRef.kaart_ref, kaart_naam: recrasRef.naam, kaart_email: recrasRef.email }
        : { code: recrasCode };
      const u = await recrasReserveer(Object.assign({ tijdslot_id: slot.id, naam: naam.trim(), email: email.trim(),
        telefoon: tel.trim()||null}, gsnNodig ? { gsn: gsnSchoon } : {},
        kaartDeel, { deelnemers: rijen.map(()=>({ spelerstype_id: gast?gast.id:"", naam: naam.trim() })),
        terugkeer: window.location.origin },
        ts.actief ? { turnstile_token: tsToken } : {}));
      if(u.status==="betalen"){
        if(u.referentie) zetInAdres("wacht", u.referentie);
        window.location.href=u.url; return;
      }
      if(u.status==="bevestigd"){
        onKlaar({referentie:u.referentie, flights:1, spelers:aantal, bedrag:0, betaald:true, recras:true});
        return;
      }
      console.warn("[upgolf] recras-reserveer: " + u.reden);
      // Een Turnstile-token is maar een keer bruikbaar: na een weigering een nieuw vak.
      if(ts.actief){ setTsToken(null); setTsRonde(n=>n+1); }
      setF(u.reden==="boeken" ? foutTekst({message:u.melding}) : recrasFoutTekst(u.reden, u.referentie));
      setBezig(false); return;
    }
    // Ronde 9: losse greenfee via Recras. Zelfde afhandeling als de Recras-kaart hierboven, maar zonder kaartdeel.
    if(losseViaRecras){
      const u = await recrasReserveer(Object.assign({ soort:"losse_greenfee", tijdslot_id: slot.id, naam: naam.trim(),
        email: email.trim(), telefoon: tel.trim()||null, gsn: gsnSchoon,
        deelnemers: rijen.map(()=>({ spelerstype_id: gast.id, naam: naam.trim() })),
        terugkeer: window.location.origin },
        ts.actief ? { turnstile_token: tsToken } : {}));
      if(u.status==="betalen"){
        if(u.referentie) zetInAdres("wacht", u.referentie);
        window.location.href=u.url; return;
      }
      if(u.status==="bevestigd"){
        onKlaar({referentie:u.referentie, flights:1, spelers:aantal, bedrag:totaalEigen, betaald:true, recras:true});
        return;
      }
      console.warn("[upgolf] recras-reserveer (losse greenfee): " + u.reden);
      if(ts.actief){ setTsToken(null); setTsRonde(n=>n+1); }
      setF(u.reden==="boeken" ? foutTekst({message:u.melding}) : losseRecrasFoutTekst(u.reden, u.referentie));
      setBezig(false); return;
    }
    // De boeking staat op dit punt al in de database. Lukt het starten van de betaling niet,
    // dan mogen we dat niet wegpoetsen: geen bevestigingsmail sturen en op de bon eerlijk
    // melden dat er nog niets betaald is. "gratis" is nadrukkelijk geen fout.
    let betaalFout = null;
    // Zet de bon meteen goed bij een reservering van nul euro. De echte stand komt daarna toch
    // uit de database, maar zonder dit staat er een tel lang "nog niet betaald" op het scherm van
    // iemand die zojuist met zijn kaart heeft betaald.
    let gratisAfgerond = false;
    // Het reserveringsnummer gaat MEE in het webadres voordat we naar de betaaldienst vertrekken.
    // Komt de gast terug met de terugknop, dan is dat het enige spoor dat er al een reservering
    // bestaat: het scherm dat hij dan ziet komt uit het geheugen van de browser en weet van niets.
    // Zie de toelichting bij de pageshow-luisteraar in data.js.
    async function probeerBetaling(bId, referentie){
      const u = await startBetaling(bId);
      if(u.status==="betalen"){
        if(referentie) zetInAdres("wacht", referentie);
        window.location.href=u.url; return true;
      }
      // De REDEN gaat naar de console en NIET naar het scherm. Hij komt ongefilterd uit de
      // betaalfunctie (`String(d.error)`) of is systeemtaal zoals "de betaaldienst gaf status 500".
      // Zulke tekst tussen haakjes in een zin voor een gast is precies wat een demo laat ontsporen.
      // De bouwer heeft hem wel nodig, dus hij blijft in de console staan.
      if(u.status==="gratis"){ gratisAfgerond = true; }
      if(u.status==="fout"){
        console.warn("[upgolf] betaling niet gestart, boeking " + bId + ": " + u.reden);
        betaalFout = true;
      }
      return false;
    }
    if(groot){
      const r=await db.rpc("boek_groep",Object.assign({p_start_tijdslot_id:slot.id,p_naam:naam.trim(),p_email:email.trim(),
        p_aantal:aantal,p_spelerstype_id:rijen[0].type,p_telefoon:tel.trim()||null}, gsnDeel));
      if(r.error){ setF(foutTekst(r.error)); setBezig(false); return; }
      const bId = r.data.boeking_id || null;
      // Ook bij nul euro. Een reservering die helemaal met een kaart of abonnement betaald is,
      // heeft niets af te rekenen, maar stond wel als "nog niet betaald" in de database en dus
      // ook zo op de bon en op de tee sheet. Het gratis-pad in mollie-betaling zet hem netjes op
      // betaald zonder iets bij de betaaldienst aan te maken; dat pad bestond al en werd alleen
      // niet aangeroepen. Gemeten op 14 augustus: reservering D5FN-NMZP, nul euro, meldde
      // "Nog niet betaald, bel ons op "+TELEFOON+"".
      if(mollie && r.data.bedrag_cent>=0 && bId){
        if(await probeerBetaling(bId, r.data.referenties && r.data.referenties[0])) return;
      }
      // Punt 1 (17 september 2026): bij een echte betaling stuurt mollie-webhook de mail,
      // zodra de betaling op betaald staat. Hier alleen nog als er niets af te rekenen valt.
      if(bId && !betaalFout && !(mollie && r.data.bedrag_cent>0)) stuurMail(bId,"bevestiging");
      onKlaar({referentie:r.data.referenties[0],flights:r.data.flights,spelers:aantal,bedrag:r.data.bedrag_cent,betaald:gratisAfgerond,betaalFout:betaalFout});
    } else {
      const deeln = rijen.map(r=>{
        const o={ spelerstype_id: r.betaal==="gast"?r.type:(lidType?lidType.id:r.type), naam:naam.trim() };
        // Een code wijst de kaart zelf aan. Er gaat dan bewust GEEN lid_id mee: bij een
        // rittenkaart hoort de plek bij de speler en niet bij de houder, en bij een abonnement
        // zoekt de database de houder zelf op via de code.
        if(r.betaal==="code"){ o.kaartcode = (r.code||"").trim(); return o; }
        if(r.betaal!=="gast") o.lid_id = lid.id;
        if(r.betaal==="kaart") o.kaart_id = strip.id; return o; });
      // Ronde 6: bij 18 holes MOET het product mee, want daar hangt de tweede starttijd aan. Bij
      // 9 holes gaat er net als voorheen geen product mee en kiest de database het zelf.
      const productDeel = (paar18 && productId) ? { p_product_id: productId } : {};
      const r=await db.rpc("boek_tijdslot",Object.assign({p_tijdslot_id:slot.id,p_naam:naam.trim(),p_email:email.trim(),
        p_deelnemers:deeln,p_telefoon:tel.trim()||null}, productDeel, gsnDeel));
      if(r.error){ setF(foutTekst(r.error)); setBezig(false); return; }
      const bId = r.data.id;
      // Ook bij nul euro. Een reservering die helemaal met een kaart of abonnement betaald is,
      // heeft niets af te rekenen, maar stond wel als "nog niet betaald" in de database en dus
      // ook zo op de bon en op de tee sheet. Het gratis-pad in mollie-betaling zet hem netjes op
      // betaald zonder iets bij de betaaldienst aan te maken; dat pad bestond al en werd alleen
      // niet aangeroepen. Gemeten op 14 augustus: reservering D5FN-NMZP, nul euro, meldde
      // "Nog niet betaald, bel ons op "+TELEFOON+"".
      if(mollie && r.data.bedrag_cent>=0 && bId){
        if(await probeerBetaling(bId, r.data.boekingsreferentie)) return;
      }
      // Punt 1 (17 september 2026): bij een echte betaling stuurt mollie-webhook de mail,
      // zodra de betaling op betaald staat. Hier alleen nog als er niets af te rekenen valt.
      if(bId && !betaalFout && !(mollie && r.data.bedrag_cent>0)) stuurMail(bId,"bevestiging");
      onKlaar({referentie:r.data.boekingsreferentie,flights:paar18?2:1,paar:paar18,spelers:aantal,bedrag:r.data.bedrag_cent,betaald:gratisAfgerond,betaalFout:betaalFout});
    }
  }

  const knoptekst = bezig ? "Bezig…" : (recras
    ? (totaal>0 ? "Naar betalen, "+euro(totaal) : "Reservering bevestigen")
    : (mollie && totaal>0 ? "Naar betalen, "+euro(totaal) : "Reservering bevestigen"));

  return (
    <>
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        <h3>{stap===1?"Met hoeveel spelers?":stap===2?"Jouw gegevens":"Kloppen de gegevens?"}</h3>
        <p className="bij">{lang(datum)} om {slot.starttijd.slice(0,5)} · stap {stap} van 3</p>

      {stap===1 && <>
        <label>Aantal spelers</label>
        {/* Snelkeuzes staan boven de teller en niet in plaats daarvan. Bij footgolf passen er
            veertig spelers in één starttijd, en dat zijn negenendertig tikken op de plusknop.
            De teller blijft eronder staan voor alles wat niet in de rij past, bijvoorbeeld elf.
            Een keuze die groter is dan wat er nog vrij is tonen we niet: die zou een gast naar
            een melding leiden in plaats van naar een reservering. */}
        {PROFIEL.snelkeuzes &&
          <div className="snelkeuze">
            {PROFIEL.snelkeuzes.filter(n=>n<=Math.min(slot.vrij, PROFIEL.maxSpelers)).map(n=>
              <button key={n} type="button" className={"sk"+(aantal===n?" aan":"")}
                aria-pressed={aantal===n} onClick={()=>zetAantal(n)}>{n}</button>)}
          </div>}
        <div className="teller"><button onClick={()=>zetAantal(aantal-1)} aria-label="Eén speler minder">-</button><div className="n">{aantal}</div><button onClick={()=>zetAantal(aantal+1)} aria-label="Eén speler erbij">+</button></div>
        {groot && <div className="let">Met {aantal} spelers passen jullie niet in één {PROFIEL.groepWoord}. Jullie starten in groepen na elkaar, de eerste om {slot.starttijd.slice(0,5)}. Op het scherm hierna staan alle starttijden.</div>}
        {/* Ronde 6: boven de vier spelers kan 18 holes online niet. Tekst goedgekeurd door Roy op
            18 september 2026, staat in data.js zodat hij op een plek staat. */}
        {teGrootVoor18 && <div className="let deels">{MELDING_18_MEER_DAN_VIER}</div>}
        {teVolVoor18 && <div className="let deels">Op deze twee starttijden {plekken18===1?"is nog maar 1 plek":"zijn nog maar "+plekken18+" plekken"} vrij. Kies een andere tijd of speel met minder spelers.</div>}
        {/* EIS 5, tweede plek: bij de spelerskeuze, dus voordat de gast zijn gegevens invult.
            Tekst wacht op goedkeuring; leeg betekent geen blok. */}
        {paar18 && uitleg18(UITLEG_18_BIJ_TIJD, slot.starttijd, slot.tweede_starttijd) &&
          <div className="let">{uitleg18(UITLEG_18_BIJ_TIJD, slot.starttijd, slot.tweede_starttijd)}</div>}
        {!groot && !apartTarief && <div><label>Tarief voor het hele gezelschap</label>
          <select value={samenType} onChange={e=>zetAlleTypes(e.target.value)}>
            {types.filter(t=>!t.is_lid).map(t=><option key={t.id} value={t.id}>{tariefNaam(t)}, {euro(prijsGast(t.id)||0)} per speler</option>)}
          </select>
          <div className="let">Jullie spelen samen op één starttijd. Wil je per speler een ander tarief, dan regelt de balie dat bij aankomst.</div>
        </div>}
        {!groot && apartTarief && <div><label>Spelers</label>
          {rijen.map((r,i)=>(
            <div className="spelerrij" key={i}><div className="nr">{i+1}</div>
              {/* EEN keuzelijst per speler, met een unieke waarde per optie. Hier stonden er twee, en de
                  eerste gaf ELK gasttype dezelfde waarde "gast". Twee opties met dezelfde waarde kan een
                  keuzelijst niet uit elkaar houden, dus wie Jeugd koos kreeg Greenfee: gemeten op 11
                  augustus, de keuze sprong terug en het bedrag bleef 21,00. De tweede lijst zette het
                  type wel goed, maar dat is een tweede handeling voor wat een gast als een keuze ziet.
                  Nu draagt elke optie zijn eigen waarde: gast:<typeId> voor een gasttarief, en
                  seizoen, kaart of lid voor de kaarthoudersopties. */}
              <select value={r.betaal==="gast" ? "gast:"+r.type : r.betaal}
                onChange={e=>{ const v=e.target.value;
                  if(v.indexOf("gast:")===0) wz(i,{betaal:"gast", type:v.slice(5)}); else wz(i,{betaal:v}); }}>
                {lid && heeftSeizoen && i===0 && <option value="seizoen">Seizoenskaart, gratis</option>}
                {lid && heeftStrip && <option value="kaart">Strippenkaart, 1 streep</option>}
                {lid && kaartTarief && lidPrijs!==null && <option value="lid">Ledentarief, {euro(lidPrijs)}</option>}
                {types.filter(t=>!t.is_lid).map(t=><option key={t.id} value={"gast:"+t.id}>{tariefNaam(t)}, {euro(prijsGast(t.id)||0)}</option>)}
                {/* Een keuze voor allebei de soorten. De code zegt zelf of het een rittenkaart of
                    een abonnement is, dus de gast hoeft dat niet te weten. */}
                {kaartTarief && <option value="code">Ik heb een kaart of abonnement</option>}
              </select>
            </div>))}
          {/* Het codeveld staat onder de rij en niet ernaast: op een telefoon is een veld van
              negen tekens naast een keuzelijst te smal om te lezen wat je typt. */}
          {rijen.map((r,i)=> r.betaal!=="code" ? null : (
            <div className="codeveld" key={"c"+i}>
              {!r.zoekModus && <>
              <label htmlFor={"code"+i}>Code van speler {i+1}</label>
              <input id={"code"+i} className="code-in" value={r.code||""} autoComplete="off" spellCheck="false"
                placeholder="ABCD-12345"
                /* De kaart wordt nagekeken zodra de code compleet is, dus zonder dat de gast
                   ergens op hoeft te klikken. Wachten op het verlaten van het veld was te
                   kwetsbaar: op een telefoon sluit je het toetsenbord en dan gebeurde er niets.
                   Uit het veld stappen of Enter drukken werkt nog steeds, voor wie plakt. */
                onChange={e=>{ const v=e.target.value; wz(i,{code:v, kaart:null});
                  controleerVolgensSoort(i, v, false); }}
                onBlur={e=>{ if(alGecontroleerd(r, e.target.value)) return; controleerVolgensSoort(i, e.target.value, true); }}
                onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); if(alGecontroleerd(r, e.target.value)) return; controleerVolgensSoort(i, e.target.value, true); } }}/>
              {r.kaartBezig && <div className="let">Even kijken…</div>}
              {!r.kaartBezig && r.kaart && r.kaart.bron!=="recras" && r.kaart.bruikbaar && r.kaart.soort==="strippenkaart" &&
                <div className="let groen">Kaart van {r.kaart.houder}, nog {r.kaart.saldo} rit{r.kaart.saldo===1?"":"ten"}.</div>}
              {!r.kaartBezig && r.kaart && r.kaart.bron!=="recras" && r.kaart.bruikbaar && r.kaart.soort==="abonnement" &&
                <div className="let groen">Abonnement van {r.kaart.houder}, geldig tot {kort(r.kaart.geldig_tot)}.</div>}
              {!r.kaartBezig && r.kaart && r.kaart.bron!=="recras" && codeFout(r.kaart) && <div className="fout">{codeFout(r.kaart)}</div>}
              {!r.kaartBezig && r.kaart && r.kaart.bron==="recras" && recrasMelding(r, i) &&
                <div className={recrasMelding(r, i).klasse}>{recrasMelding(r, i).tekst}</div>}
              {!r.kaartBezig && !r.kaart && (r.code||"").trim() &&
                <div className="let">De code is negen tekens, zoals ABCD-12345. Vul hem helemaal in, dan kijken we hem meteen na.</div>}
              {/* Voor wie de code niet meer heeft: naam plus e-mailadres in plaats van de code.
                  Geen automatisch zoeken tijdens het typen, want elke poging telt mee tegen een
                  rem op mislukte pogingen; de gast klikt zelf op "Kaart zoeken". */}
              <button type="button" className="terug" onClick={()=>wz(i,{zoekModus:true, zoekFout:null})}>
                Code kwijt? Zoek op naam en e-mailadres
              </button>
              </>}
              {r.zoekModus && <>
              <label htmlFor={"zoeknaam"+i}>Naam van de kaarthouder</label>
              <input id={"zoeknaam"+i} value={r.zoekNaam||""} autoComplete="off"
                onChange={e=>wz(i,{zoekNaam:e.target.value})}
                onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekOpNaamEmail(i, r.zoekNaam, r.zoekEmail); } }}/>
              <label htmlFor={"zoekemail"+i}>E-mailadres waarmee de kaart is gekocht</label>
              <input id={"zoekemail"+i} type="email" value={r.zoekEmail||""} autoComplete="off"
                onChange={e=>wz(i,{zoekEmail:e.target.value})}
                onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekOpNaamEmail(i, r.zoekNaam, r.zoekEmail); } }}/>
              <button type="button" className="knop stil" disabled={r.zoekBezig}
                onClick={()=>zoekOpNaamEmail(i, r.zoekNaam, r.zoekEmail)}>
                {r.zoekBezig ? "Even zoeken…" : "Kaart zoeken"}
              </button>
              {r.zoekFout && <div className="fout">{r.zoekFout}</div>}
              {!r.zoekBezig && r.kaart && r.kaart.gevonden && r.kaart.bruikbaar && r.kaart.soort==="strippenkaart" &&
                <div className="let groen">Kaart van {r.kaart.houder}, nog {r.kaart.saldo} rit{r.kaart.saldo===1?"":"ten"}.</div>}
              {!r.zoekBezig && r.kaart && r.kaart.gevonden && r.kaart.bruikbaar && r.kaart.soort==="abonnement" &&
                <div className="let groen">Abonnement van {r.kaart.houder}, geldig tot {kort(r.kaart.geldig_tot)}.</div>}
              {!r.zoekBezig && r.kaart && r.kaart.gevonden && codeFout(r.kaart) && <div className="fout">{codeFout(r.kaart)}</div>}
              {/* Ronde 2 (bouwspec 4.2 punt 1): kaarten uit Recras als keuzeknoppen. Nooit de code of de
                  houdernaam uit Recras; alleen kaartnaam, de laatste vijf tekens en de staat. */}
              {!r.zoekBezig && r.recrasKeuzes && r.recrasKeuzes.map(k=>{
                const regel = recrasKaartRegel(k);
                const gekozen = r.recrasRef && r.recrasRef.kaart_ref===k.kaart_ref;
                return (<div key={"rk"+k.kaart_ref} style={{marginTop:6}}>
                  <button type="button" className={"knop stil"+(gekozen?" gekozen":"")} disabled={k.staat!=="bruikbaar"||r.kaartBezig}
                    onClick={()=>controleerRecrasRef(i, k, aantal)}>
                    {k.kaartnaam} · eindigt op {k.code_eind}
                  </button>
                  <div className={regel.klasse}>{regel.tekst}</div>
                </div>);})}
              {!r.zoekBezig && r.kaartBezig && r.recrasRef && <div className="let">Even kijken…</div>}
              {!r.zoekBezig && !r.kaartBezig && r.recrasRef && r.kaart && r.kaart.bron==="recras" && recrasMelding(r, i) &&
                <div className={recrasMelding(r, i).klasse}>{recrasMelding(r, i).tekst}</div>}
              <button type="button" className="terug" onClick={()=>wz(i,{zoekModus:false, zoekFout:null, kaart:null, code:"", recrasKeuzes:null, recrasRef:null})}>
                Ik heb de code toch bij de hand
              </button>
              </>}
            </div>))}
        </div>}
        {groot && <div><label>Tarief</label>
          <select value={rijen[0].type} onChange={e=>setRijen([{betaal:"gast",type:e.target.value}])}>
            {types.filter(t=>!t.is_lid).map(t=><option key={t.id} value={t.id}>{tariefNaam(t)}, {euro(prijsGast(t.id)||0)}</option>)}</select></div>}
        {/* Een ingelogde kaarthouder ziet op deze ingang geen kaartopties. Dat moet hij weten,
            anders denkt hij dat er iets stuk is. Alleen tonen als hij ook echt een kaart heeft. */}
        {!kaartTarief && lid && ((lid.kaarten&&lid.kaarten.length>0)||lid.seizoenskaart) &&
          <div className="let">Je strippen- en seizoenskaart gelden voor golf. Voor {PROFIEL.naam.toLowerCase()} reken je per speler af.</div>}
        {heeftSeizoen && !groot && <div className="let groen">Seizoenskaart geldig tot {kort(lid.seizoenskaart.geldig_tot)}, jij speelt gratis.</div>}
        {strip && !groot && <div className="let groen">Strippenkaart: {strip.saldo} streep{strip.saldo===1?"":"en"} over{strepen>0?", deze boeking gebruikt er "+strepen:""}.</div>}
        {stripTekort && <div className="fout">Je strippenkaart heeft maar {strip.saldo} streep{strip.saldo===1?"":"en"}.</div>}
        {codeTekort && <div className="fout">Deze kaart heeft nog {codeTekort.saldo} rit{codeTekort.saldo===1?"":"ten"}, en je zet er {codeTekort.n} spelers op. Zet een speler op een ander tarief.</div>}
        {aboDubbel && <div className="let">Een abonnement geldt voor {aboDubbel} zelf. De tweede speler op dezelfde code betaalt het gewone tarief.</div>}
        {recrasTeGroot && <div className="fout">Met deze kaart boek je online maximaal 4 spelers per reservering.</div>}
        {recrasGemengd && <div className="fout">Combineren van deze kaart met een andere kaart kan online nog niet. Bel ons op {TELEFOON}.</div>}
        {recras
          ? <div className="som"><div className="lb">Strippenkaart (Recras): {recras.gedekt_spelers} speler{recras.gedekt_spelers===1?"":"s"}</div><div className="bd">{euro(totaal)}</div></div>
          : <div className="som"><div className="lb">{aantal} speler{aantal===1?"":"s"}{strepen>0?" · "+strepen+" streep"+(strepen===1?"":"en"):""}</div><div className="bd">{euro(totaal)}</div></div>}
        <button className="knop vol" onClick={()=>setStap(2)} disabled={stripTekort||codeNietKlaar||!!codeTekort||recrasBlokkeert||teGrootVoor18||teVolVoor18}>Verder naar je gegevens</button>
      </>}

      {stap===2 && <>
        <label>Naam</label><input value={naam} onChange={e=>setNaam(e.target.value)} placeholder="Voor- en achternaam"/>
        <label>E-mail</label><input type="email" value={email} onChange={e=>setEmail(e.target.value)}/>
        {/* Punt 2 (17 september 2026): het GSN-veld is uit het boekscherm gehaald.
            Gemeten: geen enkele edge function stuurt het naar Recras, dus Recras heeft het niet
            nodig. De instelling golf_gsn_verplicht_online en de parameter p_gsn blijven bestaan
            voor de balie; gsnNodig staat daarom hieronder hard op onwaar. */}

        <label>Telefoon (optioneel)</label><input type="tel" value={tel} onChange={e=>setTel(e.target.value)}/>
        <div className="let">Je reserveringsnummer komt op het volgende scherm te staan. Het e-mailadres gebruiken we voor de bevestiging.</div>
        {email.trim() && !geldigEmail(email) && <div className="fout">Dit lijkt geen e-mailadres. Controleer of er een apenstaartje in staat en een punt erachter.</div>}
        <button className="knop vol" onClick={()=>setStap(3)} disabled={!naam.trim()||!geldigEmail(email)||!gsnKlaar}>Verder naar controleren</button>
      </>}

      {stap===3 && <>
        <div className="details">
          <div><span>Datum</span><span>{lang(datum)}</span></div>
          <div><span>{(groot||paar18)?"Eerste starttijd":"Starttijd"}</span><span>{slot.starttijd.slice(0,5)}</span></div>
          {paar18 && <div><span>Tweede starttijd</span><span>{String(slot.tweede_starttijd).slice(0,5)}</span></div>}
          {paar18 && <div><span>Holes</span><span>2x 9 holes, twee starttijden</span></div>}
          <div><span>Spelers</span><span>{aantal}</span></div>
          <div><span>Naam</span><span>{naam.trim()}</span></div>
          <div><span>E-mail</span><span>{email.trim()}</span></div>
          {tel.trim() && <div><span>Telefoon</span><span>{tel.trim()}</span></div>}
        </div>
        {recras && <div className="details">
          <div><span>Strippenkaart (Recras)</span><span>{recras.gedekt_spelers} speler{recras.gedekt_spelers===1?"":"s"}</span></div>
        </div>}
        {recras
          ? <div className="som"><div className="lb">Te betalen via Recras</div><div className="bd">{euro(totaal)}</div></div>
          : <div className="som"><div className="lb">Totaal{strepen>0?", "+strepen+" streep"+(strepen===1?"":"en"):""}</div><div className="bd">{euro(totaal)}</div></div>}
        {groot && <div className="let">Jullie starten in groepen na elkaar. Alle starttijden staan op het scherm hierna en in je bevestiging.</div>}
        {/* EIS 5, derde en belangrijkste plek: hier legt de gast zich vast. Tekst wacht op
            goedkeuring van Roy en staat in data.js; leeg betekent geen blok. */}
        {paar18 && uitleg18(UITLEG_18_VOOR_BETALEN, slot.starttijd, slot.tweede_starttijd) &&
          <div className="let">{uitleg18(UITLEG_18_VOOR_BETALEN, slot.starttijd, slot.tweede_starttijd)}</div>}
        {!((recras || losseViaRecras) && totaal>0) && <div className="let">{(mollie||recras) && totaal>0
          ? "Je plek staat vast zodra je bevestigt. Betaal je daarna niet, dan komt de starttijd na ongeveer een half uur weer vrij."
          : (totaal===0
              ? "Voor deze reservering hoef je niets af te rekenen."
              : "Voor deze reservering wordt online niet afgerekend. Bel ons op "+TELEFOON+" als je wilt weten hoe je betaalt.")}</div>}
        {(recras || losseViaRecras) && ts.actief && ts.sitekey && <TurnstileVak key={tsRonde} sitekey={ts.sitekey} onToken={setTsToken}/>}
        {f && <div className="fout">{f}</div>}
        <div className="let">Door te boeken ga je akkoord met onze <VoorwaardenLink/>.</div>
        <button className="knop vol" onClick={()=>setBevestigOpen(true)} disabled={!naam.trim()||!geldigEmail(email)||bezig||stripTekort||recrasBlokkeert||((!!recras || losseViaRecras) && ts.actief && !tsToken)}>{knoptekst}</button>
      </>}

      <button className="terug" onClick={()=>stap===1?onSluit():setStap(stap-1)}>{stap===1?"Terug naar de tijden":"Terug"}</button>
      </div>
    </div>
    {bevestigOpen && <BevestigStap datum={datum} slot={slot} aantal={aantal} totaal={totaal} mollie={mollie||!!recras} paar18={paar18}
      onDoorgaan={()=>{ setBevestigOpen(false); reserveer(); }}
      onTerug={()=>setBevestigOpen(false)}/>}
    </>
  );
}

function Klaar({ bon, datum, slot, mollie, opnieuw }){
  const mis = !!bon.betaalFout;
  // De bon komt uit het geheugen van dit scherm. De WAARHEID staat in de database, en die kan al
  // afwijken op het moment dat dit scherm verschijnt (een groepsboeking krijgt zijn tweede starttijd
  // pas in de database, en de betaalstatus wordt daar bijgewerkt). Daarom halen we de stand op met
  // zoek_boeking en tonen we die zodra hij binnen is. Lukt dat niet, dan blijft de bon staan; dan
  // toont dit scherm minder, maar nooit iets onwaars.
  const [stand,setStand]=useState(null);
  useEffect(()=>{ let weg=false;
    (async()=>{ const r=await db.rpc("zoek_boeking",{p_referentie:bon.referentie});
      if(!weg && r.data && r.data.gevonden) setStand(r.data); })();
    return ()=>{ weg=true; };
  },[bon.referentie]);

  const w = stand ? betaalWoorden(stand.betaalstatus) : null;
  // Bij een Recras-kaart betaalt Recras; het bedrag op onze boeking is dan de greenfee-rekensom en
  // niet wat de gast betaalde. Dan blijft het bedrag van de bon staan.
  const bedrag = (stand && !bon.recras) ? (stand.bedrag_cent||0) : bon.bedrag;
  const dagTekst = stand ? lang(stand.datum) : lang(datum);
  const tijdTekst = stand ? String(stand.starttijd).slice(0,5) : slot.starttijd.slice(0,5);
  const aantalFlights = stand ? stand.aantal_boekingen : bon.flights;

  return (
    <div className="scrim"><div className="blad"><div className="klaar">
      <div className={"vink"+(mis?" letop":"")}>{mis?"!":"✓"}</div>
      <h3>{mis?"Reservering staat, betaling niet gestart":"Je staat genoteerd"}</h3>
      <p className="bij">{mis
        ? "Je plek is gereserveerd, maar het online afrekenen lukte niet. Er is nog niets betaald. Neem contact op met dit reserveringsnummer."
        : "We zien je graag op de baan. Schrijf je reserveringsnummer over, daarmee vind je deze reservering altijd terug."}</p>
      <div className="codelb">Reserveringsnummer</div>
      <div className="code">{bon.referentie}</div>
      <div className="details">
        <div><span>Datum</span><span>{dagTekst}</span></div>
        <div><span>{aantalFlights>1?"Eerste starttijd":"Starttijd"}</span><span>{tijdTekst}</span></div>
        <div><span>Spelers</span><span>{bon.spelers}</span></div>
        {aantalFlights>1 && <div><span>Starttijden</span><span>{(stand && stand.paar) || (!stand && bon.flights===2 && bon.paar) ? "2, dus 2x 9 holes" : aantalFlights+" na elkaar"}</span></div>}
        <div><span>Bedrag</span><span>{bedrag===0?"Voldaan met kaart":euro(bedrag)}</span></div>
        <div><span>Status</span><span>{w?w.kop:(bon.betaald?"Betaald":"Nog niet betaald")}</span></div>
      </div>
      {w && <div className={stand.betaalstatus==="mislukt"?"fout":"let"}>{w.bij}</div>}
      {!bon.recras && <div className="mailmelding">{mis
        ? "Er is geen bevestiging per e-mail verstuurd, want de boeking is nog niet betaald."
        : "Je krijgt een bevestiging per e-mail. Komt die niet aan, dan blijft je reserveringsnummer gewoon geldig."}</div>}
      <button className="knop vol stil" onClick={opnieuw}>Nog een tijd reserveren</button>
    </div></div></div>
  );
}

function Wachtlijst({ baan, datum }){
  const [naam,setNaam]=useState(""); const [email,setEmail]=useState("");
  const [tel,setTel]=useState(""); const [dagdeel,setDagdeel]=useState("heledag");
  const [aantal,setAantal]=useState(1); const [bezig,setBezig]=useState(false);
  const [klaar,setKlaar]=useState(false); const [fout,setFout]=useState(null);
  const [open,setOpen]=useState(false);

  async function aanmelden(){
    setBezig(true); setFout(null);
    // Let op: `email` gaat als LEEG TEKSTVELD mee en niet als null. De kolom `wachtlijst.email` staat op
    // NOT NULL, gemeten op 11 augustus: met null komt er een 23502 terug en wordt er niets opgeslagen.
    // Een leeg veld wordt wel geaccepteerd. Zolang die kolom niet nullable is, is dit de enige manier om
    // e-mail echt optioneel te maken voor de gast. Staat als punt op de databasewenslijst.
    // De wachtlijst kent geen sportkolom: `meld_wachtlijst` neemt baan, datum en dagdeel, meer niet.
    // Nagemeten op 12 augustus 2026. Zonder de sport erbij kan de balie een footgolfaanmelding niet
    // onderscheiden van een golfaanmelding, en die twee spelen op dezelfde baan op dezelfde dag.
    // Daarom gaat de sport mee in het vrije opmerkingenveld. Dat is een noodgreep en geen oplossing;
    // een echte `sport`-kolom staat op de databasewenslijst. Voor de golfingang blijft het veld leeg,
    // precies zoals het was.
    const r=await db.rpc("meld_wachtlijst",{p_baan_id:baan.id,p_datum:datum,p_dagdeel:dagdeel,
      p_naam:naam.trim(),p_email:email.trim(),p_aantal:Number(aantal),p_telefoon:tel.trim(),
      p_opmerking:PROFIEL.wachtlijstOpmerking});
    if(r.error){ setFout(foutTekst(r.error)); setBezig(false); return; }
    setKlaar(true); setBezig(false);
  }

  if(klaar) return (
    <div className="wl-klaar">
      <div className="wl-vink">✓</div>
      <div><b>Je staat op de wachtlijst</b><div className="wl-sub">Komt er een plek vrij op {kort(datum)}, dan nemen we contact met je op.</div></div>
    </div>);

  if(!open) return (
    <div className="wl-lok">
      <div className="wl-tekst">Deze dag zit vol. Wil je dat we je bellen zodra er een plek vrijkomt?</div>
      <button className="wl-knop" onClick={()=>setOpen(true)}>Zet mij op de wachtlijst</button>
    </div>);

  return (
    <div className="wl-form">
      <div className="dagdeel" style={{marginTop:0}}>Wachtlijst voor {kort(datum)}</div>
      <label className="wl-lab">Naam</label>
      <input className="wl-in" value={naam} onChange={e=>setNaam(e.target.value)} placeholder="Voor- en achternaam"/>
      {/* Telefoon staat boven e-mail en is verplicht, want de belofte op dit scherm is dat we BELLEN.
          Een wachtlijst met alleen een e-mailadres kan de balie die belofte niet waarmaken. */}
      <label className="wl-lab">Telefoon</label>
      <input className="wl-in" type="tel" value={tel} onChange={e=>setTel(e.target.value)} placeholder="06 12 34 56 78"/>
      <label className="wl-lab">E-mail (optioneel)</label>
      <input className="wl-in" type="email" value={email} onChange={e=>setEmail(e.target.value)} placeholder="jij@voorbeeld.nl"/>
      {email.trim() && !geldigEmail(email) && <div className="wl-fout">Dit lijkt geen e-mailadres. Laat het veld leeg of vul een volledig adres in.</div>}
      <div className="wl-rij">
        <div style={{flex:1}}><label className="wl-lab">Voorkeur</label>
          <select className="wl-in" value={dagdeel} onChange={e=>setDagdeel(e.target.value)}>
            <option value="heledag">Hele dag</option><option value="ochtend">Ochtend</option><option value="middag">Middag</option>
          </select></div>
        <div style={{width:96}}><label className="wl-lab">Spelers</label>
          <input className="wl-in" type="number" min="1" max={PROFIEL.wachtlijstMax} value={aantal} onChange={e=>setAantal(e.target.value)}/></div>
      </div>
      <button className="wl-knop vol" onClick={aanmelden}
        disabled={bezig||!naam.trim()||!tel.trim()||(email.trim()&&!geldigEmail(email))}>
        {bezig?"Bezig…":"Aanmelden voor wachtlijst"}</button>
      {fout && <div className="wl-fout">{fout}</div>}
    </div>);
}

// Betaalstatus in gewone taal. zoek_boeking geeft er vier: open, betaald, mislukt en terugbetaald.
// "mislukt" betekent hier ook "vervallen": markeer_mislukt zet de boeking op geannuleerd en geeft de
// starttijd weer vrij, en sinds de databasewijziging van 10 augustus zetten de annuleerfuncties de
// betaalstatus mee. Een gast moet dus niet lezen dat er "iets mislukt is" maar dat de reservering weg is.
// Let op: de betaalstand alleen is sinds 22 augustus 2026 niet meer het hele verhaal. Een reservering
// kan "Betaald" tonen terwijl hij niet meer doorgaat (betaald en daarna geannuleerd of als no-show
// afgeboekt). Die stand komt niet uit deze functie maar uit het veld gaat_door, zie StandBlok
// hieronder. De twee samen, in die volgorde, geven het eerlijke beeld.
function betaalWoorden(status){
  if(status==="betaald")      return {kop:"Betaald",           bij:"Het bedrag van deze reservering is voldaan."};
  // Hier stond tot 12 augustus 2026 dat de gast bij aankomst aan de balie kon betalen. Dat is een BELOFTE over wat de
  // balie kan, en wij hebben nergens vastgesteld dat een openstaande online reservering aan de balie
  // alsnog geind kan worden. Staat als vraag voor Stefan en Rene in het meetverslag van die dag.
  // Zolang het antwoord er niet is, wijst dit scherm naar de balie zonder te zeggen wat die gaat doen.
  if(status==="open")         return {kop:"Nog niet betaald",  bij:"De reservering staat, maar er is nog niet afgerekend. Bel ons op "+TELEFOON+" en houd je reserveringsnummer bij de hand."};
  if(status==="mislukt")      return {kop:"Vervallen",         bij:"Deze reservering is komen te vervallen en de starttijd is weer vrij. Maak een nieuwe reservering."};
  if(status==="terugbetaald") return {kop:"Terugbetaald",      bij:"Het bedrag van deze reservering is teruggestort."};
  return {kop:"Onbekend", bij:"We kunnen de stand van deze reservering niet aflezen."};
}

// Hier stond tot 15 september 2026 het component met de bel-link naar de balie. Weggehaald op besluit
// van Roy (context/besluiten.md, blok UPgolf, punt 9): UP wil niet gebeld worden, dus er staat geen
// telefoonnummer en geen bel-link in deze voorkant. Alle plekken die het component toonden, en elke
// tekst die de gast naar de telefoon verwees, zijn in dezelfde ronde aangepast (BOUWLOGBOEK, Ronde 11).

// Zijroute: een gast die zijn reserveringsnummer heeft, kan hier zijn reservering terugvinden.
// Bewust ALLEEN opzoeken en niet annuleren, en dat blijft zo. `zoek_boeking` geeft de interne id met
// opzet niet terug, dus er is voor een gast niets om een annulering op te hangen.
//
// Hier stond dat de annuleerfunctie "open staat voor iedere bezoeker". Dat is op 12 augustus 2026
// nagemeten en het klopt NIET meer: `annuleer_boeking` begint met een controle op `is_medewerker()`
// en gooit anders "Geen toegang" met code 42501. De regel bleef staan omdat de conclusie
// ("wij bouwen hier geen annuleerknop") toevallig dezelfde is, maar een reden die niet klopt is geen
// reden. Een kaarthouder annuleert via Mijn reserveringen. Hoe een gast zonder
// kaart annuleert, is sinds 15 september 2026 een open vraag: de route via de telefoon is weg (besluit 9).
// Een gevonden reservering, in beide routes hetzelfde getekend. LET OP, opnieuw gecorrigeerd op
// 22 augustus 2026 na een tweede meting, dezelfde dag: de constatering hierboven (dat
// boeking_bij_naam_email de drie velden niet gaf) is zelf alweer achterhaald. Enterprise heeft
// boeking_bij_naam_email diezelfde dag uitgebreid met exact dezelfde ladder als zoek_boeking. Nagemeten
// op staging: md5(prosrc) e49340a45dca0787a280f74b80e97e76, def 3893d6ede16dca0ea747164e07ff98f0,
// lengte 5012. Beide routes (nummer via zoek_boeking, en naam plus e-mail via boeking_bij_naam_email)
// geven nu status, gaat_door en actieve_boekingen. Dat betekent dat StandBlok hieronder in de normale
// situatie voor BEIDE routes zichzelf toont. De voorwaarde "alleen tonen als gaat_door aanwezig is"
// blijft staan als vangnet, niet als de normale gang van zaken: een route die ooit weer minder velden
// teruggeeft (een oudere edge function, een teruggedraaide migratie) laat dit component gewoon
// degraderen in plaats van breken.
function StandBlok({ r }){
  if (typeof r.gaat_door === "undefined") return null; // komt uit boeking_bij_naam_email: geen stand bekend
  if (r.gaat_door) {
    if (r.status === "deels_actief") {
      // Het bedrag en de betaalstand hierboven/-onder gelden voor de HELE groep, dus zonder dit
      // aantal erbij klopt het bedrag niet meer met wat de gast op het scherm ziet.
      return <div className="let deels">{r.paar
        ? "Van deze 2x 9 holes staat nog "+r.actieve_boekingen+" van de 2 starttijden; de andere is vervallen."
        : "Niet je hele groep gaat door: "+r.actieve_boekingen+" van "+r.aantal_boekingen+" starttijden staan nog, de rest is vervallen."}</div>;
    }
    return null; // status actief: de gewone betaalstand hieronder is dan het hele verhaal
  }
  // gaat_door is false. Dit weegt zwaarder dan de betaalstand, dus dit blok komt voor de details en
  // gebruikt de opvallendste melding die dit scherm kent (.fout), niet de neutrale .let.
  if (r.status === "no_show") {
    return <div className="fout stand">Deze reservering gaat niet door: hij staat genoteerd als niet verschenen.</div>;
  }
  if (r.status === "geannuleerd") {
    return <div className="fout stand">Deze reservering is geannuleerd en gaat niet door. Je hoeft niet te komen.</div>;
  }
  // status "gemengd": niets meer actief, maar niet alle starttijden om dezelfde reden. Voor de gast
  // maakt dat niets uit, hij hoeft alleen te weten dat er niets meer doorgaat.
  return <div className="fout stand">Deze reservering gaat niet door: de starttijden zijn vervallen (deels geannuleerd, deels als niet verschenen genoteerd).</div>;
}

// De Verplaatsen-knop staat hier alleen, niet op elk gevonden resultaat: hij verschijnt alleen als
// de aanroeper `onVerplaats` meegeeft (dat doet Opzoeken alleen in de reserveringsnummer-route, zie
// hoofdstuk 1 van BOUWOPDRACHT-ENTERPRISE-verplaatsen-en-teller-20260822.md: "Ingang met het
// reserveringsnummer"), en alleen als het zinvol is: de reservering moet nog doorgaan (r.gaat_door,
// dus alleen bekend via zoek_boeking, niet via boeking_bij_naam_email) en het mag geen boeking met
// meerdere starttijden na elkaar zijn. Bij een groep geeft zoek_boeking de VROEGSTE starttijd van
// de hele groep terug, niet per se die van de opgezochte referentie: een verplaatsknop zou dan een
// tijd tonen die niet bij het nummer hoort. Zo'n boeking verplaats je aan de balie.
//
// 18 september 2026, bevinding 1 van de livetest: een 18-holespaar is OOK meer dan een flight, maar
// het is geen groep. Daar is de vroegste starttijd juist precies de goede (de eerste negen), en
// verplaats_boeking_gast verzet sinds vandaag het hele paar in een keer. zoek_boeking geeft daarom
// het veld 'paar' terug. Alleen een ECHTE groep (groep en niet paar) gaat nog naar de balie.
function GevondenReservering({ r, onVerplaats }){
  const w = betaalWoorden(r.betaalstatus);
  const echteGroep = !!r.groep && !r.paar;
  const magVerplaatsen = onVerplaats && r.gaat_door && !echteGroep;
  return (
    <div>
      <div className="code">{r.referentie}</div>
      <StandBlok r={r}/>
      <div className="details">
        <div><span>Datum</span><span>{lang(r.datum)}</span></div>
        <div><span>{r.groep?"Eerste starttijd":"Starttijd"}</span><span>{String(r.starttijd).slice(0,5)}</span></div>
        {r.paar && <div><span>Starttijden</span><span>2, dus 2x 9 holes</span></div>}
        {r.groep && !r.paar && <div><span>Starttijden</span><span>{r.aantal_boekingen} na elkaar</span></div>}
        <div><span>Bedrag</span><span>{euro(r.bedrag_cent||0)}</span></div>
        <div><span>Status</span><span>{w.kop}</span></div>
      </div>
      <div className={r.betaalstatus==="mislukt"?"fout":"let"}>{w.bij}</div>
      {magVerplaatsen &&
        <button type="button" className="wl-knop" style={{marginTop:8}} onClick={()=>onVerplaats(r.referentie)}>
          Verplaatsen naar een andere datum of tijd
        </button>}
      {onVerplaats && r.gaat_door && echteGroep &&
        <div className="let">Deze boeking heeft meerdere starttijden na elkaar. Wil je deze starttijden verplaatsen? Bel ons op {TELEFOON}.</div>}
    </div>
  );
}

// Tekst bij een niet-bruikbare of onbekende kaart, hergebruikt door SaldoCheck en MijnUp. Zelfde
// woorden als codeFout in Boeken, als eigen top-level functie: dat blok zit binnen Boeken en kon
// niet zonder risico verplaatst worden, dus dit is een eigen kopie in plaats van een wijziging aan
// werkende code. Toegevoegd 22 augustus 2026, bouwronde saldo checken en MIJN UP.
// ---- Bevinding 2 van de livetest (18 september 2026): de voorwaarden in een venster ----
//
// Wat er misging: de link naar voorwaarden.html laadde een hele pagina. De boekgegevens staan
// alleen in het geheugen van deze pagina, dus ze waren weg zodra de gast terugkwam, of hij nu de
// link "Terug naar boeken" gebruikte of de terugknop van de browser. Het vinkje is verplicht, dus
// dit was een echte drempel.
//
// En een tweede fout op dezelfde plek: de link bij het akkoordvinkje stond BINNEN het <label>.
// Een klik op een link in een label activeert ook het label, dus het vinkje klapte om zodra de
// gast op "algemene voorwaarden" klikte. Daarvoor is de stopPropagation hieronder.
//
// De oplossing is niet een terugweg bijbouwen maar het herladen helemaal vermijden: de tekst komt
// in een venster over het scherm heen. De tekst wordt uit voorwaarden.html zelf gehaald, zodat er
// maar EEN plek is waar de voorwaarden staan; zou hij hier overgetypt worden, dan lopen de twee
// na de eerste wijziging uit elkaar en klopt er een van de twee niet meer.
// De href blijft staan: zonder javascript, of als het ophalen mislukt, werkt de gewone pagina nog.
function VoorwaardenVenster({ onSluit }){
  const [html,setHtml]=useState(null); const [fout,setFout]=useState(false);
  useEffect(()=>{
    let weg=false;
    fetch("voorwaarden.html")
      .then(r=>{ if(!r.ok) throw new Error("status "+r.status); return r.text(); })
      .then(t=>{
        if(weg) return;
        const d=new DOMParser().parseFromString(t,"text/html");
        // Weg met alles wat hier niet hoort: de terugkeerlink (die zou de pagina alsnog herladen),
        // scripts en de eigen stijlkoppelingen van die pagina.
        d.querySelectorAll('a[href="index.html"], script, link, style').forEach(e=>e.remove());
        const bron=d.querySelector("main")||d.body;
        setHtml(bron?bron.innerHTML:"");
      })
      .catch(e=>{ if(!weg){ console.warn("[upgolf] voorwaarden ophalen mislukt: "+(e&&e.message)); setFout(true); } });
    return ()=>{ weg=true; };
  },[]);
  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        <h3>Algemene voorwaarden</h3>
        {!html && !fout && <div className="wl-tekst">Voorwaarden ophalen…</div>}
        {fout && <div className="let">De voorwaarden zijn nu niet op te halen. Ze staan ook op <a href="voorwaarden.html" target="_blank" rel="noopener">deze pagina</a>. Let op: die opent apart, je reservering blijft hier staan.</div>}
        {html && <div className="voorwaardentekst" dangerouslySetInnerHTML={{__html:html}}/>}
        <button className="knop vol stil" onClick={onSluit}>Terug naar mijn reservering</button>
      </div>
    </div>
  );
}

// De link zelf, met zijn eigen venster. preventDefault houdt de pagina staan, stopPropagation
// houdt het akkoordvinkje met rust.
function VoorwaardenLink({ tekst }){
  const [open,setOpen]=useState(false);
  return (
    <>
      <a href="voorwaarden.html" onClick={e=>{ e.preventDefault(); e.stopPropagation(); setOpen(true); }}>{tekst||"algemene voorwaarden"}</a>
      {open && <VoorwaardenVenster onSluit={()=>setOpen(false)}/>}
    </>
  );
}

function codeFoutTekst(k){
  if(!k) return null;
  if(!k.gevonden) return "Deze code kennen we niet. Kijk of je hem goed hebt overgetypt.";
  if(k.bruikbaar) return null;
  if(k.reden==="geblokkeerd") return "Deze kaart is geblokkeerd. Meld je bij de balie, dan lossen we het op.";
  if(k.reden==="geen_ritten") return "Op deze kaart staan geen ritten meer. Koop een nieuwe via <a href=\"kaart.html\">Kaart kopen</a> of aan de balie.";
  if(k.reden==="verlopen") return "Deze kaart is niet meer geldig. Kies een ander tarief of bel ons op "+TELEFOON+".";
  if(k.reden==="geen_kaart") return "Bij deze gegevens staat geen actieve kaart of abonnement. Bel ons op "+TELEFOON+".";
  if(k.reden==="andere_sport") return "Deze kaart geldt voor " + (k.sport||"een andere sport") + ".";
  return "Deze kaart kunnen we nu niet gebruiken. Bel ons op "+TELEFOON+".";
}

// Toont het resultaat van kaart_bij_code of kaart_bij_naam_email, hergebruikt door SaldoCheck en
// MijnUp. Zelfde beperkte veldenset als bij het boeken (houder, saldo of geldig_tot, kaartnaam):
// meer geeft de functie toch niet terug. Toegevoegd 22 augustus 2026.
function KaartSaldoResultaat({ k }){
  if(!k || !k.gevonden) return null;
  if(k.bruikbaar && k.soort==="strippenkaart") return (
    <div className="let groen">{k.kaartnaam||"Strippenkaart"} van {k.houder}, nog {k.saldo} rit{k.saldo===1?"":"ten"}.</div>);
  if(k.bruikbaar && k.soort==="abonnement") return (
    <div className="let groen">{k.kaartnaam||"Abonnement"} van {k.houder}, geldig tot {kort(k.geldig_tot)}.</div>);
  return <div className="fout">{codeFoutTekst(k)}</div>;
}

// Ronde 2 (14 september 2026, bouwspec 4.2 punt 2): kaarten uit Recras in Saldo checken en Mijn UP.
// Geen houdernaam (Recras-displaynamen kunnen "K23" zijn). Bij meer dan een kaart per regel "eindigt op".
function RecrasKaartRegels({ res }){
  if(!res || !res.gevonden || !Array.isArray(res.kaarten)) return null;
  const meer = res.kaarten.length > 1;
  return <>{res.kaarten.map((k,n)=>{ const regel = recrasKaartRegel(k);
    return <div key={"rs"+n} className={regel.klasse}>{regel.tekst}{meer ? " Eindigt op "+k.code_eind+"." : ""}</div>; })}</>;
}

// Punt 4 van het plan van 21 augustus 2026 (bouwronde 22 augustus): saldo checken zonder inlog, als
// eigen scherm los van het boekformulier. Met de code (kaart_bij_code) of met naam plus e-mailadres
// (kaart_bij_naam_email), allebei bewust ZONDER p_sport en ZONDER p_datum: dit scherm hoort niet bij
// een specifieke boeking, dus het saldo geldt voor elke sport waarvoor de kaart geldig is. Geen
// inlog, geen account, geen sessie die blijft hangen: elk bezoek begint leeg (besluit van Roy, 14
// augustus 2026, geen kaarthoudersdashboard).
function SaldoCheck({ onSluit }){
  const [modus,setModus]=useState("code");
  const [code,setCode]=useState("");
  const [naam,setNaam]=useState(""); const [email,setEmail]=useState("");
  const [bezig,setBezig]=useState(false);
  const [kaart,setKaart]=useState(null);   // null = nog niet gezocht
  const [recras,setRecras]=useState(null); // ronde 2: uitkomst van recras-houder, null = niet gevraagd
  const [fout,setFout]=useState(null);

  function wissel(nieuw){ setModus(nieuw); setFout(null); setKaart(null); setRecras(null); }

  async function zoekCode(){
    if(!code.trim()) return;
    setBezig(true); setFout(null); setKaart(null); setRecras(null);
    // Ronde 2: een code van 15 tekens is een Recras-kaart; 9 tekens blijft ons eigen systeem.
    if(codeSoort(code)==="recras"){
      const h = await recrasHouder({zoek:"code", code:code.trim()});
      setBezig(false);
      setRecras(h); setKaart({gevonden:false, alleenRecras:true});
      return;
    }
    const r=await db.rpc("kaart_bij_code",{p_code:code.trim()});
    setBezig(false);
    if(r.error){ setFout(foutTekst(r.error)); return; }
    setKaart(r.data);
  }
  async function zoekNaam(){
    if(!naam.trim()||!email.trim()) return;
    setBezig(true); setFout(null); setKaart(null); setRecras(null);
    const [r, h] = await Promise.all([
      db.rpc("kaart_bij_naam_email",{p_naam:naam.trim(), p_email:email.trim()}),
      recrasHouder({zoek:"naam_email", naam:naam.trim(), email:email.trim()})
    ]);
    setBezig(false);
    setRecras(h);
    if(r.error){ if(!(h && h.gevonden)) setFout(foutTekst(r.error)); setKaart({gevonden:false}); return; }
    setKaart(r.data);
  }

  const recrasGevonden = !!(recras && recras.gevonden);
  const remKaart = kaart && !recrasGevonden && (kaart.reden==="te_veel_pogingen" || (recras && recras.reden==="te_veel_pogingen"));
  const recrasWeg = !!(recras && recras.reden==="recras_onbereikbaar");

  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        <h3>Saldo checken</h3>
        <p className="bij">Kijk hoeveel ritten er nog op je strippenkaart staan, of tot wanneer je abonnement geldig is. Geen inlog nodig.</p>

        {modus==="code" && <>
          <label className="wl-lab" htmlFor="saldocode">Code van je kaart of abonnement</label>
          <input id="saldocode" className="wl-in code-in" value={code} autoComplete="off" spellCheck="false"
            placeholder="ABCD-12345"
            onChange={e=>setCode(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekCode(); } }}/>
          <button className="wl-knop" onClick={zoekCode} disabled={bezig||!code.trim()}>
            {bezig?"Even kijken…":"Saldo bekijken"}</button>
          <button type="button" className="terug" onClick={()=>wissel("naam")}>
            Code kwijt? Zoek op naam en e-mailadres
          </button>
        </>}

        {modus==="naam" && <>
          <label className="wl-lab" htmlFor="saldonaam">Naam</label>
          <input id="saldonaam" className="wl-in" value={naam} autoComplete="off" placeholder="Voor- en achternaam"
            onChange={e=>setNaam(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekNaam(); } }}/>
          <label className="wl-lab" htmlFor="saldoemail">E-mailadres waarmee je de kaart kocht</label>
          <input id="saldoemail" className="wl-in" type="email" value={email} autoComplete="off" placeholder="jij@voorbeeld.nl"
            onChange={e=>setEmail(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekNaam(); } }}/>
          <button className="wl-knop" onClick={zoekNaam} disabled={bezig||!naam.trim()||!email.trim()}>
            {bezig?"Even kijken…":"Saldo bekijken"}</button>
          <button type="button" className="terug" onClick={()=>wissel("code")}>
            Ik heb de code toch bij de hand
          </button>
        </>}

        {fout && <div className="fout">{fout}</div>}
        {kaart && remKaart &&
          <div className="fout">Er is net te vaak gezocht. Wacht een paar minuten, of bel ons op {TELEFOON}.</div>}
        {/* Aanvulling van 22 augustus 2026, op verzoek van hoofd-activiteiten: op dit scherm is de
            code het ENIGE wat een gast heeft ingetypt, dus wie hier vastloopt heeft een vervolg
            nodig. De code-tekst kreeg er daarom een tweede zin bij plus een echte bel-link. De naam-tekst
            blijft ongewijzigd: die noemt zelf al "Controleer allebei", en de tekst in het boekformulier
            (Boeken, codeFout) blijft ook ongewijzigd, dat was expliciet niet de vraag.
            15 september 2026 (Ronde 11, besluit 9 van Roy, geen telefoonnummer): de bel-link en het
            belgedeelte van de code-tekst zijn weg. De tweede zin (zoek op naam en e-mailadres) blijft. */}
        {kaart && !remKaart && !kaart.gevonden && !recrasGevonden && recrasWeg &&
          <div className="fout">{RECRAS_OPZOEK_ONBEREIKBAAR}</div>}
        {kaart && !remKaart && !kaart.gevonden && !recrasGevonden && !recrasWeg &&
          <div className="niets">{modus==="code"
            ? "Deze code kennen we niet. Kijk of je hem goed hebt overgetypt. Weet je de code niet meer? Zoek dan op naam en e-mailadres."
            : "We vinden geen kaart bij deze naam en dit e-mailadres. Controleer beide, of bel ons op "+TELEFOON+"."}</div>}
        {kaart && !remKaart && kaart.gevonden && <>
          <KaartSaldoResultaat k={kaart}/>
        </>}
        {recrasGevonden && <RecrasKaartRegels res={recras}/>}

        <button className="terug" onClick={onSluit}>Sluiten</button>
      </div>
    </div>
  );
}

// Punt 5 van het plan van 21 augustus 2026 (bouwronde 22 augustus): MIJN UP als verzamelscherm achter
// de rode knop in de header. Zoekt op naam plus e-mailadres (reserveringen via boeking_bij_naam_email
// EN kaartsaldo via kaart_bij_naam_email, in een keer) of op de code van de kaart (alleen saldo, via
// kaart_bij_code: boeking_bij_naam_email kent geen code-ingang). Geen wachtwoord, geen account, geen
// sessie die blijft hangen: elk bezoek begint leeg. De optie om hier later een echte inlog achter te
// zetten blijft open als latere uitbreiding en is nu bewust niet gebouwd (besluit van Roy, 14 augustus
// 2026, geen kaarthoudersdashboard).
function MijnUp({ onSluit }){
  const [modus,setModus]=useState("naam");
  const [naam,setNaam]=useState(""); const [email,setEmail]=useState("");
  const [code,setCode]=useState("");
  const [bezig,setBezig]=useState(false);
  const [gezocht,setGezocht]=useState(false);
  const [reserveringen,setReserveringen]=useState([]);
  const [kaart,setKaart]=useState(null);
  const [recras,setRecras]=useState(null); // ronde 2: uitkomst van recras-houder
  const [fout,setFout]=useState(null);

  function wissel(nieuw){ setModus(nieuw); setFout(null); setGezocht(false); setReserveringen([]); setKaart(null); setRecras(null); }

  async function zoekOpNaam(){
    if(!naam.trim()||!email.trim()) return;
    setBezig(true); setFout(null);
    // Beide functies tegelijk aanroepen: dit scherm is de synthese van de reservering- en de
    // kaartzoekroute die al bestonden, geen nieuwe database-aanroep die het niet al kon.
    const vragen=[ db.rpc("boeking_bij_naam_email",{p_naam:naam.trim(), p_email:email.trim()}) ];
    if(PROFIEL.kaarttarieven) vragen.push(db.rpc("kaart_bij_naam_email",{p_naam:naam.trim(), p_email:email.trim()}));
    // Ronde 2 (bouwspec 4.2 punt 3): ook de kaarten uit Recras. Reserveringen blijven uit onze database.
    if(PROFIEL.kaarttarieven) vragen.push(recrasHouder({zoek:"naam_email", naam:naam.trim(), email:email.trim()}));
    const uitkomsten = await Promise.all(vragen);
    const rb = uitkomsten[0], rk = uitkomsten[1];
    setRecras(uitkomsten[2]||null);
    setBezig(false); setGezocht(true);
    if(rb.error){ setFout(foutTekst(rb.error)); setReserveringen([]); setKaart(null); return; }
    const d = rb.data||{gevonden:false};
    if(d.reden==="te_veel_pogingen"){
      setFout(null); setReserveringen([]); setKaart({gevonden:false, reden:"te_veel_pogingen"}); return;
    }
    setReserveringen(d.gevonden ? (d.reserveringen||[]) : []);
    setKaart(rk && !rk.error ? rk.data : null);
  }

  async function zoekOpCode(){
    if(!code.trim()) return;
    setBezig(true); setFout(null); setRecras(null);
    if(codeSoort(code)==="recras"){
      const h = await recrasHouder({zoek:"code", code:code.trim()});
      setBezig(false); setGezocht(true); setReserveringen([]); setKaart(null); setRecras(h);
      return;
    }
    const r=await db.rpc("kaart_bij_code",{p_code:code.trim()});
    setBezig(false); setGezocht(true); setReserveringen([]);
    if(r.error){ setFout(foutTekst(r.error)); setKaart(null); return; }
    setKaart(r.data);
  }

  const recrasGevonden = !!(recras && recras.gevonden);
  const remKaart = !recrasGevonden && ((kaart && kaart.reden==="te_veel_pogingen") || (recras && recras.reden==="te_veel_pogingen" && reserveringen.length===0));
  const kaartGevonden = (kaart && kaart.gevonden) || recrasGevonden;
  const recrasWeg = !!(recras && recras.reden==="recras_onbereikbaar");
  const nietsGevonden = gezocht && !fout && !remKaart && reserveringen.length===0 && !kaartGevonden;
  const ietsGevonden = gezocht && !fout && !remKaart && (reserveringen.length>0 || kaartGevonden);

  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        <h3>Mijn UP</h3>
        <p className="bij">Al je reserveringen{PROFIEL.kaarttarieven?" en je kaartsaldo":""} bij elkaar. Zoek op naam en e-mailadres{PROFIEL.kaarttarieven?", of op de code van je kaart":""}. Geen inlog nodig.</p>

        {modus==="naam" && <>
          <label className="wl-lab" htmlFor="mijnupnaam">Naam</label>
          <input id="mijnupnaam" className="wl-in" value={naam} autoComplete="off" placeholder="Voor- en achternaam"
            onChange={e=>setNaam(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekOpNaam(); } }}/>
          <label className="wl-lab" htmlFor="mijnupemail">E-mailadres</label>
          <input id="mijnupemail" className="wl-in" type="email" value={email} autoComplete="off" placeholder="jij@voorbeeld.nl"
            onChange={e=>setEmail(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekOpNaam(); } }}/>
          <button className="wl-knop" onClick={zoekOpNaam} disabled={bezig||!naam.trim()||!email.trim()}>
            {bezig?"Zoeken…":"Zoek mijn gegevens"}</button>
          {PROFIEL.kaarttarieven &&
            <button type="button" className="terug" onClick={()=>wissel("code")}>
              Ik heb alleen de code van mijn kaart
            </button>}
        </>}

        {modus==="code" && <>
          <label className="wl-lab" htmlFor="mijnupcode">Code van je kaart of abonnement</label>
          <input id="mijnupcode" className="wl-in code-in" value={code} autoComplete="off" spellCheck="false"
            placeholder="ABCD-12345"
            onChange={e=>setCode(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoekOpCode(); } }}/>
          <button className="wl-knop" onClick={zoekOpCode} disabled={bezig||!code.trim()}>
            {bezig?"Zoeken…":"Kaart bekijken"}</button>
          <div className="let">Met de code zien we alleen je kaartsaldo. Voor je reserveringen zoek je op naam en e-mailadres.</div>
          <button type="button" className="terug" onClick={()=>wissel("naam")}>
            Zoek op naam en e-mailadres
          </button>
        </>}

        {fout && <div className="fout">{fout}</div>}
        {gezocht && !fout && remKaart &&
          <div className="fout">Er is net te vaak gezocht. Wacht een paar minuten.</div>}
        {nietsGevonden && recrasWeg && <div className="fout">{RECRAS_OPZOEK_ONBEREIKBAAR}</div>}
        {nietsGevonden && modus==="naam" &&
          <div className="niets">We vinden niets bij deze naam en dit e-mailadres. Controleer allebei.</div>}
        {/* Aanvulling van 22 augustus 2026, zelfde reden en zelfde tekst als bij SaldoCheck: hier is
            de code het enige wat is ingetypt, dus de tweede zin hoort er ook hier bij. De knop "Zoek op
            naam en e-mailadres" staat al op dit scherm (zie modus==="code" hierboven), dus de belofte
            in de tekst klopt met wat er echt staat. De bel-link die hier ook stond is op 15 september
            2026 weggehaald (Ronde 11, besluit 9 van Roy, geen telefoonnummer). */}
        {nietsGevonden && modus==="code" && <>
          <div className="niets">Deze code kennen we niet. Kijk of je hem goed hebt overgetypt. Weet je de code niet meer? Zoek dan op naam en e-mailadres.</div>
        </>}

        {ietsGevonden && reserveringen.length>0 && <div>
          <div className="dagdeel" style={{marginTop:0}}>Reserveringen</div>
          {reserveringen.length>1 && <div className="wl-tekst">We vinden {reserveringen.length} reserveringen.</div>}
          {reserveringen.map(r=><GevondenReservering key={r.referentie} r={r}/>)}
        </div>}

        {ietsGevonden && kaartGevonden && <div>
          <div className="dagdeel">Kaartsaldo</div>
          <KaartSaldoResultaat k={kaart}/>
          <RecrasKaartRegels res={recras}/>
        </div>}

        {/* Afkeurpunt 1 van de anti-ai-poort, 22 augustus 2026: deze zin verscheen ook in code-modus,
            waar boeking_bij_naam_email niet wordt aangeroepen (zie zoekOpCode hierboven) en er dus
            nooit reserveringen bekend zijn. De zin beloofde dan iets wat het scherm niet weet. Nu
            voorwaardelijk op wat er echt getoond is: met reserveringen de zin over de status, zonder
            reserveringen (code-modus, of naam-modus waarbij alleen het kaartsaldo iets opleverde) de
            kortere zin die alleen het saldo noemt. Herzien op 22 augustus 2026, zelfde dag, tweede
            keer: boeking_bij_naam_email geeft nu zelf status/gaat_door/actieve_boekingen terug (zie
            de toelichting bij StandBlok hierboven), dus de zin "of een reservering nog doorgaat, weet
            de balie zeker" was voor het geval MET reserveringen niet meer waar: dit scherm weet het
            zelf. Voor het geval ZONDER reserveringen is er niets om die status over te tonen, dus
            die zin is daar geschrapt in plaats van herschreven. */}
        {ietsGevonden && reserveringen.length>0 &&
          <div className="let">Dit scherm toont de betaling van je reserveringen, je kaartsaldo en of een reservering nog doorgaat.</div>}
        {ietsGevonden && reserveringen.length===0 &&
          <div className="let">Dit scherm toont je kaartsaldo.</div>}

        <button className="terug" onClick={onSluit}>Sluiten</button>
      </div>
    </div>
  );
}

function Opzoeken({ onVerplaats }){
  // Naam plus e-mailadres is de STANDAARDroute, het reserveringsnummer de terugval. Precies andersom
  // dan bij de kaart, en met opzet: een reserveringsnummer heeft een gast eenmalig gekregen en kent
  // hij niet uit zijn hoofd, zijn eigen naam en e-mailadres wel.
  // De databasefunctie boeking_bij_naam_email eist beide velden en zegt nooit welk van de twee fout
  // was. Bewust geen automatisch zoeken tijdens het typen: elke poging telt mee tegen een rem.
  const [modus,setModus]=useState("naam");
  const [naam,setNaam]=useState(""); const [email,setEmail]=useState("");
  const [ref,setRef]=useState("");
  const [bezig,setBezig]=useState(false);
  // null = nog niet gezocht, [] = wel gezocht en niets gevonden.
  const [lijst,setLijst]=useState(null);
  const [fout,setFout]=useState(null);

  function wissel(nieuw){ setModus(nieuw); setFout(null); setLijst(null); }

  // Alleen zoek_boeking (modus nummer) geeft gaat_door terug, boeking_bij_naam_email (modus naam) niet.
  // Aan het eerste element aflezen in plaats van aan modus, want lijst blijft staan tot een nieuwe
  // zoekactie en volgt zo altijd de bron van de getoonde resultaten.
  const weetStand = lijst && lijst.length>0 && typeof lijst[0].gaat_door !== "undefined";

  async function zoekOpNaam(){
    if(!naam.trim() || !email.trim()) return;
    setBezig(true); setFout(null); setLijst(null);
    const r=await db.rpc("boeking_bij_naam_email",{p_naam:naam.trim(), p_email:email.trim()});
    setBezig(false);
    if(r.error){ setFout(foutTekst(r.error)); return; }
    const d=r.data||{gevonden:false};
    if(d.reden==="te_veel_pogingen"){
      setFout("Er is net te vaak gezocht. Wacht een paar minuten."); return; }
    setLijst(d.gevonden ? (d.reserveringen||[]) : []);
  }

  async function zoekOpNummer(){
    if(!ref.trim()) return;
    setBezig(true); setFout(null); setLijst(null);
    const r=await db.rpc("zoek_boeking",{p_referentie:ref.trim()});
    setBezig(false);
    if(r.error){ setFout(foutTekst(r.error)); return; }
    setLijst(r.data && r.data.gevonden ? [r.data] : []);
  }

  return (
    /* opzoek-blok toegevoegd, ronde 5, 24 augustus 2026: scoped klasse zodat de velden en labels
       in DIT blok naar Daniels vorm gaan (dikke navy rand, scherpe hoeken, hoofdletterlabels),
       zonder de kale label/input-regels aan te raken die ook door de Wachtlijst, MijnUp, Saldo
       checken en Verplaatsen-formulieren gebruikt worden. Zie stijl.css bij .opzoek-blok. */
    <div className="kaart opzoek-blok"><h2><Kop tekst="Heb je al gereserveerd?"/></h2>

      {modus==="naam" && <>
        <div className="wl-tekst">Vul je naam in en het e-mailadres waarmee je hebt gereserveerd, dan zoeken we je reserveringen erbij. Ze moeten allebei kloppen.</div>
        <label className="wl-lab" htmlFor="zoeknaam">Naam</label>
        <input id="zoeknaam" className="wl-in" value={naam} autoComplete="off"
          placeholder="Voor- en achternaam"
          onChange={e=>setNaam(e.target.value)}
          onKeyDown={e=>e.key==="Enter"&&zoekOpNaam()}/>
        <label className="wl-lab" htmlFor="zoekemail">E-mailadres</label>
        <input id="zoekemail" className="wl-in" type="email" value={email} autoComplete="off"
          placeholder="jij@voorbeeld.nl"
          onChange={e=>setEmail(e.target.value)}
          onKeyDown={e=>e.key==="Enter"&&zoekOpNaam()}/>
        <button className="wl-knop" onClick={zoekOpNaam} disabled={bezig||!naam.trim()||!email.trim()}>
          {bezig?"Zoeken…":"Zoek mijn reserveringen"}</button>
        <button type="button" className="terug" onClick={()=>wissel("nummer")}>
          Ik heb mijn reserveringsnummer bij de hand
        </button>
      </>}

      {modus==="nummer" && <>
        <div className="wl-tekst">Vul je reserveringsnummer in, dan zoeken we hem erbij. Hoofdletters en het streepje maken niet uit.</div>
        <label className="wl-lab">Reserveringsnummer</label>
        <div className="zoekrij">
          <input className="wl-in zoekveld" value={ref} onChange={e=>setRef(e.target.value)}
            placeholder="ABCD-1234" onKeyDown={e=>e.key==="Enter"&&zoekOpNummer()}/>
          <button className="wl-knop" onClick={zoekOpNummer} disabled={bezig||!ref.trim()}>{bezig?"Zoeken…":"Zoeken"}</button>
        </div>
        <button type="button" className="terug" onClick={()=>wissel("naam")}>
          Zoek liever op naam en e-mailadres
        </button>
      </>}

      {fout && <div className="wl-fout">{fout}</div>}

      {lijst && lijst.length===0 && modus==="naam" &&
        <div className="niets">We vinden geen reservering bij deze naam en dit e-mailadres. Controleer allebei.</div>}
      {lijst && lijst.length===0 && modus==="nummer" &&
        <div className="niets">Geen reservering gevonden met dit nummer. Kijk het na in je bevestiging.</div>}

      {lijst && lijst.length>0 && <div>
        {lijst.length>1 && <div className="wl-tekst">We vinden {lijst.length} reserveringen.</div>}
        {/* onVerplaats gaat alleen mee in de reserveringsnummer-route: alleen zoek_boeking geeft
            gaat_door en groep terug, wat GevondenReservering nodig heeft om de knop terecht te tonen
            (zie de toelichting daar). In de naam-route zou de knop op een gok verschijnen. */}
        {lijst.map(r=><GevondenReservering key={r.referentie} r={r} onVerplaats={modus==="nummer"?onVerplaats:undefined}/>)}
        {/* Hier stond tot 21 augustus 2026 achteraan: "Ben je kaarthouder, log dan in, dan zie en
            annuleer je alles zelf bij Mijn reserveringen." Die zin is weg. De inlogroute voor
            kaarthouders is er niet meer (besluit 6 van Roy, 14 augustus 2026), dus het scherm bood
            een weg aan die niet bestaat. De rest van de regel blijft: die zegt wat dit scherm wel en
            niet vertelt en waar je heen moet om te wijzigen. */}
        {/* Herzien op 22 augustus 2026, na het aansluiten van zoek_boeking op status/gaat_door. Hier
            stond tot dan onvoorwaardelijk "Twijfel je of je reservering doorgaat? Bel ons op "+TELEFOON+".": dat
            was een noodverband omdat het systeem het toen niet wist. Herzien op 22 augustus 2026,
            zelfde dag, tweede keer: toen dit voor het eerst herzien werd, gaf alleen zoek_boeking
            (het reserveringsnummer) de drie velden terug en boeking_bij_naam_email (naam plus
            e-mailadres) niet. Dat is nu ook achterhaald: boeking_bij_naam_email geeft ze sinds
            vandaag ook (staging, md5(prosrc) e49340a45dca0787a280f74b80e97e76, lengte 5012), dus
            weetStand hieronder staat in de praktijk voor beide routes op true. De voorwaarde zelf
            (aflezen aan het eerste element in plaats van aan de modus) blijft bewust staan als
            vangnet: een route die weer minder velden teruggeeft, laat dit scherm degraderen in
            plaats van iets beweren wat het niet weet. */}
        {weetStand
          ? <div className="let">Dit scherm toont nu de betaling en of je reservering nog doorgaat.</div>
          : <div className="let">Dit scherm toont de betaling. Twijfel je of je reservering doorgaat? Bel ons op {TELEFOON}.</div>}
      </div>}
    </div>
  );
}

// Herkent het antwoord dat PostgREST geeft als een aangeroepen functie niet bestaat (foutcode
// PGRST202, "Could not find the function ... in the schema cache"). VERPLAATS_FUNCTIE hieronder
// bestaat op 22 augustus 2026 nog NIET in de database: de bouwopdracht ligt klaar voor Enterprise
// (BOUWOPDRACHT-ENTERPRISE-verplaatsen-en-teller-20260822.md, hoofdstuk 2.1) maar is niet gebouwd.
// Dit scherm roept de functie toch al aan, zodat het werkt zodra hij er is, zonder dat de gast
// ondertussen een blanco scherm of een databasecode als "PGRST202" te zien krijgt.
function functieOntbreektFout(e){
  if(!e) return false;
  if(e.code === "PGRST202") return true;
  const m = ((e.message||"")+" "+(e.details||"")+" "+(e.hint||""));
  return m.indexOf("Could not find the function") >= 0;
}
// De naam van de nog te bouwen databasefunctie. Enterprise mag zelf een andere naam kiezen (de
// opdracht zegt letterlijk "kies zelf een passende naam"); als dat gebeurt is dit de ENE plek waar
// dat hersteld moet worden.
const VERPLAATS_FUNCTIE = "verplaats_boeking_gast";
// Ingevulde keuze van onze kant (Transparant Samenwerken), GEEN besluit van Daniel of Rene: zie
// hoofdstuk 2.2 van hetzelfde document. Letterlijk daaruit: "In het gesprek is voorgesteld: 'je kan
// op de dag zelf niet meer verplaatsen, dat is toch slecht weer' ... Er kwam geen tegenspraak op,
// maar ook geen hard besluit." De sleutelnaam hieronder is dezelfde die de databasekant gaat
// gebruiken in de tabel `instellingen`, zodat een wijziging daar dit scherm en de database in een
// keer allebei omzet. Bestaat de rij nog niet (Enterprise heeft hem nog niet aangemaakt), dan geldt
// de standaardwaarde uit hoofdstuk 2.2 hieronder: verplaatsen op de dag zelf mag dan niet.
const VERPLAATSEN_OP_DAG_ZELF_SLEUTEL = "verplaatsen_op_dag_zelf_toegestaan";
const VERPLAATSEN_OP_DAG_ZELF_STANDAARD = false;

// Punt 8 van het plan van 21 augustus 2026 (bouwronde 22 augustus, blok 2 punt 2): een gast
// verplaatst zijn eigen reservering zelf naar een andere datum en tijd, zonder inlog, met alleen
// zijn reserveringsnummer. Bereikbaar vanaf de reserveringsnummer-route in Opzoeken (zie de knop in
// GevondenReservering) en, zonder dat er al een bevestigingsmail met een link bestaat, ALVAST met een
// querystring-ingang (?verplaats=REFERENTIE) zodat een latere link op ditzelfde scherm uitkomt in
// plaats van dat er straks een tweede scherm gebouwd moet worden. Zie App() voor die ingang.
function Verplaatsen({ referentieVooraf, onSluit }){
  const [ref,setRef]=useState(referentieVooraf||"");
  const [bezig,setBezig]=useState(false);
  const [fout,setFout]=useState(null);
  const [boeking,setBoeking]=useState(null);           // resultaat van zoek_boeking, of {gevonden:false}
  const [dagZelfMag,setDagZelfMag]=useState(VERPLAATSEN_OP_DAG_ZELF_STANDAARD);
  const [baan,setBaan]=useState(null);
  const [datum,setDatum]=useState(null);
  const [sloten,setSloten]=useState([]);
  const [ladenSloten,setLadenSloten]=useState(false);
  const [gekozen,setGekozen]=useState(null);
  const [verplaatsBezig,setVerplaatsBezig]=useState(false);
  const [ontbreekt,setOntbreekt]=useState(false);
  const [klaar,setKlaar]=useState(null);                // { vanDatum, vanTijd, naarDatum, naarTijd }

  // Zelfde toetsenbordondersteuning als BevestigStap (de bevestigingsstap voor het betalen, bouwronde
  // van eerder vandaag): Escape sluit het venster.
  useEffect(()=>{
    function opToets(e){ if(e.key==="Escape") onSluit(); }
    window.addEventListener("keydown", opToets);
    return ()=>window.removeEventListener("keydown", opToets);
  },[]);

  useEffect(()=>{
    db.from("instellingen").select("waarde").eq("sleutel",VERPLAATSEN_OP_DAG_ZELF_SLEUTEL).single()
      .then(r=>{ if(r.data) setDagZelfMag(r.data.waarde==="true"); });
    db.from("banen").select("*").eq("actief",true).order("naam").then(r=>{ if(r.data&&r.data.length) setBaan(r.data[0]); });
  },[]);

  async function zoek(){
    if(!ref.trim()) return;
    setBezig(true); setFout(null); setBoeking(null); setKlaar(null);
    const r=await db.rpc("zoek_boeking",{p_referentie:ref.trim()});
    setBezig(false);
    if(r.error){ setFout(foutTekst(r.error)); return; }
    if(!r.data || !r.data.gevonden){ setBoeking({gevonden:false}); return; }
    setBoeking(r.data);
    // Startdag voor de kiezer. De "dag zelf"-grens hierboven gaat over de HUIDIGE starttijd van de
    // boeking (mag je vandaag nog verplaatsen), niet over welke dag je als nieuwe tijd mag kiezen.
    // Verplaatsen NAAR vandaag mag altijd, mits de tijd nog niet voorbij is (hoofdstuk 2.1.4,
    // "vóór aanvang" geldt voor het doelslot net zo goed als voor de bron).
    // Sinds 18 september begint de kiezer op de dag van de reservering zelf en niet op vandaag:
    // wie verplaatst wil meestal een andere tijd rond diezelfde dag, en dan is vandaag een vreemd
    // startpunt. Ligt die dag in het verleden, dan blijft het vandaag.
    const eigenDag = r.data.datum && String(r.data.datum) >= iso(new Date()) ? String(r.data.datum) : iso(new Date());
    setDatum(eigenDag);
  }
  // Automatisch zoeken als het scherm met een referentie erin geopend wordt (vanaf de knop in
  // GevondenReservering, of vanaf ?verplaats= in het adres). Alleen bij binnenkomst, niet bij elke
  // letter die de gast daarna zelf intypt.
  useEffect(()=>{ if(referentieVooraf) zoek(); },[]); // eslint-disable-line

  useEffect(()=>{ if(!baan || !datum) return; let weg=false; setLadenSloten(true);
    (async()=>{
      const r=await db.from("beschikbaarheid").select("*").eq("baan_id",baan.id).eq("datum",datum).eq("status","open").eq("sport",SPORT).order("starttijd");
      if(weg) return; setLadenSloten(false);
      const nu=new Date(); const vandaag=datum===iso(nu); const klok=nu.toTimeString().slice(0,8);
      setSloten((r.data||[]).filter(s=>s.vrij>0 && (!vandaag||s.starttijd>klok)));
    })();
    return ()=>{ weg=true; };
  },[baan,datum]);

  async function verplaats(){
    if(!gekozen) return;
    setVerplaatsBezig(true); setFout(null); setOntbreekt(false);
    const r=await db.rpc(VERPLAATS_FUNCTIE,{p_referentie:ref.trim(), p_nieuw_tijdslot_id:gekozen.id});
    setVerplaatsBezig(false);
    if(r.error){
      if(functieOntbreektFout(r.error)){
        console.warn("[upgolf] "+VERPLAATS_FUNCTIE+" bestaat nog niet in de database: "+(r.error.message||""));
        setOntbreekt(true); return;
      }
      setFout(foutTekst(r.error)); return;
    }
    // Punt A (18 september 2026): de functie geeft bij een paar allebei de nieuwe starttijden terug,
    // plus het aantal minuten ertussen. Die namen we niet over, dus de gast zag alleen de eerste en
    // dacht dat de tweede ronde was blijven staan. De database klopte; het scherm loog.
    // d kan leeg zijn als een oudere databaseversie draait; dan valt alles terug op wat we al hadden.
    const d = r.data || {};
    setKlaar({
      vanDatum: boeking.datum,
      vanTijd: String(boeking.starttijd).slice(0,5),
      naarDatum: d.nieuwe_datum || datum,
      naarTijd: String(d.nieuwe_starttijd || gekozen.starttijd).slice(0,5),
      paar: !!d.paar,
      vanTijd2: d.oude_starttijd_tweede ? String(d.oude_starttijd_tweede).slice(0,5) : null,
      naarTijd2: d.tweede_starttijd ? String(d.tweede_starttijd).slice(0,5) : null,
      minuten: d.minuten_tussen || null,
      referentie: d.referentie || ref.trim().toUpperCase(),
      referentie2: d.tweede_referentie || null,
    });
    // Uit de test van 18 september: na een geslaagde verplaatsing bleef ?verplaats= in de adresbalk
    // staan. Ververste de gast de pagina, dan opende het verplaatsscherm opnieuw met dezelfde
    // reservering en leek het alsof er niets gebeurd was, terwijl het wel gelukt was. Zodra het
    // gelukt is hoort die parameter weg. De &dag= is hier ook weg: die stond op de dag waarop de
    // site geopend werd, niet op de dag van de boeking, en klopte dus juist niet meer.
    haalUitAdres("verplaats");
    haalUitAdres("dag");
    // Punt B: de mail met de nieuwe tijd. Bewust NA setKlaar, zodat het scherm niet op de mail
    // wacht: het verplaatsen is op dit moment al gelukt en staat in de database.
    stuurVerplaatsMail(
      d.referentie || ref.trim().toUpperCase(),
      boeking.datum,
      String(boeking.starttijd).slice(0,5),
      d.oude_starttijd_tweede ? String(d.oude_starttijd_tweede).slice(0,5) : null);
  }

  // Mag deze boeking überhaupt verplaatst worden? Twee harde grenzen, allebei client-side al
  // gecontroleerd zodat de gast niet eerst een nieuwe tijd hoeft te kiezen om dan pas te horen dat
  // het sowieso niet kan. De database controleert dit straks nog een keer (hoofdstuk 2.1.4 voor
  // "vóór aanvang", 2.2 voor de dag-zelf-grens); dit is de vriendelijke variant ervoor, net als
  // stripTekort en codeTekort dat elders in dit bestand ook al zijn.
  const nu = new Date();
  const bronMoment = boeking && boeking.gevonden ? new Date(boeking.datum+"T"+boeking.starttijd) : null;
  const bronIsVandaag = boeking && boeking.gevonden && boeking.datum===iso(nu);
  const voorbij = bronMoment && bronMoment < nu;
  const dagZelfGeblokkeerd = bronIsVandaag && !dagZelfMag;
  // Bevinding 1: een paar telt hier NIET als groep; dat wordt gewoon verplaatst.
  const echteGroep = !!(boeking && boeking.groep && !boeking.paar);
  const magKiezen = boeking && boeking.gevonden && boeking.gaat_door && !echteGroep && !voorbij && !dagZelfGeblokkeerd;

  const ochtend=sloten.filter(s=>s.starttijd<"12:00:00"); const middag=sloten.filter(s=>s.starttijd>="12:00:00");
  function tegel(s){ return (
    <button key={s.id} type="button" className={"tijd"+(gekozen&&gekozen.id===s.id?" aan":"")} onClick={()=>setGekozen(s)}>
      <div className="t">{s.starttijd.slice(0,5)}</div><div className="v">{s.vrij} vrij</div></button>); }

  return (
    <div className="scrim" onClick={e=>e.target===e.currentTarget&&onSluit()}>
      <div className="blad"><div className="greep"/>
        <h3>Verplaatsen</h3>

        {!referentieVooraf && !boeking && <>
          <p className="bij">Vul je reserveringsnummer in, dan zoeken we je reservering erbij.</p>
          <label className="wl-lab" htmlFor="verplaatsref">Reserveringsnummer</label>
          <input id="verplaatsref" className="wl-in code-in" value={ref} autoComplete="off" spellCheck="false"
            placeholder="ABCD-1234" onChange={e=>setRef(e.target.value)}
            onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); zoek(); } }}/>
          <button className="wl-knop" onClick={zoek} disabled={bezig||!ref.trim()}>{bezig?"Zoeken…":"Zoeken"}</button>
        </>}

        {bezig && !boeking && <div className="wl-tekst">Zoeken…</div>}
        {fout && <div className="wl-fout">{fout}</div>}

        {boeking && !boeking.gevonden &&
          <div className="niets">Geen reservering gevonden met dit nummer. Kijk het na in je bevestiging.</div>}

        {boeking && boeking.gevonden && !klaar && <>
          <GevondenReservering r={boeking}/>

          {!boeking.gaat_door &&
            <div className="let">Deze reservering gaat niet meer door, dus er is niets om te verplaatsen.</div>}

          {boeking.gaat_door && echteGroep &&
            <div className="let">Deze boeking heeft meerdere starttijden na elkaar. Wil je deze starttijden verplaatsen? Bel ons op {TELEFOON}.</div>}

          {boeking.gaat_door && boeking.paar && !voorbij && !dagZelfGeblokkeerd &&
            <div className="let">Dit zijn twee starttijden. Kies een nieuwe eerste starttijd; de tweede negen schuift automatisch mee. Past die niet meer, dan zeggen we dat en verschuift er niets.</div>}

          {boeking.gaat_door && !echteGroep && voorbij &&
            <div className="let">Deze starttijd is al voorbij. Verplaatsen kan dan niet meer.</div>}

          {boeking.gaat_door && !echteGroep && !voorbij && dagZelfGeblokkeerd &&
            <div className="let">Deze reservering staat voor vandaag. Op de dag zelf kun je niet zelf verplaatsen.</div>}

          {magKiezen && <>
            <p className="bij" style={{marginTop:16}}>Kies een nieuwe datum en tijd. Dit scherm toont alleen {PROFIEL.naam.toLowerCase()}-tijden. Was je boeking voor de andere sport, verplaats hem dan via <a href={PROFIEL.andereIngang.href}>deze pagina</a>.</p>
            <label className="wl-lab" htmlFor="verplaatsdag">Nieuwe datum</label>
            <input id="verplaatsdag" className="wl-in" type="date" value={datum||""} min={iso(new Date())}
              onChange={e=>{ setDatum(e.target.value); setGekozen(null); }}/>

            {ladenSloten && <div className="wl-tekst">Tijden ophalen…</div>}
            {!ladenSloten && sloten.length===0 &&
              <div className="niets">Geen vrije {PROFIEL.naam.toLowerCase()}-tijden op deze dag. Kies een andere datum.</div>}
            {!ladenSloten && ochtend.length>0 && <div><div className="dagdeel">Ochtend</div><div className="raster">{ochtend.map(tegel)}</div></div>}
            {!ladenSloten && middag.length>0 && <div><div className="dagdeel">Middag</div><div className="raster">{middag.map(tegel)}</div></div>}

            {gekozen && <>
              {/* Zelfde toon als de bevestigingsstap bij het boeken (BevestigStap): geen geld terug,
                  wel gewoon zelf verplaatsen, dat is precies wat hier gebeurt. */}
              <div className="details" style={{marginTop:12}}>
                <div><span>Van</span><span>{lang(boeking.datum)}, {String(boeking.starttijd).slice(0,5)}</span></div>
                <div><span>Naar</span><span>{lang(datum)}, {gekozen.starttijd.slice(0,5)}</span></div>
              </div>
              <div className="let">Je reservering blijft dezelfde boeking, alleen de starttijd verandert. Er komt geen geld bij of terug.</div>
              {ontbreekt &&
                <div className="fout">Verplaatsen kan op dit moment nog niet. Deze functie staat nog niet klaar.</div>}
              <button className="wl-knop vol" onClick={verplaats} disabled={verplaatsBezig}>
                {verplaatsBezig?"Bezig…":"Verplaats mijn reservering"}</button>
            </>}
          </>}
        </>}

        {klaar && <>
          {klaar.paar && klaar.naarTijd2
            ? <>
                <div className="let groen">Gelukt. Allebei je starttijden zijn verzet naar {lang(klaar.naarDatum)}: de eerste negen om {klaar.naarTijd} en de tweede negen om {klaar.naarTijd2}.</div>
                <div className="details" style={{marginTop:8}}>
                  <div><span>Eerste starttijd</span><span>{klaar.naarTijd}{klaar.vanTijd?" (was "+klaar.vanTijd+")":""}</span></div>
                  <div><span>Tweede starttijd</span><span>{klaar.naarTijd2}{klaar.vanTijd2?" (was "+klaar.vanTijd2+")":""}</span></div>
                  {klaar.minuten && <div><span>Ertussen</span><span>{klaar.minuten} minuten</span></div>}
                </div>
                <div className="wl-tekst">Je reserveringsnummer blijft {klaar.referentie}. Het is en blijft een reservering.</div>
              </>
            : <>
                <div className="let groen">Gelukt. Je reservering staat nu op {lang(klaar.naarDatum)} om {klaar.naarTijd}, in plaats van {lang(klaar.vanDatum)} om {klaar.vanTijd}.</div>
                <div className="wl-tekst">Je reserveringsnummer blijft {klaar.referentie}.</div>
              </>}
        </>}

        <button className="terug" onClick={onSluit}>Sluiten</button>
      </div>
    </div>
  );
}

function App(){
  const [sessie,setSessie]=useState(undefined); const [lid,setLid]=useState(null);
  const [mijnRes,setMijnRes]=useState([]); const [toonLogin,setToonLogin]=useState(false);
  const [baan,setBaan]=useState(null); const [types,setTypes]=useState([]); const [regels,setRegels]=useState([]);
  const [producten,setProducten]=useState([]);
  const [venster,setVenster]=useState(14); const [dicht,setDicht]=useState([]);
  // De dag komt uit het adres als die er staat, anders vandaag. Zo overleeft de keuze een verversing.
  const [datum,setDatum]=useState(()=>dagUitAdres()||iso(new Date())); const [sloten,setSloten]=useState([]);
  const [alleSloten,setAlleSloten]=useState([]); const [sportDagen,setSportDagen]=useState(null);
  const [laden,setLaden]=useState(true); const [gekozen,setGekozen]=useState(null);
  const [boeken,setBoeken]=useState(false); const [bon,setBon]=useState(null);
  const [mollie,setMollie]=useState(false); const [betaalBanner,setBetaalBanner]=useState(null);
  const [openUur,setOpenUur]=useState(null);
  // Ronde 6 (18 september 2026): 9 of 18 holes. Weesp heeft negen holes, dus 18 holes zijn twee
  // starttijden achter elkaar. De gast kiest hier 9 of 18; de rest van de stroom is dezelfde.
  // De schakelaar komt uit de database en staat standaard uit, zodat deze site zich zonder
  // schakelaar precies zo gedraagt als voor vandaag.
  const [holes,setHoles]=useState(9);
  const [holes18Aan,setHoles18Aan]=useState(false);
  useEffect(()=>{ achttienHolesAan().then(setHoles18Aan, ()=>setHoles18Aan(false)); },[]);
  // Toegevoegd 22 augustus 2026: MIJN UP-verzamelscherm (punt 5) en het losse saldo-scherm (punt 4).
  // Allebei gewoon een modal-toggle, net als toonLogin hierboven; geen sessie of route erachter.
  const [toonMijnUp,setToonMijnUp]=useState(false);
  const [toonSaldoCheck,setToonSaldoCheck]=useState(false);
  // Toegevoegd 22 augustus 2026, punt 8 van het plan: zelf verplaatsen. Geopend vanaf de knop in
  // GevondenReservering (via de reserveringsnummer-route) of vanaf ?verplaats=REFERENTIE in het
  // adres, zodat een link met de referentie erin (bijvoorbeeld later vanuit de bevestigingsmail) op
  // ditzelfde scherm uitkomt in plaats van dat daar een tweede scherm voor gebouwd moet worden.
  const [toonVerplaatsen,setToonVerplaatsen]=useState(false);
  const [verplaatsRef,setVerplaatsRef]=useState("");
  useEffect(()=>{ const p=new URLSearchParams(window.location.search).get("verplaats");
    if(p){ setVerplaatsRef(p); setToonVerplaatsen(true); } },[]);

  useEffect(()=>{ db.auth.getSession().then(r=>setSessie(r.data.session));
    const s=db.auth.onAuthStateChange((_e,ses)=>setSessie(ses)); return ()=>s.data.subscription.unsubscribe(); },[]);

  const laadLid=useCallback(async()=>{
    if(!sessie){ setLid(null); setMijnRes([]); return; }
    await db.rpc("koppel_mijn_lidmaatschap");
    const p=await db.rpc("mijn_lidprofiel"); setLid(p.data||null);
    const r=await db.rpc("mijn_reserveringen"); setMijnRes(r.data||[]);
  },[sessie]);
  useEffect(()=>{ laadLid(); },[laadLid]);

  useEffect(()=>{ db.from("instellingen").select("waarde").eq("sleutel","mollie_actief").single()
    .then(r=>{ if(r.data && r.data.waarde==="true") setMollie(true); }); },[]);

  useEffect(()=>{
    const p=new URLSearchParams(window.location.search);
    // ?recras= (bouwspec Recras-kaart 4.2 punt 8): terug van de Recras-betaling. Zelfde banners als
    // ?betaald=; per poging eerst recras-boekingstatus, die de stand uit Recras op onze boeking zet.
    const viaRecras = !p.get("betaald") && !!p.get("recras");
    const ref = p.get("betaald") || p.get("recras");
    const param = viaRecras ? "recras" : "betaald";
    if(!ref) return;
    // Mollie stuurt de gast terug zodra de betaling bevestigd is, maar de webhook naar de
    // database loopt daar PARALLEL aan. Eenmalig kijken levert daardoor vaak "in verwerking"
    // op terwijl het geld er een seconde later wel is, en de gast ziet dat nooit meer.
    // Daarom een paar keer opnieuw kijken voordat we "in verwerking" als eindstand tonen.
    // Gemeten op 8 augustus 2026: de webhook was binnen 20 seconden klaar, maar de pagina
    // had toen al zijn enige controle gedaan.
    let gestopt=false;
    (async()=>{
      // Gemeten 9 augustus 2026, avond: bij een MISLUKTE betaling komt de webhook pas MINUTEN
      // later binnen, niet seconden. Langer pollen lost dat dus niet op; dat laat de gast alleen
      // langer naar "Even geduld" kijken. Daarom blijft het venster kort en zijn de teksten
      // eerlijk gemaakt: de eindtekst hieronder belooft niets en zegt wat de gast wel kan doen.
      const POGINGEN=6, WACHT=2000;   // circa 10 seconden in totaal
      for(let i=0;i<POGINGEN && !gestopt;i++){
        if(viaRecras){ const rs=await recrasStatus(ref); if(rs.status==="fout") console.warn("[upgolf] recras-boekingstatus: "+rs.reden); }
        const r=await db.rpc("zoek_boeking",{p_referentie:ref});
        const st=r.data && r.data.gevonden && r.data.betaalstatus;
        if(st==="betaald"){
          // De datum, de tijd en het bedrag staan erbij. Zonder die drie eindigt een geslaagde
          // betaling in een balk die alleen een code noemt, en moet de gast zijn eigen reservering
          // opzoeken om te zien wanneer hij moet komen. Alles hieronder komt uit zoek_boeking zelf,
          // dus er wordt niets herhaald uit het geheugen van het vorige scherm.
          const d = r.data;
          const wanneer = d.datum ? (lang(d.datum) + " om " + String(d.starttijd).slice(0,5)) : null;
          setBetaalBanner({ok:true, tt:"Betaling geslaagd",
            sl:"Reservering " + ref + " is betaald en bevestigd." +
               (wanneer ? " Je speelt op " + wanneer + "." : "") +
               // Bij ?recras= is het bedrag op onze boeking de greenfee-rekensom en niet wat de gast via
               // Recras betaalde (gemeten 14 september: banner zei 42,00 bij 21,00 plus een strip). Dan weglaten.
               (d.bedrag_cent && !viaRecras ? " Bedrag: " + euro(d.bedrag_cent) + "." : "")});
          haalUitAdres(param); haalUitAdres("wacht"); return;
        }
        if(st==="mislukt"){
          // Mislukt is een eindstand: markeer_mislukt zet de boeking op geannuleerd, annuleert
          // de deelnemers en geeft de starttijd weer vrij. Opnieuw betalen kan dus niet meer.
          setBetaalBanner({ok:false,tt:"Betaling niet gelukt",
            sl:"Reservering "+ref+" is vervallen en de starttijd is weer vrij. Maak een nieuwe reservering."});
          haalUitAdres(param); if(viaRecras) haalUitAdres("wacht"); return;
        }
        // Nog open. Eerst laten weten dat we bezig zijn, dan opnieuw kijken.
        if(i===0) setBetaalBanner({ok:true,tt:"Even geduld",sl:"We controleren de betaling voor "+ref+"."});
        if(i<POGINGEN-1) await new Promise(res=>setTimeout(res,WACHT));
      }
      if(gestopt) return;
      // Na circa tien seconden nog steeds op open, of de referentie bestaat helemaal niet.
      // Hier stond een groen vinkje met "Je krijgt een bevestiging per mail". Dat was onwaar in
      // beide gevallen: bij een afgebroken of mislukte betaling is er niets betaald, en bij een
      // onbekende referentie bestaat er niets. Geen vinkje en geen mailbelofte meer.
      // Dit blijft de OPEN-tekst, en die belooft bewust GEEN tweede poging. De betaalfunctie
      // mollie-betaling weigert een tweede poging op dezelfde boeking met een 409 zolang er al een
      // mollie_id aan hangt ("voor deze boeking loopt al een betaling"). Bron: regel 282 van
      // mollie-betaling-voorstel-2026-08-08.ts, de versie die na de deploy van 8 augustus draait;
      // de gedeployde bron zelf is niet als artefact vastgelegd. Vandaar "wacht even en
      // ververs" en niet "probeer opnieuw": de status staat nog open omdat de melding van Mollie
      // pas minuten later binnenkomt, niet omdat de gast iets moet overdoen.
      setBetaalBanner({ok:false,tt:"Betaling nog niet afgerond",
        sl:"De betaling voor "+ref+" is nog niet bevestigd. Wacht even en ververs deze pagina, of bel ons op "+TELEFOON+"."});
      haalUitAdres(param);
    })();
    return ()=>{ gestopt=true; };
  },[]);

  // De gast die vanaf het betaalscherm terugkomt zonder te betalen. Zijn reserveringsnummer staat in
  // het adres als `wacht` (gezet vlak voor het vertrek, zie probeerBetaling). Eén keer kijken wat de
  // stand is en dat eerlijk melden. Bewust GEEN herhaalde controle zoals bij `betaald`: hier is niets
  // onderweg waar we op wachten, de gast is juist weggelopen bij de betaling.
  useEffect(()=>{
    const p=new URLSearchParams(window.location.search);
    if(p.get("betaald") || p.get("recras")) return;   // die routes doen hun eigen melding
    const ref=p.get("wacht");
    if(!ref) return;
    let weg=false;
    (async()=>{
      const r=await db.rpc("zoek_boeking",{p_referentie:ref});
      if(weg) return;
      const d=r.data;
      if(d && d.gevonden && d.betaalstatus==="betaald"){
        setBetaalBanner({ok:true,tt:"Betaling geslaagd",sl:"Reservering "+ref+" is betaald en bevestigd."});
      } else if(d && d.gevonden && d.betaalstatus==="mislukt"){
        setBetaalBanner({ok:false,tt:"Reservering vervallen",
          sl:"Reservering "+ref+" is komen te vervallen en de starttijd is weer vrij. Maak een nieuwe reservering."});
      } else if(d && d.gevonden){
        setBetaalBanner({ok:false,tt:"Nog niet betaald",
          sl:"Reservering "+ref+" staat op je naam, maar er is nog niet afgerekend. Schrijf dit nummer over en bel ons op "+TELEFOON+", ook als je wilt annuleren."});
      }
      haalUitAdres("wacht");
    })();
    return ()=>{ weg=true; };
  },[]);

  useEffect(()=>{ (async()=>{
    const b=await db.from("banen").select("*").eq("actief",true).order("naam");
    const t=await db.from("spelerstypes").select("*").eq("actief",true).order("code");
    const pr=await db.from("producten").select("*").eq("actief",true); setProducten(pr.data||[]);
    if(b.data&&b.data.length){ setBaan(b.data[0]);
      const p=await db.from("prijsregels").select("*").eq("baan_id",b.data[0].id); setRegels(p.data||[]);
      const s=await db.from("sluitingsdagen").select("datum").eq("baan_id",b.data[0].id); setDicht((s.data||[]).map(x=>x.datum)); }
    if(t.data){ setTypes(t.data); const g=t.data.filter(x=>!x.is_lid);
      if(g.length) setVenster(Math.max.apply(null,g.map(x=>x.boekingsvenster_dagen))); }
  })(); },[]);

  const laad=useCallback(async()=>{
    if(!baan) return; setLaden(true); setGekozen(null);
    // Deze voorkant is de ingang voor ÉÉN sport (SPORT, uit data.js) en toont dus alleen sloten van
    // die sport. De sport komt uit de data, uit de
    // kolom `sport` op het tijdslot zelf. Bewust GEEN lijst met dagen en GEEN tijdvenster in de code:
    // zet de balie een slot in het beheersysteem op golf, dan verschijnt het hier vanzelf, en zetten ze
    // er voetgolf neer dan verdwijnt het vanzelf. Zo blijft het scherm gelijk aan wat de database
    // toestaat en hoeven wij niets te wijzigen als de bezetting verandert.
    // Ronde 6: bij 18 holes komt de lijst uit `beschikbaarheid_paren`. Die view zet de eerste en de
    // tweede starttijd al bij elkaar, met het aantal vrije plekken van allebei en het aantal minuten
    // ertussen. De voorkant rekent hier dus NIETS zelf uit; staat er geen paar in de view, dan is die
    // tijd voor 18 holes niet te boeken en hoort hij er ook niet te staan.
    const bron = holes===18 ? "beschikbaarheid_paren" : "beschikbaarheid";
    const r=await db.from(bron).select("*").eq("baan_id",baan.id).eq("datum",datum).eq("status","open").eq("sport",SPORT).order("starttijd");
    const nu=new Date(); const vandaag=datum===iso(nu); const klok=nu.toTimeString().slice(0,8);
    // Alles wat er voor deze sport op deze dag staat, ook wat vol of verstreken is. Zonder die
    // lijst kan het scherm de drie redenen voor "geen tijden" niet uit elkaar houden, en dan
    // stond er "Deze dag zit vol" op een maandag waarop er helemaal geen footgolf is.
    setAlleSloten(r.data||[]);
    // Bij 18 holes moet ook de tweede starttijd nog plek hebben, anders is het geen speelbaar paar.
    setSloten((r.data||[]).filter(s=>s.vrij>0 && (holes!==18 || s.tweede_vrij>0) && (!vandaag||s.starttijd>klok)));
    setLaden(false);
  },[baan,datum,holes]);
  useEffect(()=>{ laad(); },[laad]);

  // Ronde 6: is er geen 18-holesproduct of staat de schakelaar uit, dan bestaat de keuze niet.
  // Liever geen knop dan een knop die de database daarna weigert.
  const product18 = productVoorHoles(producten, SPORT, 18);
  const keuze18Mogelijk = holes18Aan && SPORT==="golf" && !!product18;
  // Vangnet: valt de mogelijkheid weg terwijl de gast op 18 stond, dan terug naar 9 holes in
  // plaats van een lege lijst.
  useEffect(()=>{ if(!keuze18Mogelijk && holes!==9) setHoles(9); },[keuze18Mogelijk]);

  // Grens waarboven we per uur samenvouwen. Gemeten op staging: een gewone dag geeft 30 tot 31 vrije
  // tijden, en dat leest als een muur. Acht tijden passen op een telefoon nog in beeld, dus daaronder
  // zou een uurrij alleen een extra tik zijn zonder dat er iets ingeklapt wordt.
  const DIRECT_TONEN = 8;
  const compact = sloten.length > DIRECT_TONEN;
  // Het eerste uur met vrije tijden staat open bij binnenkomst, zodat het scherm niet leeg oogt en de
  // gast meteen ziet wat een uurrij doet. Bij een nieuwe dag schuift dat mee.
  useEffect(()=>{ setOpenUur(sloten.length ? sloten[0].starttijd.slice(0,2) : null); },[sloten]);

  async function uitloggen(){ await db.auth.signOut(); setLid(null); }
  async function annuleerMijn(bId){ if(!window.confirm("Deze reservering annuleren?")) return;
    const r=await db.rpc("annuleer_mijn_boeking",{p_boeking_id:bId});
    if(r.error){ alert(foutTekst(r.error)); return; } stuurMail(bId,"annulering"); laadLid(); laad(); }

  const ledenVenster = lid ? 30 : venster;
  const dagen=[]; for(let i=0;i<=ledenVenster;i++){ const d=new Date(); d.setDate(d.getDate()+i); dagen.push(iso(d)); }

  // Op welke dagen in de strip staat deze sport überhaupt? Bij golf is dat elke dag, maar footgolf
  // staat er alleen op woensdag, zaterdag en zondag (gemeten op staging: 819 footgolfsloten, alle
  // tussen 15:00 en 17:00, alleen op die drie dagen). Zonder dit ziet een gast een strip van
  // vijftien gelijke dagen waarvan er elf leeg blijken zodra hij ze aantikt.
  // Eén vraag voor het hele venster, alleen de datumkolom.
  const eersteDag = dagen[0], laatsteDag = dagen[dagen.length-1];
  useEffect(()=>{ if(!baan) return; let weg=false;
    (async()=>{ const r=await db.from("beschikbaarheid").select("datum")
      .eq("baan_id",baan.id).eq("status","open").eq("sport",SPORT)
      .gte("datum",eersteDag).lte("datum",laatsteDag);
      if(!weg) setSportDagen(r.data ? [...new Set(r.data.map(x=>x.datum))] : null); })();
    return ()=>{ weg=true; };
  },[baan, eersteDag, laatsteDag]);

  // De dag in het adres bijhouden. De eerste keer VERVANGEN wij de invoer in de geschiedenis, want
  // anders staat er meteen een extra stap in en gaat de terugknop nergens heen. Daarna DUWEN wij er
  // een bij, zodat de terugknop naar de vorige dag gaat in plaats van de pagina te verlaten.
  const eersteAdres = React.useRef(true);
  useEffect(()=>{
    if(dagUitAdres() === datum && eersteAdres.current){ eersteAdres.current=false; return; }
    zetDagInAdres(datum, eersteAdres.current);
    eersteAdres.current = false;
  },[datum]);

  // De terugknop van de browser. Zonder dit blijft het adres wel veranderen maar het scherm niet.
  useEffect(()=>{
    const terug = () => setDatum(dagUitAdres() || iso(new Date()));
    window.addEventListener("popstate", terug);
    return () => window.removeEventListener("popstate", terug);
  },[]);

  // Vangnet voor een dag die niet te kiezen is: in het verleden, voorbij het boekingsvenster, of
  // onzin die de controle in `dagUitAdres` wel doorkwam. Dan schuiven wij naar de eerstvolgende
  // bruikbare dag in plaats van een leeg scherm te tonen. Dit werkt ook als het venster later groeit
  // doordat er iemand inlogt, want dan draait deze controle opnieuw.
  useEffect(()=>{
    if(dagen.length && dagen.indexOf(datum) < 0) setDatum(dagen[0]);
  },[datum, ledenVenster]);

  // De dagenstrip meeschuiven naar de gekozen dag. Zonder dit staat een dag die verderop ligt buiten
  // beeld en lijkt er niets gekozen. `block:"nearest"` zodat de PAGINA niet verspringt.
  useEffect(()=>{
    const el = document.querySelector(".strip .dag.aan");
    if(el && el.scrollIntoView) el.scrollIntoView({block:"nearest", inline:"center"});
  },[datum, dagen.length]);
  const gastType=types.filter(t=>!t.is_lid)[0];
  // Ronde 6: de prijs hangt aan het product, en dat product hangt aan de keuze 9 of 18 holes.
  const prijsBij=s=>{ if(!gastType) return null;
    const p=productVoorHoles(producten,s.sport,holes); return p?prijsVan(regels,gastType.id,datum,s.starttijd,p.id,s.baan_id):null; };
  const ochtend=sloten.filter(s=>s.starttijd<"12:00:00"); const middag=sloten.filter(s=>s.starttijd>="12:00:00");

  function tegel(s){ const p=prijsBij(s); const paar = holes===18 && s.tweede_starttijd; return (
    <button key={s.id} className={"tijd"+(gekozen&&gekozen.id===s.id?" aan":"")+(paar?" paar":"")} onClick={()=>setGekozen(s)}>
      <div className="t">{s.starttijd.slice(0,5)}</div>
      {/* Bij 18 holes staat de tweede starttijd op de tegel zelf. Anders kiest een gast een tijd
          zonder te zien dat er een tweede bij hoort, en dat is precies het misverstand dat we
          willen voorkomen. Beide getallen komen uit de view. */}
      {paar && <div className="t2">en {String(s.tweede_starttijd).slice(0,5)}</div>}
      <div className="p">{p===null?"balie":p===0?"gratis":euro(p)}</div>
      <div className="v">{paar ? Math.min(s.vrij, s.tweede_vrij) : s.vrij} vrij</div></button>); }

  // Op een drukke dag staan er dertig vrijwel gelijke tegels onder elkaar. Dat leest als een muur:
  // je moet aftellen in plaats van kiezen. Vanaf meer dan DIRECT_TONEN tijden vouwen we daarom per uur
  // samen, zodat de gast eerst een blok kiest en pas daarna een tijd. Onder die grens zou een
  // inklapping niets inklappen en alleen een extra tik kosten, dus dan tonen we gewoon alles.
  function blok(titel,lijst){
    if(!lijst.length) return null;
    if(!compact) return (<div><div className="dagdeel">{titel}</div><div className="raster">{lijst.map(tegel)}</div></div>);
    const uren=[];
    lijst.forEach(s=>{ const u=s.starttijd.slice(0,2); const b=uren.find(x=>x.uur===u);
      if(b) b.sloten.push(s); else uren.push({uur:u,sloten:[s]}); });
    return (<div><div className="dagdeel">{titel}</div><div className="uurlijst">
      {uren.map(u=>{ const open=openUur===u.uur;
        // De prijs in de dichtgeklapte rij, zodat de gast niet hoeft open te klappen om te zien wat een
        // blok kost. Nagemeten over veertig uren op vijf dagen: binnen een uur is de prijs steeds gelijk.
        // Toch niet blind de eerste tegel nemen: zodra er ooit een prijsregel bijkomt die halverwege een
        // uur ingaat, zou dat een prijs tonen die niet voor alle tijden in dat uur geldt. Bij meer dan
        // een prijs binnen het uur laten we hem daarom weg en zegt de rij niets in plaats van iets fouts.
        const prijzen=[...new Set(u.sloten.map(s=>{ const p=prijsBij(s); return p===null?"balie":p===0?"gratis":p; }))];
        const prijsTekst = prijzen.length===1 ? (typeof prijzen[0]==="number"?euro(prijzen[0]):prijzen[0]) : null;
        return (
        <React.Fragment key={u.uur}>
          <button className={"uurrij"+(open?" open":"")} aria-expanded={open}
            onClick={()=>setOpenUur(open?null:u.uur)}>
            <div className="uurtijd">{u.uur}:00</div>
            <div className="uuraantal">{u.sloten.length} tijd{u.sloten.length===1?"":"en"} vrij</div>
            {prijsTekst && <div className="uurprijs">{prijsTekst}</div>}
          </button>
          {open && <div className="raster">{u.sloten.map(tegel)}</div>}
        </React.Fragment>); })}
    </div></div>); }

  const seizoen = lid && lid.seizoenskaart; const strip = lid && lid.kaarten && lid.kaarten[0];

  return (
    <div>
      {/* De kop is per sport anders. Het merk blijft UPgolf, want dat is het systeem waar een gast
          in zit; welke sport hij boekt staat als merkje ernaast en nog een keer voluit in de kop.
          Op de footgolfingang moet een gast die van upevents.nl/evenement/footgolf/ komt in één
          oogopslag zien dat hij goed zit, zonder eerst een tijd aan te klikken. */}
      <div className="hero"><div className="hero-in">
        {/* Lockup herbouwd, ronde 4, 24 augustus 2026, op aanwijzing van hoofd-activiteiten (nabouw
            van Daniels header, gelijk op alle drie de pagina's): groot rood UP, een scheidingslijn,
            dan twee regels ("GOLF WEESP" / "PREMIUM BOOKING"). Zie de uitgebreide toelichting bij
            .merk-up in stijl.css voor de gemeten verhouding en waarom deze twee tekstregels nieuw
            zijn. PROFIEL.merkStaart/de oude "UPgolf"-tekst wordt hier niet meer gebruikt: dat was
            de vorige, kortere vorm van deze lockup. */}
        <div className="hero-top"><div className="merk">
            <span className="merk-up">UP</span>
            <span className="merk-scheiding"></span>
            <span className="merk-tekst">
              <span className="merk-naam">Golf Weesp</span>
              <span className="merk-sub">Premium booking</span>
            </span>
          </div>
          {/* Het "Footgolf"-badgemerkje dat hier stond is WEG, ronde 5, 24 augustus 2026: de
              anti-ai-poort vond dat het alleen op footgolf.html stond en nergens op Daniels drie
              screenshots voorkomt, terwijl de opdracht een op elke pagina identieke header vraagt.
              Het signaal dat het gaf ("je zit hier goed") zit sindsdien in de hero-titel verderop:
              die toont nu letterlijk "UP FOOTGOLF" met FOOTGOLF in rood, een grotere en eerdere
              bevestiging dan een klein pilletje naast het logo ooit was. Geen vervangend element
              hier toegevoegd. */}
          {/* Nav toegevoegd, ronde 3, 24 augustus 2026 (nabouw Daniel, letterlijke opdracht van
              Roy). Alleen de drie schermen die op de gastvoorkant echt bestaan en werken: golf,
              footgolf, kaart kopen. Geen PRODUCTEN/TEE SHEET/ADMIN/TECHNIEK, die schermen zijn er
              hier niet en dat worden dus geen dode links (besluit van hoofd-activiteiten). Actieve
              pagina krijgt een kader (className "aan"), net als Daniels ADMIN-item in screenshot 03. */}
          <nav className="hoofdnav">
            <a className={PROFIEL.sleutel==="golf"?"aan":undefined} href="index.html">Golf</a>
            <a className={PROFIEL.sleutel==="footgolf"?"aan":undefined} href="footgolf.html">Footgolf</a>
            <a href="kaart.html">Kaart kopen</a>
          </nav>
          {/* De kaarthouders-inlogknop is eruit gehaald: besluit 6 van Roy, 14 augustus 2026,
              geen kaarthoudersdashboard, de inlogroute is definitief van tafel. Een kaart of
              abonnement gebruiken kan gewoon met de code bij het boeken, zonder inloggen.
              De onderliggende inlogcode (Login-component, sessie, lid) blijft staan: een
              kaarthouder-sessie leidt nog wel tot de welkomstregel en het kaarttarief in het
              boekingsblad, dus die logica raak ik niet aan, alleen de knop die naar de
              inlogroute leidt. */}
          {/* MIJN UP: toegevoegd 22 augustus 2026, punt 5 van het plan van 21 augustus. De rode knop
              komt uit Daniels nabouw (screenshot 01/02: rood, rechts in de navy header). Hij opent
              een verzamelscherm, geen inlog en geen route: zie MijnUp hierboven. */}
          <div className="header-acties">
            <button className="mijnup-knop" onClick={()=>setToonMijnUp(true)}>MIJN UP</button>
            {sessie && <button className="login-knop" onClick={uitloggen}>Uitloggen</button>}
          </div></div>
        {lid && <div className="welkom">Welkom terug, <b>{lid.naam}</b>.</div>}
        {/* CTA-knop en titel, HERZIEN in ronde 5, 24 augustus 2026, op letterlijke opdracht van Roy
            ("1 op 1 overnemen"): ronde 4 hergebruikte PROFIEL.kop op de knop, met een gemelde
            open vraag ("wat moet hier echt staan?") omdat de tekst dubbelop was met de titel.
            Die vraag is nu beantwoord door Daniels eigen screenshot zelf tegen te lezen: de knop en
            de titel zijn bij hem GEEN dubbele tekst, het zijn twee eigen teksten met een eigen rol
            (de knop is de uitnodiging, de titel is de merknaam). Beide nu 1 op 1 overgenomen:
            - Knop: "Boek je starttijd bij UP {naam}" (CSS zet dit om in hoofdletters, net als
              Daniels "BOEK JE STARTTIJD BIJ UP GOLF"). Zelf tegen screenshot 01 gecontroleerd.
            - Titel: "UP {naam}", waarbij Kop hieronder het LAATSTE woord ("Golf"/"Footgolf") rood
              kleurt en "UP" in de gewone kopkleur (wit) blijft, exact zoals Daniels "UP" wit /
              "GOLF" rood. Voor footgolf hetzelfde patroon met "Footgolf" in plaats van "Golf",
              zoals gevraagd.
            Klikken op de knop scrollt nog steeds naar het "Wanneer wil je spelen"-blok, geen
            route- of schermwijziging, puur een anker binnen dezelfde pagina. */}
        <button type="button" className="hero-cta"
          onClick={()=>document.querySelector(".vel")?.scrollIntoView({behavior:"smooth"})}>
          {"Boek je starttijd bij UP "+PROFIEL.naam}
        </button>
        <h1><Kop tekst={"UP "+PROFIEL.naam}/></h1>
        {/* Ondertitel/slogan, ronde 5, 24 augustus 2026: Daniels eigen tekst uit zijn mockup, per
            profiel in data.js (PROFIEL.heroSlogan), zie daar de toelichting waarom footgolf hem
            leeg laat. .hero-slogan (nieuw, was .hero p) draagt de hoofdletter- en tweeregel-vorm
            die eerder al op de onderkop stond. */}
        {PROFIEL.heroSlogan && <p className="hero-slogan">{PROFIEL.heroSlogan}</p>}
        {/* De functionele regel die hier eerder de enige onderkop was ("Golfbaan Weesp, kies een
            dag en een tijd, de rest regelen we aan de balie.") blijft gewoon bestaan: hij is niet
            versierting, hij zegt wat een gast hier moet doen. Alleen kleiner gezet en onder de
            nieuwe slogan geplaatst (.hero-info, nieuwe eigen klasse, GEEN hoofdletters), zodat hij
            niet meer met de slogan om aandacht concurreert maar er wel meteen onder blijft staan. */}
        <p className="hero-info">{PROFIEL.baanVoorop ? (baan?baan.naam:"Golfbaan Weesp")+", " : ""}{PROFIEL.onderkop}</p>
        {PROFIEL.feiten.length>0 &&
          <ul className="feiten">{PROFIEL.feiten.map(f=><li key={f}>{f}</li>)}</ul>}
        {/* Carrousel-indicator, ronde 5, 24 augustus 2026: drie liggende streepjes (rood/aqua/grijs)
            zoals Daniel ze onder zijn hero-titel toont. Puur decoratief: dit wordt GEEN werkende
            carrousel, wij hebben hier maar één hero-foto en de werking van dit scherm verandert
            niet. aria-hidden zodat een schermlezer dit overslaat: wie de streepjes niet ziet, mist
            functioneel niets. */}
        <div className="hero-indicator" aria-hidden="true"><span></span><span></span><span></span></div>
        <a className="kruislink" href={PROFIEL.andereIngang.href}>{PROFIEL.andereIngang.tekst}</a>
      </div></div>

      <div className="vel">
        {betaalBanner &&
          <div className={"banner "+(betaalBanner.ok?"ok":"fout")}>
            <div className="ic">{betaalBanner.ok?"✓":"!"}</div>
            <div><div className="tt">{betaalBanner.tt}</div><div>{betaalBanner.sl}</div></div>
            <button className="sl" onClick={()=>setBetaalBanner(null)}>×</button>
          </div>}

        {lid && (seizoen||strip) &&
          <div className="lidbalk">
            {seizoen && <div className="lidchip seiz"><div className="k">Seizoenskaart</div><div className="v">tot {kort(seizoen.geldig_tot)}</div></div>}
            {strip && <div className="lidchip kaart-chip"><div className="k">{strip.naam}</div><div className="v">{strip.saldo} strepen</div></div>}
          </div>}

        {lid && mijnRes.length>0 &&
          <div className="kaart"><h2><Kop tekst="Mijn reserveringen"/></h2>
            {mijnRes.map(r=>(
              <div className="mijnboeking" key={r.boeking_id}>
                <div><div className="dt">{kort(r.datum)} · {r.starttijd.slice(0,5)}</div>
                  <div className="sub">{r.spelers} speler{r.spelers===1?"":"s"} · {r.boekingsreferentie}</div></div>
                <button className="ann" onClick={()=>annuleerMijn(r.boeking_id)}>Annuleren</button>
              </div>))}
          </div>}

        {/* Twee kolommen op een breed scherm: links kiezen en boeken, rechts opzoeken. Op een smal
            scherm zet stijl.css ze onder elkaar en blijft de volgorde zoals hij was, want dat is de
            weergave waarop een gast boekt. De Verder-balk staat nu BINNEN de linkerkolom, onder de
            tijden, in plaats van los onderaan de pagina. */}
        <div className="reserveer-kolommen">
        <div className="kolom-boeken">

        <div className="kaart"><h2><Kop tekst="Wanneer wil je spelen?"/></h2>
          <div className="strip">{dagen.map(d=>{ const dt=new Date(d+"T12:00:00"); const g=dicht.indexOf(d)>=0;
            // Een dag waarop deze sport niet gespeeld wordt, staat er bleek bij. Zolang de lijst nog
            // niet binnen is markeren we niets, want een dag ten onrechte bleek tonen is erger dan
            // een halve seconde zonder markering.
            const geenSport = !g && sportDagen!==null && sportDagen.indexOf(d)<0;
            return (<button key={d} className={"dag"+(d===datum?" aan":"")+(g?" dicht":"")+(geenSport?" leeg":"")}
              title={geenSport?("Geen "+PROFIEL.naam.toLowerCase()+" op deze dag"):undefined} onClick={()=>setDatum(d)}>
              <div className="wd">{WD[dt.getDay()]}</div><div className="nr">{dt.getDate()}</div><div className="mnd">{MND[dt.getMonth()]}</div></button>); })}</div>
        </div>

        <div className="kaart"><h2><Kop tekst="Hoe laat wil je starten?"/></h2>
          {/* Ronde 6: 9 of 18 holes. Twee knoppen in dezelfde vorm als de dagenstrip hierboven,
              zodat het leest als dezelfde soort keuze en niet als een instelling. Verschijnt
              alleen als de schakelaar aan staat en er een 18-holesproduct is. */}
          {keuze18Mogelijk &&
            <div className="holeskeuze">
              <button className={"holes"+(holes===9?" aan":"")} onClick={()=>{ setHoles(9); setGekozen(null); }}>
                <div className="hn">9 holes</div><div className="hu">Eén ronde</div></button>
              <button className={"holes"+(holes===18?" aan":"")} onClick={()=>{ setHoles(18); setGekozen(null); }}>
                <div className="hn">2x 9 holes</div><div className="hu">Twee starttijden</div></button>
            </div>}
          {/* EIS 5, eerste plek: de uitleg bij de tijdkeuze. De tekst staat in data.js en wacht op
              goedkeuring; zolang hij leeg is komt dit blok niet in beeld. */}
          {keuze18Mogelijk && holes===18 && gekozen && uitleg18(UITLEG_18_BIJ_TIJD, gekozen.starttijd, gekozen.tweede_starttijd) &&
            <div className="let">{uitleg18(UITLEG_18_BIJ_TIJD, gekozen.starttijd, gekozen.tweede_starttijd)}</div>}
          {laden && <div className="laden">Tijden ophalen…</div>}
          {/* Er zijn vier redenen waarom er geen tijden staan, en ze vragen om vier verschillende
              antwoorden. Hier stond één tekst plus altijd de wachtlijst, en dat leverde op een
              maandag "Deze dag zit vol. Wil je dat we je bellen zodra er een plek vrijkomt?" terwijl
              er die dag helemaal geen footgolf is. De wachtlijst hoort alleen bij een dag die
              werkelijk vol is; daar is bellen een antwoord, in de andere gevallen niet. */}
          {!laden && sloten.length===0 && (() => {
            const gesloten = dicht.indexOf(datum)>=0;
            const geenAanbod = !gesloten && alleSloten.length===0;
            const verstreken = !gesloten && !geenAanbod && alleSloten.every(s=>s.vrij>0);
            if(gesloten) return <div className="niets">De baan is deze dag gesloten.</div>;
            if(geenAanbod) return <div className="niets">{PROFIEL.geenAanbodTekst}</div>;
            if(verstreken) return <div className="niets">De starttijden van vandaag zijn geweest. Kies een andere dag.</div>;
            // Geen aparte regel "Deze dag is volgeboekt" erboven: de wachtlijst zegt dat zelf al,
            // en die twee stonden anders als bijna dezelfde zin onder elkaar.
            return baan ? <Wachtlijst baan={baan} datum={datum}/>
                        : <div className="niets">Deze dag is volgeboekt.</div>;
          })()}
          {!laden && blok("Ochtend",ochtend)}
          {!laden && blok("Middag",middag)}
        </div>

        {gekozen && !boeken && !bon &&
          <div className="balk"><div className="balk-in">
            <div className="info">
              <b>{gekozen.starttijd.slice(0,5)}{holes===18 && gekozen.tweede_starttijd ? " en "+String(gekozen.tweede_starttijd).slice(0,5) : ""}</b>
              {(() => { const v = holes===18 && gekozen.tweede_starttijd ? Math.min(gekozen.vrij, gekozen.tweede_vrij) : gekozen.vrij;
                return v + " plek" + (v===1?"":"ken") + " vrij"; })()}</div>
            <button className="knop" onClick={()=>setBoeken(true)}>Verder</button></div></div>}

        </div>
        <div className="kolom-opzoeken">
          <Opzoeken onVerplaats={ref=>{setVerplaatsRef(ref); setToonVerplaatsen(true);}}/>
          {/* Toegevoegd 21 augustus 2026, op verzoek van Roy: zonder dit blok was het koopscherm voor
              kaarten en abonnementen alleen bereikbaar door kaart.html met de hand in de adresbalk te
              typen. Roy vond dat zelf niet tijdens zijn oefenrun. Onder "Heb je al gereserveerd?",
              zelfde kaart-stijl, zodat het een vast onderdeel van de pagina lijkt en niet iets bijgeplakts.
              Alleen op golf: gescoperd op PROFIEL.kaarttarieven, dezelfde vlag die verderop al bepaalt
              of kaart- en seizoenstarieven getoond worden (die staat voor footgolf op false, want er
              is geen footgolfkaart, zie de toelichting bij SPORTEN.footgolf in data.js). Roy, gedicteerd
              21 augustus 2026: "Die knop hoeft niet bij foorgolf daar hebbben we nu nog geen abbo of
              strippenkaarten." Geen eigen nieuw kenmerk verzonnen, het bestaande onderscheid gevolgd. */}
          {PROFIEL.kaarttarieven &&
          <div className="kaart">
            <h2><Kop tekst="Kaart of abonnement kopen"/></h2>
            <div className="wl-tekst">Koop hier een strippenkaart of een abonnement, direct online.</div>
            <a className="wl-knop vol" style={{display:"block", textAlign:"center", textDecoration:"none"}} href="kaart.html">Kaart of abonnement kopen</a>
          </div>}

          {/* Punt 4 van het plan van 21 augustus 2026 (bouwronde 22 augustus): saldo checken zonder
              inlog, als eigen ingang naast "Kaart of abonnement kopen". Alleen op golf, dezelfde
              PROFIEL.kaarttarieven-vlag: op footgolf bestaat er nog geen kaart of abonnement om
              saldo van te checken (Roy, 21 augustus 2026: "Die knop hoeft niet bij footgolf"). */}
          {PROFIEL.kaarttarieven &&
          <div className="kaart">
            <h2><Kop tekst="Saldo checken"/></h2>
            <div className="wl-tekst">Kijk hoeveel ritten er nog op je kaart staan, of tot wanneer je abonnement geldig is.</div>
            <button type="button" className="wl-knop vol" onClick={()=>setToonSaldoCheck(true)}>Saldo checken</button>
          </div>}
        </div>
        </div>
      </div>

      {toonLogin && <Login onSluit={()=>setToonLogin(false)}/>}
      {boeken && gekozen && <Boeken slot={gekozen} datum={datum} types={types} regels={regels} producten={producten} lid={lid} mollie={mollie}
        holes={holes}
        onSluit={()=>setBoeken(false)} onKlaar={b=>{setBoeken(false); setBon(b); laadLid();}}/>}
      {bon && gekozen && <Klaar bon={bon} datum={datum} slot={gekozen} mollie={mollie} opnieuw={()=>{setBon(null); laad();}}/>}
      {toonMijnUp && <MijnUp onSluit={()=>setToonMijnUp(false)}/>}
      {toonSaldoCheck && <SaldoCheck onSluit={()=>setToonSaldoCheck(false)}/>}
      {toonVerplaatsen && <Verplaatsen referentieVooraf={verplaatsRef} onSluit={()=>{setToonVerplaatsen(false); setVerplaatsRef("");}}/>}
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
