// Direction 1 — Classic wizard.
// Step-by-step flow with a top stepper. Each step is its own screen.
// Steps: 0 PO entry · 1 Consignment · 2 PO lines · 3 Driver+paperwork · 4 Slot · 5 Review · 6 Confirmed

const WZ_STEPS = [
  { id: 0, label: "Verify PO" },
  { id: 1, label: "Consignment" },
  { id: 2, label: "PO lines" },
  { id: 3, label: "Driver" },
  { id: 4, label: "Slot" },
  { id: 5, label: "Review" },
];

/* ─────────── Helpers ─────────── */

function formatYMD(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

function timeToMinutes(hhmm) {
  const [h, m] = String(hhmm).split(":").map(Number);
  return h * 60 + m;
}

function slotsOverlap(aStart, aDur, bStart, bDur) {
  const a = timeToMinutes(aStart);
  const b = timeToMinutes(bStart);
  return a < b + bDur && b < a + aDur;
}

// Hardcoded fallbacks — used only before /api/delivery-types fetch lands.
const _DEFAULT_DURATION = { "container-40": 60, "container-20": 60, "curtain": 45, "pallets": 30, "cartons": 30 };
const _DEFAULT_MIN_NOTICE = { "container-40": 48, "container-20": 48, "curtain": 24, "pallets": 2, "cartons": 0 };

function findDeliveryTypeConfig(typeId) {
  const list = (typeof window !== "undefined" && window.__GI_DELIVERY_TYPES) || [];
  return list.find((t) => t.id === typeId) || null;
}

function defaultDurationFor(type) {
  const cfg = findDeliveryTypeConfig(type);
  if (cfg && Number.isFinite(Number(cfg.duration))) return Number(cfg.duration);
  return _DEFAULT_DURATION[type] ?? 30;
}

function minNoticeFor(type) {
  const cfg = findDeliveryTypeConfig(type);
  if (cfg && Number.isFinite(Number(cfg.minNoticeHours))) return Number(cfg.minNoticeHours);
  return _DEFAULT_MIN_NOTICE[type] ?? 0;
}

function isWithinOpeningHours(dayIdx, slot, durationMin) {
  const hours = (typeof window !== "undefined" && window.__GI_HOURS) || null;
  if (!hours) return true; // optimistic before fetch lands
  const D = window.JFH_GI;
  const date = D.DAYS[dayIdx];
  const today = hours.find((h) => Number(h.dow) === date.getDay());
  if (!today || today.open === false) return false;
  const [oh, om] = String(today.start).split(":").map(Number);
  const [ch, cm] = String(today.end).split(":").map(Number);
  const openMin = (oh || 0) * 60 + (om || 0);
  const closeMin = (ch || 0) * 60 + (cm || 0);
  const start = timeToMinutes(slot);
  return start >= openMin && start + Number(durationMin || 0) <= closeMin;
}

function isInClosure(date, slot, durationMin) {
  const closures = (typeof window !== "undefined" && window.__GI_CLOSURES) || [];
  if (!closures.length) return false;
  const key = formatYMD(date);
  const same = closures.filter((c) => c.date === key);
  if (!same.length) return false;
  for (const c of same) {
    if (c.allDay) return true;
    if (!c.start || !c.end) continue;
    const cStart = timeToMinutes(c.start);
    const cEnd = timeToMinutes(c.end);
    const sStart = timeToMinutes(slot);
    const sEnd = sStart + Number(durationMin || 0);
    if (sStart < cEnd && cStart < sEnd) return true;
  }
  return false;
}

// Build an RFC 5545 iCalendar string for a single VEVENT representing a
// booking slot. Floating local time (no timezone tag) — gets the supplier
// to add the event at the wall-clock time the warehouse expects them.
function buildIcs({ ref, summary, description, location, startDate, startTime, durationMin }) {
  const pad = (n) => String(n).padStart(2, "0");
  const yyyymmdd = `${startDate.getFullYear()}${pad(startDate.getMonth() + 1)}${pad(startDate.getDate())}`;
  const [h, m] = String(startTime).split(":").map(Number);
  const start = `${yyyymmdd}T${pad(h)}${pad(m)}00`;
  const endTotal = (h * 60 + m + Number(durationMin || 30));
  const end = `${yyyymmdd}T${pad(Math.floor(endTotal / 60))}${pad(endTotal % 60)}00`;
  const now = new Date();
  const dtstamp = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`;
  const escape = (s) => String(s || "").replace(/\\/g, "\\\\").replace(/,/g, "\\,").replace(/;/g, "\\;").replace(/\n/g, "\\n");
  return [
    "BEGIN:VCALENDAR",
    "VERSION:2.0",
    "PRODID:-//JFH Goods-In Portal//EN",
    "CALSCALE:GREGORIAN",
    "BEGIN:VEVENT",
    `UID:${ref}@jfhhorticultural.com`,
    `DTSTAMP:${dtstamp}`,
    `DTSTART:${start}`,
    `DTEND:${end}`,
    `SUMMARY:${escape(summary)}`,
    `DESCRIPTION:${escape(description)}`,
    `LOCATION:${escape(location)}`,
    "END:VEVENT",
    "END:VCALENDAR",
    "",
  ].join("\r\n");
}

// Map a booking object (from /api/bookings GET response) into the wizard's `b`
// state shape, used when amending an existing booking.
function bookingToWizardState(booking) {
  const D = window.JFH_GI;
  const slotDate = booking?.slot?.date;
  const slotDayIdx = D.DAYS.findIndex((d) => formatYMD(d) === slotDate);
  const lineQty = {};
  for (const l of booking?.lines || []) {
    if (l?.sku) lineQty[l.sku] = Number(l.qty) || 0;
  }
  // Rebuild a PO-shaped object from the booking's lines so the wizard's
  // line/review steps render the same shape as the new-booking flow.
  const po = {
    ...D.PO,
    number: booking?.po?.number || "",
    supplier: booking?.supplier?.name || "",
    supplierCode: booking?.supplier?.code || "",
    lines: (booking?.lines || []).map((l) => ({
      sku: l.sku,
      name: l.name,
      pack: "—",
      ordered: Number(l.qty) || 0,
      delivered: 0,
      unit: l.unit || "",
      price: 0,
      weight: 0,
    })),
  };
  return {
    po,
    poValidated: true,
    poNumber: booking?.po?.number || "",
    poDueDate: booking?.po?.dueDate || null,
    poIntactId: booking?.po?.intactId || "",
    supplierCode: booking?.supplier?.code || "",
    supplierName: booking?.supplier?.name || "",
    supplierEmail: booking?.supplier?.email || "",
    deliveryType: booking?.deliveryType || null,
    courier: booking?.courier || "",
    vehicleReg: booking?.driver?.vehicleReg || "",
    driverName: booking?.driver?.name || "",
    driverPhone: booking?.driver?.phone || "",
    palletCount: booking?.palletCount ?? "",
    cartonCount: booking?.cartonCount ?? "",
    paperwork: booking?.paperwork || null,
    notes: booking?.notes || "",
    lineQty,
    slotDay: slotDayIdx >= 0 ? slotDayIdx : 0,
    slotTime: booking?.slot?.time || null,
    bookingRef: booking?.ref || null,
    amending: true,
  };
}

function WizardApp() {
  const [b, setB] = useGIBooking();
  const [mode, setMode] = React.useState("new"); // 'new' or 'manage'
  const [density, setDensity] = React.useState("comfortable");
  const [slotStyle, setSlotStyle] = React.useState("grid"); // grid | list | calendar

  React.useEffect(() => { if (window.lucide && window.lucide.createIcons) window.lucide.createIcons(); }, [b.step]);

  // Fetch the admin-configured contact info once; error messages use it.
  React.useEffect(() => {
    fetch("/api/contact-info")
      .then((r) => r.json())
      .then((body) => { if (body?.ok) window.__GI_CONTACT = body.contact; })
      .catch(() => {});
  }, []);

  // Fetch admin-configured delivery types so we know each type's slot length
  // and minNoticeHours for the slot picker filters.
  const [deliveryTypes, setDeliveryTypes] = React.useState([]);
  React.useEffect(() => {
    fetch("/api/delivery-types")
      .then((r) => r.json())
      .then((body) => {
        if (body?.ok) {
          window.__GI_DELIVERY_TYPES = body.deliveryTypes;
          setDeliveryTypes(body.deliveryTypes);
        }
      })
      .catch(() => {});
  }, []);

  // Fetch opening hours + closures so the slot picker can hide closed days
  // and out-of-hours times before submit (server is still the source of truth).
  React.useEffect(() => {
    fetch("/api/opening-hours").then(r => r.json()).then(b => { if (b?.ok) window.__GI_HOURS = b.hours; }).catch(() => {});
    fetch("/api/closures").then(r => r.json()).then(b => { if (b?.ok) window.__GI_CLOSURES = b.closures; }).catch(() => {});
  }, []);

  // Admin "Amend" button opens this page with ?ref=GI-NNNN&po=PO0013439.
  // Auto-run the lookup and drop straight into amend mode.
  React.useEffect(() => {
    if (typeof window === "undefined") return;
    const qs = new URLSearchParams(window.location.search);
    const refQ = qs.get("ref");
    const poQ = qs.get("po");
    if (!refQ || !poQ) return;
    fetch("/api/bookings/lookup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ref: refQ, po: poQ }),
    })
      .then((r) => r.json())
      .then((body) => {
        if (body?.ok && body.booking) {
          setB({ ...bookingToWizardState(body.booking), step: 5 });
        }
      })
      .catch(() => {});
  }, []);

  // Tweaks-panel wiring
  React.useEffect(() => {
    function onMsg(e) {
      if (e.data && e.data.type === "__activate_edit_mode") setTweaksOpen(true);
      if (e.data && e.data.type === "__deactivate_edit_mode") setTweaksOpen(false);
    }
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);
  const [tweaksOpen, setTweaksOpen] = React.useState(false);

  const goto = (s) => setB({ step: s });
  const next = () => goto(b.step + 1);
  const back = () => goto(Math.max(0, b.step - 1));

  // Step rendering
  let content;
  if (b.step === 0) content = <StepPO b={b} setB={setB} next={next} mode={mode} setMode={setMode} />;
  else if (b.step === 1) content = <StepConsignment b={b} setB={setB} next={next} back={back} />;
  else if (b.step === 2) content = <StepLines b={b} setB={setB} next={next} back={back} />;
  else if (b.step === 3) content = <StepDriver b={b} setB={setB} next={next} back={back} />;
  else if (b.step === 4) content = <StepSlot b={b} setB={setB} next={next} back={back} slotStyle={slotStyle} />;
  else if (b.step === 5) content = <StepReview b={b} setB={setB} back={back} goto={goto} />;
  else content = <StepConfirmed b={b} setB={setB} />;

  return (
    <div className={`wz-shell gi--${density}`}>
      <GITopBar />
      {b.step < 6 ? (
        <div className="wz-stepperbar">
          <Stepper step={b.step} onJump={(s) => s < b.step && goto(s)} />
        </div>
      ) : null}
      {content}
      {tweaksOpen ? (
        <TweaksDock
          density={density} setDensity={setDensity}
          slotStyle={slotStyle} setSlotStyle={setSlotStyle}
          onClose={() => { setTweaksOpen(false); window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*"); }}
        />
      ) : null}
    </div>
  );
}

function Stepper({ step, onJump }) {
  return (
    <div className="wz-stepper" role="tablist">
      {WZ_STEPS.map((s) => {
        const state = s.id < step ? "done" : s.id === step ? "active" : "future";
        return (
          <div key={s.id} className="wz-step" data-state={state} onClick={() => onJump(s.id)}>
            <span className="wz-step__num">{state === "done" ? "✓" : s.id + 1}</span>
            <span className="wz-step__label">{s.label}</span>
          </div>
        );
      })}
    </div>
  );
}

/* ─────────── STEP 0 · PO entry ─────────── */

function bookingErrorMessage(reason, body) {
  const contact = (body && body.contact) || window.__GI_CONTACT || { email: "goodsin@jfhhorticultural.com", phone: "01270 212726" };
  const reach = `email ${contact.email} or call ${contact.phone}`;
  switch (reason) {
    case "slot_taken":
      return "That slot is already taken. Pick a different time, or " + reach + ".";
    case "closed_day":
      return "The dock is closed on that date. Pick another day, or " + reach + ".";
    case "outside_hours":
      return `That slot is outside our opening hours (${body?.open}–${body?.close}). Pick a slot inside the window, or ${reach}.`;
    case "closure_block":
      return `${body?.label || "A closure"} is in place on that date${body?.allDay === false ? ` between ${body?.start} and ${body?.end}` : ""}. Pick another time, or ${reach}.`;
    case "too_close":
      return `This delivery type needs at least ${body?.required} hours' notice. Please choose a slot further out, or ${reach}.`;
    case "too_close_to_amend":
      return `Amendments must be made at least ${body?.required} hours before the booked slot. To change it now, ${reach}.`;
    case "delivery_type_disabled":
      return "That delivery type isn't bookable at the moment. " + reach[0].toUpperCase() + reach.slice(1) + ".";
    case "unknown_delivery_type":
      return "That delivery type isn't recognised. Refresh and try again.";
    case "over_capacity":
      return `This PO has already been booked in. SKU ${body?.sku} has ${body?.available} of ${body?.outstanding} left to book.`;
    case "line_unknown":
      return `One of the PO lines (${body?.sku}) isn't on this PO in Intact anymore. Refresh the page and try again.`;
    case "intact_sync_failed":
      return "We couldn't sync your booking back to our system. Please try again, or " + reach + ".";
    case "po_not_found":
      return "Your PO can't be found anymore. Refresh the page and re-validate the PO.";
    case "missing_field":
    case "bad_format":
      return `Some details are missing or in the wrong format (${body?.field || "field"}). Go back through the wizard and check.`;
    case "no_lines":
      return "You haven't selected any PO lines. Go back to step 3 and pick what's on the delivery.";
    case "not_amendable":
      return `This booking can no longer be amended online. ${reach[0].toUpperCase()}${reach.slice(1)}.`;
    case "production_disabled":
      return "Booking isn't live yet — the production system is still being set up. " + reach[0].toUpperCase() + reach.slice(1) + ".";
    case "config":
    case "db_not_bound":
      return "The booking system isn't configured correctly. " + reach[0].toUpperCase() + reach.slice(1) + ".";
    case "db_error":
    case "upstream":
    default:
      return "Couldn't reach the booking system. Try again in a moment, or " + reach + ".";
  }
}

