/** @odoo-module
 *
 * PosStore patch — laundry order type / attributes / delivery popup chain.
 *
 * Two storage paths kept in sync:
 *   1. pos.laundryContext (OWL reactive) — drives panel + receipt re-render.
 *   2. order.laundry_* primitives        — drives serializeForORM payload.
 *
 * STRICT CONTRACT:
 *   - Does NOT override PosOrder.setup beyond seeding scalar defaults
 *     (see pos_order_patch.js)
 *   - Does NOT touch order.lines, pricing, or taxes
 *   - Does NOT push laundry fields through _load_pos_data_fields
 */
import { patch } from "@web/core/utils/patch";
import { _t } from "@web/core/l10n/translation";
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { LaundryContextStore } from "@laundry_management/js/laundry_context_store";
import { LaundryQuickCreatePartner } from "@laundry_management/js/quick_create_partner";
import { LaundryOrdersViewPopup } from "@laundry_management/js/view_laundry_orders";
import { LaundryOrderTypePopup } from "@laundry_management/js/popups/laundry_order_type_popup";
import { LaundryOrderAttributePopup } from "@laundry_management/js/popups/laundry_order_attribute_popup";
import { LaundryDeliveryDetailsPopup } from "@laundry_management/js/popups/laundry_delivery_details_popup";

function hasLaundryProduct(order) {
    return order.lines.some(
        (line) => line.product_id?.product_tmpl_id?.is_laundry_service
    );
}

function isLaundryServiceLine(line) {
    const tmpl = line.product_id?.product_tmpl_id;
    return tmpl?.is_laundry_service && !tmpl?.is_laundry_settlement;
}

function toId(v) {
    if (!v) return false;
    if (typeof v === "number") return v;
    if (typeof v === "object" && Number.isInteger(v.id)) return v.id;
    return false;
}

function toIds(arr) {
    if (!Array.isArray(arr)) return [];
    const seen = new Set();
    for (const v of arr) {
        const id = toId(v);
        if (id) seen.add(id);
    }
    return [...seen];
}

function normalizeDateTime(value) {
    if (!value || typeof value !== "string") return false;

    let v = value;

    // Remove timezone like +03:00 or -03:00
    const tzMatch = v.match(/([+-]\d{2}:\d{2})$/);
    if (tzMatch) {
        v = v.replace(tzMatch[0], "");
    }

    // Remove Z
    if (v.endsWith("Z")) {
        v = v.slice(0, -1);
    }

    // Remove milliseconds
    if (v.includes(".")) {
        v = v.split(".")[0];
    }

    // Replace T with space
    v = v.replace("T", " ");

    // Ensure seconds exist
    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(v)) {
        v += ":00";
    }

    return v;
}

