// Cashier — order taking + checkout with modifiers, split pay, tip
// Wrapper: gates the screen behind the shift-open modal so the inner body's
// hooks aren't conditionally mounted (avoids React error #310).
function CashierScreen() {
  const { currentShift, openShift, db, user, role } = usePOS();
  const [foreignShift, setForeignShift] = React.useState(null);

  // Watch for any open shift owned by another user — only one cashier at a time
  // (admins are allowed to log in alongside an active cashier).
  React.useEffect(() => {
    if (!user) return;
    return db.collection("shifts").where("status", "==", "open").onSnapshot((s) => {
      const other = s.docs
        .map((d) => ({ id: d.id, ...d.data() }))
        .find((sh) => sh.cashierUid && sh.cashierUid !== user.uid);
      setForeignShift(other || null);
    });
  }, [user]);

  // Admins don't take orders — they get a read-only monitor with the
  // force-close button. Cashiers/other roles get the normal flow.
  if (role === "admin") return <CashierMonitor foreignShift={foreignShift} />;
  if (foreignShift) return <CashierLockedScreen shift={foreignShift} />;
  if (!currentShift) return <ShiftOpenSheet onConfirm={openShift} />;
  return <CashierBody />;
}

// Prints a mid-shift snapshot (items sold + payment totals so far) for a
// still-open shift — no need to wait for close. Queries refunds/expenses
// fresh (one-off, not a live subscription — this is a one-shot print
// action) and reuses computeItemsSold from shift.jsx. The printout omits
// the drawer-count section entirely (esc-pos.js's `ongoing` flag) since
// there's nothing to reconcile against until the cashier actually counts
// the till at close.
async function printOngoingShiftSummary(foreignShift, orders, db, showToast) {
  if (!foreignShift) return;
  if (!window.PRINTER || !window.PRINTER.shift) {
    showToast("Printer not available");
    return;
  }
  try {
    const cashier = foreignShift.cashierUid;
    const today = dayKey();
    // Scoped to this shift's own window, not just "today" — a cashier can
    // open/close multiple shifts in one day, and without this a second
    // shift's summary would double-count whatever the previous shift already
    // sold/refunded/spent (see the identical fix + comment in shift.jsx).
    const openedAtMs = foreignShift.openedAt?.toDate ? foreignShift.openedAt.toDate().getTime() : 0;
    const sinceOpen = (createdAt) => {
      const ms = createdAt?.toDate ? createdAt.toDate().getTime() : Date.now();
      return ms >= openedAtMs;
    };
    const paidOrdersToday = orders.filter((o) => o.paid && o.cashierUid === cashier && sinceOpen(o.createdAt));

    const refundsSnap = await db.collection("refunds").where("cashierUid", "==", cashier).get();
    const refundedItems = [];
    const refundsByMethod = {};
    refundsSnap.docs.map((d) => d.data())
      .filter((r) => r.dateKey === today && sinceOpen(r.createdAt))
      .forEach((r) => {
        const m = (r.method || "cash").toLowerCase();
        refundsByMethod[m] = (refundsByMethod[m] || 0) + (r.total || 0);
        (r.items || []).forEach((it) => refundedItems.push(it));
      });

    const expensesSnap = await db.collection("expenses").where("cashierUid", "==", cashier).get();
    const cashExpenses = expensesSnap.docs.map((d) => d.data())
      .filter((x) => x.dateKey === today && sinceOpen(x.createdAt) && (x.method || "cash").toLowerCase() === "cash")
      .reduce((a, x) => a + (x.amount || 0), 0);

    const totals = paidOrdersToday.reduce((acc, o) => {
      const splits = o.payments && o.payments.length ? o.payments : [{ method: o.paymentMethod, amount: o.total }];
      splits.forEach((p) => {
        const m = (p.method || "other").toLowerCase();
        acc[m] = (acc[m] || 0) + (Number(p.amount) || 0);
      });
      return acc;
    }, {});
    const cashSales = totals.cash || 0;
    const qrisSales = (totals.qris || 0) - (refundsByMethod.qris || 0);
    const cardSales = (totals.card || 0) - (refundsByMethod.card || 0);
    const totalRefunds = Object.values(refundsByMethod).reduce((a, v) => a + v, 0);

    const openedAt = foreignShift.openedAt?.toDate ? foreignShift.openedAt.toDate() : null;
    const fmtTime = (d) => d ? d.toLocaleTimeString("id-ID", { hour: "2-digit", minute: "2-digit" }) : "—";

    PRINTER.shift({
      ongoing: true,
      cashierEmail: foreignShift.cashierEmail || "",
      dateKey: today,
      openedAt: fmtTime(openedAt),
      printedAt: fmtTime(new Date()),
      openingCash: foreignShift.openingCash || 0,
      totalTransactions: paidOrdersToday.length,
      itemsSold: window.computeItemsSold(paidOrdersToday, refundedItems),
      cashSales, qrisSales, cardSales,
      totalRefunds,
      cashExpenses,
    });
    showToast("Printing shift summary…");
  } catch (e) {
    showToast("Failed to print: " + e.message);
  }
}