function poValidationMessage(reason, body) {
  const contact = (body && body.contact) || window.__GI_CONTACT || { email: "goodsin@jfhhorticultural.com", phone: "01270 212726" };
  const reach = `email ${contact.email} or call ${contact.phone}`;
  const Reach = reach[0].toUpperCase() + reach.slice(1);
  switch (reason) {
    case "po_not_found":
      return `PO not recognised. Check the number, or ${reach}.`;
    case "supplier_mismatch":
      return `Supplier code doesn't match this PO. Check your code or ${reach}.`;
    case "po_inactive":
      return `This PO isn't currently bookable. ${Reach}.`;
    case "already_fully_booked":
      return `This PO has already been booked in. If something has changed, ${reach}.`;
    case "production_disabled":
      return `Booking isn't live yet — the production system is still being set up. ${Reach} if this is urgent.`;
    case "missing_fields":
      return "Enter both PO number and supplier code.";
    case "bad_request":
    case "config":
    case "upstream":
    default:
      return `Couldn't reach the booking system. Try again in a moment, or ${reach}.`;
  }
}

function StepPO({ b, setB, next, mode, setMode }) {
  const [poNum, setPoNum] = React.useState(b.poNumber || "");
  const [code, setCode] = React.useState(b.supplierCode || "");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  // Amend/cancel lookup state
  const [lookupRef, setLookupRef] = React.useState("");
  const [lookupPo, setLookupPo] = React.useState("");
  const [lookupBusy, setLookupBusy] = React.useState(false);
  const [lookupError, setLookupError] = React.useState(null);

  async function openBooking() {
    setLookupError(null);
    if (!lookupRef.trim() || !lookupPo.trim()) {
      setLookupError("Enter both booking reference and PO number.");
      return;
    }
    setLookupBusy(true);
    try {
      const res = await fetch("/api/bookings/lookup", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ref: lookupRef.trim(), po: lookupPo.trim() }),
      });
      const body = await res.json().catch(() => ({}));
      if (body?.ok && body.booking) {
        if (body.booking.status === "cancelled") {
          setLookupError("This booking has already been cancelled. To re-book, start a new booking above.");
          return;
        }
        if (body.booking.status !== "pending") {
          setLookupError(`This booking is ${body.booking.status} and can no longer be amended online. Call 01270 212726.`);
          return;
        }
        setB({ ...bookingToWizardState(body.booking), step: 5 });
        return;
      }
      setLookupError("We couldn't find that booking. Check the reference and PO number.");
    } catch (e) {
      setLookupError("Couldn't reach the booking system. Try again in a moment.");
    } finally {
      setLookupBusy(false);
    }
  }

  async function validate() {
    setError(null);
    if (!poNum.trim() || !code.trim()) { setError("Enter both PO number and supplier code."); return; }
    setBusy(true);
    try {
      const res = await fetch("/api/po-validate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ po: poNum.trim(), code: code.trim() }),
      });
      const body = await res.json().catch(() => ({}));
      if (body?.ok) {
        const realLines = Array.isArray(body.po.lines) ? body.po.lines : [];
        const realPo = {
          ...window.JFH_GI.PO, // keep ancillary mock fields (address etc.) until we wire more from Intact
          number: body.po.number,
          intactId: body.po.intactId,
          supplier: body.po.supplier.name,
          supplierCode: body.po.supplier.code,
          lines: realLines,
        };
        // Default the line quantities to what's still outstanding on each PO line.
        const newLineQty = {};
        for (const l of realLines) {
          // Default to what's still bookable on this line (available <= outstanding).
          const available = Math.max(0, Number(l.available ?? l.outstanding ?? (l.ordered - l.delivered) ?? 0));
          newLineQty[l.sku] = available;
        }
        setB({
          po: realPo,
          poNumber: body.po.number,
          poIntactId: body.po.intactId,
          supplierCode: body.po.supplier.code,
          supplierName: body.po.supplier.name,
          poDueDate: body.po.dueDate,
          poStatus: body.po.status,
          poValidated: true,
          lineQty: newLineQty,
          fullDelivery: true,
        });
        next();
        return;
      }
      setError(poValidationMessage(body?.reason, body));
    } catch (e) {
      setError("Couldn't reach the booking system. Try again in a moment, or email goodsin@jfhhorticultural.com.");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="wz-card">
      <div className="wz-eyebrow">Step 1 of 6 · Verify your PO</div>
      <h1 className="wz-h1">Book a delivery into Sandbach.</h1>
      <p className="wz-lede">Enter the JFH purchase order number we sent you and your supplier code. We'll match it to the order on our system and pull through the lines.</p>

      <div className="wz-land-cards">
        <div className="wz-panel">
          <div className="wz-panel__h">
            <GIIcon name="file-check-2" size={20} color="var(--jfh-mountain-green)" />
            New booking
          </div>
          <div style={{ display: "grid", gap: 16 }}>
            <GIField label="JFH Purchase order number" hint="It's at the top of the PO — starts with PO.">
              <GIMonoInput value={poNum} onChange={(e) => setPoNum(e.target.value.toUpperCase())} placeholder="PO0013439" autoFocus />
            </GIField>
            <GIField label="Supplier code" hint="Your JFH supplier code, as shown on the PO.">
              <GIInput value={code} onChange={(e) => setCode(e.target.value)} placeholder="1120" />
            </GIField>
            {error ? <div className="gi-field__error"><GIIcon name="alert-triangle" size={14} /> {error}</div> : null}
            <div style={{ display: "flex", gap: 10, marginTop: 4, alignItems: "center" }}>
              <GIBtn size="lg" onClick={validate} disabled={busy} iconAfter={busy ? null : "arrow-right"}>{busy ? "Validating…" : "Validate & continue"}</GIBtn>
              <span className="wz-savehint"><GIIcon name="shield-check" size={13} /> We never share your PO details.</span>
            </div>
          </div>
        </div>
        <div className="wz-panel wz-panel--alt">
          <div className="wz-panel__h">
            <GIIcon name="calendar-clock" size={20} color="rgba(255,255,255,0.85)" />
            <span style={{ color: "white" }}>Already booked?</span>
          </div>
          <p>Amend a delivery time, change the vehicle, or cancel a booking. You'll need the reference we sent in your confirmation email.</p>
          <div style={{ display: "grid", gap: 12, marginTop: 14 }}>
            <GIField label="Booking reference">
              <GIMonoInput value={lookupRef} onChange={(e) => setLookupRef(e.target.value.toUpperCase())} placeholder="GI-0000" />
            </GIField>
            <GIField label="PO number">
              <GIMonoInput value={lookupPo} onChange={(e) => setLookupPo(e.target.value.toUpperCase())} placeholder="PO0013439" />
            </GIField>
            {lookupError ? <div className="gi-field__error" style={{ color: "rgba(255,255,255,0.95)" }}><GIIcon name="alert-triangle" size={14} /> {lookupError}</div> : null}
            <GIBtn variant="accent" iconAfter={lookupBusy ? null : "arrow-right"} onClick={openBooking} disabled={lookupBusy}>{lookupBusy ? "Looking up…" : "Open my booking"}</GIBtn>
          </div>
          <div className="wz-perks">
            <div><GIIcon name="check" size={14} color="var(--jfh-fern-green)" /> 15-min slots, Monday–Friday</div>
            <div><GIIcon name="check" size={14} color="var(--jfh-fern-green)" /> Amend up to 2 hours before</div>
            <div><GIIcon name="check" size={14} color="var(--jfh-fern-green)" /> No login required</div>
          </div>
        </div>
      </div>

      <div style={{ marginTop: 24, padding: "16px 20px", background: "white", border: "1px solid var(--border-1)", borderRadius: "var(--radius-md)", display: "flex", alignItems: "center", gap: 12, fontSize: 13.5, color: "var(--fg-3)" }}>
        <GIIcon name="info" size={16} color="var(--jfh-cactus)" />
        Open <strong style={{ color: "var(--fg-1)" }}>Monday to Friday, 08:00–16:00.</strong> Last bookings accepted at 15:30. Out-of-hours? Call <strong style={{ color: "var(--fg-1)" }}>01270 212726</strong>.
      </div>
    </div>
  );
}

