{{--
    Product grid + cart for the Sales Order form — see App\Filament\Forms\Components\SalesOrderProductGrid.

    State is the `items` form array, mixing two line shapes:
      catalog  { product_id, quantity, unit_price, vat_rate }
      custom   { is_custom: true, product_name, custom_description,
                 custom_specification, quantity, unit_price, vat_rate }

    Bound with a deferred entangle (no round-trips while picking), so the normal
    validation/submit pipeline receives exactly what SalesOrderService consumes.
    Search, category filters, the custom-item builder and the cart are pure
    client-side Alpine.

    The branch-priced product payload is FETCHED, not rendered: this markup
    carries no catalog at all, and Alpine asks for one (once per branch) over
    the renderless $wire.salesOrderCatalog(). It used to be rendered inline on
    every request, which meant every unrelated interaction with the form — the
    customer dropdown, a payment row, a keystroke in the note — rebuilt and
    re-serialized the entire catalog server-side before the page could respond.
    That is what made this form feel slow, and why the whole subtree is now
    wire:ignore'd: Livewire never morphs it, so a form re-render costs nothing
    here.

    Stock is displayed but never blocks a line — a sales order is for goods due
    later, and stock is checked for real at conversion. Custom items have no
    stock at all.
--}}
@php
    $statePath = $getStatePath();
    $isDisabled = $isDisabled();
    // The branch field is this field's sibling in the same form state, so its
    // path is this one's with the final segment swapped (`data.items` →
    // `data.branch_id`). Derived rather than hardcoded so that renaming or
    // nesting the container cannot leave the client silently reading a path
    // that no longer exists — which would read as "no branch picked".
    $branchStatePath = preg_replace('/[^.]+$/', 'branch_id', $statePath);
@endphp