patch(PosStore.prototype, {
    setup(...args) {
        const result = super.setup(...args);
        if (!this.laundryContext) {
            this.laundryContext = new LaundryContextStore();
        }
        if (this._settleDuesInFlight === undefined) {
            this._settleDuesInFlight = false;
        }
        return result;
    },

    /**
     * Defensive override for the core configurator helper.
     *
     * In Odoo 19 core, `doHaveConflictWith` accesses `value.id` and
     * `selectedValues.map(v => v.id)` with no null-check. When the
     * loaded POS data has even ONE incomplete PTAV reference (most
     * commonly after a partial product re-seed, attribute rename, or
     * orphan ir_model_data row), the configurator render throws:
     *
     *     TypeError: can't access property "id", value is undefined
     *         at doHaveConflictWith
     *
     * The exception leaves the POS in an unrecoverable state mid-popup
     * and the cashier can't add the laundry product at all.
     *
     * This guard filters out null/undefined entries and short-circuits
     * to "no conflict" when `value` itself is malformed. It changes
     * NO business behavior — exclusions still apply for valid PTAVs.
     */
    doHaveConflictWith(value, selectedValues) {
        if (!value || value.id === undefined) {
            return false;
        }
        const safeSelected = Array.isArray(selectedValues)
            ? selectedValues.filter((v) => v && v.id !== undefined)
            : [];
        return super.doHaveConflictWith(value, safeSelected);
    },

    // ─── POS Mode (settle_due isolation) ──────────────────────────────────
    isSettleDueOrder(order) {
        return !!order?.uiState?.is_laundry_settle_due;
    },

    /** Reactive accessor for templates: "sale" | "settle_due". */
    get currentLaundryMode() {
        return this.isSettleDueOrder(this.getOrder()) ? "settle_due" : "sale";
    },

    _markSettleDue(order) {
        if (order?.uiState) {
            order.uiState.is_laundry_settle_due = true;
        }
    },

    _clearSettleDue(order) {
        if (order?.uiState) {
            order.uiState.is_laundry_settle_due = false;
        }
    },

    /**
     * Cashier-initiated exit from settle mode.
     * ALWAYS drops the settlement order AND creates a fresh clean sale
     * order — never leaves the POS in a zero-order state.
     *
     * Confirmation tone differs based on what the cashier has staged:
     *   • No payment lines yet → light confirm ("Cancel settlement?").
     *   • Amount already entered → stronger confirm warning that the
     *     payment lines on this settle order will be discarded.
     * Cleanup is the same in both cases — the settle order is removed
     * locally so any payment lines on it go with it.
     */
    async exitSettleDueMode() {
        const order = this.getOrder();
        if (!this.isSettleDueOrder(order)) {
            return;
        }
        const hasPaymentLines = (order.payment_ids || []).some(
            (p) => (p.amount || 0) > 0
        );
        const dialogProps = hasPaymentLines
            ? {
                title: _t("Discard settlement payment?"),
                body: _t(
                    "Settlement has payment lines entered. Leaving now "
                    + "discards them. Continue?"
                ),
                confirmLabel: _t("Discard & Exit"),
                cancelLabel: _t("Stay"),
            }
            : {
                title: _t("Cancel settlement and return to POS?"),
                body: _t(
                    "No payment entered yet. The settlement will be "
                    + "cancelled and you will return to the product screen."
                ),
                confirmLabel: _t("Exit"),
                cancelLabel: _t("Stay"),
            };
        const confirmed = await ask(this.env.services.dialog, dialogProps);
        if (!confirmed) {
            return;
        }
        this._clearSettleDue(order);
        this.removeOrder(order, false);
        // Always hand the cashier a clean new sale order — no stranded state.
        this.addNewOrder();
        this.navigate("ProductScreen");
    },

    /**
     * Nuke every open order in the POS except an optional keeper. Used
     * when entering settle-due mode to enforce single-order isolation.
     *
     * DATA-LOSS PROTECTION: if any of the to-be-removed orders still has
     * lines, prompt ONCE before discarding. Returns true if the removal
     * completed, false if the cashier cancelled.
     */
    async _removeAllOpenOrders({ except = null } = {}) {
        const keepUuid = except?.uuid || null;
        const snapshot = this.getOpenOrders().slice();
        const doomed = snapshot.filter((o) => o.uuid !== keepUuid);
        if (!doomed.length) {
            return true;
        }
        const hasLines = doomed.some((o) => (o.lines?.length || 0) > 0);
        if (hasLines) {
            const confirmed = await ask(this.env.services.dialog, {
                title: _t("Discard Open Orders?"),
                body: _t(
                    "Starting a settlement will discard %s open order(s) that still contain items. Continue?",
                    doomed.length
                ),
                confirmLabel: _t("Continue"),
                cancelLabel: _t("Cancel"),
            });
            if (!confirmed) {
                return false;
            }
        }
        for (const o of doomed) {
            this._clearSettleDue(o);
            this.removeOrder(o, false);
        }
        return true;
    },

    /**
     * Navigation gate — every cashier action that would switch the active
     * order context (tab click, "+" new order, navbar register click)
     * must funnel through this helper FIRST.
     *
     *   targetOrder = null                → "create new order" intent
     *   targetOrder = some open order     → "switch to that order" intent
     *
     * Returns true if the navigation should proceed, false if blocked.
     * If the user confirms the exit, the active settle order is dropped
     * here so callers can proceed without further cleanup.
     */
    async confirmExitSettleIfNeeded(targetOrder) {
        const current = this.getOrder();
        if (!this.isSettleDueOrder(current)) {
            return true;
        }
        // Switching to the same settle order itself is a no-op for safety.
        if (targetOrder && targetOrder.uuid === current.uuid) {
            return true;
        }
        const confirmed = await ask(this.env.services.dialog, {
            title: _t("Exit Settle Dues Mode?"),
            body: _t(
                "You are settling dues for %s. Leaving will cancel the current settlement.",
                current.getPartner()?.name || _t("a customer")
            ),
            confirmLabel: _t("Exit"),
            cancelLabel: _t("Cancel"),
        });
        if (!confirmed) {
            return false;
        }
        this._clearSettleDue(current);
        this.removeOrder(current, false);
        return true;
    },

    /**
     * Hard guard: while a settle-due order is active, the only line that
     * can land on it is the settlement product itself. Any other product
     * click is silently rejected with a notification.
     */
    async addLineToCurrentOrder(vals, opts = {}, configure = true) {
        const order = this.getOrder();
        if (this.isSettleDueOrder(order)) {
            const tmplVal = vals?.product_tmpl_id;
            const tmpl = typeof tmplVal === "number"
                ? this.data.models["product.template"]?.get(tmplVal)
                : tmplVal;
            if (!tmpl?.is_laundry_settlement) {
                this.notification.add(
                    _t("Cannot add products while settling dues. Exit settle mode first."),
                    { type: "warning" }
                );
                return null;
            }
        }
        // Laundry configuration now runs through the CORE Odoo configurator
        // (ProductConfiguratorPopup). We don't intercept here — core's
        // handleConfigurableProduct / openConfigurator fires naturally for
        // any template whose attributes expose >1 value. We only enhance
        // the popup's presentation via XML inheritance + scoped SCSS
        // (see xml/product_configurator_popup.xml).
        return super.addLineToCurrentOrder(vals, opts, configure);
    },

    /**
     * HARD BLOCK: once a settle-due order is active, no new orders can
     * be created. No confirm — just stop. Any path that tries to create
     * a new order during settle mode is either a mistake or an escape
     * route we haven't caught upstream; either way, block it.
     */
    addNewOrder(data = {}) {
        if (this.isSettleDueOrder(this.getOrder())) {
            this.notification.add(
                _t("Finish settlement first."),
                { type: "warning" }
            );
            return null;
        }
        return super.addNewOrder(data);
    },

    /**
     * HARD CONTROL on order switching. If the active order is settle-due
     * and the target is a different order:
     *   - ask confirmation
     *   - cancel → block the switch
     *   - confirm → drop the settle order cleanly, then proceed
     *
     * The function becomes async. Native sync callers (e.g. TicketScreen)
     * do not await, but the user-facing UI gates (OrderTabs.selectFloating-
     * Order, Navbar.onClickRegister) run confirmExitSettleIfNeeded first
     * and clear settle mode before super.setOrder runs, so this patch is
     * defense-in-depth for any escape route that bypasses those gates.
     */
    async setOrder(order) {
        const current = this.getOrder();
        if (
            current &&
            this.isSettleDueOrder(current) &&
            order &&
            order.uuid !== current.uuid
        ) {
            const confirmed = await ask(this.env.services.dialog, {
                title: _t("Exit Settle Dues Mode?"),
                body: _t("The current settlement will be cancelled."),
                confirmLabel: _t("Exit"),
                cancelLabel: _t("Cancel"),
            });
            if (!confirmed) {
                return;
            }
            this._clearSettleDue(current);
            this.removeOrder(current, false);
        }
        return super.setOrder(order);
    },

    /** Always release settle-state when an order is dropped (success OR exit). */
    removeOrder(order, removeFromServer = true) {
        if (order?.uiState) {
            order.uiState.is_laundry_settle_due = false;
        }
        return super.removeOrder(order, removeFromServer);
    },

    /**
     * Fail-safe navigation: while a settle-due order is active, the ONLY
     * legitimate screen is PaymentScreen. Any other navigation target is
     * rewritten to PaymentScreen so the cashier never lands on a screen
     * that would corrupt the settlement context (no blank screen, no
     * stranded products on the wrong order).
     *
     * BUT: when the cashier presses Back (PaymentScreen → ProductScreen),
     * silently snapping them back used to feel like a trap. We now also
     * SCHEDULE the existing exit-confirmation dialog on the next
     * microtask. The cashier sees PaymentScreen for one frame, then a
     * clear "Cancel settlement?" dialog. Confirming → settle order
     * removed, fresh sale order, ProductScreen. Cancelling → still on
     * PaymentScreen, can keep settling. No new buttons, no XML changes.
     *
     * Legit exit paths (exitSettleDueMode itself, removeOrder during
     * payment success) clear the settle flag BEFORE calling navigate,
     * so they pass through unaffected and never trigger the prompt.
     */
    navigate(routeName, routeParams = {}) {
        const current = this.getOrder();
        if (
            this.isSettleDueOrder(current) &&
            routeName !== "PaymentScreen"
        ) {
            // Schedule exit-confirmation but don't await — keep navigate
            // sync so callers that don't await still work cleanly.
            Promise.resolve().then(() => this.exitSettleDueMode());
            return super.navigate("PaymentScreen", { orderUuid: current.uuid });
        }
        return super.navigate(routeName, routeParams);
    },

    /** Mirror a patch into BOTH the reactive store and the order primitives. */
    _writeLaundryContext(order, patchObj) {
        if (!order) {
            return;
        }
        // Reactive store — drives panel + receipt re-render.
        this.laundryContext.set(order.uuid, patchObj);
        // Order primitives — drive serializeForORM payload.
        if ("type_id" in patchObj) {
            order.laundry_order_type_id = patchObj.type_id || false;
        }
        if ("attribute_ids" in patchObj) {
            order.laundry_order_attribute_ids = Array.isArray(patchObj.attribute_ids)
                ? patchObj.attribute_ids.slice()
                : [];
        }
        if ("is_delivery" in patchObj) {
            order.laundry_is_delivery = !!patchObj.is_delivery;
        }
        if ("delivery_address" in patchObj) {
            order.laundry_delivery_address = patchObj.delivery_address || false;
        }
        if ("delivery_scheduled_at" in patchObj) {
            order.laundry_delivery_scheduled_at =
                patchObj.delivery_scheduled_at || false;
        }
    },

    async postSyncAllOrders(orders) {
        await super.postSyncAllOrders(orders);
        for (const order of orders) {
            if (order.laundry_order_id) {
                this.notification.add(_t("Laundry Order Created"), {
                    type: "success",
                });
            }
        }
    },

    async pay() {
        const currentOrder = this.getOrder();
        if (!currentOrder) {
            return;
        }
        // Settle-due orders already have a partner (gated in settleLaundryDues).
        if (
            !this.isSettleDueOrder(currentOrder) &&
            hasLaundryProduct(currentOrder) &&
            !currentOrder.getPartner()
        ) {
            const confirmed = await ask(this.env.services.dialog, {
                title: _t("Customer Required"),
                body: _t(
                    "This order contains laundry items. Please select or create a customer."
                ),
            });
            if (confirmed) {
                const partner = await this.selectPartner();
                if (!partner) {
                    return;
                }
            } else {
                return;
            }
        }
        return super.pay();
    },

    async editPartner(partner) {
        if (partner) {
            return super.editPartner(partner);
        }
        const result = await makeAwaitable(this.dialog, LaundryQuickCreatePartner, {});
        if (!result) {
            return false;
        }
        const existing = await this.data.call(
            "res.partner",
            "laundry_find_by_phone",
            [result.phone]
        );
        if (existing) {
            const useExisting = await ask(this.env.services.dialog, {
                title: _t("Customer Already Exists"),
                body: _t(
                    'A customer "%s" already exists with this phone number. Use existing customer?',
                    existing.name
                ),
            });
            if (useExisting) {
                const partners = await this.data.read("res.partner", [existing.id]);
                return partners[0] || false;
            }
            return false;
        }
        const partnerId = await this.data.call(
            "res.partner",
            "laundry_quick_create",
            [{ name: result.name, phone: result.phone, street: result.street || "" }]
        );
        const partners = await this.data.read("res.partner", [partnerId]);
        return partners[0] || false;
    },

    async settleLaundryDues() {
        // Re-entrancy guard — defends against double-clicks while we
        // hit the server for dues / before the order is materialized.
        if (this._settleDuesInFlight) {
            return;
        }
        this._settleDuesInFlight = true;
        try {
            const currentOrder = this.getOrder();
            const partner = currentOrder?.getPartner();
            if (!partner) {
                this.notification.add(_t("Please select a customer first."), {
                    type: "warning",
                });
                const selectedPartner = await this.selectPartner();
                if (!selectedPartner) {
                    return;
                }
                this._settleDuesInFlight = false;
                return this.settleLaundryDues();
            }

            // Reuse an existing settle-due order for the same partner
            // instead of creating a duplicate one. Whether we reuse or
            // create fresh, SINGLE-ORDER MODE must hold: every other
            // open order is removed before we navigate.
            const existing = this.getOpenOrders().find(
                (o) =>
                    this.isSettleDueOrder(o) &&
                    o.getPartner()?.id === partner.id
            );
            if (existing) {
                const ok = await this._removeAllOpenOrders({ except: existing });
                if (!ok) {
                    return;
                }
                // Our patched setOrder short-circuits when target === current
                // settle order; safe to call here.
                this.setOrder(existing);
                this.navigate("PaymentScreen", { orderUuid: existing.uuid });
                return;
            }

            const dues = await this.data.call(
                "res.partner",
                "get_laundry_dues",
                [partner.id]
            );

            if (!dues || dues.total_due <= 0) {
                this.notification.add(
                    _t('"%s" has no outstanding laundry dues.', partner.name),
                    { type: "info" }
                );
                return;
            }

            const settlementProduct = this.models["product.product"]
                .getAll()
                .find((p) => p.product_tmpl_id?.is_laundry_settlement);

            if (!settlementProduct) {
                this.notification.add(
                    _t("Settlement product not configured. Please check laundry settings."),
                    { type: "danger" }
                );
                return;
            }

            // ─── SINGLE-ORDER MODE ─────────────────────────────────────
            // Nuke every open order BEFORE creating the settle order.
            // Data-loss protection lives inside _removeAllOpenOrders —
            // it prompts once if any order has lines and returns false
            // if the cashier cancels.
            const ok = await this._removeAllOpenOrders();
            if (!ok) {
                return;
            }

            const order = this.addNewOrder();

            // Mark BEFORE adding the settlement line so any concurrent
            // attempt to add other products to this order is blocked.
            // The settlement product itself is whitelisted in
            // addLineToCurrentOrder.
            this._markSettleDue(order);
            order.setPartner(partner);

            await this.addLineToCurrentOrder(
                {
                    product_id: settlementProduct,
                    product_tmpl_id: settlementProduct.product_tmpl_id,
                    price_unit: dues.total_due,
                    qty: 1,
                },
                {},
                false
            );

            this.navigate("PaymentScreen", {
                orderUuid: order.uuid,
            });
        } finally {
            this._settleDuesInFlight = false;
        }
    },

    /** True iff order has at least one laundry-service line (excludes settlement). */
    orderHasLaundryServiceLine(order) {
        return !!order?.lines?.some(isLaundryServiceLine);
    },

    async runLaundryOrderTypeFlow(order) {
        if (!order) {
            return;
        }
        const config = this.config;

        // Step 1 — Main type
        const types = (this.models["laundry.order.type"]?.getAll() || [])
            .slice()
            .sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
        if (!types.length) {
            this.notification.add(
                _t("No laundry order types configured. Ask a manager to add some."),
                { type: "warning" }
            );
            if (config.require_laundry_order_type) {
                return;
            }
        }

        const partner = order.getPartner();
        const ctx = this.laundryContext.get(order.uuid);
        const defaultTypeId =
            toId(ctx.type_id) ||
            toId(order.laundry_order_type_id) ||
            toId(partner?.default_laundry_order_type_id) ||
            toId(config.default_laundry_order_type_id) ||
            false;

        const chosenType = await makeAwaitable(this.dialog, LaundryOrderTypePopup, {
            types: types,
            defaultTypeId: defaultTypeId,
            showIcons: !!config.show_order_type_icons,
            allowSkip: !config.require_laundry_order_type,
            title: _t("Select Order Type"),
        });
        if (chosenType === undefined) {
            return; // user closed without confirming
        }

        this._writeLaundryContext(order, {
            type_id: chosenType ? chosenType.id : false,
        });

        // Step 2 — Attributes
        let chosenAttributes = [];
        if (config.enable_laundry_attributes) {
            const allAttrs = (this.models["laundry.order.attribute"]?.getAll() || [])
                .slice()
                .sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
            if (allAttrs.length) {
                const suggestedIds = toIds(chosenType?.attribute_ids);
                const partnerDefaultIds = toIds(partner?.default_laundry_attribute_ids);
                const preselected = [
                    ...new Set([...suggestedIds, ...partnerDefaultIds]),
                ];
                const result = await makeAwaitable(
                    this.dialog,
                    LaundryOrderAttributePopup,
                    {
                        attributes: allAttrs,
                        preselectedIds: preselected,
                        title: _t("Select Attributes (optional)"),
                    }
                );
                if (result !== undefined) {
                    chosenAttributes = result;
                }
            }
        }
        this._writeLaundryContext(order, {
            attribute_ids: chosenAttributes.map((a) => a.id),
        });

        // Step 3 — Delivery details
        const typeIsDelivery = !!chosenType?.is_delivery;
        const attrIsDelivery = chosenAttributes.some((a) => a.is_delivery_related);
        const isDelivery = typeIsDelivery || attrIsDelivery;
        this._writeLaundryContext(order, { is_delivery: isDelivery });

        if (isDelivery && config.require_delivery_details_if_needed) {
            const requireAddress =
                !!chosenType?.requires_address || attrIsDelivery;
            const requireTime = !!chosenType?.requires_scheduled_time;
            const result = await makeAwaitable(
                this.dialog,
                LaundryDeliveryDetailsPopup,
                {
                    defaultAddress: ctx.delivery_address || "",
                    defaultScheduledAt: ctx.delivery_scheduled_at || "",
                    requireAddress: requireAddress,
                    requireScheduledTime: requireTime,
                }
            );
            if (result) {
                this._writeLaundryContext(order, {
                    delivery_address: result.address || false,
                    delivery_scheduled_at: normalizeDateTime(result.scheduledAt) || false,
                });
            }
        } else if (!isDelivery) {
            this._writeLaundryContext(order, {
                delivery_address: false,
                delivery_scheduled_at: false,
            });
        }
    },

    async editLaundryOrderType() {
        const order = this.getOrder();
        if (!order) {
            return;
        }
        if (!this.config?.allow_change_laundry_order_type_before_payment) {
            this.notification.add(_t("Changing order type is disabled."), {
                type: "warning",
            });
            return;
        }
        await this.runLaundryOrderTypeFlow(order);
    },

    /**
     * Entry point invoked by the laundry-orders popup's "Collect Payment"
     * button. Reuses the existing settleLaundryDues flow end-to-end so
     * the well-tested settle-due isolation, navigation guards, and
     * accounting RPC are inherited verbatim — no parallel collection
     * pipeline, no new payment logic, no accounting changes.
     *
     * Steps:
     *   1. Resolve the partner from POS data.
     *   2. Make sure the active POS order has that partner set so
     *      settleLaundryDues doesn't pop the partner-selection dialog
     *      again.
     *   3. Hand off to settleLaundryDues — which:
     *        • drops every other open order (with a confirm if any
     *          carry lines) so the cashier is in a single-order state,
     *        • spins up the settle-due context order,
     *        • hides Customer Account / pay-later methods,
     *        • navigates to PaymentScreen,
     *        • on Validate calls settle_laundry_dues_rpc → cash bank
     *          statement OR account.payment + reconcile + FIFO
     *          distribution across the customer's open laundry orders
     *          (this order included).
     *
     * Idempotent: if a settle-due is already active for the same
     * partner, settleLaundryDues reuses it instead of creating a
     * duplicate.
     */
    async collectLaundryOrderPayment(partnerId) {
        if (!partnerId) {
            this.notification.add(
                _t("Cannot collect payment: customer not specified."),
                { type: "warning" }
            );
            return;
        }
        const partner = this.models["res.partner"].get(partnerId);
        if (!partner) {
            this.notification.add(
                _t("Customer not loaded in POS data. Refresh POS and retry."),
                { type: "danger" }
            );
            return;
        }
        let currentOrder = this.getOrder();
        if (!currentOrder) {
            currentOrder = this.addNewOrder();
        }
        const cur = currentOrder?.getPartner();
        if (!cur || cur.id !== partner.id) {
            currentOrder?.setPartner(partner);
        }
        await this.settleLaundryDues();
    },

    async viewLaundryOrders() {
        const currentOrder = this.getOrder();
        let partner = currentOrder?.getPartner();
        if (!partner) {
            partner = await this.selectPartner();
            if (!partner) {
                return;
            }
        }
        // Popup owns its own data lifecycle — fetches on mount + search,
        // refetches the affected order after each workflow action.
        this.dialog.add(LaundryOrdersViewPopup, {
            partnerId: partner.id,
            partnerName: partner.name,
            // `mobile` is optional in some Odoo distributions — guard.
            partnerPhone: partner.phone || partner.mobile || "",
        });
    },
});