/* ─────────── PO summary block (reused across steps 1-5) ─────────── */

function POSummary({ po, onChange }) {
  return (
    <div className="wz-posum">
      <div>
        <div className="wz-posum__num">{po.number}</div>
        <div className="wz-posum__sub">{po.supplier} · raised {po.raised}</div>
      </div>
      <div className="wz-posum__cols">
        <div className="wz-posum__col"><span>Delivering to</span><span>{po.depot}</span></div>
        <div className="wz-posum__col"><span>Terms</span><span>{po.incoterm} · {po.terms}</span></div>
        <div className="wz-posum__col"><span>Raised by</span><span>{po.raisedBy}</span></div>
      </div>
    </div>
  );
}

/* ─────────── STEP 1 · Consignment (delivery type + courier) ─────────── */

function StepConsignment({ b, setB, next, back }) {
  const DTS = window.JFH_GI.DELIVERY_TYPES;
  const canNext = b.deliveryType && b.courier.trim();

  return (
    <div className="wz-card">
      <POSummary po={b.po} />
      <div className="wz-eyebrow">Step 2 of 6 · Consignment details</div>
      <h1 className="wz-h1">What's being delivered and who's bringing it?</h1>

      <div className="wz-panel">
        <div className="wz-panel__h">Delivery type <small>· choose one</small></div>
        <div className="wz-tiles">
          {DTS.map((dt) => (
            <button key={dt.id} type="button" className="wz-tile" data-on={b.deliveryType === dt.id} onClick={() => setB({ deliveryType: dt.id })}>
              <span className="wz-tile__icon"><GIIcon name={dt.icon} size={22} /></span>
              <span>
                <div className="wz-tile__label">{dt.label}</div>
                <div className="wz-tile__sub">{dt.sub}</div>
              </span>
            </button>
          ))}
        </div>
      </div>

      <div className="wz-panel">
        <div className="wz-panel__h">Courier / freight forwarder</div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
          <GIField label="Carrier name" hint="Whoever's bringing the load.">
            <GIInput
              value={b.courier}
              onChange={(e) => setB({ courier: e.target.value })}
              placeholder="e.g. Pollock (Scotrans) Ltd"
              autoComplete="off"
            />
          </GIField>
          <GIField label="Your reference" optional hint="Carrier or shipment reference — appears on our gate manifest.">
            <GIInput placeholder="e.g. CMR-2026-554" />
          </GIField>
        </div>
      </div>

      <WizardFooter back={back} next={next} canNext={!!canNext} />
    </div>
  );
}

