"use client";

/**
 * The till, rebuilt on the Medora design system (Stage 7, 24 Oct 2026) to the design's till layout
 * (`/design-system/layout-patterns`, "POS till"): POS bar, product search and tiles, customer, basket,
 * totals, and the tender panel with split payment (PRD 1.4) and real STK push. The API prices every
 * basket (`POST /sales/quote`, PRD 1.5 part 2) and records the tenders; the till never invents a total.
 *
 * Past sales, held sales, quotation, receipt and returns still use the old screens (`legacy.tsx`) until
 * they're restyled. What the design shows but the API can't do yet is named, never faked: Paybill/Till
 * M-Pesa confirmation (C2B, Phase 2), card-not-present charging (no gateway), batch choice at the till
 * (PS-03; the API sells earliest expiry first), and a delivery's pickup point and cost.
 */

import * as React from "react";
import { useRouter } from "next/navigation";
import { FileTextIcon, MinusIcon, PauseIcon, PencilIcon, PlusIcon, PrinterIcon, SearchIcon, Trash2Icon, XIcon } from "lucide-react";

import { PosBar, type PosShift, type TillShortcut } from "@/components/medora/pos-bar";
import { ProductTile } from "@/components/medora/product";
import { SchemeCard } from "@/components/medora/parties-ops";
import { DeliveryPanel, OpenShiftForm, TillCustomerPicker, type DeliveryDetails, type TillCustomer } from "@/components/medora/pos-ops";
import { TenderPanel, type TenderResult } from "@/components/medora/sale";
import { RecordDrawer } from "@/components/medora/drawer";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/toast";
import { BranchesApi, ClaimsApi, CustomerGroupsApi, CustomersApi, MpesaApi, PreordersApi, QuotationsApi, ProductsApi, SalesApi, SchemesApi, ShiftsApi, TaxCodesApi, type SaleQuote, type SalesListRow, type SchemeMembership } from "@/lib/api";
import { formatDate, formatKES, localToday } from "@/lib/format";
import { useSettings } from "@/lib/SettingsContext";
import { mapProduct, type ApiProduct, type Product } from "./till-products";
import { PrescriptionForm, type RxFromSale } from "../prescriptions/prescription-form";
import { HeldDialog, PastSalesDialog, ReturnDialog } from "./till-records";
import { useMenuPermissions } from "@/lib/permissions";
import { QuotationDialog, TillReceiptDialog, receiptData, rxFromSale, type StoredSale } from "./till-receipt";
import { Loading } from "@/components/medora/loading";

/* ------------------------------------ Types ------------------------------------ */

/**
 * One basket line. `discount` is per unit, as the API stores it. `manual`: the cashier set the price or discount by
 * hand, so the customer's group doesn't reprice it (plan D).
 */
type Line = { productId: number; qty: number; price: number; discount: number; manual?: boolean };
type Quote = SaleQuote;
const wire = (l: Line) => ({ productId: l.productId, quantity: l.qty, unitPrice: l.price, discount: l.discount, manual: l.manual || undefined });
type ApiCustomer = { id: number; name: string; phone?: string | null; groupId?: number | null; schemeId?: number | null; schemeMemberNo?: string | null };
const r2 = (n: number) => Math.round(n * 100) / 100;
/** Plan H (AC-13): the scheme's share of a member's bill, as the API's `schemeSplit` (no cover limit is no cap). */
const schemeShare = (total: number, m: SchemeMembership) => {
  if (m.status !== "Active" || !m.active) return 0;
  const remaining = m.limit == null ? Infinity : Math.max(0, r2(m.limit - m.used));
  return Math.min(r2(total * (1 - m.copayPct / 100)), remaining);
};

const noDelivery: DeliveryDetails = { on: false, zone: "", fee: "", destination: "", pickup: "", by: "Own rider", rider: "", riderPhone: "", company: "", cost: "" };
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));

const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(" ") : m || fallback;
};

/** A short tone, as the old till played on a scan and a completed sale (setting: notif_sound). */
function beep(freqs: number[]) {
  try {
    const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
    const ctx = new Ctx();
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.connect(gain);
    gain.connect(ctx.destination);
    freqs.forEach((f, i) => osc.frequency.setValueAtTime(f, ctx.currentTime + i * 0.08));
    gain.gain.setValueAtTime(0.15, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 0.35);
  } catch {
    /* no audio: silent */
  }
}

/* ------------------------------------- Page ------------------------------------- */