<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
    <div
        {{--
            wire:ignore: this subtree owns its own state (the fetched catalog
            and everything derived from it) and the server renders none of it,
            so Livewire must leave it alone on every morph. The entangled
            `items` binding below is a JS-side bridge and keeps working
            regardless — only DOM patching is suppressed.

            The key is stable for the same reason. It used to be keyed on the
            branch so that a branch switch tore the component down and rebuilt
            it against a freshly rendered payload; the catalog is now fetched
            per branch instead (see loadCatalog()), which does that job
            directly and without re-rendering the form. `branchId` still tracks
            which branch the loaded lots belong to, and lotsAreCurrent() is
            still the guard that stops a line being priced from the wrong one.
        --}}
        wire:ignore
        wire:key="sales-order-grid"
        x-data="{
            products: [],
            branchId: null,
            catalogLoading: false,
            // Monotonic token identifying the newest catalog fetch, so a slow
            // response for a branch the operator has already moved on from
            // cannot overwrite a newer one.
            catalogRequest: 0,
            items: @entangle($statePath),
            disabled: @js($isDisabled),
            vatEnabled: @js($vatEnabled),
            labelPrefix: @js($labelPrefix),
            search: '',
            mainCat: '',
            subCat: '',

            // The lot picker. `lotProduct` is the catalog card being added
            // (or re-priced from the cart); `lotLineIndex` is null when adding
            // a new line and the cart index when changing an existing line's
            // lot; `lotPick` is the chosen lot's id, confirmed by the Add button.
            showLots: false,
            lotProduct: null,
            lotLineIndex: null,
            lotPick: null,

            // The custom-item builder. `editingIndex` is null while composing a
            // new line and holds the cart index while amending an existing one.
            showCustom: false,
            editingIndex: null,
            custom: { product_name: '', custom_description: '', custom_specification: '', quantity: 1, unit_price: 0, vat_rate: 0 },

            init() {
                this.items ??= [];
                this.$watch('mainCat', () => { this.subCat = ''; });

                // First paint carries no catalog — go and get the one for
                // whichever branch the form is already on (the operator's sole
                // branch, an edited order's own branch, or the ambient
                // context's). Null means the field is blank and the grid shows
                // the pick-a-branch prompt instead.
                this.loadCatalog(@js($initialBranchId));

                // The branch select is ->live() and announces itself on change
                // (see SalesOrderResource::form). Nothing else in the form can
                // change what this grid shows, so nothing else refetches it.
                window.addEventListener('sales-order-branch-changed', (e) => {
                    this.loadCatalog(e.detail?.branchId ?? null);
                });

                // Page-wide scanner capture (see scan-capture-script.blade.php,
                // rendered into every tenant page): a scan reaches
                // handleScan() no matter what has focus. Declined while the
                // lot picker or the custom-item builder is open, or while a
                // branch change is in flight — same reasoning as addToCart()
                // above.
                window.VellixScan?.register(this.$root, (code) => {
                    if (this.disabled || this.showLots || this.showCustom) return true;

                    this.handleScan(code);

                    return true;
                });
            },

            // Fetch the branch-priced catalog. Renderless on the server, so
            // this costs one query round trip and no form re-render.
            async loadCatalog(branchId) {
                const wanted = branchId === null || branchId === undefined || branchId === ''
                    ? null
                    : Number(branchId);

                if (wanted === null) {
                    this.catalogRequest++;
                    this.products = [];
                    this.branchId = null;
                    this.catalogLoading = false;
                    return;
                }

                // Already holding this branch's catalog — nothing to do. This is
                // what makes re-entering the same branch, or a duplicate event,
                // free.
                if (this.branchId === wanted && this.products.length > 0) return;

                const token = ++this.catalogRequest;
                this.catalogLoading = true;

                let result = null;
                try {
                    result = await $wire.salesOrderCatalog(wanted);
                } catch (e) {
                    // Fall through to the empty state rather than leaving the
                    // grid spinning forever on a dropped request.
                }

                // A newer fetch has started since; its answer wins.
                if (token !== this.catalogRequest) return;

                this.catalogLoading = false;

                if (! result?.ok) {
                    this.products = [];
                    this.branchId = null;
                    return;
                }

                this.products = result.products ?? [];
                this.branchId = result.branchId ?? null;
            },

            // ── Catalog products ──────────────────────────────────────────
            matches(p) {
                const q = this.search.trim().toLowerCase();
                if (q && ! (
                    p.name.toLowerCase().includes(q)
                    || (p.sku || '').toLowerCase().includes(q)
                    || (p.barcode || '').toLowerCase().includes(q)
                )) return false;
                if (this.mainCat && p.main_category !== this.mainCat) return false;
                if (this.subCat && p.sub_category !== this.subCat) return false;
                return true;
            },

            visibleProducts() {
                return this.products
                    .filter((p) => this.matches(p))
                    .sort((a, b) => {
                        const aInStock = Number(a.lot_count || 0) > 0;
                        const bInStock = Number(b.lot_count || 0) > 0;

                        return Number(bInStock) - Number(aInStock);
                    });
            },

            mainCategories() {
                return [...new Set(this.products.map((p) => p.main_category).filter(Boolean))];
            },

            subCategories() {
                return [...new Set(this.products
                    .filter((p) => ! this.mainCat || p.main_category === this.mainCat)
                    .map((p) => p.sub_category)
                    .filter(Boolean))];
            },

            productFor(line) {
                return line.is_custom ? null : this.products.find((p) => p.id === line.product_id);
            },

            // A cart line's identity is (product, lot): the same product from
            // two consignments is two lines at two prices, and picking the
            // same lot again just adds to that line.
            lineFor(productId, batchId) {
                return this.items.find((l) => ! l.is_custom && l.product_id === productId && l.batch_id === batchId);
            },

            lotFor(line) {
                return this.productFor(line)?.lots?.find((lot) => lot.id === line.batch_id) ?? null;
            },

            priceLabel(p) {
                if (! p.lot_count || p.price_min === null) return '—';
                if (p.lot_count === 1 || p.price_min === p.price_max) return this.money(p.price_min);
                return this.money(p.price_min) + ' – ' + this.money(p.price_max);
            },

            // The branch the FORM is on right now, read straight out of the
            // Livewire snapshot the browser already holds — a synchronous
            // local read, not a request. Null while the field is blank.
            formBranchId() {
                const raw = $wire.get('{{ $branchStatePath }}');
                return raw === null || raw === undefined || raw === '' ? null : Number(raw);
            },

            // Do the lots this component is holding still describe the branch
            // on the form? `branchId` is set only when a catalog fetch lands,
            // and holds the branch that fetch was priced for, so this is an
            // exact answer rather than an inference: it is false for precisely
            // as long as a branch change is in flight, and true once the
            // matching catalog has arrived.
            //
            // A blank branch field is never current: there is nothing priced to
            // add from, and the grid is showing the pick-a-branch prompt.
            lotsAreCurrent() {
                const formBranch = this.formBranchId();
                if (formBranch === null) return false;
                return formBranch === this.branchId;
            },

            // Tap a card. One sellable lot → take it silently (adding to that
            // lot's line, or creating it); several → let the operator pick the
            // consignment each time, so the same product can sit on the order
            // once per lot; none → no priced lot on file, so say so instead of
            // silently doing nothing.
            //
            // No round trip: `p.lots` already holds every sellable lot for this
            // branch, chosen by the same filters and the same sell order the
            // server applies (BatchService::sellableFor), and it is what the
            // card itself is displaying — `priceLabel(p)` and the card's no-lot
            // state read `p.lot_count`, which counts those same lots. Asking
            // the server here used to let the click contradict the card the
            // operator was looking at, and re-rendered the entire form — the
            // whole catalogue, its stock, its price ranges and every lot list —
            // to do it. That is what made the click feel like a page load.
            //
            // Do not reintroduce a lookup for the stale-branch case below. A
            // branch change re-renders under a new wire:key, which tears this
            // component down; Livewire also defers any action fired while that
            // change is in flight until it completes. So a fetch started here
            // resolves into a component that no longer exists, and the click is
            // swallowed. Ignoring the click is the honest handling: the cards on
            // screen belong to the branch being navigated away from, and the
            // imminent re-render is what makes the next click right.
            //
            // Every string and comment in here must stay clear of raw double
            // quotes: this whole component is one double-quoted x-data
            // attribute, and one stray quote truncates it at the HTML parser,
            // spilling the rest of the JS onto the page as text.
            addToCart(p) {
                if (this.disabled || ! this.lotsAreCurrent()) return;

                const lots = p.lots ?? [];

                /*
                 * Sales orders are NOT invoices.
                 *
                 * If the product currently has no sellable stock, it can still
                 * be added to the order.
                 *
                 * SalesOrderService::quoteLot() will resolve the latest priced
                 * receipt and save:
                 *
                 *     batch_id = null
                 *
                 * This means the order is only a quotation/promise at this stage.
                 * Stock is NOT deducted.
                 *
                 * At invoice conversion, InvoiceService resolves the real
                 * fulfilment batch after stock has been received through a GRN.
                 */
                if (lots.length === 0) {
                    this.pushLine(p, {
                        id: null,
                        is_quote_only: true,
                        price: Number(p.quote_price ?? p.price_min ?? 0),
                        batch: 'To be received',
                        available: 0,
                    });

                    return;
                }
                // Batch tracking OFF: batches are an internal receiving detail,
                // not a selling decision — always take the front lot (the
                // latest GRN price, per orderedLots() on the server) rather
                // than asking the rep to pick a consignment. Same rule the POS
                // terminal's addProduct() applies for is_batch_tracked === false.
                /*
                 * Batch tracking OFF or only one available lot:
                 * use it directly.
                 */
                if (p.is_batch_tracked === false || lots.length === 1) {
                    this.pushLine(p, lots[0]);
                    return;
                }

                /*
                 * Multiple available lots:
                 * keep the existing lot picker.
                 */
                this.lotProduct = { ...p, lots };
                this.lotLineIndex = null;
                this.lotPick = lots[0].id;
                this.showLots = true;
            },

            changeLot(line, index) {
                if (! this.lotsAreCurrent()) return;
                const p = this.productFor(line);
                // Non-batch products sell from the combined branch pool, not a
                // chosen consignment — there is no lot to change.
                if (! p || p.is_batch_tracked === false) return;
                const lots = p.lots ?? [];
                if (lots.length < 2) return;
                this.lotProduct = { ...p, lots };
                this.lotLineIndex = index;
                this.lotPick = line.batch_id;
                this.showLots = true;
            },

            closeLots() {
                this.showLots = false;
                this.lotProduct = null;
                this.lotLineIndex = null;
                this.lotPick = null;
            },

            confirmLot() {
                if (! this.lotPick || ! this.lotProduct) return;
                const lot = this.lotProduct.lots.find((l) => l.id === this.lotPick);
                if (! lot) return;

                if (this.lotLineIndex === null) {
                    this.pushLine(this.lotProduct, lot);
                } else {
                    const line = this.items[this.lotLineIndex];
                    // Quote-only lots (no real stock row) must never leave an
                    // id sitting in batch_id — see pushLine() below for why.
                    line.batch_id = lot.is_quote_only ? null : lot.id;
                    line.unit_price = lot.price;
                    this.items.splice(this.lotLineIndex, 1, line);
                }
                this.closeLots();
            },

            pushLine(p, lot) {
                // One line per (product, lot): the same lot tapped again
                // grows that line, a different lot becomes a new line.
                const existing = this.lineFor(p.id, lot.id);
                if (existing) {
                    existing.quantity = Math.round((Number(existing.quantity) + this.qtyStep(p)) * 100) / 100;
                    return;
                }
                this.items.push({
                    product_id: p.id,
                    is_custom: false,
                    // A quote-only lot is a price to quote from, not a real
                    // stock row — it has no id the server can look up as a
                    // batch. Sending one through as batch_id would have
                    // SalesOrderService::create()/update() try to load a
                    // StockBatch that doesn't exist (or, worse, collide with
                    // an unrelated real batch id). Force null here so the
                    // server falls back to its own quoteLot() resolution
                    // instead of trusting a client-supplied id for a lot that
                    // was never a row in stock_batches.
                    batch_id: lot.is_quote_only ? null : lot.id,
                    quantity: this.qtyStep(p),
                    unit_price: lot.price,
                    vat_rate: p.vat_rate,
                });
            },

            // Enter in the search box (see grid-header.blade.php), driven by a
            // barcode scanner (scan then Enter) or a rep who typed the full
            // SKU/barcode.
            scanAdd(raw) {
                const q = String(raw ?? '').trim();
                if (q) this.handleScan(q);
            },

            // The one place a scanned (or typed-then-Enter'd) code becomes an
            // order line, shared by the search box and the page-wide scan
            // listener (see init() above).
            //
            // Exact code match wins over an ambiguous text search; otherwise
            // the scan does nothing unless the current filter has already
            // narrowed to exactly one product. A matched product goes through
            // addToCart() itself — the SAME path a tapped card takes: zero
            // lots quotes, one lot (or batch tracking off) adds straight
            // through, more than one active lot opens the lot picker so the
            // rep always chooses the consignment.
            handleScan(code) {
                if (this.disabled || ! this.lotsAreCurrent()) return;
                if (this.scanPrintedLabel(code)) return;

                const q = code.trim().toLowerCase();
                if (! q) return;

                const exact = this.products.find((p) =>
                    (p.barcode && p.barcode.toLowerCase() === q) || (p.sku && p.sku.toLowerCase() === q)
                );
                const visible = this.visibleProducts();
                const target = exact || (visible.length === 1 ? visible[0] : null);
                if (! target) return;

                this.addToCart(target);
            },

            // A scan of one of the shop's own printed labels: a whole line —
            // product, lot and quantity, decided when the label was made —
            // not a product identifier to search for. Recognised by prefix so
            // an ordinary SKU/barcode scan never pays for the lookup. Returns
            // true when the scan was handled here, whether a line was added
            // or the label was refused.
            //
            // An order records no serials — which physical unit ships is
            // settled at invoicing, not when the order is taken — so a
            // serial-tracked label resolves to its product, lot and quantity
            // only, same as {@see resolveScan()} on the server.
            scanPrintedLabel(code) {
                const prefix = this.labelPrefix || '';
                if (! prefix || ! code.toUpperCase().startsWith(prefix.toUpperCase())) return false;

                $wire.resolveScan(code, this.branchId).then((result) => {
                    if (! result?.ok) {
                        new FilamentNotification()
                            .title('Label not usable')
                            .body(result?.message ?? 'That label could not be resolved.')
                            .danger()
                            .send();

                        return;
                    }

                    this.addLabelLine(result);
                }).catch(() => {
                    new FilamentNotification()
                        .title('Could not read that label')
                        .body('Check the connection and scan again.')
                        .danger()
                        .send();
                });

                return true;
            },

            // Puts a resolved label straight on the order — same shape as
            // pushLine() above, since it feeds the same items array.
            addLabelLine(label) {
                const lot = label.lot;
                const existing = this.lineFor(label.product_id, lot.id);
                if (existing) {
                    existing.quantity = Math.round((Number(existing.quantity) + Number(label.quantity)) * 10000) / 10000;
                    return;
                }

                const product = this.products.find((p) => p.id === label.product_id) ?? null;

                this.items.push({
                    product_id: label.product_id,
                    is_custom: false,
                    batch_id: lot.id,
                    quantity: Number(label.quantity) || 1,
                    unit_price: Number(lot.sale_price) || 0,
                    vat_rate: product?.vat_rate ?? 0,
                });
            },

            qtyStep(p) {
                return p?.base_unit?.is_fractional ? 0.5 : 1;
            },

            bump(line, delta) {
                if (this.disabled) return;
                const step = this.qtyStep(this.productFor(line));
                const next = Math.round((Number(line.quantity) + delta * step) * 100) / 100;
                line.quantity = Math.max(step, next);
            },

            setQty(line, value) {
                const step = this.qtyStep(this.productFor(line));
                const parsed = Number(value);
                line.quantity = Number.isFinite(parsed) && parsed > 0 ? parsed : step;
            },

            removeLine(index) {
                if (this.disabled) return;
                this.items.splice(index, 1);
            },

            clearCart() {
                if (this.disabled) return;
                this.items = [];
                this.closeCustom();
            },

            // ── Custom items ──────────────────────────────────────────────
            openCustom() {
                if (this.disabled) return;
                this.editingIndex = null;
                this.custom = { product_name: '', custom_description: '', custom_specification: '', quantity: 1, unit_price: 0, vat_rate: 0 };
                this.showCustom = true;
                this.$nextTick(() => this.$refs.customName?.focus());
            },

            editCustom(index) {
                if (this.disabled) return;
                this.editingIndex = index;
                this.custom = { ...this.items[index] };
                this.showCustom = true;
                this.$nextTick(() => this.$refs.customName?.focus());
            },

            closeCustom() {
                this.showCustom = false;
                this.editingIndex = null;
            },

            customIsValid() {
                return String(this.custom.product_name || '').trim().length > 0
                    && Number(this.custom.quantity) > 0
                    && Number(this.custom.unit_price) >= 0;
            },

            saveCustom() {
                if (! this.customIsValid()) return;

                const line = {
                    is_custom: true,
                    product_id: null,
                    product_name: String(this.custom.product_name).trim(),
                    custom_description: this.custom.custom_description || null,
                    custom_specification: this.custom.custom_specification || null,
                    quantity: Number(this.custom.quantity),
                    unit_price: Number(this.custom.unit_price),
                    vat_rate: Number(this.custom.vat_rate) || 0,
                };

                if (this.editingIndex === null) {
                    this.items.push(line);
                } else {
                    this.items.splice(this.editingIndex, 1, line);
                }

                this.closeCustom();
            },

            // ── Totals ────────────────────────────────────────────────────
            customCount() {
                return this.items.filter((l) => l.is_custom).length;
            },

            lineNet(l) {
                return Number(l.quantity) * Number(l.unit_price);
            },

            subtotal() {
                return this.items.reduce((s, l) => s + this.lineNet(l), 0);
            },

            vatTotal() {
                if (! this.vatEnabled) return 0;
                return this.items.reduce((s, l) => s + this.lineNet(l) * (Number(l.vat_rate) || 0) / 100, 0);
            },

            money(n) {
                return new Intl.NumberFormat('en-LK', { style: 'currency', currency: 'LKR' }).format(Number(n) || 0);
            },

            fmtQty(n, p) {
                return p?.base_unit?.is_fractional ? Number(n).toFixed(2) : String(Number(n));
            },

            unitLabel(line) {
                const unit = this.productFor(line)?.base_unit;
                return unit ? (unit.code || unit.name) : null;
            },

            lineName(l) {
                return l.is_custom ? (l.product_name || 'Custom item') : (this.productFor(l)?.name ?? ('#' + l.product_id));
            },

            // ── Shared product-card contract (filament.pos.product-card) ──
            // Same signatures as the POS terminal's posTerminal(): the card
            // partial calls these, not the screen's own cart API. No
            // in-cart guard: re-tapping resolves the lot again, so the same
            // product can be ordered once per consignment.
            tapProduct(p) {
                if (this.disabled) return;
                this.addToCart(p);
            },

            cartQtyFor(p) {
                return this.items
                    .filter((l) => ! l.is_custom && l.product_id === p.id)
                    .reduce((s, l) => s + Number(l.quantity || 0), 0);
            },

            formatQty(value) {
                const n = Number(value) || 0;
                return Number.isInteger(n) ? String(n) : n.toFixed(3);
            },

            formatQtyWithUnit(value, unit) {
                const qty = this.formatQty(value);
                const label = unit?.code || unit?.name;
                return label ? qty + ' ' + label : qty;
            },
        }"
        {{--
            @container, and every breakpoint below is @sm/@lg/... (container
            query units), not sm:/lg:. This field is nested inside Filament's
            own 2-column resource-form grid, so on a real desktop layout it
            only ever gets roughly HALF the viewport width — a plain
            sm:/lg:/xl: breakpoint fires off the browser window, not this
            field's actual width, so at a wide-viewport-but-narrow-column
            layout (the normal case here) every product card and form field
            below was cramming into 3-4 columns that only had room for 1-2,
            which is what made this look broken. Container queries make each
            grid respond to the width it actually has, at any nesting depth.
        --}}
        class="fi-sales-order-grid @container space-y-4"
    >
        {{-- ── Toolbar ─────────────────────────────────────────────────── --}}
        @component('filament.pos.grid-header')
            @slot('actions')
                {{-- The custom-order entry point, deliberately first and always
                     visible — a custom item is the reason many of these orders
                     exist, and it must not be buried under the catalog. --}}
                <button
                    type="button"
                    x-on:click="openCustom()"
                    x-bind:disabled="disabled"
                    class="inline-flex shrink-0 items-center justify-center gap-2 rounded-lg border border-dashed border-primary-400 bg-primary-50 px-4 py-2 text-sm font-semibold text-primary-700 transition hover:bg-primary-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-primary-500/50 dark:bg-primary-500/10 dark:text-primary-400 dark:hover:bg-primary-500/20"
                >
                    <x-filament::icon icon="heroicon-m-sparkles" class="h-4 w-4" />
                    Add custom item
                </button>
            @endslot
        @endcomponent

        {{-- ── Custom item builder ─────────────────────────────────────── --}}
        <div
            x-show="showCustom"
            x-cloak
            x-transition
            class="rounded-xl border-2 border-primary-200 bg-primary-50/50 p-4 dark:border-primary-500/30 dark:bg-primary-500/5"
        >
            <div class="mb-3 flex items-center justify-between">
                <h3 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
                    <x-filament::icon icon="heroicon-m-sparkles" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
                    <span x-text="editingIndex === null ? 'New custom item' : 'Edit custom item'"></span>
                </h3>
                <button
                    type="button"
                    x-on:click="closeCustom()"
                    class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
                >
                    <x-filament::icon icon="heroicon-m-x-mark" class="h-5 w-5" />
                </button>
            </div>

            <div class="grid gap-3 @sm:grid-cols-2 @3xl:grid-cols-4">
                <div class="@sm:col-span-2">
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
                        Item name <span class="text-danger-600">*</span>
                    </label>
                    <input
                        type="text"
                        x-ref="customName"
                        x-model="custom.product_name"
                        x-on:keydown.enter.prevent="saveCustom()"
                        placeholder="e.g. Two-tier birthday cake"
                        maxlength="255"
                        class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800"
                    >
                </div>

                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">Quantity <span class="text-danger-600">*</span></label>
                    <input type="number" min="0.01" step="any" x-model.number="custom.quantity" class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800">
                </div>

                <div>
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">Unit price (Rs.) <span class="text-danger-600">*</span></label>
                    <input type="number" min="0" step="0.01" x-model.number="custom.unit_price" class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800">
                </div>

                <div class="@sm:col-span-2">
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">Description</label>
                    <textarea
                        x-model="custom.custom_description"
                        rows="2"
                        placeholder="What is being made"
                        class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800"
                    ></textarea>
                </div>

                <div class="@sm:col-span-2">
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">Exact details from the customer</label>
                    <textarea
                        x-model="custom.custom_specification"
                        rows="2"
                        placeholder="Sizes, colours, wording, packaging — anything they specified"
                        class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800"
                    ></textarea>
                </div>

                <div x-show="vatEnabled">
                    <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">VAT rate (%)</label>
                    <input type="number" min="0" max="100" step="0.01" x-model.number="custom.vat_rate" class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800">
                </div>

                <div class="flex items-end">
                    <p class="text-xs text-gray-500 dark:text-gray-400">
                        Line total<br>
                        <span class="text-sm font-bold text-gray-900 dark:text-white" x-text="money((Number(custom.quantity) || 0) * (Number(custom.unit_price) || 0) * (1 + (vatEnabled ? (Number(custom.vat_rate) || 0) : 0) / 100))"></span>
                    </p>
                </div>
            </div>

            <div class="mt-3 flex items-center gap-2">
                <button
                    type="button"
                    x-on:click="saveCustom()"
                    x-bind:disabled="! customIsValid()"
                    class="inline-flex items-center gap-1.5 rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-primary-500 disabled:cursor-not-allowed disabled:opacity-40"
                >
                    <x-filament::icon icon="heroicon-m-check" class="h-4 w-4" />
                    <span x-text="editingIndex === null ? 'Add to order' : 'Save changes'"></span>
                </button>
                <button
                    type="button"
                    x-on:click="closeCustom()"
                    class="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
                >
                    Cancel
                </button>
                <p x-show="! customIsValid()" class="text-xs text-gray-400 dark:text-gray-500">
                    A name, a quantity and a price are needed.
                </p>
            </div>
        </div>

        {{-- ── Lot picker ──────────────────────────────────────────────── --}}
        <div
            x-show="showLots"
            x-cloak
            x-transition
            class="fixed inset-0 z-50 flex items-center justify-center bg-gray-950/50 p-4"
            x-on:click.self="closeLots()"
        >
            <div class="max-h-[80vh] w-full max-w-lg overflow-y-auto rounded-2xl bg-white shadow-xl dark:bg-gray-900">
                <div class="flex items-center justify-between border-b border-gray-100 px-5 py-4 dark:border-white/10">
                    <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
                        Pick a lot — <span class="text-primary-600 dark:text-primary-400" x-text="lotProduct?.name ?? ''"></span>
                    </h3>
                    <button
                        type="button"
                        x-on:click="closeLots()"
                        class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
                    >
                        <x-filament::icon icon="heroicon-m-x-mark" class="h-5 w-5" />
                    </button>
                </div>

                <div class="space-y-2 p-4">
                    <p class="text-xs text-gray-500 dark:text-gray-400">
                        This product was received on more than one consignment at different prices — the order quotes from the lot you pick here.
                    </p>
                    <template x-for="lot in (lotProduct?.lots ?? [])" :key="lot.id">
                        <label
                            class="flex cursor-pointer items-center gap-3 rounded-xl border p-3 transition"
                            :class="lotPick === lot.id ? 'border-primary-500 bg-primary-50/60 dark:bg-primary-500/10' : 'border-gray-200 hover:border-gray-300 dark:border-white/10'"
                        >
                            <input type="radio" name="lot-pick" :value="lot.id" x-model="lotPick" class="h-4 w-4 shrink-0 accent-primary-600">
                            <div class="min-w-0 flex-1">
                                <p class="flex flex-wrap items-center gap-x-2 text-sm font-medium text-gray-900 dark:text-white">
                                    <span x-text="lot.batch"></span>
                                </p>
                                <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                                    <span class="font-semibold text-gray-900 dark:text-white" x-text="money(lot.price)"></span>
                                    <span x-text="lot.cost !== null ? ' · cost ' + money(lot.cost) : ''"></span>
                                    <span x-text="lot.expiry ? ' · expires ' + lot.expiry : ''"></span>
                                    <span x-text="' · ' + fmtQty(lot.available, lotProduct) + ' ' + (lotProduct?.base_unit?.code || '') + ' left'"></span>
                                </p>
                            </div>
                        </label>
                    </template>
                </div>

                <div class="flex items-center justify-end gap-2 border-t border-gray-100 px-5 py-3 dark:border-white/10">
                    <button
                        type="button"
                        x-on:click="closeLots()"
                        class="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
                    >
                        Cancel
                    </button>
                    <button
                        type="button"
                        x-on:click="confirmLot()"
                        x-bind:disabled="! lotPick"
                        class="inline-flex items-center gap-1.5 rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-primary-500 disabled:cursor-not-allowed disabled:opacity-40"
                    >
                        <x-filament::icon icon="heroicon-m-check" class="h-4 w-4" />
                        <span x-text="lotLineIndex === null ? 'Add to order' : 'Use this lot'"></span>
                    </button>
                </div>
            </div>
        </div>

        <div class="grid gap-4 @3xl:grid-cols-3">
            {{-- ── Product grid ────────────────────────────────────────── --}}
            {{-- @container of its own: its rendered width differs from the
                 outer one above once the @3xl:grid-cols-3 split is active
                 (2/3 of it, not the full width), so the card grid below needs
                 to query THIS box, not the outer one. --}}
            <div class="@container @3xl:col-span-2">
                {{-- No branch on the form yet: nothing is priced, so don't
                     show cards with a "No sellable lot" state on every one
                     of them — say plainly what's needed instead. --}}
                <div
                    x-show="branchId === null && ! catalogLoading"
                    x-cloak
                    class="flex min-h-48 items-center justify-center rounded-xl border border-dashed border-gray-300 bg-gray-50 p-8 text-center dark:border-white/10 dark:bg-white/[0.02]"
                >
                    <div>
                        <div class="text-sm font-semibold text-gray-700 dark:text-gray-200">
                            Please select a branch
                        </div>

                        <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
                            Select a branch above to view available products and sellable lots.
                        </div>
                    </div>
                </div>

                {{-- The catalog is in flight. Skeleton tiles rather than a
                     spinner: the grid keeps its shape, so the cards do not
                     jump into place underneath the operator's cursor. --}}
                <div
                    x-show="catalogLoading"
                    x-cloak
                    class="grid max-h-[60vh] grid-cols-2 gap-3 overflow-hidden pr-1 @sm:grid-cols-3 @lg:grid-cols-4"
                >
                    <template x-for="i in 8" :key="i">
                        <div class="min-h-40 animate-pulse rounded-xl border border-gray-200 bg-gray-100 dark:border-white/10 dark:bg-white/5"></div>
                    </template>
                </div>

                <div
                    x-show="branchId !== null && ! catalogLoading"
                    x-cloak
                    class="grid max-h-[60vh] grid-cols-2 gap-3 overflow-y-auto pr-1 @sm:grid-cols-3 @lg:grid-cols-4"
                >
                        {{-- The custom-item tile leads the grid, so "we also do
                             made-to-order" is the first thing a rep sees. --}}
                        <button
                            type="button"
                            x-on:click="openCustom()"
                            x-bind:disabled="disabled"
                            class="group flex min-h-40 cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-primary-300 bg-primary-50/40 p-4 text-center transition hover:-translate-y-0.5 hover:border-primary-500 hover:bg-primary-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-primary-500/40 dark:bg-primary-500/5 dark:hover:bg-primary-500/10"
                        >
                            <span class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-600 text-white shadow-sm">
                                <x-filament::icon icon="heroicon-m-sparkles" class="h-5 w-5" />
                            </span>
                            <span class="text-sm font-semibold text-primary-700 dark:text-primary-400">Custom item</span>
                            <span class="text-[11px] leading-snug text-gray-500 dark:text-gray-400">
                                Made to order — not from the catalog, no stock needed
                            </span>
                        </button>

                        <template x-for="p in visibleProducts()" :key="p.id">
                            @include('filament.pos.product-card', [
                                'dimClass' => "cartQtyFor(p) > 0 ? 'ring-2 ring-primary-500' : 'cursor-pointer hover:shadow-md'",
                                'showSku' => true,
                                'noLotExpr' => 'p.lot_count === 0',
                            ])
                        </template>

                        <div
                            x-show="visibleProducts().length === 0"
                            class="col-span-full flex flex-col items-center gap-2 py-16 text-center"
                        >
                            <x-filament::icon icon="heroicon-m-shopping-bag" class="h-10 w-10 text-gray-300 dark:text-gray-600" />
                            <p class="text-sm text-gray-500 dark:text-gray-400">No products match your search.</p>
                        </div>
                </div>
            </div>

            {{-- ── Order lines ─────────────────────────────────────────── --}}
            <div class="flex flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900">
                <div class="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-white/10">
                    <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
                        Order lines
                        <span class="ms-1 rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-semibold text-primary-700 dark:bg-primary-500/10 dark:text-primary-400" x-text="items.length"></span>
                        <template x-if="customCount() > 0">
                            <span class="ms-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700 dark:bg-amber-500/10 dark:text-amber-400">
                                <span x-text="customCount()"></span> custom
                            </span>
                        </template>
                    </h3>
                    <button
                        type="button"
                        x-show="items.length > 0"
                        x-on:click="clearCart()"
                        class="text-xs font-medium text-gray-400 hover:text-danger-600 dark:hover:text-danger-400"
                    >
                        Clear
                    </button>
                </div>

                <div class="max-h-[45vh] flex-1 space-y-3 overflow-y-auto p-4">
                    <template x-for="(line, index) in items" :key="index">
                        <div
                            class="rounded-lg border p-2.5"
                            :class="line.is_custom ? 'border-primary-200 bg-primary-50/40 dark:border-primary-500/30 dark:bg-primary-500/5' : 'border-gray-100 dark:border-white/10'"
                        >
                            <div class="flex items-start gap-3">
                                <div class="min-w-0 flex-1">
                                    <p class="flex items-center gap-1.5 truncate text-sm font-medium text-gray-900 dark:text-white">
                                        <template x-if="line.is_custom">
                                            <x-filament::icon icon="heroicon-m-sparkles" class="h-3.5 w-3.5 shrink-0 text-primary-600 dark:text-primary-400" />
                                        </template>
                                        <span class="truncate" x-text="lineName(line)"></span>
                                    </p>
                                    <p class="text-xs text-gray-500 dark:text-gray-400">
                                        <span x-text="money(line.unit_price)"></span>
                                        <template x-if="vatEnabled && Number(line.vat_rate) > 0">
                                            <span x-text="' + ' + line.vat_rate + '% VAT'"></span>
                                        </template>
                                    </p>
                                    <template x-if="! line.is_custom && productFor(line)?.is_batch_tracked !== false && lotFor(line)">
                                        <p class="mt-0.5 flex flex-wrap items-center gap-x-2 text-[10px] text-gray-400 dark:text-gray-500">
                                            <span class="rounded bg-gray-100 px-1.5 py-0.5 font-medium dark:bg-gray-800" x-text="'Lot ' + lotFor(line).batch"></span>
                                            <button
                                                type="button"
                                                x-on:click.stop="changeLot(line, index)"
                                                x-show="(productFor(line)?.lots?.length ?? 0) > 1"
                                                class="font-medium text-primary-600 hover:underline dark:text-primary-400"
                                            >
                                                Change lot
                                            </button>
                                        </p>
                                    </template>
                                    <template x-if="line.is_custom && line.custom_description">
                                        <p class="mt-0.5 line-clamp-2 text-xs text-gray-500 dark:text-gray-400" x-text="line.custom_description"></p>
                                    </template>
                                    <p class="mt-0.5 text-xs font-semibold tabular-nums text-primary-600 dark:text-primary-400" x-text="money(lineNet(line) * (1 + (vatEnabled ? (Number(line.vat_rate) || 0) : 0) / 100))"></p>
                                </div>
                                <div class="flex shrink-0 flex-col items-center gap-0.5">
                                <div class="flex items-center gap-1">
                                    <button
                                        type="button"
                                        x-on:click="bump(line, -1)"
                                        class="flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-600 hover:text-primary-600 dark:bg-gray-800 dark:text-gray-300"
                                    >
                                        <x-filament::icon icon="heroicon-m-minus" class="h-3.5 w-3.5" />
                                    </button>
                                    <input
                                        type="number"
                                        min="0.01"
                                        step="any"
                                        :value="line.quantity"
                                        x-on:change="setQty(line, $event.target.value)"
                                        class="h-6 w-14 rounded-md border-gray-200 bg-white px-1 text-center text-xs font-semibold tabular-nums text-gray-900 dark:border-white/10 dark:bg-gray-800 dark:text-white"
                                    >
                                    <button
                                        type="button"
                                        x-on:click="bump(line, 1)"
                                        class="flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-600 hover:text-primary-600 dark:bg-gray-800 dark:text-gray-300"
                                    >
                                        <x-filament::icon icon="heroicon-m-plus" class="h-3.5 w-3.5" />
                                    </button>
                                    <template x-if="line.is_custom">
                                        <button
                                            type="button"
                                            x-on:click="editCustom(index)"
                                            class="ms-1 flex h-6 w-6 items-center justify-center rounded-md text-gray-400 hover:bg-primary-50 hover:text-primary-600 dark:hover:bg-primary-500/10 dark:hover:text-primary-400"
                                        >
                                            <x-filament::icon icon="heroicon-m-pencil-square" class="h-3.5 w-3.5" />
                                        </button>
                                    </template>
                                    <button
                                        type="button"
                                        x-on:click="removeLine(index)"
                                        class="flex h-6 w-6 items-center justify-center rounded-md text-gray-400 hover:bg-danger-50 hover:text-danger-600 dark:hover:bg-danger-500/10 dark:hover:text-danger-400"
                                    >
                                        <x-filament::icon icon="heroicon-m-trash" class="h-3.5 w-3.5" />
                                    </button>
                                </div>
                                <span x-show="unitLabel(line)" class="text-[9px] leading-none text-gray-500 dark:text-gray-400" x-text="unitLabel(line)"></span>
                                </div>
                            </div>
                        </div>
                    </template>

                    <div
                        x-show="items.length === 0"
                        class="flex flex-col items-center gap-2 py-14 text-center"
                    >
                        <x-filament::icon icon="heroicon-m-shopping-cart" class="h-10 w-10 text-gray-300 dark:text-gray-600" />
                        <p class="text-sm text-gray-500 dark:text-gray-400">
                            Nothing ordered yet — pick from the catalog, or add a custom item.
                        </p>
                    </div>
                </div>

                <div class="space-y-1.5 border-t border-gray-100 px-4 py-3 text-sm dark:border-white/10">
                    <div class="flex justify-between text-gray-500 dark:text-gray-400">
                        <span>Subtotal</span>
                        <span class="tabular-nums" x-text="money(subtotal())"></span>
                    </div>
                    <div class="flex justify-between text-gray-500 dark:text-gray-400" x-show="vatEnabled">
                        <span>VAT</span>
                        <span class="tabular-nums" x-text="money(vatTotal())"></span>
                    </div>
                    <div class="flex justify-between border-t border-gray-100 pt-1.5 text-base font-bold text-gray-900 dark:border-white/10 dark:text-white">
                        <span>Order total</span>
                        <span class="tabular-nums" x-text="money(subtotal() + vatTotal())"></span>
                    </div>
                    <p class="pt-1 text-[11px] text-gray-400 dark:text-gray-500">
                        Before any order-level discount. Catalog lines are repriced from their lot when the order is invoiced.
                    </p>
                </div>
            </div>
        </div>
    </div>
</x-dynamic-component>