/* ─────────── STEP 2 · PO lines ─────────── */

function StepLines({ b, setB, next, back }) {
  const lines = b.po.lines;
  // Each line carries `available` = outstanding minus active bookings on the
  // same PO. Cap quantity selection at what's still bookable (so the supplier
  // can't over-book a PO that's already partly booked by another delivery).
  const remainingMap = Object.fromEntries(
    lines.map((l) => [
      l.sku,
      Number(l.available ?? l.outstanding ?? window.JFH_GI.lineRemaining(l)),
    ])
  );
  const totalRemaining = Object.values(remainingMap).reduce((s, n) => s + n, 0);
  const totalSelected = Object.values(b.lineQty).reduce((s, n) => s + Number(n || 0), 0);

  function selectAll() {
    setB({ lineQty: remainingMap, fullDelivery: true });
  }
  function clearAll() {
    setB({ lineQty: Object.fromEntries(lines.map(l => [l.sku, 0])), fullDelivery: false });
  }
  function setQty(sku, q) {
    const cap = remainingMap[sku];
    const next = { ...b.lineQty, [sku]: Math.max(0, Math.min(cap, Number(q) || 0)) };
    setB({ lineQty: next, fullDelivery: lines.every(l => next[l.sku] === remainingMap[l.sku]) });
  }
  function toggleLine(sku) {
    const cur = b.lineQty[sku] || 0;
    setQty(sku, cur > 0 ? 0 : remainingMap[sku]);
  }

  const selectedCount = lines.filter(l => (b.lineQty[l.sku] || 0) > 0).length;
  const canNext = totalSelected > 0;

  return (
    <div className="wz-card">
      <POSummary po={b.po} />
      <div className="wz-eyebrow">Step 3 of 6 · Confirm what's on the delivery</div>
      <h1 className="wz-h1">Which lines and quantities are arriving?</h1>
      <p className="wz-lede">By default we've pre-filled the outstanding quantity on every PO line. Untick anything that isn't on this load, or edit the quantity for part deliveries.</p>

      <div className="wz-panel">
        <div className="wz-lines-toolbar">
          <div className="wz-lines-toolbar__left">
            <div className="wz-lines-mode">
              <button data-on={b.fullDelivery === true} onClick={selectAll}>All outstanding</button>
              <button data-on={b.fullDelivery === false} onClick={clearAll}>Part delivery</button>
            </div>
            <span style={{ fontSize: 13, color: "var(--fg-3)" }}>
              {selectedCount} of {lines.length} lines · <strong style={{ color: "var(--fg-1)" }}>{totalSelected.toLocaleString()}</strong> of {totalRemaining.toLocaleString()} units outstanding
            </span>
          </div>
          <div className="wz-lines-toolbar__right">
            <GIBtn variant="ghost" size="sm" icon="check-square" onClick={selectAll}>Select all</GIBtn>
            <GIBtn variant="ghost" size="sm" icon="square" onClick={clearAll}>Clear all</GIBtn>
          </div>
        </div>

        <table className="wz-table">
          <thead>
            <tr>
              <th style={{ width: 36 }}></th>
              <th>Line</th>
              <th>Pack</th>
              <th style={{ textAlign: "right" }}>Ordered</th>
              <th style={{ textAlign: "right" }}>Already in</th>
              <th>Delivering now</th>
            </tr>
          </thead>
          <tbody>
            {lines.map((l) => {
              const qty = b.lineQty[l.sku] || 0;
              const on = qty > 0;
              return (
                <tr key={l.sku} data-off={!on}>
                  <td><GICheck checked={on} onChange={() => toggleLine(l.sku)} /></td>
                  <td>
                    <div className="wz-table__name">{l.name}</div>
                    <div className="wz-table__sku">{l.sku}</div>
                  </td>
                  <td><div className="wz-table__pack">{l.pack}</div></td>
                  <td style={{ textAlign: "right" }}>{l.ordered.toLocaleString()}</td>
                  <td style={{ textAlign: "right", color: l.delivered > 0 ? "var(--jfh-cactus)" : "var(--fg-3)" }}>{l.delivered > 0 ? l.delivered.toLocaleString() : "—"}</td>
                  <td>
                    <div className="wz-table__qty">
                      <GIQty value={qty} onChange={(v) => setQty(l.sku, v)} max={remainingMap[l.sku]} />
                      <span className="wz-table__qty-of">of {remainingMap[l.sku]} {l.unit}</span>
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      <WizardFooter back={back} next={next} canNext={canNext} nextLabel="Continue to driver details" />
    </div>
  );
}

/* ─────────── STEP 3 · Driver, vehicle & paperwork ─────────── */

function StepDriver({ b, setB, next, back }) {
  const drop = React.useRef(null);
  function onPick(file) {
    if (!file) return;
    setB({ paperwork: { name: file.name, sizeKb: Math.round(file.size / 1024) } });
  }
  function onDragOver(e) { e.preventDefault(); drop.current && drop.current.setAttribute("data-hover", "true"); }
  function onDragLeave() { drop.current && drop.current.removeAttribute("data-hover"); }
  function onDrop(e) { e.preventDefault(); drop.current && drop.current.removeAttribute("data-hover"); onPick(e.dataTransfer.files[0]); }

  const email = b.supplierEmail || "";
  const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
  const canNext = emailLooksValid;

  const showPallets = ["pallets","container-20","container-40","curtain"].includes(b.deliveryType);
  const showCartons = ["cartons","pallets"].includes(b.deliveryType);

  return (
    <div className="wz-card">
      <POSummary po={b.po} />
      <div className="wz-eyebrow">Step 4 of 6 · Driver, vehicle &amp; paperwork</div>
      <h1 className="wz-h1">Tell us who's arriving.</h1>
      <p className="wz-lede">We use this to pre-clear your driver at the gate. You can amend any of it up to 2 hours before your slot.</p>

      <div className="wz-panel">
        <div className="wz-panel__h">Confirmation email <small>· we'll send your booking reference and any updates here</small></div>
        <GIField label="Email for confirmation" hint="Required — this is where we send your booking ref and amendment notices.">
          <GIInput
            type="email"
            value={email}
            onChange={(e) => setB({ supplierEmail: e.target.value })}
            placeholder="dispatch@yoursupplier.com"
            autoFocus
          />
        </GIField>
        {email && !emailLooksValid ? (
          <div className="gi-field__error" style={{ marginTop: 8 }}>
            <GIIcon name="alert-triangle" size={13} /> That doesn't look like a valid email address.
          </div>
        ) : null}
      </div>

      <div className="wz-panel">
        <div className="wz-panel__h">Vehicle &amp; driver <small>· all fields optional — you can fill them in later</small></div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
          <GIField label="Vehicle registration" hint="UK or EU plate." optional>
            <GIMonoInput value={b.vehicleReg} onChange={(e) => setB({ vehicleReg: e.target.value.toUpperCase() })} placeholder="AB23 CDE" />
          </GIField>
          <GIField label="Trailer / unit number" optional>
            <GIMonoInput placeholder="TRL-00921" />
          </GIField>
          <GIField label="Driver name" optional>
            <GIInput value={b.driverName} onChange={(e) => setB({ driverName: e.target.value })} placeholder="e.g. Marius Popescu" />
          </GIField>
          <GIField label="Driver mobile" hint="So we can reach them if they're delayed." optional>
            <GIInput value={b.driverPhone} onChange={(e) => setB({ driverPhone: e.target.value })} placeholder="+44 7700 900123" />
          </GIField>
        </div>
      </div>

      <div className="wz-panel">
        <div className="wz-panel__h">Load count</div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16 }}>
          {showPallets ? (
            <GIField label="Pallets on this delivery">
              <GIInput type="number" value={b.palletCount} onChange={(e) => setB({ palletCount: e.target.value })} placeholder="e.g. 12" />
            </GIField>
          ) : null}
          {showCartons ? (
            <GIField label="Cartons / parcels" optional>
              <GIInput type="number" value={b.cartonCount} onChange={(e) => setB({ cartonCount: e.target.value })} placeholder="e.g. 30" />
            </GIField>
          ) : null}
          <GIField label="Notes for the goods-in team" optional>
            <GITextarea value={b.notes} onChange={(e) => setB({ notes: e.target.value })} placeholder="Anything we should know — split load, frozen goods, oversized…" />
          </GIField>
        </div>
      </div>

      <div className="wz-panel">
        <div className="wz-panel__h">Inbound paperwork <small>· optional</small></div>
        <p style={{ margin: "0 0 12px", fontSize: 13.5, color: "var(--fg-3)" }}>Upload the CMR, packing list or invoice. PDF, CSV or image — max 10MB.</p>
        <div ref={drop} className="wz-drop" data-has={!!b.paperwork} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}>
          {b.paperwork ? (
            <React.Fragment>
              <GIIcon name="file-check" size={28} />
              <strong>{b.paperwork.name}</strong>
              <span>{b.paperwork.sizeKb} KB · attached</span>
              <GIBtn variant="ghost" size="sm" icon="x" onClick={() => setB({ paperwork: null })}>Remove</GIBtn>
            </React.Fragment>
          ) : (
            <React.Fragment>
              <GIIcon name="upload-cloud" size={28} color="var(--jfh-cactus)" />
              <strong>Drag &amp; drop a file</strong>
              <span>or <label style={{ color: "var(--jfh-mountain-green)", cursor: "pointer", textDecoration: "underline" }}>browse from your computer<input type="file" hidden onChange={(e) => onPick(e.target.files[0])} accept=".pdf,.csv,.png,.jpg,.jpeg" /></label></span>
            </React.Fragment>
          )}
        </div>
      </div>

      <WizardFooter back={back} next={next} canNext={canNext} nextLabel="Continue to slot picker" />
    </div>
  );
}

/* ─────────── STEP 4 · Slot picker ─────────── */

function StepSlot({ b, setB, next, back, slotStyle }) {
  const D = window.JFH_GI;
  const day = D.DAYS[b.slotDay];

  const slots = D.SLOTS;
  const duration = defaultDurationFor(b.deliveryType);
  const minNoticeHours = minNoticeFor(b.deliveryType);
  // `earliestBookable` = now + minNoticeHours. Slots before this are blocked.
  // `now` is captured per-render so the picker doesn't go stale.
  const earliestBookable = React.useMemo(() => {
    const t = new Date();
    t.setHours(t.getHours() + (minNoticeHours || 0));
    return t;
  }, [minNoticeHours]);

  const [availability, setAvailability] = React.useState(null);
  const [loadError, setLoadError] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    const from = formatYMD(D.DAYS[0]);
    const to = formatYMD(D.DAYS[D.DAYS.length - 1]);
    fetch(`/api/slots?from=${from}&to=${to}`)
      .then(r => r.json())
      .then(body => {
        if (cancelled) return;
        if (body?.ok) setAvailability(body);
        else setLoadError("Couldn't load slot availability — try refreshing.");
      })
      .catch(() => { if (!cancelled) setLoadError("Couldn't load slot availability — try refreshing."); });
    return () => { cancelled = true; };
  }, []);

  function slotDateTime(dayIdx, slot) {
    const dt = new Date(D.DAYS[dayIdx]);
    const [h, m] = String(slot).split(":").map(Number);
    dt.setHours(h, m, 0, 0);
    return dt;
  }

  function blocked(slot, dayIdx = b.slotDay) {
    // 1. Past slots — never bookable.
    const slotTime = slotDateTime(dayIdx, slot);
    if (slotTime.getTime() < Date.now()) return true;
    // 2. Below-min-notice slots.
    if (slotTime.getTime() < earliestBookable.getTime()) return true;
    // 3. Outside opening hours.
    if (!isWithinOpeningHours(dayIdx, slot, duration)) return true;
    // 4. Inside a closure window.
    if (isInClosure(D.DAYS[dayIdx], slot, duration)) return true;
    // 5. Existing-booking overlap (server is source of truth).
    if (!availability) return false; // optimistic while loading
    const dateKey = formatYMD(D.DAYS[dayIdx]);
    const taken = (availability.dates && availability.dates[dateKey]) || [];
    return taken.some((t) => {
      if (b.amending && b.bookingRef && t.time === b.slotTime && dateKey === formatYMD(D.DAYS[b.slotDay])) return false;
      return slotsOverlap(slot, duration, t.time, t.durationMinutes);
    });
  }

  const canNext = !!b.slotTime;

  function pickSlot(slot) {
    if (blocked(slot)) return;
    setB({ slotTime: slot });
  }

  const counts = D.DAYS.map((_, di) => slots.filter(s => !blocked(s, di)).length);

  return (
    <div className="wz-card">
      <POSummary po={b.po} />
      <div className="wz-eyebrow">Step 5 of 6 · Pick an arrival slot</div>
      <h1 className="wz-h1">When's your driver arriving?</h1>
      <p className="wz-lede">We open the goods-in dock in 15-minute windows from 08:00 to 16:00, Monday–Friday. Greyed out slots are already booked.</p>

      <div className="wz-panel">
        {loadError ? (
          <div className="gi-field__error" style={{ marginBottom: 12 }}>
            <GIIcon name="alert-triangle" size={14} /> {loadError}
          </div>
        ) : !availability ? (
          <div style={{ fontSize: 12, color: "var(--fg-3)", marginBottom: 12 }}>Loading availability…</div>
        ) : null}
        <div className="wz-panel__h">Arrival day</div>
        <div className="wz-daytabs">
          {D.DAYS.map((d, i) => (
            <button key={i} type="button" className="wz-daytab" data-on={b.slotDay === i} onClick={() => setB({ slotDay: i, slotTime: null })}>
              <span className="wz-daytab__dow">{D.fmtDay(d).split(" ")[0]}</span>
              <span className="wz-daytab__date">{D.fmtDay(d).split(" ").slice(1).join(" ")}</span>
              <span className="wz-daytab__free">{counts[i]} slots free</span>
            </button>
          ))}
        </div>

        <div className="wz-panel__h" style={{ marginTop: 18 }}>Available times <small>· {D.fmtDayLong(day)} · Sandbach goods-in</small></div>

        {(() => {
          const visibleSlots = slots.filter((s) => !blocked(s));
          if (visibleSlots.length === 0) {
            return (
              <div style={{ padding: 24, textAlign: "center", color: "var(--fg-3)", background: "var(--jfh-paper)", border: "1px dashed var(--border-2)", borderRadius: "var(--radius-md)" }}>
                No bookable slots on this day. Try another date.
              </div>
            );
          }
          if (slotStyle === "grid") {
            return (
              <div className="wz-slots">
                {visibleSlots.map((s) => (
                  <button key={s} type="button" className="wz-slot"
                    data-on={b.slotTime === s}
                    onClick={() => pickSlot(s)}>{s}</button>
                ))}
              </div>
            );
          }
          if (slotStyle === "list") {
            return (
              <div className="wz-slotlist">
                {visibleSlots.map((s) => (
                  <div key={s} className="wz-slotlist__row" data-on={b.slotTime === s} onClick={() => pickSlot(s)}>
                    <span className="wz-slotlist__time">{s}</span>
                    <span className="wz-slotlist__meta">Available · 15 min window</span>
                  </div>
                ))}
              </div>
            );
          }
          return <CalendarSlot b={b} setB={setB} blocked={blocked} pickSlot={pickSlot} slots={visibleSlots} />;
        })()}
      </div>

      <WizardFooter back={back} next={next} canNext={canNext} nextLabel="Review &amp; confirm" />
    </div>
  );
}