export default function TillPage() {
  const router = useRouter();
  const { s, isOn, settings } = useSettings();
  const { can, loaded: permsLoaded } = useMenuPermissions();
  const may = (page: string, action: string) => permsLoaded && can(page, action);
  const today = localToday();

  const [user, setUser] = React.useState<{ id?: number; name?: string; username?: string; role?: string; branchId?: number | null }>({});
  const [branchName, setBranchName] = React.useState("");
  const [shift, setShift] = React.useState<PosShift | null>(null);
  const [checkingShift, setCheckingShift] = React.useState(true);
  const [lastClose, setLastClose] = React.useState<{ cashLeft: number | null; mpesa: number | null; by: string; at: string } | null>(null);
  const [opening, setOpening] = React.useState(false);

  const [products, setProducts] = React.useState<Product[] | null>(null);
  const [customers, setCustomers] = React.useState<TillCustomer[]>([]);
  const [customer, setCustomer] = React.useState<TillCustomer | null>(null);
  const [q, setQ] = React.useState("");
  const [cat, setCat] = React.useState("All");
  const [lines, setLines] = React.useState<Line[]>([]);
  const [editing, setEditing] = React.useState<{ productId: number; price: string; discount: string } | null>(null);

  const [etims, setEtims] = React.useState(false);
  const [delivery, setDelivery] = React.useState<DeliveryDetails>(noDelivery);
  const [quote, setQuote] = React.useState<Quote | null>(null);
  const [quoting, setQuoting] = React.useState(false);
  const [paying, setPaying] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [credit, setCredit] = React.useState<{ creditLimit: number; balance: number } | null>(null);
  const mpesaCheckout = React.useRef<string | null>(null);
  // Plan F (IG-06): a payment made to the paybill or till, found by its M-Pesa code (C2B)
  const mpesaC2b = React.useRef<string | null>(null);

  const [done, setDone] = React.useState<{ sale: StoredSale; reprint: boolean } | null>(null);
  const [taxNames, setTaxNames] = React.useState<Record<string, string>>({});
  const [showPast, setShowPast] = React.useState(false);
  const [showHeld, setShowHeld] = React.useState(false);
  // PS-10: the bar's held badge counts the baskets still held (refreshed after a hold, a sale and the held list)
  const [heldCount, setHeldCount] = React.useState(0);
  const countHeld = React.useCallback(() => {
    SalesApi.held("held")
      .then((h: unknown[]) => setHeldCount(h.length))
      .catch(() => {});
  }, []);
  React.useEffect(countHeld, [countHeld]);
  /** AC-15 (plan H): the till's quotation is saved (QUO number) before it is printed. */
  const [tillQuotation, setTillQuotation] = React.useState<{ number: string; validUntil: string } | null>(null);
  const [savingQuotation, setSavingQuotation] = React.useState(false);
  /** The quotation this basket sells (`/pos?quote=`), marked Converted by the sale. */
  const [fromQuotation, setFromQuotation] = React.useState<{ id: number; number: string } | null>(null);
  /** AC-13 (plan H Q2): the customer's scheme, and whether this sale bills the scheme its share. */
  const [scheme, setScheme] = React.useState<SchemeMembership | null>(null);
  const [billScheme, setBillScheme] = React.useState(false);
  const [rxFor, setRxFor] = React.useState<RxFromSale | null>(null);
  const [returnFor, setReturnFor] = React.useState<SalesListRow | null>(null);
  /** PS-10: the held sale this basket was recalled from; completing marks it Sold, holding again replaces it. */
  const [fromHeldId, setFromHeldId] = React.useState<number | null>(null);
  /** PS-14 (plan E): the preorder this basket collects; its deposit is already on the customer's account. */
  const [preorder, setPreorder] = React.useState<{ id: number; number: string; deposit: number } | null>(null);
  /** PS-08: F5 changes the quantity of the last line. */
  const [qtyFor, setQtyFor] = React.useState<{ productId: number; qty: string } | null>(null);

  const byId = React.useMemo(() => new Map((products ?? []).map((p) => [p.id, p])), [products]);

  /* ── Loading ── */
  const loadProducts = React.useCallback(async () => {
    const data: ApiProduct[] = await ProductsApi.getAll();
    setProducts(data.map(mapProduct));
  }, []);
  const checkShift = React.useCallback(async () => {
    try {
      const sh = await ShiftsApi.getActive();
      setShift(sh?.id ? { status: "open", openedAt: new Date(sh.startTime), float: Number(sh.startBalance) || 0 } : null);
      if (!sh?.id) {
        // PS-19: what the last shift here left, to compare the opening count with
        const lc = await ShiftsApi.lastClose().catch(() => null);
        setLastClose(lc ? { cashLeft: lc.cashLeft, mpesa: lc.mpesa, by: lc.by, at: lc.at ? new Date(lc.at).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) : "" } : null);
      }
    } catch {
      setShift(null);
    } finally {
      setCheckingShift(false);
    }
  }, []);

  React.useEffect(() => {
    let u: typeof user = {};
    try {
      u = JSON.parse(localStorage.getItem("user") || "{}");
    } catch {
      /* the layout redirects */
    }
    setUser(u);
    BranchesApi.getAll()
      .then((bs: { id: number; name: string }[]) => setBranchName(bs.find((b) => b.id === u.branchId)?.name ?? (u.branchId ? "Your branch" : "All branches")))
      .catch(() => setBranchName(""));
    void checkShift();
    loadProducts().catch((e) => toast.add({ type: "error", title: "Couldn't load products", description: apiError(e, "Try again.") }));
    TaxCodesApi.list()
      .then((ts: { code: string; name: string }[]) => setTaxNames(Object.fromEntries(ts.map((t) => [t.code, t.name]))))
      .catch(() => setTaxNames({}));
    // PS-05 (plan E): what applies to each customer at the till, from their group and scheme
    Promise.all([CustomersApi.getAll() as Promise<ApiCustomer[]>, CustomerGroupsApi.list().catch(() => []), SchemesApi.list().catch(() => [])])
      .then(([cs, groups, schemes]) => {
        const group = new Map(groups.filter((g) => g.active).map((g) => [g.id, g]));
        const scheme = new Map(schemes.filter((x) => x.active).map((x) => [x.id, x]));
        setCustomers(
          cs.map((c) => {
            const g = c.groupId != null ? group.get(c.groupId) : undefined;
            const sc = c.schemeId != null ? scheme.get(c.schemeId) : undefined;
            return {
              id: c.id,
              name: c.name,
              phone: c.phone ?? "",
              memberNo: c.schemeMemberNo ?? undefined,
              priceGroup: g ? { name: g.priceGroup ?? `${g.name} (retail)`, discountPct: g.discountPct } : undefined,
              scheme: sc ? `${sc.name}${c.schemeMemberNo ? `, member ${c.schemeMemberNo}` : ""}` : undefined,
            };
          }),
        );
      })
      .catch(() => setCustomers([]));
  }, [checkShift, loadProducts]);

  // Credit headroom for the chosen customer (credit tender, CU-05)
  React.useEffect(() => {
    if (!customer || customer.id < 0) return setCredit(null);
    CustomersApi.getBalance(customer.id)
      .then((b: { creditLimit: number; balance: number }) => setCredit({ creditLimit: Number(b.creditLimit) || 0, balance: Number(b.balance) || 0 }))
      .catch(() => setCredit(null));
  }, [customer]);
  React.useEffect(() => {
    setBillScheme(false);
    if (!customer || customer.id < 0) return setScheme(null);
    ClaimsApi.member(customer.id)
      .then(setScheme)
      .catch(() => setScheme(null)); // 404: not a member
  }, [customer]);

  /* ── Basket ── */
  const add = React.useCallback(
    (p: Product) => {
      setLines((ls) => {
        const ex = ls.find((l) => l.productId === p.id);
        const nextQty = (ex?.qty ?? 0) + 1;
        if (!p.isService && p.manageStock !== false && nextQty > p.stock) {
          toast.add({ type: "error", title: p.stock <= 0 ? `${p.name} is out of stock` : `Only ${p.stock} of ${p.name} in stock` });
          return ls;
        }
        return ex ? ls.map((l) => (l.productId === p.id ? { ...l, qty: nextQty } : l)) : [...ls, { productId: p.id, qty: 1, price: p.price, discount: 0 }];
      });
    },
    [],
  );
  const setQty = (productId: number, delta: number) =>
    setLines((ls) =>
      ls
        .map((l) => {
          if (l.productId !== productId) return l;
          const p = byId.get(productId);
          const qty = l.qty + delta;
          if (delta > 0 && p && !p.isService && p.manageStock !== false && qty > p.stock) {
            toast.add({ type: "error", title: `Only ${p.stock} of ${p.name} in stock` });
            return l;
          }
          return { ...l, qty };
        })
        .filter((l) => l.qty > 0),
    );
  const clear = () => {
    setLines([]);
    setCustomer(null);
    setDelivery(noDelivery);
    setEtims(false);
    setQuote(null);
    setFromHeldId(null);
    setPreorder(null);
    setFromQuotation(null);
    setBillScheme(false);
  };

  // Barcode wedge scanner (settings: barcode_scanner_enabled, barcode_scanner_timeout), as the old till
  const productsRef = React.useRef(products);
  productsRef.current = products;
  React.useEffect(() => {
    if (!isOn("barcode_scanner_enabled")) return;
    let buffer = "";
    let last = 0;
    const gap = parseInt(s("barcode_scanner_timeout", "40"), 10) || 40;
    const onKey = (e: KeyboardEvent) => {
      const now = Date.now();
      const diff = now - last;
      last = now;
      if (e.key.length === 1) buffer = diff > gap ? e.key : buffer + e.key;
      else if (e.key === "Enter" && buffer.length >= 3) {
        const hit = (productsRef.current ?? []).find((p) => p.barcode && p.barcode.trim().toLowerCase() === buffer.trim().toLowerCase());
        if (hit) {
          e.preventDefault();
          e.stopPropagation();
          add(hit);
          if (isOn("notif_sound")) beep([523.25, 659.25]);
          toast.add({ title: `Scanned: ${hit.name}` });
        }
        buffer = "";
      }
    };
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  }, [isOn, s, add]);

  /* ── The API's totals (PRD 1.5 part 2) ── */
  const customerId = customer && customer.id > 0 ? customer.id : null;
  // Plan D: a different customer starts from retail; the quote then applies their group, if any
  const pricedFor = React.useRef<number | null>(null);
  React.useEffect(() => {
    if (pricedFor.current === customerId) return;
    pricedFor.current = customerId;
    setLines((ls) => (ls.some((l) => !l.manual) ? ls.map((l) => (l.manual ? l : { ...l, price: byId.get(l.productId)?.price ?? l.price, discount: 0 })) : ls));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [customerId]);
  const zones: { name: string; fee: number }[] = React.useMemo(() => {
    try {
      const z = JSON.parse(s("delivery_zones", "[]"));
      return Array.isArray(z) ? z.filter((x: { name?: string }) => x?.name).map((x: { name: string; fee: number }) => ({ name: x.name, fee: Number(x.fee) || 0 })) : [];
    } catch {
      return [];
    }
  }, [s]);
  const freeAbove = Number(s("delivery_free_above", "0")) || undefined;
  // Settings → Delivery "Default fee": what a delivery charges until a zone (or the cashier) sets another
  const defaultFee = Number(s("delivery_default_fee", "0")) || 0;
  const goods = lines.reduce((sum, l) => sum + l.qty * (l.price - l.discount), 0);
  const deliveryFee = delivery.on ? (freeAbove != null && goods >= freeAbove ? 0 : Number(delivery.fee) || 0) : 0;

  React.useEffect(() => {
    if (!lines.length) return setQuote(null);
    let live = true;
    setQuoting(true);
    const t = setTimeout(() => {
      SalesApi.quote({ items: lines.map(wire), deliveryFee, isEtims: etims, customerId: customerId ?? undefined, groupPricing: true, promotions: true })
        .then((qt) => {
          if (!live) return;
          setQuote(qt);
          // Plans D and E: take the group's prices or the promotions' discounts for the lines they priced; unchanged lines leave the basket as it is
          const priced = new Map(qt.items.map((i) => [i.productId, i]));
          if (lines.some((l) => !l.manual && priced.has(l.productId) && (priced.get(l.productId)!.unitPrice !== l.price || (priced.get(l.productId)!.discount ?? 0) !== l.discount)))
            setLines((ls) => ls.map((l) => (l.manual || !priced.has(l.productId) ? l : { ...l, price: priced.get(l.productId)!.unitPrice, discount: priced.get(l.productId)!.discount ?? 0 })));
        })
        .catch((e) => live && toast.add({ type: "error", title: "Couldn't price the basket", description: apiError(e, "Try again.") }))
        .finally(() => live && setQuoting(false));
    }, 250);
    return () => {
      live = false;
      clearTimeout(t);
    };
  }, [lines, deliveryFee, etims, customerId]);

  /* ── Payment ── */
  const mpesa = {
    // Paybill and Till payments are confirmed by C2B (plan F): offered once the branch's shortcode is registered for it
    stkOnly: !isOn("mpesa_c2b_registered") || !s("mpesa_shortcode", ""),
    paybill: s("mpesa_shortcode", ""),
    till: s("mpesa_till", "") || s("mpesa_shortcode", ""),
    onVerify: async ({ code, amount }: { code?: string; accountRef: string; amount: number }) => {
      try {
        const found = await MpesaApi.lookup(code, amount);
        mpesaC2b.current = found.code;
        mpesaCheckout.current = null;
      mpesaC2b.current = null;
        return found.code;
      } catch (e) {
        toast.add({ type: "error", title: "Payment not found", description: apiError(e, "Check the code and try again.") });
        return null;
      }
    },
    onSendPrompt: async (phone: string, amount: number) => {
      mpesaC2b.current = null;
      if (!isOn("mpesa_enabled")) {
        toast.add({ type: "error", title: "M-Pesa isn't turned on", description: "Turn it on under Settings → Payments at the till." });
        return { state: "failed" as const };
      }
      try {
        const res = await SalesApi.initiateMpesa(phone, Math.ceil(amount));
        const id: string | undefined = res?.CheckoutRequestID;
        if (!id) return { state: "failed" as const };
        mpesaCheckout.current = id;
        for (let i = 0; i < 20; i++) {
          await wait(3000);
          const st = await SalesApi.checkMpesaStatus(id).catch(() => null);
          if (st?.status === "SUCCESS") return { state: "confirmed" as const, receiptNo: st.receiptNumber ?? undefined };
          if (st?.status === "FAILED") return { state: "failed" as const };
        }
        return { state: "timeout" as const };
      } catch (e) {
        toast.add({ type: "error", title: "M-Pesa request failed", description: apiError(e, "Check the M-Pesa keys under Settings.") });
        return { state: "failed" as const };
      }
    },
    onCheckStatus: async () => {
      const id = mpesaCheckout.current;
      if (!id) return { state: "unpaid" as const };
      const st = await SalesApi.checkMpesaStatus(id).catch(() => null);
      return st?.status === "SUCCESS" ? { state: "confirmed" as const, receiptNo: st.receiptNumber ?? undefined } : { state: "unpaid" as const };
    },
  };
  // There's no card gateway: card payments are taken on the bank's terminal and its approval code entered
  const card = { onCharge: async () => ({ approved: false, reason: "card-not-present payments aren't available. Take it on the terminal and enter the approval code" }) };

  const complete = async (r: TenderResult) => {
    if (!quote) return;
    setSaving(true);
    try {
      const sale = await SalesApi.create({
        customerName: customer?.name || undefined,
        customerId: customer && customer.id > 0 ? customer.id : undefined,
        cashierId: user.id,
        status: "Completed",
        items: lines.map(wire),
        groupPricing: true, // plan D: the API prices a group member's lines, as the quote did
        promotions: true, // plan E: and applies running promotions, as the quote did
        isEtims: etims,
        total: quote.total, // checked by the API (409 if it disagrees)
        payments: [...(schemePart > 0 ? [{ method: "Scheme" as const, amount: schemePart }] : []), ...r.payments.map((p) => ({ method: p.method, amount: p.amount, reference: p.reference || undefined, tendered: p.tendered, mpesaCheckoutId: p.method === "M-Pesa" && !mpesaC2b.current ? mpesaCheckout.current ?? undefined : undefined, c2bCode: p.method === "M-Pesa" ? mpesaC2b.current ?? undefined : undefined }))],
        quotationId: fromQuotation?.id,
        // PS-14: what the preorder's deposit covers stays owing, settled by the deposit already on account
        isCredit: r.credit > 0 || depositUsed > 0,
        fromHeldId: fromHeldId ?? undefined,
        fromPreorderId: preorder?.id,
        ...(delivery.on
          ? {
              isDelivery: true,
              deliveryFee,
              deliveryAddress: delivery.destination || undefined,
              deliveryLocality: delivery.zone || undefined,
              // PS-13 (plan E): who carries it, from where, and what it costs the pharmacy
              deliveryBy: delivery.by,
              riderName: delivery.by === "Own rider" ? delivery.rider || undefined : undefined,
              riderPhone: delivery.by === "Own rider" ? delivery.riderPhone || undefined : undefined,
              courierCompany: delivery.by === "Courier" ? delivery.company || undefined : undefined,
              deliveryPickup: delivery.pickup || undefined,
              deliveryCost: delivery.cost.trim() && Number(delivery.cost) >= 0 ? Number(delivery.cost) : undefined,
            }
          : {}),
      });
      setDone({ sale: sale as StoredSale, reprint: false });
      countHeld();
      mpesaCheckout.current = null;
      setPaying(false);
      if (isOn("notif_sound")) beep([880]);
      void loadProducts();
    } catch (e) {
      const res = (e as { response?: { status?: number; data?: { totals?: Quote } } })?.response;
      if (res?.status === 409) {
        toast.add({ type: "error", title: "The total changed", description: apiError(e, "Check the basket and take payment again.") });
        setQuote((qt) => (qt && res.data?.totals ? { ...qt, ...res.data.totals } : qt));
      } else toast.add({ type: "error", title: "Sale not recorded", description: apiError(e, "Nothing was charged by the till. Try again.") });
    } finally {
      setSaving(false);
    }
  };

  const hold = async () => {
    if (!lines.length) return;
    try {
      await SalesApi.create({
        customerName: customer?.name || undefined,
        customerId: customer && customer.id > 0 ? customer.id : undefined,
        cashierId: user.id,
        status: "Held",
        paymentMethod: "Cash",
        items: lines.map(wire),
        groupPricing: true, // plan D: the API prices a group member's lines, as the quote did
        promotions: true,
        fromHeldId: fromHeldId ?? undefined,
      });
      toast.add({ title: "Sale on hold", description: "Find it under Held sales." });
      clear();
      countHeld();
    } catch (e) {
      toast.add({ type: "error", title: "Couldn't hold the sale", description: apiError(e, "Try again.") });
    }
  };

  /** Recall a held basket (PS-10). It stays held until this basket is sold or held again, so nothing is lost if it's abandoned. */
  const recall = async (id: number) => {
    if (lines.length) return toast.add({ type: "error", title: "Finish or hold this basket first", description: "Recalling would replace what's in the basket now." });
    try {
      const sale: { id: number; receiptId: string; customerId?: number | null; customerName?: string | null; discountSource?: string | null; items: { productId: number; quantity: number; unitPrice: number | string; discount?: number | string | null }[] } = await SalesApi.getOne(id);
      // A line held at other than retail keeps its price (it may have been set by hand). Held while a group or
      // promotions priced it (plan E), it's repriced instead.
      const repriced = sale.discountSource === "Group" || sale.discountSource === "Promotion";
      setLines(sale.items.map((i) => ({ productId: i.productId, qty: i.quantity, price: Number(i.unitPrice), discount: Number(i.discount ?? 0), manual: !repriced && (Number(i.unitPrice) !== byId.get(i.productId)?.price || Number(i.discount ?? 0) > 0) })));
      setCustomer(sale.customerName ? customers.find((c) => c.id === sale.customerId) ?? { id: -1, name: sale.customerName, phone: "" } : null);
      setFromHeldId(sale.id);
      setShowHeld(false);
      toast.add({ title: `Held sale ${sale.receiptId} recalled` });
    } catch (e) {
      toast.add({ type: "error", title: "Couldn't recall it", description: apiError(e, "Try again.") });
    }
  };

  const onShortcut = (a: TillShortcut) => {
    if (a === "pay" && lines.length && quote) setPaying(true);
    else if (a === "hold") void hold();
    else if (a === "search") document.getElementById("till-search")?.focus();
    else if (a === "clear") clear();
    // PS-08 (plan E)
    else if (a === "customer") {
      const box = document.querySelector<HTMLElement>('[aria-label="Find a customer"]');
      if (box) box.focus();
      else toast.add({ title: `${customer?.name ?? "A customer"} is chosen`, description: "Use Change to choose another." });
    }
    else if (a === "quantity") {
      const last = lines[lines.length - 1];
      if (last) setQtyFor({ productId: last.productId, qty: String(last.qty) });
    } else if (a === "drawer") toast.add({ title: "No cash drawer is connected", description: "Open the drawer by hand. A drawer that opens from the till comes with the printer setup." });
    else if (a === "complete" && paying) {
      // TenderPanel's submit reads "Complete sale" only once a split, M-Pesa or card payment is done; for cash it stays
      // "Pay KES …" and is enabled once the cash covers the total (card 023). Disabled means it doesn't cover it yet.
      const isSubmit = (t: string) => t === "Complete sale" || /^Pay\b/.test(t);
      const btn = Array.from(document.querySelectorAll<HTMLButtonElement>('[role="dialog"] button')).find((b) => isSubmit(b.textContent?.trim() ?? "") && !b.disabled);
      if (btn) btn.click();
      else toast.add({ title: "Payment doesn't cover the total yet" });
    }
  };

  /* ── AC-15 (plan H): the till's quotation is saved, then printed; a saved one opens at ?quote= ── */
  const saveQuotation = async () => {
    setSavingQuotation(true);
    try {
      const q = await QuotationsApi.create({ customerId: customer && customer.id > 0 ? customer.id : undefined, customerName: customer?.name || undefined, isEtims: etims, items: lines.map((l) => ({ productId: l.productId, quantity: l.qty, unitPrice: l.price, discount: l.discount })), source: "Till" });
      setTillQuotation({ number: q.number, validUntil: q.validUntil });
    } catch (e) {
      toast.add({ type: "error", title: "Quotation not saved", description: apiError(e, "Try again.") });
    } finally {
      setSavingQuotation(false);
    }
  };
  const quoteLoaded = React.useRef(false);
  React.useEffect(() => {
    if (quoteLoaded.current || !products || !shift) return;
    const id = Number(new URLSearchParams(window.location.search).get("quote"));
    if (!id) return;
    quoteLoaded.current = true;
    window.history.replaceState(null, "", "/pos");
    QuotationsApi.get(id)
      .then((q) => {
        if (q.status !== "Open") return toast.add({ type: "error", title: `${q.number} is ${q.status.toLowerCase()}`, description: q.convertedTo ? `Already sold as ${q.convertedTo}.` : undefined });
        const missing = q.lines.filter((l) => !byId.get(l.productId));
        // The quoted prices are honoured: marked as set by hand so group prices and promotions don't change them
        setLines(q.lines.filter((l) => byId.get(l.productId)).map((l) => ({ productId: l.productId, qty: l.qty, price: l.price, discount: l.discount, manual: true })));
        if (q.customerId) setCustomer(customers.find((c) => c.id === q.customerId) ?? { id: q.customerId, name: q.customer, phone: "" });
        setEtims(q.isEtims);
        setFromQuotation({ id: q.id, number: q.number });
        toast.add({ title: `Selling quotation ${q.number}`, description: missing.length ? `Not on the till any more: ${missing.map((l) => l.name).join(", ")}.` : "At the quoted prices." });
      })
      .catch((e) => toast.add({ type: "error", title: "Couldn't open the quotation", description: apiError(e, "Try again from Accounts → Receivables.") }));
  }, [products, shift, byId, customers]);

  /* ── PS-14: collecting a preorder (POS → Preorders opens the till at ?preorder=) ── */
  const preorderLoaded = React.useRef(false);
  React.useEffect(() => {
    if (preorderLoaded.current || !products || !shift) return;
    const id = Number(new URLSearchParams(window.location.search).get("preorder"));
    if (!id) return;
    preorderLoaded.current = true;
    window.history.replaceState(null, "", "/pos");
    PreordersApi.forTill(id)
      .then((po) => {
        const p = byId.get(po.productId);
        if (!p) return toast.add({ type: "error", title: `${po.product} isn't on the till`, description: "It may have been deleted." });
        if (!p.isService && p.manageStock !== false && p.stock < po.quantity) toast.add({ type: "error", title: `Only ${p.stock} of ${p.name} in stock`, description: `Preorder ${po.number} needs ${po.quantity}.` });
        setLines([{ productId: p.id, qty: po.quantity, price: p.price, discount: 0 }]);
        setCustomer(customers.find((c) => c.id === po.customerId) ?? { id: po.customerId, name: po.customer, phone: po.phone });
        setPreorder({ id: po.id, number: po.number, deposit: po.deposit });
        toast.add({ title: `Collecting preorder ${po.number}`, description: po.deposit ? `${formatKES(po.deposit, { decimals: true })} deposit already paid.` : undefined });
      })
      .catch((e) => toast.add({ type: "error", title: "Couldn't open the preorder", description: apiError(e, "Try again from POS → Preorders.") }));
  }, [products, shift, byId, customers]);

  /* ── Render ── */
  if (checkingShift) {
    return (
      <div className="medora flex h-full min-h-80 items-center justify-center p-6">
        <Loading what="the till" shape="cards" className="w-full max-w-5xl" />
      </div>
    );
  }

  if (!shift) {
    return (
      <div className="medora flex h-full min-h-96 items-center justify-center p-4">
        <section className="w-full max-w-xl space-y-4 rounded-lg border border-border bg-card p-6">
          <h1 className="text-lg font-semibold">Open a shift to sell</h1>
          <OpenShiftForm
            busy={opening}
            lastClose={lastClose}
            onOpen={async (o) => {
              setOpening(true);
              try {
                await ShiftsApi.open(o.cash, { startMpesa: o.mpesa, note: o.note });
                await checkShift();
                toast.add({ title: "Shift opened" });
              } catch (e) {
                toast.add({ type: "error", title: "Shift not opened", description: apiError(e, "Try again.") });
              } finally {
                setOpening(false);
              }
            }}
          />
        </section>
      </div>
    );
  }

  const list = (products ?? []).filter((p) => (cat === "All" || p.category === cat) && (!q.trim() || `${p.name} ${p.barcode ?? ""}`.toLowerCase().includes(q.trim().toLowerCase())));
  const cats = ["All", ...Array.from(new Set((products ?? []).map((p) => p.category))).sort()];
  const subtotal = quote?.subtotal ?? goods + deliveryFee;
  const vat = quote?.tax ?? 0;
  const total = quote?.total ?? subtotal;
  // PS-14: the deposit (on account) covers part of the total; the rest is paid now
  const depositUsed = preorder && quote ? Math.min(preorder.deposit, quote.total) : 0;
  // AC-13: a scheme member's share goes to the scheme as a claim; the customer pays the rest (not with a preorder)
  const schemePart = billScheme && scheme && !preorder && quote ? schemeShare(quote.total, scheme) : 0;
  const dueNow = r2(total - depositUsed - schemePart);

  return (
    <div className="medora -m-4 flex h-[calc(100svh-3.5rem)] flex-col md:-m-6">
      <PosBar
        till="Till"
        branch={branchName}
        cashier={{ name: user.name || user.username || "You", role: user.role || "" }}
        shift={shift}
        onCloseShift={() => router.push("/shift")}
        held={heldCount}
        onHeld={() => setShowHeld(true)}
        onShortcut={onShortcut}
      />
      <div className="flex flex-wrap items-center gap-2 border-b border-border bg-background px-3 py-2">
        <Button size="sm" variant="outline" onClick={() => setShowPast(true)}>
          <FileTextIcon data-icon="inline-start" />
          Past sales
        </Button>
        <Button size="sm" variant="outline" onClick={() => setShowHeld(true)}>
          Held sales
        </Button>
        <Button size="sm" variant="outline" disabled={!lines.length} onClick={() => void hold()}>
          <PauseIcon data-icon="inline-start" />
          Hold
        </Button>
        <Button size="sm" variant="outline" disabled={!lines.length || savingQuotation} onClick={() => void saveQuotation()}>
          <PrinterIcon data-icon="inline-start" />
          Quotation
        </Button>
        <Button size="sm" variant="ghost" className="ml-auto" disabled={!lines.length} onClick={clear}>
          <XIcon data-icon="inline-start" />
          Clear sale
        </Button>
      </div>

      <div className="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[1fr_380px]">
        <div className="flex min-w-0 flex-col border-r border-border bg-muted/40">
          <div className="space-y-2 border-b border-border bg-background p-3">
            <InputGroup>
              <InputGroupAddon>
                <SearchIcon />
              </InputGroupAddon>
              <InputGroupInput id="till-search" placeholder="Search name or scan barcode" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search products" />
            </InputGroup>
            <Tabs value={cat} onValueChange={(v) => setCat(String(v))}>
              <TabsList variant="line" className="w-full justify-start overflow-x-auto">
                {cats.map((c) => (
                  <TabsTrigger key={c} value={c} className="flex-none">
                    {c}
                  </TabsTrigger>
                ))}
              </TabsList>
            </Tabs>
          </div>
          <div className="grid flex-1 grid-cols-2 content-start gap-2 overflow-auto p-3 sm:grid-cols-3 xl:grid-cols-4">
            {products === null
              ? Array.from({ length: 8 }, (_, i) => <ProductTile key={i} loading product={{ name: "", price: 0, stock: 0 } as never} />)
              : list.map((p) => (
                  // The tile is itself a button with no handler of its own; its click bubbles to this wrapper
                  <div key={p.id} onClick={() => add(p)}>
                    <ProductTile
                      product={{ id: p.id, name: p.name, strength: p.code || "", form: p.unit || "each", price: p.price, stock: p.stock, minStock: 0, requiresRx: !!p.rx, type: p.isService ? "Service" : "Product", category: p.category } as never}
                      expiryDate={p.expiryDate ?? undefined}
                      today={today}
                      selected={lines.some((l) => l.productId === p.id)}
                    />
                  </div>
                ))}
            {products && !list.length ? <p className="col-span-full py-12 text-center text-sm text-muted-foreground">No products match</p> : null}
          </div>
        </div>

        <div className="flex min-h-0 flex-col bg-background">
          <div className="border-b border-border p-3">
            <TillCustomerPicker
              customers={customers}
              selected={customer && credit && credit.creditLimit > 0 ? { ...customer, creditHeadroom: Math.max(0, credit.creditLimit - credit.balance) } : customer}
              onSelect={setCustomer}
              onAdd={({ name, phone }) => {
                // The picker wants the customer back at once: show it now, save it, then swap in the saved one
                const temp: TillCustomer = { id: -Date.now(), name, phone };
                CustomersApi.create({ name, phone })
                  .then((c: ApiCustomer) => {
                    const saved = { id: c.id, name: c.name, phone: c.phone ?? phone };
                    setCustomers((cs) => [...cs, saved]);
                    setCustomer((cur) => (cur?.id === temp.id ? saved : cur));
                  })
                  .catch((e) => {
                    toast.add({ type: "error", title: "Customer not saved", description: apiError(e, "The sale can go ahead as a walk-in.") });
                    setCustomer((cur) => (cur?.id === temp.id ? null : cur));
                  });
                return temp;
              }}
            />
          </div>
          <div className="flex-1 overflow-auto">
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Item</TableHead>
                  <TableHead className="text-center">Qty</TableHead>
                  <TableHead className="text-right">Total</TableHead>
                  <TableHead className="w-16" />
                </TableRow>
              </TableHeader>
              <TableBody>
                {lines.map((l) => {
                  const p = byId.get(l.productId);
                  const taxCode = quote?.lines.find((x) => x.productId === l.productId)?.taxCode;
                  return (
                    <TableRow key={l.productId}>
                      <TableCell className="whitespace-normal">
                        <div className="font-medium">{p?.name ?? `#${l.productId}`}</div>
                        <div className="text-xs text-muted-foreground tabular-nums">
                          {formatKES(l.price, { decimals: true })}
                          {l.discount ? ` less ${formatKES(l.discount, { decimals: true })}` : ""}
                          {p && l.price !== p.price ? " (price changed)" : ""}
                          {etims && taxCode ? ` · ${taxCode}` : ""}
                        </div>
                      </TableCell>
                      <TableCell>
                        <div className="mx-auto flex w-fit items-center gap-1">
                          <Button variant="outline" size="icon-xs" aria-label={`One less ${p?.name ?? ""}`} onClick={() => setQty(l.productId, -1)}>
                            <MinusIcon />
                          </Button>
                          <span className="w-6 text-center text-sm tabular-nums">{l.qty}</span>
                          <Button variant="outline" size="icon-xs" aria-label={`One more ${p?.name ?? ""}`} onClick={() => setQty(l.productId, 1)}>
                            <PlusIcon />
                          </Button>
                        </div>
                      </TableCell>
                      <TableCell className="text-right tabular-nums">{formatKES(l.qty * (l.price - l.discount), { decimals: true })}</TableCell>
                      <TableCell className="whitespace-nowrap">
                        <Button variant="ghost" size="icon-xs" aria-label={`Change price or discount of ${p?.name ?? ""}`} onClick={() => setEditing({ productId: l.productId, price: String(l.price), discount: l.discount ? String(l.discount) : "" })}>
                          <PencilIcon />
                        </Button>
                        <Button variant="ghost" size="icon-xs" aria-label={`Remove ${p?.name ?? ""}`} onClick={() => setQty(l.productId, -l.qty)}>
                          <Trash2Icon />
                        </Button>
                      </TableCell>
                    </TableRow>
                  );
                })}
                {!lines.length ? (
                  <TableRow>
                    <TableCell colSpan={4} className="py-8 text-center text-muted-foreground">
                      Cart is empty
                    </TableCell>
                  </TableRow>
                ) : null}
              </TableBody>
            </Table>
          </div>
          <div className="space-y-3 border-t border-border p-3">
            {preorder ? (
              <p className="rounded-md border border-border bg-muted/40 px-2 py-1.5 text-xs">
                Collecting preorder <span className="font-mono">{preorder.number}</span>
                {preorder.deposit ? `: ${formatKES(preorder.deposit, { decimals: true })} deposit already paid, on the customer's account.` : "."}
              </p>
            ) : null}
            {fromQuotation ? (
              <p className="rounded-md border border-border bg-muted/40 px-2 py-1.5 text-xs">
                Selling quotation <span className="font-mono">{fromQuotation.number}</span> at its quoted prices.
              </p>
            ) : null}
            {quote?.pricing ? (
              <p className="text-xs text-muted-foreground">
                {quote.pricing.group} customer: {quote.pricing.priceGroup ? `${quote.pricing.priceGroup} prices` : "retail prices"}
                {quote.pricing.discountPct ? `, ${quote.pricing.discountPct}% off` : ""}. Prices you change by hand stay as typed.
              </p>
            ) : null}
            {quote?.promotions?.applied.length ? (
              <ul aria-label="Promotions applied" className="space-y-0.5 text-xs">
                {quote.promotions.applied.map((a) => (
                  <li key={a.promotionId} className="flex justify-between gap-2 text-status-success-text">
                    <span>{a.name}</span>
                    <span className="tabular-nums">−{formatKES(a.amount, { decimals: true })}</span>
                  </li>
                ))}
              </ul>
            ) : null}
            {quote?.promotions?.note ? <p className="text-xs text-muted-foreground">{quote.promotions.note}</p> : null}
            {quote?.promotions?.missed.length ? (
              <p className="text-xs text-muted-foreground">{quote.promotions.missed.map((m) => `Spend ${formatKES(m.short, { decimals: true })} more for ${m.name}`).join(" · ")}</p>
            ) : null}
            <dl className="space-y-1 text-sm tabular-nums">
              <div className="flex justify-between">
                <dt className="text-muted-foreground">Subtotal{deliveryFee ? " (with delivery)" : ""}</dt>
                <dd>{formatKES(subtotal, { decimals: true })}</dd>
              </div>
              <div className="flex justify-between">
                <dt className="text-muted-foreground">VAT (eTIMS sales only)</dt>
                <dd>{formatKES(vat, { decimals: true })}</dd>
              </div>
              <div className="flex justify-between border-t border-border pt-2 text-lg font-semibold">
                <dt>Total</dt>
                <dd className="flex items-center gap-2">
                  {quoting ? <Spinner className="size-3.5" /> : null}
                  {formatKES(total, { decimals: true })}
                </dd>
              </div>
            </dl>
            <button
              type="button"
              disabled={!lines.length || !quote || quoting}
              onClick={() => setPaying(true)}
              className="h-12 w-full cursor-pointer rounded-lg bg-action-accent-default text-base font-semibold text-fg-on-accent outline-none hover:bg-action-accent-hover active:bg-action-accent-active focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
            >
              Pay {formatKES(dueNow, { decimals: true })}
            </button>
          </div>
        </div>
      </div>

      {paying && quote ? (
        <RecordDrawer open onOpenChange={(o) => !o && !saving && setPaying(false)} variant="edit" width="lg" saving={saving} title="Take payment">
          <div className="grid gap-4 lg:grid-cols-[1fr_auto]">
            <div className="space-y-4">
              <Field orientation="horizontal" className="items-start justify-between rounded-lg border border-border p-3">
                <div className="space-y-0.5">
                  <FieldLabel htmlFor="etims">Send to KRA eTIMS</FieldLabel>
                  <FieldDescription>Adds VAT by each product&apos;s tax code. Sales not sent to eTIMS carry no VAT.</FieldDescription>
                </div>
                <Switch id="etims" checked={etims} onCheckedChange={setEtims} disabled={saving} />
              </Field>
              {scheme && !preorder ? (
                <div className="space-y-2">
                  <Field orientation="horizontal" className="items-start justify-between rounded-lg border border-border p-3">
                    <div className="space-y-0.5">
                      <FieldLabel htmlFor="bill-scheme">Bill {scheme.scheme}</FieldLabel>
                      <FieldDescription>The scheme&apos;s share becomes a claim; the customer pays the co-pay and anything over the cover.</FieldDescription>
                    </div>
                    <Switch id="bill-scheme" checked={billScheme} onCheckedChange={setBillScheme} disabled={saving || scheme.status !== "Active" || !scheme.active} />
                  </Field>
                  {scheme.limit != null ? (
                    <SchemeCard
                      member={{ scheme: scheme.scheme, memberNo: scheme.memberNo ?? "", relation: scheme.relation ?? "Principal", principal: scheme.principal ?? undefined, limit: scheme.limit, used: scheme.used, copayPct: scheme.copayPct, validUntil: scheme.validUntil ?? "", status: scheme.active ? (scheme.status ?? "Active") : "Suspended" }}
                      bill={billScheme ? total : undefined}
                      formatDate={(d) => (d ? formatDate(d) : "no end date")}
                    />
                  ) : billScheme ? (
                    <p className="text-sm">
                      Scheme pays {formatKES(schemePart, { decimals: true })} (no cover limit), {scheme.copayPct}% co-pay.
                    </p>
                  ) : null}
                </div>
              ) : null}
              {isOn("delivery_enabled") ? (
                <DeliveryPanel value={delivery} onChange={(d) => setDelivery(d.on && !delivery.on && !d.fee && defaultFee ? { ...d, fee: String(defaultFee) } : d)} zones={zones} basket={goods} freeAbove={freeAbove} />
              ) : null}
            </div>
            {quoting ? (
              <div className="flex w-full max-w-sm items-center justify-center rounded-lg border border-border p-8">
                <Spinner />
              </div>
            ) : dueNow <= 0 ? (
              <div className="flex w-full max-w-sm flex-col gap-3 rounded-lg border border-border p-4 text-sm">
                <p>The deposit on preorder {preorder?.number} covers the whole {formatKES(quote.total, { decimals: true })}. Nothing more to pay.</p>
                <Button size="lg" disabled={saving} onClick={() => void complete({ payments: [], credit: 0 })}>
                  {saving ? <Spinner /> : null}
                  Complete sale
                </Button>
              </div>
            ) : (
              <TenderPanel
                key={`${dueNow}-${customer?.id ?? "walk-in"}`}
                total={dueNow}
                hasCustomer={!!customer && customer.id > 0}
                // A preorder's deposit is the credit on this sale; more credit goes through the account as usual
                creditLimit={preorder ? 0 : (credit?.creditLimit ?? 0)}
                creditBalance={credit?.balance ?? 0}
                receiptNo="New sale"
                state={saving ? "processing" : "default"}
                mpesa={mpesa}
                card={card}
                onComplete={(r) => void complete(r)}
              />
            )}
          </div>
        </RecordDrawer>
      ) : null}

      <Dialog open={!!qtyFor} onOpenChange={(o) => !o && setQtyFor(null)}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{qtyFor ? byId.get(qtyFor.productId)?.name : ""}</DialogTitle>
            <DialogDescription>How many? (F5)</DialogDescription>
          </DialogHeader>
          {qtyFor ? (
            <Field>
              <FieldLabel htmlFor="ln-qty">Quantity</FieldLabel>
              <Input id="ln-qty" autoFocus inputMode="numeric" className="tabular-nums" value={qtyFor.qty} onChange={(e) => setQtyFor({ ...qtyFor, qty: e.target.value.replace(/\D/g, "") })} />
            </Field>
          ) : null}
          <DialogFooter>
            <Button variant="outline" onClick={() => setQtyFor(null)}>
              Cancel
            </Button>
            <Button
              onClick={() => {
                if (!qtyFor) return;
                const line = lines.find((l) => l.productId === qtyFor.productId);
                const n = Number(qtyFor.qty);
                if (line && Number.isInteger(n) && n >= 0) setQty(qtyFor.productId, n - line.qty);
                setQtyFor(null);
              }}
            >
              Apply
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      <Dialog open={!!editing} onOpenChange={(o) => !o && setEditing(null)}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{editing ? byId.get(editing.productId)?.name : ""}</DialogTitle>
            <DialogDescription>Change the price for this sale, or give a discount per unit.</DialogDescription>
          </DialogHeader>
          {editing ? (
            <div className="grid gap-3 sm:grid-cols-2">
              <Field>
                <FieldLabel htmlFor="ln-price">Unit price (KES)</FieldLabel>
                <Input id="ln-price" inputMode="decimal" className="tabular-nums" value={editing.price} onChange={(e) => setEditing({ ...editing, price: e.target.value.replace(/[^0-9.]/g, "") })} />
              </Field>
              <Field>
                <FieldLabel htmlFor="ln-disc">Discount per unit (KES)</FieldLabel>
                <Input id="ln-disc" inputMode="decimal" className="tabular-nums" value={editing.discount} onChange={(e) => setEditing({ ...editing, discount: e.target.value.replace(/[^0-9.]/g, "") })} />
              </Field>
            </div>
          ) : null}
          <DialogFooter>
            <Button variant="outline" onClick={() => setEditing(null)}>
              Cancel
            </Button>
            <Button
              onClick={() => {
                if (!editing) return;
                const price = Number(editing.price);
                const discount = Number(editing.discount) || 0;
                if (!(price >= 0) || discount < 0 || discount > price) return toast.add({ type: "error", title: "The discount can't be more than the price" });
                setLines((ls) => ls.map((l) => (l.productId === editing.productId ? { ...l, price, discount, manual: true } : l)));
                setEditing(null);
              }}
            >
              Apply
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {done ? (
        <TillReceiptDialog
          data={receiptData(done.sale, {
            branch: done.sale.branchId == null || done.sale.branchId === user.branchId ? branchName : "",
            settings,
            taxNames,
            reprintBy: done.reprint ? user.name || user.username || "" : undefined,
          })}
          title={done.reprint ? "Reprint receipt" : "Sale complete"}
          autoPrint={!done.reprint && isOn("receipt_auto_print")}
          doneLabel={done.reprint ? "Close" : "New sale"}
          onDone={() => {
            if (!done.reprint) clear();
            setDone(null);
          }}
          onGenerateRx={() => setRxFor(rxFromSale(done.sale))}
        />
      ) : null}
      {showPast ? (
        <PastSalesDialog
          canReturn={may("POS/Credit notes", "Create")}
          onClose={() => setShowPast(false)}
          onReprint={async (saleId) => {
            try {
              setDone({ sale: await SalesApi.getOne(saleId), reprint: true });
            } catch (e) {
              toast.add({ type: "error", title: "Couldn't load the sale", description: apiError(e, "Try again.") });
            }
          }}
          onGenerateRx={(row) =>
            setRxFor({
              saleId: row.id,
              receiptId: row.receipt,
              items: row.items.map((i) => ({ id: i.productId, name: i.product, qty: i.qty })),
              customer: row.customer ? { name: row.customer } : null,
            })
          }
          onReturn={(row) => {
            setShowPast(false);
            setReturnFor(row);
          }}
        />
      ) : null}
      {rxFor ? <PrescriptionForm rx={null} fromSale={rxFor} onClose={() => setRxFor(null)} onSaved={() => setRxFor(null)} /> : null}
      {returnFor ? (
        <ReturnDialog
          row={returnFor}
          canReverse={may("POS/Credit notes", "Approve")}
          onClose={() => setReturnFor(null)}
          onDone={() => {
            setReturnFor(null);
            void loadProducts();
          }}
        />
      ) : null}
      {showHeld ? <HeldDialog canDelete={may("POS/POS sales", "Delete")} onRecall={(id) => void recall(id)} onClose={() => { setShowHeld(false); countHeld(); }} /> : null}
      {tillQuotation ? (
        <QuotationDialog
          reference={tillQuotation.number}
          validUntil={tillQuotation.validUntil}
          lines={lines.map((l) => {
            const p = byId.get(l.productId);
            return { code: p?.code ?? "", name: p?.name ?? `#${l.productId}`, unit: p?.unit, qty: l.qty, price: l.price, discount: l.discount, taxCode: quote?.lines.find((x) => x.productId === l.productId)?.taxCode };
          })}
          etims={etims}
          customer={customer?.name ?? null}
          branch={branchName}
          settings={settings}
          printedBy={user.name || user.username || ""}
          onClose={() => setTillQuotation(null)}
        />
      ) : null}

      <style>{`
        @media print {
          body * { visibility: hidden !important; }
          .print\\:block, .print\\:block * { visibility: visible !important; }
          .print\\:block { position: fixed; left: 0; top: 0; width: 80mm; padding: 8mm; background: white; }
          .print\\:hidden { display: none !important; }
        }
      `}</style>
    </div>
  );
}