// Admin-only monitor view: live shift info, recent orders, force-close button.
function CashierMonitor({ foreignShift }) {
  const { orders, db, showToast } = usePOS();
  const [printingSummary, setPrintingSummary] = React.useState(false);

  const forceClose = async () => {
    if (!foreignShift) return;
    if (!confirm(`Force-close the shift opened by ${foreignShift.cashierEmail || foreignShift.cashierUid}? They will be signed out of the cashier section.`)) return;
    await db.collection("shifts").doc(foreignShift.id).update({
      status: "closed",
      closedAt: firebase.firestore.FieldValue.serverTimestamp(),
      closedBy: "admin",
      forceClosed: true,
    });
    showToast("Shift force-closed");
  };

  const printSummary = async () => {
    setPrintingSummary(true);
    await printOngoingShiftSummary(foreignShift, orders, db, showToast);
    setPrintingSummary(false);
  };

  const opened = foreignShift?.openedAt?.toDate ? foreignShift.openedAt.toDate() : null;
  const todays = orders.filter((o) => o.paid).slice(0, 30);
  const totalSales = todays.reduce((s, o) => s + (o.total || 0), 0);
  const totalOrders = todays.length;

  return (
    <div className="admin-pane" style={{ padding: 20 }}>
      <header className="pane-head">
        <div>
          <div className="eyebrow mono">CASHIER MONITOR · ADMIN VIEW</div>
          <h2>{foreignShift ? "Cashier on duty" : "No active cashier shift"}</h2>
        </div>
      </header>

      {foreignShift ? (
        <div className="panel-card" style={{ marginBottom: 16 }}>
          <div className="row" style={{ gap: 16, alignItems: "flex-start", justifyContent: "space-between", flexWrap: "wrap" }}>
            <div>
              <div className="muted mono" style={{ fontSize: 11 }}>CASHIER</div>
              <div style={{ fontSize: 18, fontWeight: 500 }}>{foreignShift.cashierEmail || foreignShift.cashierUid}</div>
              {opened && <div className="muted mono" style={{ fontSize: 12, marginTop: 4 }}>
                Opened {opened.toLocaleString("en-GB", { timeZone: "Asia/Jakarta" })} WIB
              </div>}
              <div className="muted mono" style={{ fontSize: 12 }}>
                Opening float: {money(foreignShift.openingCash || 0)}
              </div>
            </div>
            <div className="row" style={{ gap: 8 }}>
              <button className="btn-ghost-sm" onClick={printSummary} disabled={printingSummary}>
                {printingSummary ? "Printing…" : "Print shift summary"}
              </button>
              <button className="btn-ghost-sm danger" onClick={forceClose}>Force close shift</button>
            </div>
          </div>
        </div>
      ) : (
        <div className="panel-card muted mono" style={{ padding: 20, marginBottom: 16 }}>
          No cashier is signed in right now. When a cashier opens a shift, you'll see them here.
        </div>
      )}

      <div className="kpis" style={{ marginBottom: 16 }}>
        <div className="kpi-card">
          <div className="kpi-label mono">ORDERS TODAY</div>
          <Mono className="kpi-value">{totalOrders}</Mono>
          <div className="kpi-delta mono">paid</div>
        </div>
        <div className="kpi-card accent">
          <div className="kpi-label mono">REVENUE TODAY</div>
          <Mono className="kpi-value">{money(totalSales)}</Mono>
          <div className="kpi-delta mono">{totalOrders ? `avg ${money(Math.round(totalSales / totalOrders))}` : "—"}</div>
        </div>
      </div>

      <div className="panel-card no-pad">
        <table className="data-table">
          <thead><tr><th>Time</th><th>#</th><th>Customer</th><th>Table</th><th>Items</th><th>Payment</th><th>Total</th></tr></thead>
          <tbody>
            {todays.length === 0 && <tr><td colSpan="7" className="muted mono" style={{ textAlign: "center", padding: 24 }}>No paid orders yet today.</td></tr>}
            {todays.map((o) => (
              <tr key={o.id}>
                <td><Mono>{o.createdAt?.toDate ? o.createdAt.toDate().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) : "—"}</Mono></td>
                <td><Mono>{o.shortId}</Mono></td>
                <td>{o.customer}</td>
                <td><Mono>{o.table}</Mono></td>
                <td><Mono>{o.items.reduce((a, i) => a + i.qty, 0)}</Mono></td>
                <td><Mono>{(o.paymentMethod || "—").toUpperCase()}</Mono></td>
                <td><Mono>{money(o.total)}</Mono></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function CashierLockedScreen({ shift }) {
  const opened = shift.openedAt?.toDate ? shift.openedAt.toDate() : null;
  return (
    <div className="cashier-locked">
      <div className="cashier-locked-card">
        <div className="eyebrow mono">CASHIER · LOCKED</div>
        <div className="lock-icon">🔒</div>
        <h2>Another cashier is signed in</h2>
        <p className="muted">
          The POS allows only one open cashier session at a time. The current
          cashier must close their shift before you can take over.
        </p>
        <div className="lock-info mono">
          <div><span className="muted">Cashier</span><strong>{shift.cashierEmail || shift.cashierUid}</strong></div>
          {opened && <div><span className="muted">Opened</span><strong>{opened.toLocaleString("en-GB", { timeZone: "Asia/Jakarta" })} WIB</strong></div>}
        </div>
        <button className="btn-ghost" onClick={() => firebase.auth().signOut()}>Sign out</button>
      </div>
    </div>
  );
}

function CashierBody({ foreignShift }) {
  const { products, ingredients, placeOrder, updateOrder, prebookings, consumePrebooking, pushLiveCart, orders, decrementStock,
          currentShift, closeShift, db, showToast } = usePOS();

  const forceCloseForeign = async () => {
    if (!foreignShift) return;
    if (!confirm(`Force-close the shift opened by ${foreignShift.cashierEmail || foreignShift.cashierUid}? They will be signed out of the cashier section.`)) return;
    await db.collection("shifts").doc(foreignShift.id).update({
      status: "closed",
      closedAt: firebase.firestore.FieldValue.serverTimestamp(),
      closedBy: "admin",
      forceClosed: true,
    });
    showToast("Shift force-closed");
  };
  const [closingShift, setClosingShift] = React.useState(false);
  const [receiptsOpen, setReceiptsOpen] = React.useState(false);
  const [openBillsOpen, setOpenBillsOpen] = React.useState(false);
  const [expenseOpen, setExpenseOpen] = React.useState(false);
  const [printerCheckOpen, setPrinterCheckOpen] = React.useState(false);
  const [refundFor, setRefundFor] = React.useState(null);
  const [search, setSearch] = React.useState("");
  const [cat, setCat] = React.useState("All");
  const [cart, setCart] = React.useState([]);
  const [customer, setCustomer] = React.useState("");
  const [table, setTable] = React.useState("");
  const [modProduct, setModProduct] = React.useState(null);
  const [checkout, setCheckout] = React.useState(null); // {mode:'pay'}
  const [activePrebook, setActivePrebook] = React.useState(null);
  const [editingOrderId, setEditingOrderId] = React.useState(null);

  const openBills = orders.filter((o) => o.paid === false && o.status !== "closed");

  const filtered = products.filter((p) =>
    (cat === "All" || p.category === cat) &&
    (!search || p.name.toLowerCase().includes(search.toLowerCase()))
  );

  const onPick = (p) => {
    if (!hasStock(p)) { showToast(`${p.name} is out of stock`); return; }
    const mods = (p.modifiers && p.modifiers.length) ? p.modifiers : window.MODIFIERS[p.category];
    if (mods && mods.length) { setModProduct(p); }
    else { addToCart({ ...p, mods: [], modPrice: 0, qty: 1, cogs: p.cogs || 0, notes: "" }); }
  };

  // Qty of a product already committed by the cart — both direct lines for
  // that product, and indirectly via any OTHER cart line whose recipe/combo
  // consumes it as a component (a product can itself be a combo ingredient,
  // competing for the same stock as someone ordering it a la carte).
  // Replaces the old cartQtyForProduct, which only summed direct lines.
  const cartProductConsumption = (productId) => cart.reduce((s, l) => {
    let add = l.id === productId ? l.qty : 0;
    const prod = products.find((p) => p.id === l.id);
    const recipe = prod && Array.isArray(prod.recipe) ? prod.recipe : [];
    recipe.forEach((r) => {
      const targetType = r.targetType || "ingredient"; // legacy lines (pre-combo) had no targetType — always an ingredient
      const targetId = r.targetId || r.ingredientId;
      if (targetType === "product" && targetId === productId) add += (Number(r.qty) || 0) * l.qty;
    });
    return s + add;
  }, 0);

  // Ingredient qty already committed by every recipe-based cart line (not
  // just other lines of the same product — two different recipe items can
  // share an ingredient).
  const cartIngredientConsumption = (ingredientId) => cart.reduce((s, l) => {
    const prod = products.find((p) => p.id === l.id);
    const recipe = prod && Array.isArray(prod.recipe) ? prod.recipe : [];
    const r = recipe.find((r) => (r.targetType || "ingredient") === "ingredient" && (r.targetId || r.ingredientId) === ingredientId);
    if (!r) return s;
    return s + (Number(r.qty) || 0) * l.qty;
  }, 0);

  // How many more units of a recipe/combo product the current stock of its
  // components — ingredients and/or other products, net of what the cart
  // already reserves — can still produce. A line with qty <= 0 (bad data)
  // is treated as non-blocking.
  const recipeAvail = (p) => {
    if (!Array.isArray(p.recipe) || !p.recipe.length) return Infinity;
    return Math.min(...p.recipe.map((r) => {
      const qty = Number(r.qty) || 0;
      if (qty <= 0) return Infinity;
      const targetType = r.targetType || "ingredient";
      const targetId = r.targetId || r.ingredientId;
      if (targetType === "product") {
        const comp = products.find((x) => x.id === targetId);
        const stock = Number(comp?.stock) || 0;
        if (stock >= 999) return Infinity;
        return Math.floor((stock - cartProductConsumption(targetId)) / qty);
      }
      const ing = ingredients.find((i) => i.id === targetId);
      const stock = Number(ing?.stock) || 0;
      if (stock >= 999) return Infinity;
      return Math.floor((stock - cartIngredientConsumption(targetId)) / qty);
    }));
  };

  // A product with stock >= 999 is "unlimited" (admin convention). Otherwise
  // it's available only if its stock exceeds what the cart already holds,
  // directly or via another line's combo. Recipe/combo products (non-empty
  // `recipe`) resolve availability through their components instead of
  // their own `stock` field.
  const hasStock = (p) => (Array.isArray(p.recipe) && p.recipe.length)
    ? recipeAvail(p) > 0
    : (Number(p.stock) || 0) >= 999 || (Number(p.stock) || 0) > cartProductConsumption(p.id);

  const addToCart = (line) => {
    const prod = products.find((x) => x.id === line.id);
    if (prod && !hasStock(prod)) { showToast(`${line.name} is out of stock`); return; }
    setCart((prev) => {
      const key = line.id + "|" + line.mods.join(",") + "|" + (line.notes || "");
      const i = prev.findIndex((x) => x._key === key);
      if (i >= 0) {
        const next = [...prev];
        next[i] = { ...next[i], qty: next[i].qty + line.qty };
        return next;
      }
      return [...prev, { ...line, _key: key, notes: line.notes || "" }];
    });
  };

  const changeQty = (key, d) => setCart((prev) => prev.map((x) => {
    if (x._key !== key) return x;
    if (d > 0) {
      const prod = products.find((p) => p.id === x.id);
      if (prod && Array.isArray(prod.recipe) && prod.recipe.length) {
        if (recipeAvail(prod) <= 0) { showToast(`${x.name} is out of stock`); return x; }
      } else {
        const stock = Number(prod?.stock) || 0;
        if (stock < 999 && stock <= cartProductConsumption(x.id)) { showToast(`${x.name} is out of stock`); return x; }
      }
    }
    return { ...x, qty: Math.max(0, x.qty + d) };
  }).filter((x) => x.qty > 0));
  const removeLine = (key) => setCart((prev) => prev.filter((x) => x._key !== key));
  const setLineNotes = (key, notes) => setCart((prev) => prev.map((x) =>
    x._key === key ? { ...x, notes } : x));

  // Per-product bulk/grosir discount. When the product's total cart qty reaches the
  // product's bulkQty threshold, the item's base price is discounted by a
  // percentage or replaced with a fixed unit price (admin-configured).
  // Modifiers are always charged at full price on top.
  // Always derive from the product's current catalog price (not the line's
  // stored price) so re-loading an already-discounted open bill doesn't
  // discount it a second time.
  const baseOf = (l) => {
    const prod = products.find((p) => p.id === l.id);
    return prod ? (Number(prod.price) || 0) : (Number(l.price) || 0);
  };
  // The threshold counts the product's total qty across ALL cart lines, not
  // just this line — variants/modifiers split one product into several
  // lines (Donat Oma Rainbow + Tiramisu + Meses = 3 lines of qty 1), and
  // they must still add up toward the bulk discount.
  const productQtyInCart = (id) =>
    cart.reduce((s, x) => s + (x.id === id ? (Number(x.qty) || 0) : 0), 0);
  const linePrice = (l) => {
    const base = baseOf(l);
    const prod = products.find((p) => p.id === l.id);
    const q = Number(prod?.bulkQty) || 0;
    if (!q || productQtyInCart(l.id) < q) return base;
    const val = Number(prod.bulkValue) || 0;
    if (prod.bulkType === "price") return Math.max(0, val);
    return Math.max(0, Math.round(base * (1 - val / 100))); // percent
  };
  const lineUnit = (l) => linePrice(l) + (l.modPrice || 0);
  const lineFull = (l) => baseOf(l) + (l.modPrice || 0);

  const subtotal = cart.reduce((s, l) => s + lineUnit(l) * l.qty, 0);
  const bulkSavings = cart.reduce((s, l) => s + (lineFull(l) - lineUnit(l)) * l.qty, 0);
  const depositCredit = activePrebook ? activePrebook.depositPaid : 0;

  // Normalize a cart line into a stored order item, applying the bulk
  // discount to the persisted price so receipts, reports and refunds all
  // use the discounted figure. Recipe/combo products additionally get a
  // `recipeSnapshot` (the recipe as it stood at sale time — decrement and
  // any later refund must both consume/restore against this snapshot, not
  // a live re-read of product.recipe, so an in-between recipe edit can't
  // desync a refund from what was actually deducted) and a `cogs` computed
  // from each component's cost instead of the product's flat cogs field —
  // ingredient avgCost for an ingredient line, the component product's own
  // (already-current) cogs for a product/combo line.
  const toItem = (l, qty = l.qty, withStatus = false) => {
    const prod = products.find((p) => p.id === l.id);
    const recipe = prod && Array.isArray(prod.recipe) ? prod.recipe : [];
    let recipeSnapshot, cogs = l.cogs || 0;
    if (recipe.length) {
      recipeSnapshot = recipe.map((r) => {
        const targetType = r.targetType || "ingredient";
        const targetId = r.targetId || r.ingredientId;
        const entry = {
          targetType, targetId,
          targetName: r.targetName || r.ingredientName || "",
          qty: Number(r.qty) || 0,
        };
        // Snapshot a combo component's own station too — never re-read live
        // later, same reasoning as the rest of this snapshot: which printer/
        // KDS screen a combo needs to reach at sale time must stay fixed
        // even if the component's station is recategorized afterward.
        if (targetType === "product") {
          entry.station = (products.find((p) => p.id === targetId) || {}).station || "bar";
        }
        return entry;
      });
      cogs = recipeSnapshot.reduce((s, r) => {
        if (r.targetType === "product") {
          const comp = products.find((p) => p.id === r.targetId);
          return s + r.qty * (Number(comp?.cogs) || 0);
        }
        const ing = ingredients.find((i) => i.id === r.targetId);
        return s + r.qty * (Number(ing?.avgCost) || 0);
      }, 0);
    }
    return {
      id: l.id, sku: l.sku || "", name: l.name, price: linePrice(l), qty,
      station: l.station, mods: l.mods || [], modPrice: l.modPrice || 0,
      cogs, notes: l.notes || "",
      ...(recipeSnapshot ? { recipeSnapshot } : {}),
      ...(withStatus ? { status: l.status || "new" } : {}),
    };
  };

  // Push live cart to /displays/main so customer-display mirrors in real time.
  React.useEffect(() => {
    const t = setTimeout(() => {
      pushLiveCart(cart.map((l) => ({
        id: l.id, name: l.name, price: linePrice(l), qty: l.qty,
        station: l.station, mods: l.mods, modPrice: l.modPrice,
      })), {
        subtotal, depositApplied: depositCredit,
        total: Math.max(0, subtotal - depositCredit),
        customer: customer || null, table: table || null,
        shortId: null,
      });
    }, 200);
    return () => clearTimeout(t);
  }, [cart, customer, table, depositCredit]);

  const todayPb = prebookings.filter((p) => p.status === "paid_deposit");

  const loadPb = (pb) => {
    setActivePrebook(pb);
    setCustomer(pb.customerName);
    setCart(pb.items.map((i) => ({
      ...i, mods: [], modPrice: 0, _key: i.id + "|",
      station: (products.find((p) => p.id === i.id) || {}).station || "bar",
    })));
  };

  const resetCart = () => {
    setCart([]); setActivePrebook(null); setCustomer(""); setTable("");
    setEditingOrderId(null); setCheckout(null);
  };

  // Save the cart as an open bill: order is created with paid:false and sent
  // to the kitchen immediately. Cashier can pay later by tapping it in the
  // 'Open bills' banner.
  // Compute which lines in the current cart are NEW vs. the original Firestore
  // order items (matched by id + modifiers + notes). Returns positive qty
  // deltas only — additions to be fired to the kitchen/bar/waitress.
  const computeAddedItems = (originalItems) => {
    const keyFor = (i) => `${i.id}|${(i.mods || []).join(",")}|${i.notes || ""}`;
    const origMap = new Map();
    (originalItems || []).forEach((it) => {
      origMap.set(keyFor(it), (origMap.get(keyFor(it)) || 0) + (it.qty || 0));
    });
    const added = [];
    cart.forEach((l) => {
      const orig = origMap.get(keyFor(l)) || 0;
      const delta = l.qty - orig;
      if (delta > 0) {
        added.push(toItem(l, delta));
      }
    });
    return added;
  };

  // Expand a combo item into one printable row per distinct station among
  // its components, so e.g. a combo bundling a kitchen dish with a bar
  // drink reaches BOTH printers instead of only whichever single station
  // the combo product itself happens to be tagged with. A plain item (or a
  // recipe made only of ingredients, no product components) passes through
  // unchanged. Only used for the kitchen/bar tickets below — the floor/
  // waitress ticket keeps the original combined view, since whoever
  // delivers it hands the whole combo over at once.
  const expandForPrint = (items) => items.flatMap((it) => {
    const comboParts = Array.isArray(it.recipeSnapshot)
      ? it.recipeSnapshot.filter((r) => r.targetType === "product" && r.station)
      : [];
    if (!comboParts.length) return [it];
    const byStation = {};
    comboParts.forEach((r) => {
      (byStation[r.station] || (byStation[r.station] = [])).push(r.targetName || r.targetId);
    });
    return Object.entries(byStation).map(([station, names]) => ({
      ...it, station, name: `${it.name} - ${names.join(", ")}`, // plain hyphen: em dash gets stripped by esc-pos.js's sanitize() (thermal printers are latin1/CP437-only)
    }));
  });

  // All stations a cart line touches — its own `station`, plus (for a
  // combo) every distinct station among its product components. Used to
  // decide whether an order needs a kitchenStatus/barStatus at all; a
  // combo bundling a kitchen dish with a bar drink must set both.
  const cartLineStations = (l) => {
    const prod = products.find((p) => p.id === l.id);
    const recipe = prod && Array.isArray(prod.recipe) ? prod.recipe : [];
    const stations = new Set([l.station]);
    recipe.forEach((r) => {
      if ((r.targetType || "ingredient") !== "product") return;
      const comp = products.find((p) => p.id === (r.targetId || r.ingredientId));
      if (comp?.station) stations.add(comp.station);
    });
    return stations;
  };

  const fireStationPrints = (shortId, items) => {
    if (!items.length || !window.PRINTER) return;
    const base = { shortId, table: table || "T", customer: customer || "Walk-in" };
    const printItems = expandForPrint(items);
    if (printItems.some((i) => i.station === "kitchen")) PRINTER.kitchen({ ...base, items: printItems });
    if (printItems.some((i) => i.station === "bar")) PRINTER.bar({ ...base, items: printItems });
    PRINTER.waitress({ ...base, items });
  };

  const saveOpenBill = async () => {
    const shortId = makeShortId();
    if (editingOrderId) {
      // Update existing open bill — only NEW items (vs. what's already in
      // Firestore) get fired to the printers.
      const orig = openBills.find((o) => o.id === editingOrderId);
      const addedItems = computeAddedItems(orig?.items || []);
      await updateOrder(editingOrderId, {
        items: cart.map((l) => toItem(l, l.qty, true)),
        subtotal, total: subtotal - depositCredit,
        customer: customer || "Walk-in", table: table || "T",
      });
      fireStationPrints(orig?.shortId || shortId, addedItems);
      if (addedItems.length) decrementStock(addedItems, editingOrderId);
    } else {
      await placeOrder({
        shortId, table: table || "T", customer: customer || "Walk-in",
        items: cart.map((l) => toItem(l, l.qty, true)),
        subtotal, depositApplied: depositCredit,
        total: subtotal - depositCredit,
        paid: false,
        barStatus: cart.some((i) => cartLineStations(i).has("bar")) ? "new" : null,
        kitchenStatus: cart.some((i) => cartLineStations(i).has("kitchen")) ? "new" : null,
        prebookingId: activePrebook?.id || null,
      });
      if (activePrebook) consumePrebooking(activePrebook.id);
      const allItems = cart.map((l) => toItem(l));
      fireStationPrints(shortId, allItems);
    }
    resetCart();
  };

  const placeFinal = async (payments) => {
    const shortId = makeShortId();
    const hasBar = cart.some((i) => cartLineStations(i).has("bar"));
    const hasKitchen = cart.some((i) => cartLineStations(i).has("kitchen"));
    const total = subtotal - depositCredit + (payments.uniqueCode || 0);
    if (editingOrderId) {
      // Pay an existing open bill — keep kitchen status as-is
      await updateOrder(editingOrderId, {
        items: cart.map((l) => toItem(l, l.qty, true)),
        subtotal, depositApplied: depositCredit,
        uniqueCode: payments.uniqueCode || 0, total,
        tip: payments.tip || 0,
        payments: payments.splits,
        paymentMethod: payments.splits.length === 1 ? payments.splits[0].method : "split",
        paid: true,
        customer: customer || "Walk-in", table: table || "T",
      });
    } else {
      await placeOrder({
        shortId, table: table || "T", customer: customer || "Walk-in",
        items: cart.map((l) => toItem(l, l.qty, true)),
        subtotal, depositApplied: depositCredit,
        uniqueCode: payments.uniqueCode || 0, total,
        tip: payments.tip || 0,
        payments: payments.splits,
        paymentMethod: payments.splits.length === 1 ? payments.splits[0].method : "split",
        paid: true,
        barStatus: hasBar ? "new" : null,
        kitchenStatus: hasKitchen ? "new" : null,
        prebookingId: activePrebook?.id || null,
      });
      if (activePrebook) consumePrebooking(activePrebook.id);
    }
    if (window.PRINTER) {
      const origBill = editingOrderId ? openBills.find((o) => o.id === editingOrderId) : null;
      const effId = origBill?.shortId || shortId;
      const allCartItems = cart.map((l) => toItem(l));
      // Receipt always shows the full bill.
      const pd = {
        shortId: effId, table: table || "T", customer: customer || "Walk-in",
        items: allCartItems,
        subtotal, depositApplied: depositCredit,
        uniqueCode: payments.uniqueCode || 0, total,
        tip: payments.tip || 0, payments: payments.splits,
        paymentMethod: payments.splits.length === 1 ? payments.splits[0].method : "split",
      };
      PRINTER.receipt(pd);
      // Station prints: for new orders, send all items. For paid open bills,
      // only send items that weren't already on the original open-bill ticket.
      const itemsToFire = editingOrderId ? computeAddedItems(origBill?.items || []) : allCartItems;
      fireStationPrints(effId, itemsToFire);
      // For new orders, placeOrder() already decremented stock. For an
      // edited open bill, only the newly-added items need to be decremented.
      if (editingOrderId && itemsToFire.length) decrementStock(itemsToFire, editingOrderId);
    }
    resetCart();
  };

  const loadBill = (o) => {
    setEditingOrderId(o.id);
    setCustomer(o.customer || "");
    setTable(o.table || "");
    setCart(o.items.map((i) => ({
      ...i, _key: i.id + "|" + (i.mods || []).join(",") + "|" + (i.notes || ""),
    })));
  };

  return (
    <div className="cashier">
      {foreignShift && (
        <div className="admin-override-bar mono">
          <span>👑 Admin override · Active cashier: <strong>{foreignShift.cashierEmail || foreignShift.cashierUid}</strong> (opened {foreignShift.openedAt?.toDate ? foreignShift.openedAt.toDate().toLocaleString("en-GB", { timeZone: "Asia/Jakarta", hour: "2-digit", minute: "2-digit" }) + " WIB" : "—"})</span>
          <button className="btn-ghost-sm danger" onClick={forceCloseForeign}>Force close their shift</button>
        </div>
      )}
      <section className="cashier-menu">
        <div className="menu-head">
          <div className="search-wrap">
            <span className="kbd-hint mono">⌘K</span>
            <input
              className="search" placeholder="Search the menu…"
              value={search} onChange={(e) => setSearch(e.target.value)}
            />
          </div>
          <div className="cat-bar">
            {(() => {
              const dynamic = ["All", ...Array.from(new Set(products.map((p) => p.category).filter(Boolean))).sort()];
              return dynamic;
            })().map((c) => (
              <button
                key={c}
                className={"cat-chip" + (cat === c ? " active" : "")}
                onClick={() => setCat(c)}
              >
                {c}
              </button>
            ))}
          </div>
        </div>

        {todayPb.length > 0 && (
          <div className="prebook-banner">
            <span className="banner-label mono">TODAY'S PRE-BOOKS</span>
            {todayPb.map((pb) => (
              <button key={pb.id} className="prebook-chip" onClick={() => loadPb(pb)}>
                <span>{pb.customerName}</span>
                <Mono>{pb.items.reduce((a, i) => a + i.qty, 0)} items · deposit {money(pb.depositPaid)}</Mono>
              </button>
            ))}
          </div>
        )}

        {openBills.length > 0 && (
          <div className="prebook-banner openbill-banner">
            <span className="banner-label mono">OPEN BILLS</span>
            {openBills.map((o) => (
              <button key={o.id} className={"prebook-chip" + (editingOrderId === o.id ? " active" : "")} onClick={() => loadBill(o)}>
                <span>#{o.shortId} · {o.customer}{o.visitTime ? ` · ${o.visitTime}` : ""}</span>
                <Mono>T{o.table} · {money(o.total)}</Mono>
              </button>
            ))}
          </div>
        )}

        <div className="product-grid">
          {filtered.map((p) => {
            const soldOut = !hasStock(p);
            const isRecipe = Array.isArray(p.recipe) && p.recipe.length > 0;
            // Show stock net of what's already in the current cart, so adding
            // to the ticket visibly counts down even before checkout. Recipe
            // items resolve this through their ingredients instead.
            const avail = isRecipe ? recipeAvail(p) : (Number(p.stock) || 0) - cartProductConsumption(p.id);
            const unlimited = isRecipe ? avail === Infinity : p.stock >= 999;
            return (
            <button key={p.id} className="product-card" onClick={() => onPick(p)}
              disabled={soldOut} style={soldOut ? { opacity: 0.45 } : undefined}>
              <ProductSwatch color={p.swatch} label={p.name} size={44} />
              <div className="product-meta">
                <div className="product-name">{p.name}</div>
                <div className="product-desc">{p.desc}</div>
                <div className="product-foot">
                  <Mono className="product-price">{money(p.price)}</Mono>
                  <span className={"mono " + ((avail <= 20) ? "stock-low" : "muted")} style={{ fontSize: 11 }}>
                    {unlimited ? "∞" : avail <= 0 ? "Out of stock" : avail + " left"}
                  </span>
                </div>
              </div>
            </button>
          );
          })}
        </div>
      </section>

      <section className="cashier-cart">
        <header className="cart-head">
          <div className="eyebrow mono">
            {editingOrderId ? "OPEN BILL · " + (openBills.find((o) => o.id === editingOrderId)?.shortId || "") : "CURRENT ORDER"}
          </div>
          {activePrebook && <span className="badge-prebook mono">PRE-BOOK</span>}
        </header>

        <div className="cart-fields">
          <label className="field">
            <span className="field-label mono">Customer</span>
            <input value={customer} onChange={(e) => setCustomer(e.target.value)} placeholder="Walk-in" />
          </label>
          <label className="field field-table">
            <span className="field-label mono">Table</span>
            <input value={table} onChange={(e) => setTable(e.target.value)} placeholder="—" />
          </label>
        </div>

        <div className="cart-list">
          {cart.length === 0 && (
            <div className="cart-empty">
              <div className="empty-mark">⌘</div>
              <p>Tap a product to start a ticket.</p>
            </div>
          )}
          {cart.map((l) => (
            <div className="cart-line" key={l._key}>
              <div className="cart-line-main">
                <div className="cart-line-name">{l.name}</div>
                {l.mods.length > 0 && (
                  <div className="cart-line-mods mono">{l.mods.join(" · ")}</div>
                )}
              </div>
              <div className="cart-line-qty">
                <button onClick={() => changeQty(l._key, -1)}>–</button>
                <Mono>{l.qty}</Mono>
                <button onClick={() => changeQty(l._key, 1)}>+</button>
              </div>
              <div className="cart-line-sub" style={{ textAlign: "right" }}>
                <Mono>{money(lineUnit(l) * l.qty)}</Mono>
                {lineUnit(l) < lineFull(l) && (
                  <div className="mono stock-low" style={{ fontSize: 10 }}>
                    bulk <s>{money(lineFull(l) * l.qty)}</s>
                  </div>
                )}
              </div>
              <button className="cart-line-x" onClick={() => removeLine(l._key)}>×</button>
              <input
                className="cart-line-note"
                placeholder="+ note (e.g. extra hot, no ice)"
                value={l.notes || ""}
                onChange={(e) => setLineNotes(l._key, e.target.value)}
              />
            </div>
          ))}
        </div>

        <div className="cart-totals">
          {(bulkSavings > 0 || depositCredit > 0) && (
            <div className="total-row"><span>Subtotal</span><Mono>{money(subtotal)}</Mono></div>
          )}
          {bulkSavings > 0 && (
            <div className="total-row total-credit"><span>Bulk discount</span><Mono>−{money(bulkSavings)}</Mono></div>
          )}
          {depositCredit > 0 && (
            <div className="total-row total-credit"><span>Deposit applied</span><Mono>−{money(depositCredit)}</Mono></div>
          )}
          <div className="total-row total-grand">
            <span>Due</span><Mono>{money(Math.max(0, subtotal - depositCredit))}</Mono>
          </div>
        </div>

        {cart.length > 0 && (!customer.trim() || !table.trim()) && (
          <div className="cart-required mono">Enter customer name and table to checkout.</div>
        )}
        <div className="cart-actions">
          <button className="btn-ghost btn-sm" disabled={!cart.length} onClick={resetCart}>Clear</button>
          <button
            className="btn-ghost btn-sm"
            onClick={() => setOpenBillsOpen(true)}
            disabled={openBills.length === 0}
            title="Load a previously saved open bill">
            Bills{openBills.length > 0 ? ` (${openBills.length})` : ""}
          </button>
          <button
            className="btn-ghost btn-savebill btn-sm"
            disabled={!cart.length || !customer.trim() || !table.trim()}
            onClick={saveOpenBill}
            title="Send the order to the kitchen and pay later">
            {editingOrderId ? "Update" : "Save"}
          </button>
          <button
            className="btn-primary btn-sm"
            disabled={!cart.length || !customer.trim() || !table.trim()}
            onClick={() => setCheckout({})}>
            {editingOrderId ? "Pay bill →" : "Pay →"}
          </button>
        </div>
        <ShiftStats orders={orders} cashierUid={currentShift.cashierUid} openingCash={currentShift.openingCash || 0} />
        <div className="shift-tools">
          <button className="shift-tool" onClick={() => setPrinterCheckOpen(true)}>🖨 Printers</button>
          <button className="shift-tool" onClick={() => setExpenseOpen(true)}>Expenses</button>
          <button className="shift-tool" onClick={() => setReceiptsOpen(true)}>Receipts</button>
          <button className="shift-tool shift-tool-close" onClick={() => setClosingShift(true)}>Close shift</button>
        </div>
      </section>

      {modProduct && (
        <ModifierSheet
          product={modProduct}
          onClose={() => setModProduct(null)}
          onAdd={(line) => { addToCart(line); setModProduct(null); }}
        />
      )}

      {checkout && (
        <CheckoutSheet
          total={Math.max(0, subtotal - depositCredit)}
          onClose={() => setCheckout(null)}
          onConfirm={placeFinal}
        />
      )}

      {closingShift && (
        <ShiftCloseSheet
          shift={currentShift}
          orders={orders}
          onClose={() => setClosingShift(false)}
          onConfirm={async (payload) => {
            await closeShift(payload);
            setClosingShift(false);
          }}
        />
      )}

      {receiptsOpen && (
        <ReceiptsListSheet
          onClose={() => setReceiptsOpen(false)}
          onRefund={(o) => { setReceiptsOpen(false); setRefundFor(o); }}
        />
      )}
      {refundFor && (
        <RefundSheet order={refundFor} onClose={() => setRefundFor(null)} />
      )}
      {openBillsOpen && (
        <OpenBillsSheet
          bills={openBills}
          editingOrderId={editingOrderId}
          onClose={() => setOpenBillsOpen(false)}
          onLoad={(o) => { loadBill(o); setOpenBillsOpen(false); }}
        />
      )}
      {printerCheckOpen && <PrinterCheckSheet onClose={() => setPrinterCheckOpen(false)} />}
      {expenseOpen && (
        <ExpenseSheet
          cashierUid={currentShift.cashierUid}
          cashierEmail={currentShift.cashierEmail}
          onClose={() => setExpenseOpen(false)}
        />
      )}
    </div>
  );
}


// "Printer check" — lists every physical printer the print server knows
// about (grouped by IP, since one printer can serve several job types —
// e.g. .171 = cashier + bar + shift) and prints a real test slip on each.
// /api/print-status alone only proves the print server is up; a test slip
// is the only thing that proves it can actually reach each printer.
const PRINT_TYPE_LABELS = { cashier: "Cashier", bar: "Bar", kitchen: "Kitchen", waitress: "Floor", shift: "Shift" };
// Which job type to send the test through for a given printer — never
// "shift" unless it's the only one (it renders a shift-summary layout).
const TEST_TYPE_ORDER = ["cashier", "kitchen", "waitress", "bar", "shift"];

function PrinterCheckSheet({ onClose }) {
  const [server, setServer] = React.useState({ state: "loading" });
  const [results, setResults] = React.useState({});

  const loadStatus = async () => {
    setServer({ state: "loading" });
    setResults({});
    if (window.PRINT_ENABLED === false) { setServer({ state: "error", error: "Printing is disabled on this device" }); return; }
    try {
      const r = await fetch("/api/print-status", { cache: "no-store" });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.ok) { setServer({ state: "error", error: j.error || ("HTTP " + r.status) }); return; }
      const byIp = {};
      Object.entries(j.printers || {}).forEach(([type, p]) => {
        if (!p || !p.ip) return;
        const key = p.ip + ":" + (p.port || 9100);
        (byIp[key] = byIp[key] || { ip: p.ip, port: p.port || 9100, types: [] }).types.push(type);
      });
      const printers = Object.values(byIp).map((g) => ({
        ...g,
        types: TEST_TYPE_ORDER.filter((t) => g.types.includes(t)),
        testType: TEST_TYPE_ORDER.find((t) => g.types.includes(t)),
      })).sort((a, b) => a.ip.localeCompare(b.ip, undefined, { numeric: true }));
      setServer({ state: "ok", printers });
    } catch (e) {
      setServer({ state: "error", error: e.message || "Print server unreachable" });
    }
  };
  React.useEffect(() => { loadStatus(); }, []);

  const testOne = async (p) => {
    setResults((r) => ({ ...r, [p.ip]: { state: "testing" } }));
    const res = await PRINTER.test(p.testType);
    setResults((r) => ({ ...r, [p.ip]: res.ok ? { state: "ok" } : { state: "fail", error: res.error } }));
  };
  // Different printers in parallel — the print server already serializes
  // jobs per printer IP, so this can't collide on a shared printer.
  const testAll = () => (server.printers || []).forEach(testOne);
  const busy = Object.values(results).some((r) => r.state === "testing");

  return (
    <div className="sheet-wrap" onMouseDown={onClose}>
      <div className="sheet" onMouseDown={(e) => e.stopPropagation()}>
        <header className="sheet-head">
          <div>
            <div className="eyebrow mono">PRINTER CHECK</div>
            <h3>Test all printers</h3>
            <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>Prints a small "PRINTER CHECK OK" slip on each printer.</div>
          </div>
          <button className="icon-x" onClick={onClose}>×</button>
        </header>
        <div className="sheet-body">
          {server.state === "loading" && <div className="muted mono" style={{ padding: "12px 0" }}>Contacting print server…</div>}
          {server.state === "error" && (
            <div className="printer-check-row fail">
              <div>
                <div style={{ fontWeight: 500 }}>Print server unreachable</div>
                <div className="mono printer-check-err">{server.error}</div>
                <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>Check the print server box (power, network cable) and the café internet.</div>
              </div>
              <button className="btn-ghost-sm" onClick={loadStatus}>Retry</button>
            </div>
          )}
          {server.state === "ok" && server.printers.length === 0 && (
            <div className="muted mono" style={{ padding: "12px 0" }}>No printers configured on the print server.</div>
          )}
          {server.state === "ok" && server.printers.map((p) => {
            const r = results[p.ip] || { state: "idle" };
            return (
              <div key={p.ip} className={"printer-check-row " + r.state}>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontWeight: 500 }}>{p.types.map((t) => PRINT_TYPE_LABELS[t] || t).join(" · ")}</div>
                  <div className="mono muted" style={{ fontSize: 11 }}>{p.ip}:{p.port}</div>
                  {r.state === "fail" && <div className="mono printer-check-err">{r.error}</div>}
                </div>
                <div className="row" style={{ gap: 8 }}>
                  <span className={"printer-check-status mono " + r.state}>
                    {r.state === "testing" ? "Printing…" : r.state === "ok" ? "✓ Printed" : r.state === "fail" ? "✕ Failed" : ""}
                  </span>
                  <button className="btn-ghost-sm" disabled={r.state === "testing"} onClick={() => testOne(p)}>Test</button>
                </div>
              </div>
            );
          })}
        </div>
        <footer className="sheet-foot">
          <button className="btn-ghost" onClick={onClose}>Close</button>
          <button className="btn-primary" disabled={server.state !== "ok" || !server.printers.length || busy} onClick={testAll}>
            <span>{busy ? "Printing…" : "Test all printers"}</span><span>🖨</span>
          </button>
        </footer>
      </div>
    </div>
  );
}

// Sheet to pick an open (unpaid) bill and load it into the cart.
function OpenBillsSheet({ bills, editingOrderId, onClose, onLoad }) {
  return (
    <div className="sheet-wrap" onMouseDown={onClose}>
      <div className="sheet sheet-wide" onMouseDown={(e) => e.stopPropagation()}>
        <header className="sheet-head">
          <div>
            <div className="eyebrow mono">OPEN BILLS</div>
            <h3>Load a saved bill</h3>
            <div className="muted mono" style={{ fontSize: 12 }}>
              {bills.length} unpaid · tap a row to load
            </div>
          </div>
          <button className="icon-x" onClick={onClose}>×</button>
        </header>
        <div className="sheet-body">
          {bills.length === 0 && (
            <div className="muted mono">No open bills yet. Use "Save" to park a ticket for later.</div>
          )}
          {bills.length > 0 && (
            <table className="data-table">
              <thead><tr><th>#</th><th>Customer</th><th>Table</th><th>Booking</th><th>Items</th><th>Saved</th><th>Total</th><th></th></tr></thead>
              <tbody>
                {bills.map((o) => (
                  <tr key={o.id} className={editingOrderId === o.id ? "row-selected" : ""}>
                    <td><Mono>{o.shortId}</Mono></td>
                    <td>{o.customer}</td>
                    <td><Mono>{o.table}</Mono></td>
                    <td>
                      {o.prebookingId ? (
                        <div style={{ fontSize: 12, lineHeight: 1.5 }}>
                          <Mono>{o.dateKey || "—"}{o.visitTime ? " · " + o.visitTime : ""}</Mono>
                          <div className="muted">
                            {o.room || "—"}{o.partySize ? ` · ${o.partySize}p` : ""}
                          </div>
                        </div>
                      ) : <span className="muted">—</span>}
                    </td>
                    <td><Mono>{o.items.reduce((a, i) => a + i.qty, 0)}</Mono></td>
                    <td><Mono>{o.createdAt?.toDate ? o.createdAt.toDate().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) : "—"}</Mono></td>
                    <td><Mono>{money(o.total)}</Mono></td>
                    <td><button className="btn-primary btn-sm" onClick={() => onLoad(o)}>Load</button></td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </div>
  );
}

// Modifier popover
function ModifierSheet({ product, onClose, onAdd }) {
  const groups = (product.modifiers && product.modifiers.length)
    ? product.modifiers
    : (window.MODIFIERS[product.category] || []);
  const [picks, setPicks] = React.useState(() => {
    const init = {};
    groups.forEach((g) => { init[g.group] = g.type === "single" ? [g.options[0].id] : []; });
    return init;
  });
  const [qty, setQty] = React.useState(1);

  const toggle = (group, optId, type) => {
    setPicks((p) => {
      if (type === "single") return { ...p, [group]: [optId] };
      const cur = p[group] || [];
      return { ...p, [group]: cur.includes(optId) ? cur.filter((x) => x !== optId) : [...cur, optId] };
    });
  };

  let modPrice = 0;
  const labels = [];
  groups.forEach((g) => {
    (picks[g.group] || []).forEach((id) => {
      const opt = g.options.find((o) => o.id === id);
      if (!opt) return;
      modPrice += opt.delta;
      if (opt.delta !== 0 || g.options.length > 1) labels.push(opt.label);
    });
  });

  return (
    <div className="sheet-wrap" onMouseDown={onClose}>
      <div className="sheet" onMouseDown={(e) => e.stopPropagation()}>
        <header className="sheet-head">
          <div className="sheet-product">
            <ProductSwatch color={product.swatch} label={product.name} size={56} />
            <div>
              <div className="eyebrow mono">{product.category}</div>
              <h3>{product.name}</h3>
              <div className="muted">{product.desc}</div>
            </div>
          </div>
          <button className="icon-x" onClick={onClose}>×</button>
        </header>

        <div className="sheet-body">
          {groups.map((g) => (
            <div className="mod-group" key={g.group}>
              <div className="mod-group-head">
                <span className="mono eyebrow">{g.group}</span>
                <span className="muted mono">{g.type === "single" ? "Pick one" : "Multiple"}</span>
              </div>
              <div className="mod-options">
                {g.options.map((o) => {
                  const picked = (picks[g.group] || []).includes(o.id);
                  return (
                    <button
                      key={o.id}
                      className={"mod-opt" + (picked ? " picked" : "")}
                      onClick={() => toggle(g.group, o.id, g.type)}
                    >
                      <span>{o.label}</span>
                      <Mono>{o.delta > 0 ? "+" + money(o.delta) : o.delta < 0 ? money(o.delta) : "·"}</Mono>
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>

        <footer className="sheet-foot">
          <div className="qty-stepper">
            <button onClick={() => setQty((q) => Math.max(1, q - 1))}>–</button>
            <Mono>{qty}</Mono>
            <button onClick={() => setQty((q) => q + 1)}>+</button>
          </div>
          <button
            className="btn-primary"
            onClick={() => onAdd({ ...product, mods: labels, modPrice, qty, cogs: product.cogs || 0, notes: "" })}
          >
            Add — <Mono>{money((product.price + modPrice) * qty)}</Mono>
          </button>
        </footer>
      </div>
    </div>
  );
}

// Checkout: split pay + tip + Indonesian-style 2-digit unique code added
// only when QRIS is the payment method (for bank reconciliation).
function CheckoutSheet({ total, onClose, onConfirm }) {
  const { db, currentShift } = usePOS();
  // Random 0–30 rupiah unique code (added to QRIS totals for bank reconciliation).
  const [uniqueCode, setUniqueCode] = React.useState(() => Math.floor(Math.random() * 31));
  const rerollCode = () => setUniqueCode(Math.floor(Math.random() * 31));
  const [splits, setSplits] = React.useState([{ method: "cash", amount: total }]);

  const hasQris = splits.some((s) => s.method === "qris");
  const appliedCode = hasQris ? uniqueCode : 0;
  const due = total + appliedCode;

  React.useEffect(() => {
    setSplits((prev) => {
      if (prev.length === 1) return [{ ...prev[0], amount: due }];
      const head = prev.slice(0, -1);
      const sumHead = head.reduce((s, p) => s + p.amount, 0);
      return [...head, { ...prev[prev.length - 1], amount: Math.max(0, due - sumHead) }];
    });
  }, [total, appliedCode]);

  const paid = splits.reduce((s, p) => s + (Number(p.amount) || 0), 0);
  const balance = due - paid;

  // Push QRIS payment + unique code + final total to customer display.
  const qrisSplit = splits.find((s) => s.method === "qris");
  React.useEffect(() => {
    const payload = qrisSplit
      ? { method: "qris", amount: Number(qrisSplit.amount) || due, ready: true }
      : null;
    db.collection("displays").doc("main").set({
      payment: payload,
      uniqueCode: appliedCode,
      total: due,
    }, { merge: true });
    return () => {
      db.collection("displays").doc("main").set({ payment: null, uniqueCode: 0 }, { merge: true });
    };
  }, [qrisSplit?.amount, !!qrisSplit, due, appliedCode]);

  // ── QRIS auto-confirm ────────────────────────────────────────────────
  // While a QRIS split is active, keep a `pending_payments` doc alive with
  // the exact amount due (incl. unique code). The Android notification-
  // listener app (android-qris-listener/) reports the Livin Mandiri
  // payment-received push to POST /api/qris-notify (server.js), which
  // flips this doc's status to 'matched' when it finds exactly one waiting
  // doc for that amount. We auto-fire the same onConfirm the manual button
  // uses — no order-write logic is duplicated here.
  const [pendingId, setPendingId] = React.useState(null);
  const [autoMatched, setAutoMatched] = React.useState(false);
  const latestRef = React.useRef({ splits, appliedCode, onConfirm });
  latestRef.current = { splits, appliedCode, onConfirm };

  React.useEffect(() => {
    if (!hasQris) return;
    let live = true;
    let createdId = null;
    db.collection("pending_payments").add({
      amount: due,
      status: "waiting",
      prebookingId: null,
      cashierUid: currentShift?.cashierUid || null,
      cashierEmail: currentShift?.cashierEmail || null,
      uniqueCode: appliedCode,
      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
    }).then((ref) => {
      if (!live) { ref.delete().catch(() => {}); return; }
      createdId = ref.id;
      setPendingId(ref.id);
    });
    return () => {
      live = false;
      setPendingId(null);
      if (createdId) {
        const docRef = db.collection("pending_payments").doc(createdId);
        docRef.get().then((snap) => {
          if (snap.exists && snap.data().status === "waiting") docRef.delete();
        }).catch(() => {});
      }
    };
  }, [hasQris]);

  // Keep the pending doc's amount in sync as the cashier edits split
  // amounts or rerolls the unique code.
  React.useEffect(() => {
    if (!pendingId) return;
    db.collection("pending_payments").doc(pendingId)
      .update({ amount: due, uniqueCode: appliedCode }).catch(() => {});
  }, [pendingId, due, appliedCode]);

  // Auto-confirm the instant the backend reports a matching notification.
  const confirmingRef = React.useRef(false);
  React.useEffect(() => {
    if (!pendingId) return;
    confirmingRef.current = false;
    return db.collection("pending_payments").doc(pendingId).onSnapshot((snap) => {
      const data = snap.data();
      if (data?.status === "matched" && !confirmingRef.current) {
        confirmingRef.current = true;
        setAutoMatched(true);
        db.collection("pending_payments").doc(pendingId).update({
          status: "confirmed",
          confirmedAt: firebase.firestore.FieldValue.serverTimestamp(),
        }).catch(() => {});
        const { splits: s, appliedCode: c, onConfirm: fn } = latestRef.current;
        fn({ tip: 0, splits: s, uniqueCode: c });
      }
    });
  }, [pendingId]);

  const setSplitAmount = (i, v) => {
    setSplits((prev) => {
      const next = [...prev];
      next[i] = { ...next[i], amount: Math.max(0, Math.floor(Number(v) || 0)) };
      return next;
    });
  };
  const setSplitMethod = (i, m) => setSplits((prev) => prev.map((p, idx) => idx === i ? { ...p, method: m } : p));

  return (
    <div className="sheet-wrap" onMouseDown={onClose}>
      <div className="sheet sheet-wide" onMouseDown={(e) => e.stopPropagation()}>
        <header className="sheet-head">
          <div>
            <div className="eyebrow mono">CHECKOUT</div>
            <h3>Take payment</h3>
          </div>
          <button className="icon-x" onClick={onClose}>×</button>
        </header>

        <div className="checkout-body">
          <div className="checkout-summary">
            <div className="big-due">
              <span className="eyebrow mono">DUE</span>
              <Mono className="big-num">{money(due)}</Mono>
            </div>
            <div className="due-rows">
              <div className="due-row"><span>Subtotal</span><Mono>{money(total)}</Mono></div>
              {hasQris && (
                <div className="due-row">
                  <span>Unique code (QRIS) <button type="button" className="btn-link" style={{ marginLeft: 6 }} onClick={rerollCode}>↻</button></span>
                  <Mono>+{money(uniqueCode)}</Mono>
                </div>
              )}
            </div>
            {hasQris && (
              <div className={"qris-auto-status mono" + (autoMatched ? " matched" : "")}>
                {autoMatched
                  ? "✅ Payment detected — confirming…"
                  : "⏳ Waiting for QRIS notification… (or confirm manually below)"}
              </div>
            )}
          </div>

          <div className="checkout-splits">
            <div className="split-head">
              <span className="eyebrow mono">PAYMENT {splits.length > 1 ? "(SPLIT)" : ""}</span>
              <button
                className="btn-link"
                onClick={() => setSplits((p) => [...p, { method: "card", amount: 0 }])}
              >+ split</button>
            </div>
            {splits.map((s, i) => (
              <div className="split-row" key={i}>
                <div className="method-toggle">
                  {["cash", "qris", "card"].map((m) => (
                    <button
                      key={m}
                      className={"method" + (s.method === m ? " active" : "")}
                      onClick={() => setSplitMethod(i, m)}
                    >{m.toUpperCase()}</button>
                  ))}
                </div>
                <input
                  className="amount-input mono"
                  inputMode="numeric"
                  value={s.amount}
                  onChange={(e) => setSplitAmount(i, e.target.value.replace(/\D/g, ""))}
                />
                {splits.length > 1 && (
                  <button className="row-x" onClick={() => setSplits((p) => p.filter((_, k) => k !== i))}>×</button>
                )}
              </div>
            ))}
            <div className={"balance " + (balance === 0 ? "ok" : balance > 0 ? "short" : "over")}>
              <span>Balance</span>
              <Mono>{balance >= 0 ? money(balance) : "+ " + money(-balance) + " change"}</Mono>
            </div>
          </div>
        </div>

        <footer className="sheet-foot">
          <button className="btn-ghost" onClick={onClose}>Cancel</button>
          <button
            className="btn-primary"
            disabled={balance > 0}
            onClick={() => onConfirm({ tip: 0, splits, uniqueCode: appliedCode })}
          >
            Confirm payment
          </button>
        </footer>
      </div>
    </div>
  );
}

function WifiBar() {
  const { db, showToast } = usePOS();
  const [wifi, setWifi] = React.useState({ ssid: "", password: "" });
  const [draft, setDraft] = React.useState("");
  const [ssidDraft, setSsidDraft] = React.useState("");
  const [editing, setEditing] = React.useState(false);

  React.useEffect(() => {
    return db.collection("displays").doc("wifi").onSnapshot((doc) => {
      if (doc.exists) {
        const d = doc.data();
        setWifi({ ssid: d.ssid || "", password: d.password || "" });
      }
    });
  }, []);

  const save = async () => {
    await db.collection("displays").doc("wifi").set(
      { ssid: ssidDraft.trim(), password: draft.trim(), updatedAt: firebase.firestore.FieldValue.serverTimestamp() },
      { merge: true }
    );
    setEditing(false);
    showToast("Wi-Fi updated");
  };

  return (
    <div className="wifi-bar mono">
      {!editing && <span className="muted">📶 Wi-Fi · {wifi.ssid || "—"}</span>}
      {editing ? (
        <React.Fragment>
          <input
            value={ssidDraft}
            onChange={(e) => setSsidDraft(e.target.value)}
            placeholder="network name"
            style={{ flex: 1, padding: "4px 8px", fontSize: 13 }}
          />
          <input
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            placeholder="today's password"
            style={{ flex: 1, padding: "4px 8px", fontSize: 13 }}
          />
          <button className="btn-link" onClick={save}>Save</button>
          <button className="btn-link" onClick={() => setEditing(false)}>Cancel</button>
        </React.Fragment>
      ) : (
        <React.Fragment>
          <span>Password: <strong>{wifi.password || "—"}</strong></span>
          <button className="btn-link" onClick={() => { setDraft(wifi.password || ""); setSsidDraft(wifi.ssid || ""); setEditing(true); }}>
            {wifi.password || wifi.ssid ? "Change" : "Set"}
          </button>
        </React.Fragment>
      )}
    </div>
  );
}

// Live KPIs for the cashier: how many paid transactions today, cash total,
// QRIS total. Pulled from the orders subscription so it auto-updates as new
// payments come in or refunds are processed.
// Stats strip is narrow (4 columns in the cart panel) — drop the "Rp "
// prefix so full figures like 1.204.000 fit without truncating.
const statMoney = (n) => money(n).replace(/^(-?)Rp\s?/, "$1");

function ShiftStats({ orders, cashierUid, openingCash }) {
  const { db } = usePOS();
  const [refundsByMethod, setRefundsByMethod] = React.useState({});

  // Today's refunds for this cashier, grouped by method. Single-field query
  // + client-side dateKey filter to avoid a composite index; live via
  // onSnapshot so the totals net down the moment a refund is issued.
  React.useEffect(() => {
    if (!cashierUid) { setRefundsByMethod({}); return; }
    const today = dayKey();
    return db.collection("refunds")
      .where("cashierUid", "==", cashierUid)
      .onSnapshot((s) => {
        const byMethod = {};
        s.docs.map((d) => d.data())
          .filter((r) => r.dateKey === today)
          .forEach((r) => {
            const m = (r.method || "cash").toLowerCase();
            byMethod[m] = (byMethod[m] || 0) + (r.total || 0);
          });
        setRefundsByMethod(byMethod);
      }, (e) => console.warn("Cashier stats refunds query failed:", e));
  }, [cashierUid]);

  const stats = React.useMemo(() => {
    const paid = orders.filter((o) => o.paid && (!cashierUid || o.cashierUid === cashierUid));
    let cash = 0, qris = 0, card = 0;
    paid.forEach((o) => {
      const splits = (o.payments && o.payments.length) ? o.payments
        : [{ method: o.paymentMethod, amount: o.total }];
      splits.forEach((p) => {
        const m = (p.method || "").toLowerCase();
        const amt = Number(p.amount) || 0;
        if (m === "cash") cash += amt;
        else if (m === "qris") qris += amt;
        else if (m === "card") card += amt;
      });
    });
    cash -= refundsByMethod.cash || 0;
    qris -= refundsByMethod.qris || 0;
    card -= refundsByMethod.card || 0;
    return { count: paid.length, cash, qris, card };
  }, [orders, cashierUid, refundsByMethod]);

  return (
    <div className="shift-stats">
      <div className="shift-stat">
        <span className="stat-label mono">TRX</span>
        <Mono className="stat-value">{stats.count}</Mono>
      </div>
      <div className="shift-stat">
        <span className="stat-label mono">CASH</span>
        <Mono className="stat-value">{statMoney(stats.cash)}</Mono>
      </div>
      <div className="shift-stat">
        <span className="stat-label mono">QRIS</span>
        <Mono className="stat-value">{statMoney(stats.qris)}</Mono>
      </div>
      {stats.card > 0 && (
        <div className="shift-stat">
          <span className="stat-label mono">CARD</span>
          <Mono className="stat-value">{statMoney(stats.card)}</Mono>
        </div>
      )}
      {openingCash != null && (
        <div className="shift-stat">
          <span className="stat-label mono">FLOAT</span>
          <Mono className="stat-value">{statMoney(openingCash)}</Mono>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { CashierScreen, WifiBar, OpenBillsSheet, ShiftStats });