function CalendarSlot({ b, setB, blocked, pickSlot, slots }) {
  // Render a small May 2026 calendar with only the bookable days enabled.
  const D = window.JFH_GI;
  const month = 4, year = 2026;
  const first = new Date(year, month, 1);
  const startCol = (first.getDay() + 6) % 7; // Mon-first
  const daysIn = new Date(year, month + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < startCol; i++) cells.push({ muted: true, label: "" });
  for (let d = 1; d <= daysIn; d++) {
    const isAvail = D.DAYS.some((dd) => dd.getDate() === d && dd.getMonth() === month);
    const idx = D.DAYS.findIndex((dd) => dd.getDate() === d && dd.getMonth() === month);
    cells.push({ label: d, avail: isAvail, on: idx === b.slotDay, idx });
  }

  return (
    <div className="wz-cal">
      <div className="wz-cal__calendar">
        <div className="wz-cal__monthhead"><span>May 2026</span><span style={{ color: "var(--fg-3)", fontWeight: 500 }}>·</span></div>
        <div className="wz-cal__grid">
          {["M","T","W","T","F","S","S"].map((d, i) => <span key={i} className="wz-cal__dow">{d}</span>)}
          {cells.map((c, i) => (
            <span key={i} className="wz-cal__day"
              data-muted={c.muted}
              data-avail={c.avail || undefined}
              data-on={c.on || undefined}
              onClick={() => c.avail && setB({ slotDay: c.idx, slotTime: null })}>
              {c.label}
            </span>
          ))}
        </div>
        <div style={{ marginTop: 12, fontSize: 12, color: "var(--fg-3)", display: "flex", flexDirection: "column", gap: 4 }}>
          <span><span style={{ display: "inline-block", width: 10, height: 10, background: "var(--jfh-pistachio)", borderRadius: 3, marginRight: 6 }} /> Bookable</span>
          <span><span style={{ display: "inline-block", width: 10, height: 10, background: "var(--jfh-mountain-green)", borderRadius: 3, marginRight: 6 }} /> Selected</span>
        </div>
      </div>
      <div className="wz-cal__times">
        {slots.length === 0 ? (
          <div style={{ padding: 24, textAlign: "center", color: "var(--fg-3)", fontSize: 13 }}>
            No bookable slots on this day.
          </div>
        ) : (
          slots.map((s) => (
            <button key={s} type="button" className="wz-slot" data-on={b.slotTime === s} onClick={() => pickSlot(s)}>{s}</button>
          ))
        )}
      </div>
    </div>
  );
}

/* ─────────── STEP 5 · Review ─────────── */

function StepReview({ b, setB, back, goto }) {
  const D = window.JFH_GI;
  const dt = D.DELIVERY_TYPES.find(d => d.id === b.deliveryType);
  const courier = b.courier;
  const selectedLines = b.po.lines.filter(l => (b.lineQty[l.sku] || 0) > 0);
  const totalUnits = selectedLines.reduce((s, l) => s + (b.lineQty[l.sku] || 0), 0);
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [cancelling, setCancelling] = React.useState(false);

  async function cancelBooking() {
    if (!b.bookingRef) return;
    const ok = await window.GIDialog.confirm({
      title: `Cancel booking ${b.bookingRef}?`,
      message: "This frees the slot and removes the date from our system. You can't undo this.",
      confirmLabel: "Cancel booking",
      cancelLabel: "Keep booking",
      danger: true,
    });
    if (!ok) return;
    setError(null);
    setCancelling(true);
    try {
      const res = await fetch(`/api/bookings/${encodeURIComponent(b.bookingRef)}/cancel`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({}),
      });
      const body = await res.json().catch(() => ({}));
      if (body?.ok) {
        setB({ step: 6, cancelled: true });
        return;
      }
      setError(bookingErrorMessage(body?.reason, body));
    } catch (e) {
      setError("Couldn't reach the booking system. Try again in a moment.");
    } finally {
      setCancelling(false);
    }
  }

  async function confirm() {
    setError(null);
    setSubmitting(true);
    try {
      const slotDate = formatYMD(D.DAYS[b.slotDay]);
      const payload = {
        poNumber: b.poNumber,
        poDueDate: b.poDueDate || null,
        supplierCode: b.supplierCode,
        supplierName: b.supplierName,
        supplierEmail: b.supplierEmail || null,
        deliveryType: b.deliveryType,
        courier: b.courier || null,
        vehicleReg: b.vehicleReg || null,
        driverName: b.driverName || null,
        driverPhone: b.driverPhone || null,
        palletCount: b.palletCount !== "" && b.palletCount != null ? Number(b.palletCount) : null,
        cartonCount: b.cartonCount !== "" && b.cartonCount != null ? Number(b.cartonCount) : null,
        paperwork: b.paperwork || null,
        notes: b.notes || null,
        slotDate,
        slotTime: b.slotTime,
        lines: selectedLines.map(l => ({ sku: l.sku, name: l.name, qty: b.lineQty[l.sku] || 0, unit: l.unit })),
      };
      const url = b.amending && b.bookingRef ? `/api/bookings/${encodeURIComponent(b.bookingRef)}` : "/api/bookings";
      const method = b.amending && b.bookingRef ? "PUT" : "POST";
      const res = await fetch(url, {
        method,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      const body = await res.json().catch(() => ({}));
      if (body?.ok) {
        setB({ bookingRef: body.ref || body.booking?.ref || b.bookingRef, step: 6 });
        return;
      }
      setError(bookingErrorMessage(body?.reason, body));
    } catch (e) {
      setError("Couldn't reach the booking system. Try again in a moment, or email goodsin@jfhhorticultural.com.");
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <div className="wz-card">
      <POSummary po={b.po} />
      <div className="wz-eyebrow">Step 6 of 6 · {b.amending ? "Amend booking" : "Last check"}</div>
      <h1 className="wz-h1">{b.amending ? `Amend booking ${b.bookingRef}.` : "Review & confirm your booking."}</h1>
      <p className="wz-lede">
        {b.amending
          ? "Edit any section below, then save. You can also cancel this booking entirely."
          : "Everything below will be on the gate manifest. Click any section to edit it before you confirm."}
      </p>
      {b.amending && (
        <div style={{ display: "flex", justifyContent: "flex-end", margin: "0 4px 12px" }}>
          <GIBtn variant="danger-ghost" icon="x-circle" onClick={cancelBooking} disabled={cancelling || submitting}>
            {cancelling ? "Cancelling…" : "Cancel this booking instead"}
          </GIBtn>
        </div>
      )}

      <div className="wz-panel">
        <div className="wz-review">
          <div className="wz-review__block">
            <h4>Consignment <a className="jfh-link" style={{ marginLeft: 6, cursor: "pointer" }} onClick={() => goto(1)}>edit</a></h4>
            <div className="wz-review__row"><span>Delivery type</span><span>{dt ? dt.label : "—"}</span></div>
            <div className="wz-review__row"><span>Courier</span><span>{courier || "—"}</span></div>
          </div>
          <div className="wz-review__block">
            <h4>Slot <a className="jfh-link" style={{ marginLeft: 6, cursor: "pointer" }} onClick={() => goto(4)}>edit</a></h4>
            <div className="wz-review__row"><span>Day</span><span>{D.fmtDayLong(D.DAYS[b.slotDay])}</span></div>
            <div className="wz-review__row"><span>Time</span><span style={{ fontWeight: 700, color: "var(--jfh-mountain-green)" }}>{b.slotTime || "—"}</span></div>
            <div className="wz-review__row"><span>Where</span><span>Sandbach goods-in</span></div>
          </div>
          <div className="wz-review__block">
            <h4>Contact &amp; driver <a className="jfh-link" style={{ marginLeft: 6, cursor: "pointer" }} onClick={() => goto(3)}>edit</a></h4>
            <div className="wz-review__row"><span>Email</span><span>{b.supplierEmail || "—"}</span></div>
            <div className="wz-review__row"><span>Vehicle reg</span><span className="gi-mono">{b.vehicleReg || "—"}</span></div>
            <div className="wz-review__row"><span>Driver</span><span>{b.driverName || "—"}</span></div>
            <div className="wz-review__row"><span>Mobile</span><span>{b.driverPhone || "—"}</span></div>
          </div>
          <div className="wz-review__block">
            <h4>Load <a className="jfh-link" style={{ marginLeft: 6, cursor: "pointer" }} onClick={() => goto(3)}>edit</a></h4>
            <div className="wz-review__row"><span>Pallets</span><span>{b.palletCount || "—"}</span></div>
            <div className="wz-review__row"><span>Cartons</span><span>{b.cartonCount || "—"}</span></div>
            <div className="wz-review__row"><span>Paperwork</span><span>{b.paperwork ? b.paperwork.name : "Not attached"}</span></div>
          </div>
        </div>
      </div>

      <div className="wz-panel">
        <div className="wz-panel__h">PO lines on this delivery <small>· {selectedLines.length} lines · {totalUnits.toLocaleString()} units</small></div>
        <table className="wz-table">
          <thead>
            <tr><th>Line</th><th>Pack</th><th style={{ textAlign: "right" }}>Qty</th></tr>
          </thead>
          <tbody>
            {selectedLines.map(l => (
              <tr key={l.sku}>
                <td><div className="wz-table__name">{l.name}</div><div className="wz-table__sku">{l.sku}</div></td>
                <td><div className="wz-table__pack">{l.pack}</div></td>
                <td style={{ textAlign: "right", fontFamily: "var(--font-display)", fontWeight: 700 }}>{(b.lineQty[l.sku] || 0).toLocaleString()} {l.unit}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {error ? (
        <div className="gi-field__error" style={{ margin: "12px 4px 0" }}>
          <GIIcon name="alert-triangle" size={14} /> {error}
        </div>
      ) : null}
      <WizardFooter
        back={back}
        next={confirm}
        canNext={!submitting}
        nextLabel={submitting ? (b.amending ? "Saving…" : "Booking…") : (b.amending ? "Save changes" : "Confirm booking")}
        nextIcon={submitting ? null : "check"}
      />
    </div>
  );
}

/* ─────────── STEP 6 · Confirmed ─────────── */

function StepConfirmed({ b, setB }) {
  const D = window.JFH_GI;
  const headline = b.cancelled ? "Booking cancelled." : b.amending ? "Booking updated." : "Delivery booked.";
  const recipient = b.supplierEmail || b.po.contact;
  const lede = b.cancelled
    ? <>Booking <strong>{b.bookingRef}</strong> is now cancelled. The slot is released and we've cleared the date in our system.</>
    : <>We've held your slot at <strong>{b.slotTime}</strong> on <strong>{D.fmtDayLong(D.DAYS[b.slotDay])}</strong> for {b.supplierName || b.po.supplier}. A confirmation is on its way to <strong>{recipient}</strong>.</>;

  function downloadIcs() {
    if (!b.bookingRef || !b.slotTime || !b.slotDay && b.slotDay !== 0) return;
    const day = D.DAYS[b.slotDay];
    const ics = buildIcs({
      ref: b.bookingRef,
      summary: `Goods-in delivery · JFH Sandbach (${b.bookingRef})`,
      description: `PO ${b.poNumber} · supplier code ${b.supplierCode}\\nPortal: ${window.location.origin}/`,
      location: "JFH Warehouse, Unit 2, Lower Complex, Lodge Road, Sandbach, Cheshire CW11 3HP",
      startDate: day,
      startTime: b.slotTime,
      durationMin: defaultDurationFor(b.deliveryType),
    });
    const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${b.bookingRef}.ics`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function downloadPdf() {
    if (!b.bookingRef) return;
    window.open(`/api/bookings/${encodeURIComponent(b.bookingRef)}/print`, "_blank");
  }

  return (
    <div className="wz-conf">
      <div className="wz-conf__tick">
        <svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
          {b.cancelled
            ? <><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></>
            : <polyline points="20 6 9 17 4 12"></polyline>}
        </svg>
      </div>
      <h1 className="wz-conf__h">{headline}</h1>
      <p className="wz-conf__p">{lede}</p>
      <div className="wz-conf__ref">
        <span>Booking reference</span>
        <span>{b.bookingRef}</span>
      </div>
      <div className="wz-conf__actions">
        {!b.cancelled ? (
          <>
            <GIBtn icon="download" onClick={downloadPdf}>Download confirmation (PDF)</GIBtn>
            <GIBtn variant="secondary" icon="calendar-plus" onClick={downloadIcs}>Add to driver's calendar</GIBtn>
          </>
        ) : null}
        <GIBtn variant="ghost" icon="rotate-ccw" onClick={() => window.location.reload()}>Book another</GIBtn>
      </div>
      <div className="wz-conf__detail">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, fontSize: 14 }}>
          <div>
            <div style={{ fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--fg-3)", marginBottom: 8 }}>Address on the day</div>
            <div style={{ color: "var(--fg-1)", lineHeight: 1.5 }}>
              JFH Warehouse, Unit 2<br />Lower Complex, Lodge Road<br />Sandbach, Cheshire CW11 3HP
            </div>
          </div>
          <div>
            <div style={{ fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--fg-3)", marginBottom: 8 }}>If you need to amend</div>
            <div style={{ color: "var(--fg-1)", lineHeight: 1.5 }}>
              Use the reference above on the portal up to 2 hours before, or call goods-in on <strong>01270 212726</strong>.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ─────────── Footer + Tweaks ─────────── */

function WizardFooter({ back, next, canNext, nextLabel, nextIcon }) {
  return (
    <div className="wz-footer">
      <div className="wz-footer__left">
        <GIBtn variant="ghost" icon="arrow-left" onClick={back}>Back</GIBtn>
      </div>
      <div className="wz-footer__right">
        <span className="wz-savehint"><GIIcon name="save" size={13} /> Progress saved on this device</span>
        <GIBtn size="lg" onClick={next} disabled={!canNext} iconAfter={nextIcon || "arrow-right"} icon={nextIcon ? nextIcon : null}>{nextLabel || "Continue"}</GIBtn>
      </div>
    </div>
  );
}

function TweaksDock({ density, setDensity, slotStyle, setSlotStyle, onClose }) {
  return (
    <div style={{
      position: "fixed", right: 24, bottom: 24, width: 280, zIndex: 100,
      background: "white", border: "1px solid var(--border-2)", borderRadius: "var(--radius-lg)",
      boxShadow: "var(--shadow-3)", padding: 16,
      fontFamily: "var(--font-body)"
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
        <strong style={{ fontFamily: "var(--font-display)", fontWeight: 700 }}>Tweaks</strong>
        <button onClick={onClose} style={{ background: "transparent", border: 0, cursor: "pointer", color: "var(--fg-3)" }}>×</button>
      </div>
      <div style={{ display: "grid", gap: 14, fontSize: 13 }}>
        <div>
          <div style={{ marginBottom: 6, fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--fg-3)", fontWeight: 600 }}>Density</div>
          <div style={{ display: "flex", background: "var(--jfh-sea-shell)", borderRadius: 999, padding: 3 }}>
            {["comfortable","compact"].map(v => (
              <button key={v} onClick={() => setDensity(v)} style={{ flex: 1, padding: "6px 10px", borderRadius: 999, border: 0, background: density === v ? "white" : "transparent", color: "var(--fg-1)", fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 12.5, cursor: "pointer", boxShadow: density === v ? "var(--shadow-1)" : "none" }}>{v}</button>
            ))}
          </div>
        </div>
        <div>
          <div style={{ marginBottom: 6, fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--fg-3)", fontWeight: 600 }}>Slot picker</div>
          <div style={{ display: "flex", background: "var(--jfh-sea-shell)", borderRadius: 999, padding: 3 }}>
            {["grid","list","calendar"].map(v => (
              <button key={v} onClick={() => setSlotStyle(v)} style={{ flex: 1, padding: "6px 10px", borderRadius: 999, border: 0, background: slotStyle === v ? "white" : "transparent", color: "var(--fg-1)", fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 12.5, cursor: "pointer", boxShadow: slotStyle === v ? "var(--shadow-1)" : "none", textTransform: "capitalize" }}>{v}</button>
            ))}
          </div>
        </div>
        <div style={{ fontSize: 12, color: "var(--fg-3)", lineHeight: 1.4, paddingTop: 6, borderTop: "1px solid var(--border-1)" }}>
          The classic wizard always uses a step layout. Try the bold and depot variants for single-page comparisons.
        </div>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<WizardApp />);
