<?php
/**
 * Booking Control Centre
 * ----------------------
 * A single command centre for a booking: search by id / name / agent / dates,
 * then drive every booking action from one tabbed panel — status (cancel /
 * reinstate / check-in / no-show), colour coding, rooming, rates, extras,
 * folios, proformas & invoicing, communications, and a live activity timeline.
 *
 * It is deliberately self-contained: the same file serves the page (GET) and
 * every AJAX action (GET/POST with ?api=...). It reuses the system's real
 * endpoints where they exist (reservation_form.php, proforma_invoice.php,
 * move_booking.php, email_proforma.php) and performs safe direct DB updates
 * for status / colour / quick fields.
 *
 * PHP 7.2 compatible. No match(), str_contains(), or nullsafe operators.
 */


require_once __DIR__ . '/../../includes/bootstrap.php';

/*
   Shared payment engine.

   Payment posting, folio resolution, the ROE conversion, the 3% POS/Card
   surcharge and the optional gratuity all live in payment_support.php - the
   SAME file payments.php uses. Nothing about taking a payment is
   re-implemented here.

   That matters: this module and payments.php would otherwise each own a copy
   of the surcharge and ROE maths, and they would drift. fetch_rate_plan()
   already proved how that ends - three copies, guarded by function_exists(),
   silently disagreeing about what a booking costs depending on include order.
   Money in and out of a folio is not the place to repeat it.
*/
require_once __DIR__ . '/../../includes/payment_support.php';

/*
   Shared rate engine.

   The Control Centre changes rooming dates/batonka, which changes what should be
   priced, so it must reprice using the SAME rules as reservation_form.php
   rather than re-implementing them.

   fetch_rate_plan() may already exist (functions.php defines one, and
   rate_support.php defines another - both guard with function_exists, so
   whichever loads first wins). We only hunt for rate_support.php when nothing
   has defined it yet. Both known signatures accept
   ($pdo, $roomTypeId, $agentId, $stayDate, $mealPlan), so our 5-arg call is
   compatible with either.
*/
if (!function_exists('fetch_rate_plan') || !function_exists('calculate_room_night_total')) {
    foreach ([
        __DIR__ . '/rate_support.php',
        __DIR__ . '/../../includes/rate_support.php',
        __DIR__ . '/../includes/rate_support.php',
        __DIR__ . '/../reservations/rate_support.php',
        __DIR__ . '/../../rate_support.php',
        __DIR__ . '/../../includes/functions.php',
    ] as $ratePath) {
        if (is_file($ratePath)) {
            require_once $ratePath;
            if (function_exists('fetch_rate_plan') && function_exists('calculate_room_night_total')) {
                break;
            }
        }
    }
}
require_login();

/* --------------------------------------------------------------------------
   Small helpers
-------------------------------------------------------------------------- */
if (!function_exists('bcc_h')) {
    function bcc_h($v): string
    {
        return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
    }
}

function bcc_column_exists(PDO $pdo, string $table, string $column): bool
{
    static $cache = [];
    $key = $table . '.' . $column;
    if (array_key_exists($key, $cache)) {
        return $cache[$key];
    }
    // Prefer SHOW COLUMNS (works without information_schema privileges).
    try {
        $safeTable = str_replace('`', '``', $table);
        $stmt = $pdo->query("SHOW COLUMNS FROM `{$safeTable}`");
        $found = false;
        if ($stmt) {
            foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) {
                $name = $col['Field'] ?? ($col['field'] ?? '');
                if (strcasecmp((string)$name, $column) === 0) { $found = true; break; }
            }
            $cache[$key] = $found;
            return $found;
        }
    } catch (Throwable $e) {
        // fall through to information_schema
    }
    try {
        $stmt = $pdo->prepare("
            SELECT COUNT(*)
            FROM information_schema.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = :t
              AND COLUMN_NAME = :c
        ");
        $stmt->execute([':t' => $table, ':c' => $column]);
        $cache[$key] = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable $e) {
        $cache[$key] = false;
    }
    return $cache[$key];
}

function bcc_table_exists(PDO $pdo, string $table): bool
{
    static $cache = [];
    if (array_key_exists($table, $cache)) {
        return $cache[$table];
    }
    try {
        $safeTable = str_replace('`', '``', $table);
        $stmt = $pdo->query("SHOW TABLES LIKE '" . str_replace("'", "''", $table) . "'");
        if ($stmt) {
            $cache[$table] = ($stmt->fetch() !== false);
            return $cache[$table];
        }
    } catch (Throwable $e) {
        // fall through
    }
    try {
        $stmt = $pdo->prepare("
            SELECT COUNT(*)
            FROM information_schema.TABLES
            WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t
        ");
        $stmt->execute([':t' => $table]);
        $cache[$table] = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable $e) {
        $cache[$table] = false;
    }
    return $cache[$table];
}

function bcc_current_user_id(): int
{
    return (int)($_SESSION['user_id'] ?? $_SESSION['id'] ?? 0);
}

/* =========================================================================
   NIGHTSBRIDGE REAL-TIME AVAILABILITY PUSH (Booking Control Centre)
   -------------------------------------------------------------------------
   Almost every mutating action in this file returns through bcc_json(), which
   exits immediately. Rather than bolt a push call onto each of the ~8 inventory-
   changing actions (reinstate, set_status, cancel_policy, add/save/delete
   rooming row, release_rooms, apply_juggle) - fragile, since each has a
   different success-exit point - we centralise it:

     1. An inventory-changing action calls bcc_mark_nb_dirty($orderId) right
        before its SUCCESS bcc_json().
     2. bcc_json() fires ONE push for that order just before it exits.

   This means the push can never be bypassed by an early exit, and can never
   double-fire. All failures are swallowed - a slow or dead push must never
   affect the Control Centre action, which has already committed. The 02:00
   nightly flush remains the backstop.

   Without this, BCC cancels/reinstates/rooming edits only reached Nightsbridge
   on the nightly sweep: reinstate/add-room could OVERBOOK (NB keeps selling a
   room BCC just re-consumed); cancel/release could UNDER-SELL (freed room not
   re-opened). Same proven pattern as move_booking.php and reservation_form.php.
   ========================================================================= */
if (!function_exists('bcc_mark_nb_dirty')) {
    function bcc_mark_nb_dirty(int $orderId): void
    {
        if ($orderId > 0) {
            $GLOBALS['bcc_nb_dirty_order_id'] = $orderId;
        }
    }
}

if (!function_exists('bcc_nightsbridge_availability_push')) {
    function bcc_nightsbridge_availability_push(int $orderId): void
    {
        try {
            if ($orderId <= 0) {
                return;
            }
            if (!isset($GLOBALS['pdo']) || !($GLOBALS['pdo'] instanceof PDO)) {
                return;
            }
            $pdo = $GLOBALS['pdo'];

            // Resolve the order's property and the affected room type(s).
            $propertyId = 0;
            try {
                $os = $pdo->prepare("SELECT property_id FROM orders WHERE id = :id LIMIT 1");
                $os->execute([':id' => $orderId]);
                $propertyId = (int)$os->fetchColumn();
            } catch (Throwable $e) {
                return; // can't resolve property - let the nightly flush cover it
            }

            // Only push for the Nightsbridge-connected property.
            if (defined('NIGHTSBRIDGE_PROPERTY_ID') && $propertyId !== (int)NIGHTSBRIDGE_PROPERTY_ID) {
                return;
            }

            // Affected room types: COALESCE(physical room type, rooming's own
            // room_type_id) so it resolves whether or not a physical room is
            // currently assigned (a released/held row may have room_id = 0).
            $typeIds = array();
            try {
                $rmgHasType = bcc_column_exists($pdo, 'order_rooming', 'room_type_id');
                $rtidExpr = $rmgHasType
                    ? 'COALESCE(r.room_type_id, rmg.room_type_id)'
                    : 'r.room_type_id';
                $ts = $pdo->prepare("
                    SELECT DISTINCT {$rtidExpr} AS rtid
                    FROM order_rooming rmg
                    LEFT JOIN rooms r ON r.id = rmg.room_id
                    WHERE rmg.order_id = :id
                ");
                $ts->execute([':id' => $orderId]);
                foreach ($ts->fetchAll(PDO::FETCH_COLUMN) as $rtid) {
                    if ((int)$rtid > 0) {
                        $typeIds[] = (int)$rtid;
                    }
                }
            } catch (Throwable $e) {
                $typeIds = array(); // fall back to a full push below
            }

            // Load the push library if not already present.
            if (!function_exists('nb_run_inventory_push')) {
                $candidates = array(
                    __DIR__ . '/../../api/nightsbridge_inventory_push.php',
                    __DIR__ . '/../api/nightsbridge_inventory_push.php',
                    rtrim((string)(isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''), '/') . '/rooms/api/nightsbridge_inventory_push.php',
                );
                foreach ($candidates as $lib) {
                    if (is_file($lib)) {
                        if (!defined('NB_INVENTORY_PUSH_LIB')) {
                            define('NB_INVENTORY_PUSH_LIB', true);
                        }
                        require_once $lib;
                        break;
                    }
                }
            }
            if (!function_exists('nb_run_inventory_push')) {
                return; // library not deployed - nightly flush covers it
            }

            // Merge in any explicitly-stashed types (e.g. a deleted rooming row
            // whose row no longer exists to be resolved from order_rooming).
            if (!empty($GLOBALS['bcc_nb_extra_type_ids']) && is_array($GLOBALS['bcc_nb_extra_type_ids'])) {
                foreach ($GLOBALS['bcc_nb_extra_type_ids'] as $extraTid) {
                    if ((int)$extraTid > 0) {
                        $typeIds[] = (int)$extraTid;
                    }
                }
                $GLOBALS['bcc_nb_extra_type_ids'] = array(); // clear
            }

            $ids = array_values(array_unique(array_filter(array_map('intval', $typeIds))));
            nb_run_inventory_push($pdo, $ids ?: null, 'booking_control_centre', false);
        } catch (Throwable $e) {
            // Intentionally silent - the action has already committed.
        }
    }
}

function bcc_json($data): void
{
    /*
       If an inventory-changing action marked this order dirty, push the freed/
       consumed availability to Nightsbridge in real time before we exit. Guarded
       so it fires at most once per request and never affects the response.
    */
    if (!empty($GLOBALS['bcc_nb_dirty_order_id'])) {
        $dirtyOrderId = (int)$GLOBALS['bcc_nb_dirty_order_id'];
        $GLOBALS['bcc_nb_dirty_order_id'] = 0; // clear first, so a push failure can't loop
        bcc_nightsbridge_availability_push($dirtyOrderId);
    }

    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function bcc_money(float $n, string $symbol = '$'): string
{
    return $symbol . number_format($n, 2);
}


/* Resolve a staff/user id to a clear display name for booking ownership. */
function bcc_user_display_name(PDO $pdo, int $userId): string
{
    if ($userId <= 0) {
        return 'Unknown user';
    }

    foreach (['users', 'user', 'staff', 'employees'] as $table) {
        if (!bcc_table_exists($pdo, $table) || !bcc_column_exists($pdo, $table, 'id')) {
            continue;
        }

        $parts = [];
        if (bcc_column_exists($pdo, $table, 'name')) $parts[] = "NULLIF(TRIM(name),'')";
        if (bcc_column_exists($pdo, $table, 'fullname')) $parts[] = "NULLIF(TRIM(fullname),'')";
        if (bcc_column_exists($pdo, $table, 'full_name')) $parts[] = "NULLIF(TRIM(full_name),'')";
        if (bcc_column_exists($pdo, $table, 'firstname')) {
            if (bcc_column_exists($pdo, $table, 'lastname')) {
                $parts[] = "NULLIF(TRIM(CONCAT(COALESCE(firstname,''),' ',COALESCE(lastname,''))),'')";
            } else {
                $parts[] = "NULLIF(TRIM(firstname),'')";
            }
        }
        if (bcc_column_exists($pdo, $table, 'username')) $parts[] = "NULLIF(TRIM(username),'')";
        if (bcc_column_exists($pdo, $table, 'email')) $parts[] = "NULLIF(TRIM(email),'')";
        if (!$parts) continue;

        try {
            $st = $pdo->prepare("SELECT COALESCE(" . implode(',', $parts) . ") FROM `{$table}` WHERE id = :id LIMIT 1");
            $st->execute([':id' => $userId]);
            $name = trim((string)$st->fetchColumn());
            if ($name !== '') return $name;
        } catch (Throwable $e) {
            // Try the next known user table.
        }
    }

    return 'User #' . $userId;
}

/* Log an action if the system's action log exists (best-effort). */
function bcc_log(PDO $pdo, int $orderId, string $action, string $detail): void
{
    if (!bcc_table_exists($pdo, 'order_action_log')) {
        return;
    }
    try {
        $stmt = $pdo->prepare("
            INSERT INTO order_action_log (order_id, user_id, action, detail, created_at)
            VALUES (:order_id, :user_id, :action, :detail, NOW())
        ");
        $stmt->execute([
            ':order_id' => $orderId,
            ':user_id'  => bcc_current_user_id(),
            ':action'   => $action,
            ':detail'   => mb_substr($detail, 0, 500),
        ]);
    } catch (Throwable $e) {
        // non-fatal
    }
}


/*
 * Keep a direct confirmation stamp on the booking as well as the normal
 * order_action_log timeline entry. Older installations may not yet have these
 * columns, so add them best-effort and continue using the action log if ALTER
 * permission is unavailable.
 */
function bcc_ensure_confirmation_audit_columns(PDO $pdo): void
{
    if (!bcc_column_exists($pdo, 'orders', 'confirmed_at')) {
        try {
            $pdo->exec("ALTER TABLE orders ADD COLUMN confirmed_at DATETIME NULL");
        } catch (Throwable $e) {
            // order_action_log still records the confirmation.
        }
    }

    if (!bcc_column_exists($pdo, 'orders', 'confirmed_by')) {
        try {
            $pdo->exec("ALTER TABLE orders ADD COLUMN confirmed_by INT UNSIGNED NULL");
        } catch (Throwable $e) {
            // order_action_log still records the confirming user.
        }
    }
}


/* --------------------------------------------------------------------------
   Extras paid by an agent other than the booking agent
-------------------------------------------------------------------------- */
function bcc_ensure_extra_agent_payer_table(PDO $pdo): void
{
    try {
        $pdo->exec("CREATE TABLE IF NOT EXISTS booking_extra_agent_payers (
            transaction_id BIGINT UNSIGNED NOT NULL,
            order_id BIGINT UNSIGNED NOT NULL,
            agent_id BIGINT UNSIGNED NOT NULL,
            folio_id BIGINT UNSIGNED NOT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (transaction_id),
            KEY idx_beap_order (order_id),
            KEY idx_beap_agent (agent_id),
            KEY idx_beap_folio (folio_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");

        /*
         * Compatibility with reservation_form.php / extras.php installations
         * that use the newer column names. Keep both naming schemes available,
         * but never reuse one PDO placeholder for two columns.
         */
        $columns = [
            'transaction_id'       => '`transaction_id` BIGINT UNSIGNED NULL',
            'folio_transaction_id' => '`folio_transaction_id` BIGINT UNSIGNED NULL',
            'order_id'             => '`order_id` BIGINT UNSIGNED NOT NULL DEFAULT 0',
            'agent_id'             => '`agent_id` BIGINT UNSIGNED NULL',
            'payer_agent_id'       => '`payer_agent_id` BIGINT UNSIGNED NULL',
            'folio_id'             => '`folio_id` BIGINT UNSIGNED NOT NULL DEFAULT 0',
            'service_id'           => '`service_id` BIGINT UNSIGNED NULL',
            'created_at'           => '`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP',
        ];
        foreach ($columns as $name => $definition) {
            if (!bcc_column_exists($pdo, 'booking_extra_agent_payers', $name)) {
                try {
                    $pdo->exec("ALTER TABLE booking_extra_agent_payers ADD COLUMN {$definition}");
                } catch (Throwable $ignore) {
                    // Continue; the action below will report a real schema error.
                }
            }
        }

        // Synchronise legacy and modern aliases for rows already saved.
        if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id')
            && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'folio_transaction_id')) {
            $pdo->exec("UPDATE booking_extra_agent_payers
                       SET transaction_id = COALESCE(transaction_id, folio_transaction_id),
                           folio_transaction_id = COALESCE(folio_transaction_id, transaction_id)
                       WHERE transaction_id IS NULL OR folio_transaction_id IS NULL");
        }
        if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'agent_id')
            && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'payer_agent_id')) {
            $pdo->exec("UPDATE booking_extra_agent_payers
                       SET agent_id = COALESCE(agent_id, payer_agent_id),
                           payer_agent_id = COALESCE(payer_agent_id, agent_id)
                       WHERE agent_id IS NULL OR payer_agent_id IS NULL");
        }
    } catch (Throwable $e) {
        // The API action will return a clear message if the table cannot be used.
    }
}

function bcc_save_extra_agent_payer(PDO $pdo, int $transactionId, int $orderId, int $agentId, int $folioId, int $serviceId = 0): void
{
    $cols = [];
    $vals = [];
    $params = [];

    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id')) {
        $cols[] = 'transaction_id';
        $vals[] = ':legacy_transaction_id';
        $params[':legacy_transaction_id'] = $transactionId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'folio_transaction_id')) {
        $cols[] = 'folio_transaction_id';
        $vals[] = ':folio_transaction_id';
        $params[':folio_transaction_id'] = $transactionId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'order_id')) {
        $cols[] = 'order_id';
        $vals[] = ':order_id';
        $params[':order_id'] = $orderId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'agent_id')) {
        $cols[] = 'agent_id';
        $vals[] = ':legacy_agent_id';
        $params[':legacy_agent_id'] = $agentId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'payer_agent_id')) {
        $cols[] = 'payer_agent_id';
        $vals[] = ':payer_agent_id';
        $params[':payer_agent_id'] = $agentId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'folio_id')) {
        $cols[] = 'folio_id';
        $vals[] = ':folio_id';
        $params[':folio_id'] = $folioId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'service_id')) {
        $cols[] = 'service_id';
        $vals[] = ':service_id';
        $params[':service_id'] = $serviceId > 0 ? $serviceId : null;
    }

    if (!$cols) {
        throw new RuntimeException('booking_extra_agent_payers has no supported columns.');
    }

    // Remove either alias first so an edit/retry cannot create a duplicate row.
    $where = [];
    $deleteParams = [];
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id')) {
        $where[] = 'transaction_id = :delete_legacy_transaction_id';
        $deleteParams[':delete_legacy_transaction_id'] = $transactionId;
    }
    if (bcc_column_exists($pdo, 'booking_extra_agent_payers', 'folio_transaction_id')) {
        $where[] = 'folio_transaction_id = :delete_folio_transaction_id';
        $deleteParams[':delete_folio_transaction_id'] = $transactionId;
    }
    if ($where) {
        $pdo->prepare('DELETE FROM booking_extra_agent_payers WHERE ' . implode(' OR ', $where))
            ->execute($deleteParams);
    }

    $sql = 'INSERT INTO booking_extra_agent_payers (`' . implode('`,`', $cols) . '`) VALUES (' . implode(',', $vals) . ')';
    $pdo->prepare($sql)->execute($params);
}

function bcc_agent_name(PDO $pdo, int $agentId): string
{
    if ($agentId <= 0 || !bcc_table_exists($pdo, 'agents')) return '';
    try {
        $st = $pdo->prepare("SELECT TRIM(CONCAT(COALESCE(firstname,''),' ',COALESCE(lastname,''))) FROM agents WHERE id=:id LIMIT 1");
        $st->execute([':id'=>$agentId]);
        return trim((string)$st->fetchColumn());
    } catch (Throwable $e) { return ''; }
}

function bcc_get_or_create_other_agent_folio(PDO $pdo, int $orderId, int $propertyId, int $agentId): int
{
    $agentName = bcc_agent_name($pdo, $agentId);
    if ($agentName === '') throw new RuntimeException('Selected paying agent was not found.');
    $folioName = 'Other Payer - ' . $agentName;
    $hasType = bcc_column_exists($pdo, 'folios', 'folio_type');

    $sql = "SELECT id FROM folios WHERE order_id=:oid AND folio_name=:fn"
         . ($hasType ? " AND folio_type='other'" : '') . " ORDER BY id ASC LIMIT 1";
    $st = $pdo->prepare($sql);
    $st->execute([':oid'=>$orderId, ':fn'=>$folioName]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    $cols=['order_id']; $vals=[':oid']; $p=[':oid'=>$orderId];
    if (bcc_column_exists($pdo,'folios','property_id')) { $cols[]='property_id'; $vals[]=':pid'; $p[':pid']=$propertyId; }
    if (bcc_column_exists($pdo,'folios','folio_no')) { $cols[]='folio_no'; $vals[]=':fno'; $p[':fno']='FOL-O-'.$orderId.'-'.$agentId; }
    if (bcc_column_exists($pdo,'folios','folio_name')) { $cols[]='folio_name'; $vals[]=':fn'; $p[':fn']=$folioName; }
    if ($hasType) { $cols[]='folio_type'; $vals[]="'other'"; }
    if (bcc_column_exists($pdo,'folios','status')) { $cols[]='status'; $vals[]='1'; }
    $pdo->prepare("INSERT INTO folios (`".implode('`,`',$cols)."`) VALUES (".implode(',',$vals).")")->execute($p);
    return (int)$pdo->lastInsertId();
}

bcc_ensure_extra_agent_payer_table($pdo);

/* --------------------------------------------------------------------------
   Currency
-------------------------------------------------------------------------- */
function bcc_currency_symbol(string $code): string
{
    $map = [
        'USD' => '$', 'EUR' => '€', 'GBP' => '£', 'ZAR' => 'R',
        'ZWG' => 'ZWG ', 'ZWL' => 'ZWL ', 'BWP' => 'P',
    ];
    return isset($map[$code]) ? $map[$code] : ($code . ' ');
}

/*
   Canonical "this booking still holds inventory" filter.
   Mirrors rf_active_order_status_sql() in reservation_form.php so the Control
   Centre and the reservation form agree on what counts as a live booking.
   The previous inline filter here only excluded '0','cancelled','canceled',
   so no-show / void / deleted bookings were wrongly treated as occupying a
   room and could falsely block a legitimate date change.
*/
function bcc_active_status_sql(string $alias = 'o'): string
{
    $alias = preg_replace('/[^A-Za-z0-9_]/', '', $alias);
    if ($alias === '') {
        $alias = 'o';
    }
    /*
       A LEFT JOIN to orders must never let an orphan order_rooming row hold
       inventory. COALESCE(o.status,1) previously treated a missing order as
       active, which produced messages such as "occupied by booking #150080"
       even though that booking no longer existed in orders.
    */
    return "{$alias}.id IS NOT NULL AND LOWER(TRIM(CAST(COALESCE({$alias}.status, 1) AS CHAR))) NOT IN ('0','cancelled','canceled','void','deleted','no_show','noshow')";
}

/*
   Per-night room-type inventory check for a proposed rooming date range.

   Physical-room clash detection answers "is THIS room taken?". This answers the
   different and equally important question: "does the room TYPE still have
   inventory on every night of the new range?" Extending a checkout or pulling a
   check-in earlier can oversell a type even when the specific physical room
   looks free, which is exactly how quiet overbooking happens.

   Returns a list of nights that would be oversold:
     [ ['date' => 'Y-m-d', 'total' => int, 'occupied' => int, 'over' => int], ... ]
   $excludeRoomingId is the row being edited (its current dates must not count
   against the new dates it is moving to).
*/
function bcc_room_type_oversold_nights(
    PDO $pdo,
    int $propertyId,
    int $roomTypeId,
    string $checkIn,
    string $checkOut,
    int $excludeRoomingId = 0
): array {
    if ($propertyId <= 0 || $roomTypeId <= 0 || $checkIn === '' || $checkOut === '') {
        return [];
    }
    if (!bcc_table_exists($pdo, 'rooms') || !bcc_table_exists($pdo, 'order_rooming')) {
        return [];
    }
    if (!bcc_column_exists($pdo, 'rooms', 'room_type_id')) {
        return [];
    }

    // How many physical rooms exist for this type at this property?
    try {
        $where = "room_type_id = :rt";
        $params = [':rt' => $roomTypeId];
        if (bcc_column_exists($pdo, 'rooms', 'property_id')) {
            $where .= " AND property_id = :pid";
            $params[':pid'] = $propertyId;
        }
        $st = $pdo->prepare("SELECT COUNT(*) FROM rooms WHERE {$where}");
        $st->execute($params);
        $totalRooms = (int)$st->fetchColumn();
    } catch (Throwable $e) {
        return [];
    }
    if ($totalRooms <= 0) {
        return [];
    }

    $startTs = strtotime($checkIn);
    $endTs   = strtotime($checkOut);
    if (!$startTs || !$endTs || $endTs <= $startTs) {
        return [];
    }

    $hasOrders = bcc_table_exists($pdo, 'orders');
    $statusSql = ($hasOrders && bcc_column_exists($pdo, 'orders', 'status'))
        ? ' AND ' . bcc_active_status_sql('o')
        : '';
    $joinOrders = $hasOrders ? ' INNER JOIN orders o ON o.id = rmg.order_id ' : '';

    /*
       Same rule as bcc_room_type_availability(): a row holding a physical room
       consumes that ROOM's real type; only unassigned rows fall back to their
       own room_type_id. Without this, rows with a NULL/stale room_type_id are
       invisible here and an oversell can slip through unwarned.
    */
    $typeMatchSql = "rmg.room_type_id = :rt";
    $joinRooms = '';
    if (bcc_column_exists($pdo, 'order_rooming', 'room_id')
        && bcc_table_exists($pdo, 'rooms')
        && bcc_column_exists($pdo, 'rooms', 'room_type_id')) {
        $joinRooms = " LEFT JOIN rooms rm_t ON rm_t.id = rmg.room_id ";
        $typeMatchSql = "(
            CASE
                WHEN rmg.room_id IS NOT NULL AND rmg.room_id > 0 THEN rm_t.room_type_id
                ELSE rmg.room_type_id
            END
        ) = :rt";
    }

    $sql = "
        SELECT COUNT(*)
        FROM order_rooming rmg
        {$joinOrders}
        {$joinRooms}
        WHERE {$typeMatchSql}
          AND DATE(rmg.check_in) <= :day
          AND DATE(rmg.check_out) > :day
          " . ($excludeRoomingId > 0 ? " AND rmg.id <> :xid " : "") . "
          {$statusSql}
    ";
    try {
        $occStmt = $pdo->prepare($sql);
    } catch (Throwable $e) {
        return [];
    }

    $oversold = [];
    for ($ts = $startTs; $ts < $endTs; $ts = strtotime('+1 day', $ts)) {
        $day = date('Y-m-d', $ts);
        $p = [':rt' => $roomTypeId, ':day' => $day];
        if ($excludeRoomingId > 0) {
            $p[':xid'] = $excludeRoomingId;
        }
        try {
            $occStmt->execute($p);
            $occupied = (int)$occStmt->fetchColumn();
        } catch (Throwable $e) {
            continue;
        }
        // +1 for the row we are about to place on this night.
        if (($occupied + 1) > $totalRooms) {
            $oversold[] = [
                'date'     => $day,
                'total'    => $totalRooms,
                'occupied' => $occupied,
                'over'     => ($occupied + 1) - $totalRooms,
            ];
        }
    }

    return $oversold;
}

/*
   Resolve the actual column names on booking_color_codes, since schemas vary.
   Returns ['hex'=>col|null, 'text'=>col|null, 'label'=>col|null, 'key'=>col|null,
            'priority'=>col|null, 'active'=>col|null] using the first match found.
*/
function bcc_colour_cols(PDO $pdo): array
{
    static $resolved = null;
    if ($resolved !== null) {
        return $resolved;
    }
    if (!bcc_table_exists($pdo, 'booking_color_codes')) {
        $resolved = ['hex' => null, 'text' => null, 'label' => null, 'key' => null, 'priority' => null, 'active' => null];
        return $resolved;
    }
    $pick = function (array $cands) use ($pdo) {
        foreach ($cands as $c) {
            if (bcc_column_exists($pdo, 'booking_color_codes', $c)) {
                return $c;
            }
        }
        return null;
    };
    $resolved = [
        'hex'      => $pick(['hex_color', 'hex_colour', 'bg_color', 'bg_colour', 'background_color', 'color', 'colour', 'color_hex', 'colour_hex', 'hex']),
        'text'     => $pick(['text_color', 'text_colour', 'font_color', 'foreground', 'text_hex']),
        'label'    => $pick(['status_label', 'rule_name', 'label', 'name', 'title', 'description']),
        'key'      => $pick(['status_key', 'rule_key', 'key', 'code', 'slug']),
        'priority' => $pick(['priority', 'priority_no', 'sort_order', 'sort', 'position', 'ordering']),
        'active'   => $pick(['is_active', 'active', 'enabled']),
    ];
    return $resolved;
}

/* --------------------------------------------------------------------------
   Core data fetchers
-------------------------------------------------------------------------- */
function bcc_fetch_order(PDO $pdo, int $orderId): ?array
{
    if ($orderId <= 0) {
        return null;
    }

    $hasGuests   = bcc_table_exists($pdo, 'guests');
    $hasAgents   = bcc_table_exists($pdo, 'agents');
    $hasProps    = bcc_table_exists($pdo, 'properties');
    $hasRT       = bcc_table_exists($pdo, 'room_types');
    $hasColours  = bcc_table_exists($pdo, 'booking_color_codes');
    $hasColourId = bcc_column_exists($pdo, 'orders', 'booking_color_code_id');

    $sel = ["o.*"];
    $sel[] = bcc_column_exists($pdo, 'orders', 'order_no') ? "o.order_no" : "o.id AS order_no";
    $sel[] = ($hasGuests && bcc_column_exists($pdo, 'guests', 'firstname'))
        ? "TRIM(CONCAT(COALESCE(g.firstname,''),' ',COALESCE(g.lastname,''))) AS guest_name"
        : "'' AS guest_name";
    $sel[] = ($hasAgents && bcc_column_exists($pdo, 'agents', 'firstname'))
        ? "TRIM(CONCAT(COALESCE(a.firstname,''),' ',COALESCE(a.lastname,''))) AS agent_name"
        : "'' AS agent_name";
    $sel[] = $hasRT ? "rt.title AS room_type_title" : "'' AS room_type_title";
    $sel[] = $hasProps ? "p.name AS property_name" : "'' AS property_name";
    $sel[] = ($hasProps && bcc_column_exists($pdo, 'properties', 'currency_code')) ? "p.currency_code AS property_currency_code" : "'USD' AS property_currency_code";
    $cc = bcc_colour_cols($pdo);
    $sel[] = ($hasColours && $cc['label']) ? "cc.`{$cc['label']}` AS colour_label" : "NULL AS colour_label";
    $sel[] = ($hasColours && $cc['hex']) ? "cc.`{$cc['hex']}` AS colour_hex" : "NULL AS colour_hex";
    $sel[] = ($hasColours && $cc['text']) ? "cc.`{$cc['text']}` AS colour_text" : "NULL AS colour_text";

    $joins = '';
    if ($hasGuests && bcc_column_exists($pdo, 'orders', 'guest_id')) {
        $joins .= " LEFT JOIN guests g ON g.id = o.guest_id ";
    }
    if ($hasAgents && bcc_column_exists($pdo, 'orders', 'agent_id')) {
        $joins .= " LEFT JOIN agents a ON a.id = o.agent_id ";
    }
    if ($hasRT && bcc_column_exists($pdo, 'orders', 'room_type_id')) {
        $joins .= " LEFT JOIN room_types rt ON rt.id = o.room_type_id ";
    }
    if ($hasProps && bcc_column_exists($pdo, 'orders', 'property_id')) {
        $joins .= " LEFT JOIN properties p ON p.id = o.property_id ";
    }
    if ($hasColours && $hasColourId) {
        $joins .= " LEFT JOIN booking_color_codes cc ON cc.id = o.booking_color_code_id ";
    }

    $sql = "SELECT " . implode(', ', $sel) . "
            FROM orders o
            {$joins}
            WHERE o.id = :id
            LIMIT 1";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([':id' => $orderId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        return null;
    }

    $creatorId = 0;
    foreach (['created_by', 'user_id', 'created_user_id', 'entered_by', 'booked_by_user_id'] as $creatorCol) {
        if (array_key_exists($creatorCol, $row) && (int)$row[$creatorCol] > 0) {
            $creatorId = (int)$row[$creatorCol];
            break;
        }
    }
    $row['creator_id'] = $creatorId;
    $row['creator_name'] = bcc_user_display_name($pdo, $creatorId);

    $createdAt = '';
    foreach (['created_at', 'ordered_on', 'date_created', 'created_on'] as $dateCol) {
        if (!empty($row[$dateCol])) {
            $createdAt = (string)$row[$dateCol];
            break;
        }
    }
    $row['booking_created_at'] = $createdAt;

    return $row;
}

function bcc_search_orders(PDO $pdo, array $filters): array
{
    $where = [];
    $params = [];

    $hasGuests   = bcc_table_exists($pdo, 'guests');
    $hasAgents   = bcc_table_exists($pdo, 'agents');
    $hasProps    = bcc_table_exists($pdo, 'properties');
    $hasColours  = bcc_table_exists($pdo, 'booking_color_codes');
    $hasOrderNo  = bcc_column_exists($pdo, 'orders', 'order_no');
    $hasRooms    = bcc_column_exists($pdo, 'orders', 'totalrooms');
    $hasColourId = bcc_column_exists($pdo, 'orders', 'booking_color_code_id');
    $hasTotalAmt = bcc_column_exists($pdo, 'orders', 'totalamount');
    $hasCheckIn  = bcc_column_exists($pdo, 'orders', 'check_in');
    $hasCheckOut = bcc_column_exists($pdo, 'orders', 'check_out');
    $hasStatus   = bcc_column_exists($pdo, 'orders', 'status');

    // Guest / agent name expressions depend on whether those tables/columns exist.
    $guestExpr = ($hasGuests && bcc_column_exists($pdo, 'guests', 'firstname'))
        ? "TRIM(CONCAT(COALESCE(g.firstname,''),' ',COALESCE(g.lastname,'')))"
        : "''";
    $agentExpr = ($hasAgents && bcc_column_exists($pdo, 'agents', 'firstname'))
        ? "TRIM(CONCAT(COALESCE(a.firstname,''),' ',COALESCE(a.lastname,'')))"
        : "''";

    $term = trim((string)($filters['q'] ?? ''));
    if ($term !== '') {
        $ors = [
            "{$guestExpr} LIKE :term",
            "{$agentExpr} LIKE :term2",
            "CAST(o.id AS CHAR) = :exact",
        ];
        $params[':term']  = '%' . $term . '%';
        $params[':term2'] = '%' . $term . '%';
        $params[':exact'] = $term;
        if ($hasOrderNo) {
            $ors[] = "o.order_no LIKE :term3";
            $params[':term3'] = '%' . $term . '%';
        }
        $where[] = '(' . implode(' OR ', $ors) . ')';
    }

    $agentId = (int)($filters['agent_id'] ?? 0);
    if ($agentId > 0 && bcc_column_exists($pdo, 'orders', 'agent_id')) {
        $where[] = "o.agent_id = :agent_id";
        $params[':agent_id'] = $agentId;
    }

    $status = trim((string)($filters['status'] ?? ''));
    if ($status !== '' && $status !== 'all' && $hasStatus) {
        $where[] = "o.status = :status";
        $params[':status'] = $status;
    }

    $propertyId = (int)($filters['property_id'] ?? 0);
    if ($propertyId > 0 && bcc_column_exists($pdo, 'orders', 'property_id')) {
        $where[] = "o.property_id = :property_id";
        $params[':property_id'] = $propertyId;
    }

    /*
       Arrival / departure date range.
       Wrap the column in DATE() so the comparison is day-accurate whether the
       column is DATE or DATETIME. Comparing a DATETIME directly against
       'YYYY-MM-DD' silently drops bookings on the boundary day (a check_in of
       '2026-07-31 14:00' is NOT <= '2026-07-31'), which made the arrival /
       departure search miss the very days being searched for.
    */
    $dateField = ($filters['date_field'] ?? 'check_in') === 'check_out' ? 'check_out' : 'check_in';
    if (($dateField === 'check_in' && !$hasCheckIn) || ($dateField === 'check_out' && !$hasCheckOut)) {
        $dateField = '';
    }
    $from = trim((string)($filters['date_from'] ?? ''));
    $to   = trim((string)($filters['date_to'] ?? ''));
    if ($dateField !== '' && $from !== '') {
        $where[] = "DATE(o.{$dateField}) >= :dfrom";
        $params[':dfrom'] = substr($from, 0, 10);
    }
    if ($dateField !== '' && $to !== '') {
        $where[] = "DATE(o.{$dateField}) <= :dto";
        $params[':dto'] = substr($to, 0, 10);
    }

    // Build the SELECT list defensively.
    $sel = ["o.id"];
    $sel[] = $hasStatus ? "o.status" : "'' AS status";
    $sel[] = $hasCheckIn ? "o.check_in" : "NULL AS check_in";
    $sel[] = $hasCheckOut ? "o.check_out" : "NULL AS check_out";
    $sel[] = $hasTotalAmt ? "o.totalamount" : "0 AS totalamount";
    $sel[] = bcc_column_exists($pdo, 'orders', 'property_id') ? "o.property_id" : "0 AS property_id";
    $sel[] = $hasOrderNo ? "o.order_no" : "o.id AS order_no";
    $sel[] = $hasRooms ? "o.totalrooms" : "1 AS totalrooms";
    $sel[] = "{$guestExpr} AS guest_name";
    $sel[] = "{$agentExpr} AS agent_name";
    $sel[] = $hasProps ? "p.name AS property_name" : "'' AS property_name";
    $sel[] = ($hasProps && bcc_column_exists($pdo, 'properties', 'currency_code')) ? "p.currency_code AS property_currency_code" : "'USD' AS property_currency_code";
    $cc = bcc_colour_cols($pdo);
    $sel[] = ($hasColours && $cc['hex']) ? "cc.`{$cc['hex']}` AS colour_hex" : "NULL AS colour_hex";
    $sel[] = ($hasColours && $cc['label']) ? "cc.`{$cc['label']}` AS colour_label" : "NULL AS colour_label";

    // Joins only when both sides exist.
    $joins = '';
    if ($hasGuests && bcc_column_exists($pdo, 'orders', 'guest_id')) {
        $joins .= " LEFT JOIN guests g ON g.id = o.guest_id ";
    }
    if ($hasAgents && bcc_column_exists($pdo, 'orders', 'agent_id')) {
        $joins .= " LEFT JOIN agents a ON a.id = o.agent_id ";
    }
    if ($hasProps && bcc_column_exists($pdo, 'orders', 'property_id')) {
        $joins .= " LEFT JOIN properties p ON p.id = o.property_id ";
    }
    if ($hasColours && $hasColourId) {
        $joins .= " LEFT JOIN booking_color_codes cc ON cc.id = o.booking_color_code_id ";
    }

    $sql = "SELECT " . implode(', ', $sel) . "
            FROM orders o
            {$joins}
            " . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
            ORDER BY o.id DESC
            LIMIT 60";

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}

function bcc_fetch_rooming(PDO $pdo, int $orderId): array
{
    if (!bcc_table_exists($pdo, 'order_rooming')) {
        return [];
    }
    $stmt = $pdo->prepare("
        SELECT rmg.*, rm.room_no AS actual_room_no, rt.title AS room_type_title
        FROM order_rooming rmg
        LEFT JOIN rooms rm ON rm.id = rmg.room_id
        LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
        WHERE rmg.order_id = :id
        ORDER BY rmg.room_no ASC, rmg.id ASC
    ");
    $stmt->execute([':id' => $orderId]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}


function bcc_fetch_room_types(PDO $pdo, int $propertyId): array
{
    if (!bcc_table_exists($pdo, 'room_types')) {
        return [];
    }
    $titleCol = null;
    foreach (['title', 'name', 'room_type', 'description'] as $c) {
        if (bcc_column_exists($pdo, 'room_types', $c)) { $titleCol = $c; break; }
    }
    if (!$titleCol) {
        return [];
    }
    $where = [];
    $params = [];
    if ($propertyId > 0 && bcc_column_exists($pdo, 'room_types', 'property_id')) {
        $where[] = "property_id = :pid";
        $params[':pid'] = $propertyId;
    }
    try {
        $sql = "SELECT id, `{$titleCol}` AS title FROM room_types"
             . ($where ? " WHERE " . implode(' AND ', $where) : '')
             . " ORDER BY `{$titleCol}` ASC";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

/*
   Live availability for a room type over a date range.
   Returns total rooms of the type, the fewest free on any night of the range
   ("bookable"), and a per-night breakdown so the UI can show exactly which
   nights are tight. Mirrors the counting rules used by bcc_room_type_oversold_nights
   (checkout day frees the room; cancelled/no-show/void never hold stock).
*/
function bcc_room_type_availability(PDO $pdo, int $propertyId, int $roomTypeId, string $checkIn, string $checkOut): array
{
    $out = ['total' => 0, 'bookable' => 0, 'nights' => []];
    if ($propertyId <= 0 || $roomTypeId <= 0 || $checkIn === '' || $checkOut === '') {
        return $out;
    }
    if (!bcc_table_exists($pdo, 'rooms') || !bcc_table_exists($pdo, 'order_rooming')) {
        return $out;
    }
    if (!bcc_column_exists($pdo, 'rooms', 'room_type_id')) {
        return $out;
    }

    try {
        $where = "room_type_id = :rt";
        $params = [':rt' => $roomTypeId];
        if (bcc_column_exists($pdo, 'rooms', 'property_id')) {
            $where .= " AND property_id = :pid";
            $params[':pid'] = $propertyId;
        }
        $st = $pdo->prepare("SELECT COUNT(*) FROM rooms WHERE {$where}");
        $st->execute($params);
        $total = (int)$st->fetchColumn();
    } catch (Throwable $e) {
        return $out;
    }
    $out['total'] = $total;
    if ($total <= 0) {
        return $out;
    }

    $startTs = strtotime($checkIn);
    $endTs   = strtotime($checkOut);
    if (!$startTs || !$endTs || $endTs <= $startTs) {
        return $out;
    }

    $hasOrders = bcc_table_exists($pdo, 'orders');
    $statusSql = ($hasOrders && bcc_column_exists($pdo, 'orders', 'status'))
        ? ' AND ' . bcc_active_status_sql('o')
        : '';
    $joinOrders = $hasOrders ? ' INNER JOIN orders o ON o.id = rmg.order_id ' : '';

    /*
       Which rooming rows consume stock of this type on a night?

       A row's own room_type_id can be NULL or stale (older/migrated bookings,
       or rows written before the type was set). Trusting it alone made the
       type-level count MISS rows that are physically holding a room - so the
       banner could report "1 room free" while the room dropdown correctly
       showed none, because the dropdown checks the physical room instead.

       Rule: if the row has an assigned room, that ROOM's real type decides
       which type it consumes. Only fall back to the row's room_type_id when
       there is no room assigned yet.
    */
    $typeMatchSql = "rmg.room_type_id = :rt";
    $joinRooms = '';
    if (bcc_column_exists($pdo, 'order_rooming', 'room_id')
        && bcc_table_exists($pdo, 'rooms')
        && bcc_column_exists($pdo, 'rooms', 'room_type_id')) {
        $joinRooms = " LEFT JOIN rooms rm_t ON rm_t.id = rmg.room_id ";
        $typeMatchSql = "(
            CASE
                WHEN rmg.room_id IS NOT NULL AND rmg.room_id > 0 THEN rm_t.room_type_id
                ELSE rmg.room_type_id
            END
        ) = :rt";
    }

    try {
        $occStmt = $pdo->prepare("
            SELECT COUNT(*)
            FROM order_rooming rmg
            {$joinOrders}
            {$joinRooms}
            WHERE {$typeMatchSql}
              AND DATE(rmg.check_in) <= :day
              AND DATE(rmg.check_out) > :day
              {$statusSql}
        ");
    } catch (Throwable $e) {
        return $out;
    }

    $minFree = $total;
    for ($ts = $startTs; $ts < $endTs; $ts = strtotime('+1 day', $ts)) {
        $day = date('Y-m-d', $ts);
        try {
            $occStmt->execute([':rt' => $roomTypeId, ':day' => $day]);
            $occupied = (int)$occStmt->fetchColumn();
        } catch (Throwable $e) {
            continue;
        }
        $free = max(0, $total - $occupied);
        $out['nights'][] = ['date' => $day, 'total' => $total, 'occupied' => $occupied, 'free' => $free];
        if ($free < $minFree) {
            $minFree = $free;
        }
    }
    $out['bookable'] = $minFree;

    return $out;
}

/*
   JUGGLE SUGGESTIONS
   ------------------
   When no single room of a type is free for the WHOLE requested range, space
   can often still be made by moving one existing booking to another room that
   is free for ITS dates. This finds those moves.

   Strategy per candidate room:
     1. Find the live bookings blocking that room during the requested range.
        Only consider rooms blocked by exactly ONE booking - moving a single
        booking is a realistic front-desk action; chains are not.
     2. For that blocker, look for another room it could move to, free for the
        blocker's own full stay, and not one we are about to hand out.
     3. Prefer a destination of the SAME room type (a clean like-for-like move).
        If none exists, offer a cross-type move as a fallback, clearly flagged,
        since ignoring room types is exactly the "juggle to create space"
        escape hatch - but it must be a conscious decision, not silent.

   Returns a list of suggestions, same-type ones first:
     [ ['room_id','room_no','blocker_order_id','blocker_in','blocker_out',
        'move_to_room_id','move_to_room_no','move_to_type_title',
        'same_type' => bool], ... ]
*/
function bcc_juggle_suggestions(
    PDO $pdo,
    int $propertyId,
    int $roomTypeId,
    string $checkIn,
    string $checkOut,
    int $limit = 5,
    int $currentOrderId = 0
): array {
    if ($propertyId <= 0 || $roomTypeId <= 0 || $checkIn === '' || $checkOut === '') {
        return [];
    }
    if (!bcc_table_exists($pdo, 'order_rooming') || !bcc_table_exists($pdo, 'rooms')) {
        return [];
    }
    if (!bcc_column_exists($pdo, 'order_rooming', 'room_id')) {
        return [];
    }

    $hasOrders = bcc_table_exists($pdo, 'orders');
    $statusSql = ($hasOrders && bcc_column_exists($pdo, 'orders', 'status'))
        ? ' AND ' . bcc_active_status_sql('o') : '';
    $joinOrders = $hasOrders ? ' INNER JOIN orders o ON o.id = rmg.order_id ' : '';

    $allRooms = bcc_fetch_rooms($pdo, $propertyId);
    if (!$allRooms) {
        return [];
    }

    // Bookings blocking a given room during the requested window.
    try {
        $blockers = $pdo->prepare("
            SELECT rmg.id, rmg.order_id, DATE(rmg.check_in) AS cin, DATE(rmg.check_out) AS cout
            FROM order_rooming rmg
            {$joinOrders}
            WHERE rmg.room_id = :room_id
              AND DATE(rmg.check_in) < :checkout
              AND DATE(rmg.check_out) > :checkin
              {$statusSql}
        ");
        // Is a room completely free across an arbitrary range (ignoring one row)?
        $isFree = $pdo->prepare("
            SELECT COUNT(*)
            FROM order_rooming rmg
            {$joinOrders}
            WHERE rmg.room_id = :room_id
              AND rmg.id <> :ignore_id
              AND DATE(rmg.check_in) < :checkout
              AND DATE(rmg.check_out) > :checkin
              {$statusSql}
        ");
    } catch (Throwable $e) {
        return [];
    }

    $suggestions = [];

    foreach ($allRooms as $room) {
        if ((int)$room['room_type_id'] !== $roomTypeId) {
            continue;
        }
        $roomId = (int)$room['id'];

        try {
            $blockers->execute([':room_id' => $roomId, ':checkout' => $checkOut, ':checkin' => $checkIn]);
            $rows = $blockers->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) {
            continue;
        }

        /*
           If THIS booking holds the room during the window at all, no juggle of
           anyone else can free it - our own row would still be sitting there.
           Skip the room entirely rather than offering a move that achieves
           nothing.
        */
        if ($currentOrderId > 0) {
            $selfHolds = false;
            foreach ($rows as $r) {
                if ((int)$r['order_id'] === $currentOrderId) {
                    $selfHolds = true;
                    break;
                }
            }
            if ($selfHolds) {
                continue;
            }
        }

        // Free already (shouldn't reach here) or too tangled to juggle simply.
        if (count($rows) !== 1) {
            continue;
        }

        $blk = $rows[0];
        $blkRowId = (int)$blk['id'];
        $blkIn    = (string)$blk['cin'];
        $blkOut   = (string)$blk['cout'];

        /*
           NEVER suggest juggling the booking we are adding a room to.

           If the only thing "blocking" this room is THIS booking's own rooming
           row, the room is already on the booking. Offering to move that guest
           out so the room can be given back to the same booking is circular and
           nonsensical - it was reported exactly this way: a property with one
           Family Room, already allocated to the booking, showed "not available"
           plus an offer to move the booking out of it to free it for itself.

           A room already held by this booking is simply not a juggle candidate.
        */
        if ($currentOrderId > 0 && (int)$blk['order_id'] === $currentOrderId) {
            continue;
        }

        // Where could this blocker go? Same type first, then any type.
        $sameTypeMove = null;
        $anyTypeMove  = null;

        foreach ($allRooms as $dest) {
            $destId = (int)$dest['id'];
            if ($destId === $roomId) {
                continue;
            }
            try {
                $isFree->execute([
                    ':room_id'   => $destId,
                    ':ignore_id' => $blkRowId,
                    ':checkout'  => $blkOut,
                    ':checkin'   => $blkIn,
                ]);
                if ((int)$isFree->fetchColumn() !== 0) {
                    continue; // destination busy for the blocker's stay
                }
            } catch (Throwable $e) {
                continue;
            }

            $destIsSameType = ((int)$dest['room_type_id'] === $roomTypeId);

            /*
               A same-type destination must not be one we are about to offer as
               free for the new stay - that would just move the problem. Check it
               is genuinely occupied during the requested window, or is the room
               we are trying to free.
            */
            if ($destIsSameType) {
                try {
                    $blockers->execute([':room_id' => $destId, ':checkout' => $checkOut, ':checkin' => $checkIn]);
                    $destBusyInWindow = count($blockers->fetchAll(PDO::FETCH_ASSOC) ?: []);
                } catch (Throwable $e) {
                    $destBusyInWindow = 0;
                }
                if ($destBusyInWindow === 0) {
                    // Destination is free in the window anyway - no juggle needed.
                    continue;
                }
                if ($sameTypeMove === null) {
                    $sameTypeMove = $dest;
                }
            } elseif ($anyTypeMove === null) {
                $anyTypeMove = $dest;
            }

            if ($sameTypeMove !== null) {
                break; // best option found
            }
        }

        $chosen = $sameTypeMove !== null ? $sameTypeMove : $anyTypeMove;
        if ($chosen === null) {
            continue;
        }

        $suggestions[] = [
            'room_id'            => $roomId,
            'room_no'            => (string)$room['room_no'],
            'blocker_row_id'     => $blkRowId,
            'blocker_order_id'   => (int)$blk['order_id'],
            'blocker_in'         => $blkIn,
            'blocker_out'        => $blkOut,
            'move_to_room_id'    => (int)$chosen['id'],
            'move_to_room_no'    => (string)$chosen['room_no'],
            'move_to_type_title' => (string)($chosen['room_type_title'] ?? ''),
            'same_type'          => ($sameTypeMove !== null),
        ];

        if (count($suggestions) >= $limit) {
            break;
        }
    }

    // Same-type moves are cleaner - surface them first.
    usort($suggestions, function ($a, $b) {
        return ($b['same_type'] ? 1 : 0) - ($a['same_type'] ? 1 : 0);
    });

    return $suggestions;
}

/* Get (or create) the booking's master folio, so inline extras have a home. */

/*
   PRICE ONE ROOM NIGHT FROM ITS REAL OCCUPANCY
   --------------------------------------------
   The shared rate engine correctly handles normal adult-led rooms, but some
   family bookings place all children in a separate room while the parents are
   in another room. A room with 0 adults and 2 children must therefore not be
   forced into a single-adult rate or priced at zero.

   Children-only rule:
     child amount = sharing rate x child percentage x number of children
     adult amount = 0
     no single supplement is applied

   All other occupancy combinations continue through the shared canonical
   calculate_room_night_total() function.
*/
function bcc_calculate_room_night_total(
    int $adults,
    int $children,
    float $sharingRate,
    float $singleRate,
    float $singleSupplement,
    float $childRatePercent
): array {
    $adults = max(0, $adults);
    $children = max(0, $children);

    if ($adults === 0 && $children > 0) {
        $childPercent = max(0, $childRatePercent) / 100;
        $childTotal = round($children * $sharingRate * $childPercent, 2);

        return [
            'adult_total' => 0.00,
            'child_total' => $childTotal,
            'room_total' => $childTotal,
            'applied_rate_type' => 'children only',
        ];
    }

    return calculate_room_night_total(
        $adults,
        $children,
        $sharingRate,
        $singleRate,
        $singleSupplement,
        $childRatePercent
    );
}

/*
   REPRICE A BOOKING FROM ITS ROOMING ROWS
   ---------------------------------------
   rel_orders_prices is the source of truth for money, but it was only ever
   rebuilt by reservation_form.php. So changing a room's dates here left stale
   nights behind: move a stay from the 24th to the 25th and the 24th kept its
   priced row, kept its folio room charge, and kept appearing on the proforma
   for a night nobody is staying.

   This rebuilds the nightly rows from the CURRENT rooming rows using the shared
   rate engine in rate_support.php, so the Control Centre prices exactly the way
   the reservation form does.

   Manual overrides are preserved: an overridden night keeps its overridden
   total, because losing a negotiated rate silently would be worse than a stale
   row. Nights that no longer exist are removed outright.
*/
function bcc_reprice_order(PDO $pdo, int $orderId): array
{
    $out = ['ok' => false, 'nights' => 0, 'removed' => 0, 'kept_overrides' => 0, 'reason' => ''];

    if ($orderId <= 0 || !bcc_table_exists($pdo, 'rel_orders_prices') || !bcc_table_exists($pdo, 'order_rooming')) {
        $out['reason'] = 'Pricing tables unavailable.';
        return $out;
    }
    if (!function_exists('fetch_rate_plan') || !function_exists('calculate_room_night_total')) {
        $missing = [];
        if (!function_exists('fetch_rate_plan')) { $missing[] = 'fetch_rate_plan()'; }
        if (!function_exists('calculate_room_night_total')) { $missing[] = 'calculate_room_night_total()'; }
        $out['reason'] = 'Rate engine unavailable - ' . implode(' and ', $missing)
            . ' not defined. Place rate_support.php beside this file (in modules/reservations/) '
            . 'or in includes/, then reload.';
        return $out;
    }

    // Booking context.
    $os = $pdo->prepare("SELECT * FROM orders WHERE id = :id LIMIT 1");
    $os->execute([':id' => $orderId]);
    $order = $os->fetch(PDO::FETCH_ASSOC);
    if (!$order) {
        $out['reason'] = 'Booking not found.';
        return $out;
    }
    $propertyId = (int)($order['property_id'] ?? 0);
    $agentId    = (int)($order['agent_id'] ?? 0) ?: null;
    $mealPlan   = function_exists('normalize_meal_plan_for_rates')
        ? normalize_meal_plan_for_rates($order['meal_plan'] ?? 'HB')
        : 'HB';

    // Current rooming rows drive everything.
    $rs = $pdo->prepare("SELECT * FROM order_rooming WHERE order_id = :id ORDER BY room_no ASC, id ASC");
    $rs->execute([':id' => $orderId]);
    $rooming = $rs->fetchAll(PDO::FETCH_ASSOC) ?: [];
    if (!$rooming) {
        $out['reason'] = 'No rooming rows to price.';
        return $out;
    }

    // Preserve overrides, keyed by date + room slot.
    $keep = [];
    if (bcc_column_exists($pdo, 'rel_orders_prices', 'rate_overridden')) {
        try {
            $ex = $pdo->prepare("SELECT * FROM rel_orders_prices WHERE order_id = :id AND COALESCE(rate_overridden,0) = 1");
            $ex->execute([':id' => $orderId]);
            foreach ($ex->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $k = substr((string)$r['date'], 0, 10) . '|' . (int)($r['room_id'] ?? 0);
                $keep[$k] = $r;
            }
        } catch (Throwable $e) { /* ignore */ }
    }

    $hasCol = function ($c) use ($pdo) { return bcc_column_exists($pdo, 'rel_orders_prices', $c); };

    $pdo->beginTransaction();
    try {
        $pdo->prepare("DELETE FROM rel_orders_prices WHERE order_id = :id")->execute([':id' => $orderId]);

        $cols = ['order_id', 'date', 'total'];
        foreach (['property_id','room_id','room_type_id','rate_plan_id','rate_plan_title','rate_plan_currency',
                  'adults_count','kids_count','sharing_rate','single_rate','single_supplement',
                  'child_rate_percent','adult_total','child_total','applied_rate_type',
                  'rate_overridden','original_total','override_total','override_value',
                  'override_reason','override_type','override_components'] as $c) {
            if ($hasCol($c)) { $cols[] = $c; }
        }
        $ph = array_map(function ($c) { return ':' . $c; }, $cols);
        $ins = $pdo->prepare("INSERT INTO rel_orders_prices (" . implode(',', array_map(function($c){return "`$c`";}, $cols)) . ") VALUES (" . implode(',', $ph) . ")");

        $nights = 0;
        foreach ($rooming as $slot) {
            $cin  = substr((string)($slot['check_in'] ?? ''), 0, 10);
            $cout = substr((string)($slot['check_out'] ?? ''), 0, 10);
            $startTs = strtotime($cin);
            $endTs   = strtotime($cout);
            if (!$startTs || !$endTs || $endTs <= $startTs) { continue; }

            $roomTypeId = (int)($slot['room_type_id'] ?? 0);
            $roomId     = (int)($slot['room_id'] ?? 0);
            $adults     = max(0, (int)($slot['adults'] ?? 0));
            $children   = max(0, (int)($slot['children'] ?? 0));

            for ($ts = $startTs; $ts < $endTs; $ts = strtotime('+1 day', $ts)) {
                $day = date('Y-m-d', $ts);
                $plan = fetch_rate_plan($pdo, $roomTypeId, $agentId, $day, $mealPlan);

                $sharing = (float)($plan['sharing_rate'] ?? 0);
                $single  = (float)($plan['single_rate'] ?? 0);
                $suppl   = (float)($plan['single_supplement'] ?? 0);
                $childPc = (float)($plan['child_rate_percent'] ?? 50);

                $calc = bcc_calculate_room_night_total($adults, $children, $sharing, $single, $suppl, $childPc);
                $total = (float)$calc['room_total'];

                $row = [
                    ':order_id' => $orderId,
                    ':date'     => $day,
                    ':total'    => round($total, 2),
                ];
                $set = function ($c, $v) use (&$row, $hasCol) { if ($hasCol($c)) { $row[':' . $c] = $v; } };
                $set('property_id', $propertyId);
                $set('room_id', $roomId > 0 ? $roomId : null);
                $set('room_type_id', $roomTypeId);
                $set('rate_plan_id', (int)($plan['rate_plan_id'] ?? 0) ?: null);
                $set('rate_plan_title', (string)($plan['title'] ?? ''));
                $set('rate_plan_currency', (string)($plan['currency'] ?? ''));
                $set('adults_count', $adults);
                $set('kids_count', $children);
                $set('sharing_rate', $sharing);
                $set('single_rate', $single);
                $set('single_supplement', $suppl);
                $set('child_rate_percent', $childPc);
                $set('adult_total', (float)$calc['adult_total']);
                $set('child_total', (float)$calc['child_total']);
                $set('applied_rate_type', (string)$calc['applied_rate_type']);
                $set('rate_overridden', 0);
                $set('original_total', null);
                $set('override_total', null);
                $set('override_value', null);
                $set('override_reason', null);
                $set('override_type', null);
                $set('override_components', null);

                // Re-apply a preserved override for this exact night + room.
                $k = $day . '|' . $roomId;
                if (isset($keep[$k])) {
                    $old = $keep[$k];
                    $row[':total'] = round((float)$old['total'], 2);
                    $set('rate_overridden', 1);
                    $set('original_total', $hasCol('original_total') ? ($old['original_total'] ?? round($total, 2)) : null);
                    $set('override_total', $old['override_total'] ?? null);
                    $set('override_value', $old['override_value'] ?? null);
                    $set('override_reason', $old['override_reason'] ?? null);
                    $set('override_type', $old['override_type'] ?? null);
                    $set('override_components', $old['override_components'] ?? null);
                    $out['kept_overrides']++;
                }

                $ins->execute($row);
                $nights++;
            }
        }

        // Keep the order header in step.
        $sum = $pdo->prepare("SELECT COALESCE(SUM(total),0) FROM rel_orders_prices WHERE order_id = :id");
        $sum->execute([':id' => $orderId]);
        $grand = round((float)$sum->fetchColumn(), 2);
        $osets = [];
        $oparams = [':id' => $orderId];
        $gi = 0;
        foreach (['base_price', 'amount', 'totalamount'] as $c) {
            if (bcc_column_exists($pdo, 'orders', $c)) {
                $k = ':g' . $gi++;
                $osets[] = "`{$c}` = {$k}";
                $oparams[$k] = $grand;
            }
        }
        if ($osets) {
            $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :id")
                ->execute($oparams);
        }

        $pdo->commit();
        $out['ok'] = true;
        $out['nights'] = $nights;
        return $out;
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) { $pdo->rollBack(); }
        $out['reason'] = $e->getMessage();
        return $out;
    }
}

/*
   Rebuild the master folio's ACCOMMODATION postings from rel_orders_prices.
   Without this the ledger keeps a room charge for a night that no longer
   exists - which is how a removed 24 Dec night stayed on the folio and the
   proforma. Only accommodation rows are touched; extras are never harmed.
*/
function bcc_rebuild_room_charges(PDO $pdo, int $orderId): void
{
    if ($orderId <= 0 || !bcc_table_exists($pdo, 'folio_transactions') || !bcc_table_exists($pdo, 'rel_orders_prices')) {
        return;
    }
    try {
        $os = $pdo->prepare("SELECT property_id FROM orders WHERE id = :id LIMIT 1");
        $os->execute([':id' => $orderId]);
        $propertyId = (int)($os->fetchColumn() ?: 0);

        $folioId = bcc_get_or_create_master_folio($pdo, $orderId, $propertyId);
        if ($folioId <= 0) { return; }

        // Remove ONLY accommodation postings.
        $pdo->prepare("
            DELETE FROM folio_transactions
            WHERE order_id = :oid
              AND LOWER(COALESCE(source_table,'')) = 'rel_orders_prices'
        ")->execute([':oid' => $orderId]);

        $rs = $pdo->prepare("SELECT * FROM rel_orders_prices WHERE order_id = :oid ORDER BY date ASC, id ASC");
        $rs->execute([':oid' => $orderId]);
        $rows = $rs->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $cols = ['folio_id','order_id','tx_date','tx_type','description','debit','credit','source_table','source_id'];
        $vals = [':folio_id',':order_id',':tx_date',"'room_charge'",':description',':debit','0',"'rel_orders_prices'",':source_id'];
        if (bcc_column_exists($pdo, 'folio_transactions', 'property_id')) {
            array_unshift($cols, 'property_id'); array_unshift($vals, ':property_id');
        }
        $ins = $pdo->prepare("INSERT INTO folio_transactions (" . implode(',', $cols) . ") VALUES (" . implode(',', $vals) . ")");

        foreach ($rows as $r) {
            $amt = round((float)$r['total'], 2);
            if ($amt <= 0) { continue; }
            $day = substr((string)$r['date'], 0, 10);
            $desc = 'Accommodation charge for ' . $day
                  . (trim((string)($r['rate_plan_title'] ?? '')) !== '' ? ' | ' . trim((string)$r['rate_plan_title']) : '');
            $p = [
                ':folio_id' => $folioId,
                ':order_id' => $orderId,
                ':tx_date'  => $day . ' 00:00:00',
                ':description' => $desc,
                ':debit'    => $amt,
                ':source_id' => (int)$r['id'],
            ];
            if (bcc_column_exists($pdo, 'folio_transactions', 'property_id')) {
                $p[':property_id'] = $propertyId;
            }
            $ins->execute($p);
        }

        bcc_recalc_folio($pdo, $folioId);
    } catch (Throwable $e) {
        // non-fatal
    }
}

/*
   Room folios for a booking, one per allocated room, so an extra can be
   charged to the GUEST'S ROOM ACCOUNT rather than the agent's master folio.
   Mirrors rf_fetch_booking_room_folio_targets() in reservation_form.php:
   adaptive to whichever room-link column the folios table uses.
   Returns [ ['folio_id','room_no','label'], ... ]
*/
function bcc_fetch_room_folio_targets(PDO $pdo, int $orderId, int $propertyId): array
{
    if ($orderId <= 0 || !bcc_table_exists($pdo, 'folios') || !bcc_table_exists($pdo, 'order_rooming')) {
        return [];
    }

    $stmt = $pdo->prepare("
        SELECT rmg.id AS rooming_id, rmg.room_no, rmg.room_id, rm.room_no AS actual_room_no
        FROM order_rooming rmg
        LEFT JOIN rooms rm ON rm.id = rmg.room_id
        WHERE rmg.order_id = :oid
        ORDER BY rmg.room_no ASC, rmg.id ASC
    ");
    try {
        $stmt->execute([':oid' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
    if (!$rows) {
        return [];
    }

    // Which column links a folio to a specific room?
    $linkCol = null;
    foreach (['order_rooming_id', 'room_id', 'room_no'] as $c) {
        if (bcc_column_exists($pdo, 'folios', $c)) { $linkCol = $c; break; }
    }
    if ($linkCol === null) {
        return [];
    }
    $hasType = bcc_column_exists($pdo, 'folios', 'folio_type');

    $targets = [];
    foreach ($rows as $r) {
        $roomingId = (int)($r['rooming_id'] ?? 0);
        $roomId    = (int)($r['room_id'] ?? 0);
        $roomNo    = (int)($r['room_no'] ?? 0);
        $label     = trim((string)($r['actual_room_no'] ?? '')) !== ''
            ? 'Room ' . trim((string)$r['actual_room_no'])
            : 'Room ' . $roomNo;

        /*
           Resolve the link value. A room slot that has no physical room assigned
           yet still needs a folio - the guest can incur charges before the room
           number is allocated. Only the room_id link genuinely requires an
           assigned room; the others key off the slot itself.
        */
        $linkVal = null;
        if ($linkCol === 'order_rooming_id') { $linkVal = $roomingId; }
        elseif ($linkCol === 'room_id')      { $linkVal = $roomId; }
        elseif ($linkCol === 'room_no')      { $linkVal = $roomNo; }
        if (!$linkVal || $linkVal <= 0) {
            continue; // cannot key a folio to this slot on this schema
        }

        $folioId = 0;
        try {
            $typeSql = $hasType ? " AND folio_type = 'room' " : '';
            $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :oid AND `{$linkCol}` = :lv {$typeSql} LIMIT 1");
            $sel->execute([':oid' => $orderId, ':lv' => $linkVal]);
            $folioId = (int)($sel->fetchColumn() ?: 0);

            if ($folioId <= 0) {
                $cols = ['order_id', $linkCol];
                $vals = [':oid', ':lv'];
                $p = [':oid' => $orderId, ':lv' => $linkVal];
                if (bcc_column_exists($pdo, 'folios', 'property_id')) {
                    $cols[] = 'property_id'; $vals[] = ':pid'; $p[':pid'] = $propertyId;
                }
                if (bcc_column_exists($pdo, 'folios', 'folio_no')) {
                    $cols[] = 'folio_no'; $vals[] = ':fno'; $p[':fno'] = 'FOL-R-' . $orderId . '-' . $roomNo;
                }
                if (bcc_column_exists($pdo, 'folios', 'folio_name')) {
                    $cols[] = 'folio_name'; $vals[] = ':fnm'; $p[':fnm'] = $label . ' Folio';
                }
                if ($hasType) { $cols[] = 'folio_type'; $vals[] = "'room'"; }
                if (bcc_column_exists($pdo, 'folios', 'status')) { $cols[] = 'status'; $vals[] = '1'; }
                $pdo->prepare("INSERT INTO folios (" . implode(',', array_map(function($c){return "`$c`";}, $cols)) . ") VALUES (" . implode(',', $vals) . ")")
                    ->execute($p);
                $folioId = (int)$pdo->lastInsertId();
            }
        } catch (Throwable $e) {
            continue;
        }

        if ($folioId > 0) {
            $targets[] = ['folio_id' => $folioId, 'room_no' => $roomNo, 'label' => $label];
        }
    }

    return $targets;
}

function bcc_get_or_create_master_folio(PDO $pdo, int $orderId, int $propertyId): int
{
    if ($orderId <= 0 || !bcc_table_exists($pdo, 'folios')) {
        return 0;
    }
    $hasType = bcc_column_exists($pdo, 'folios', 'folio_type');
    try {
        $sql = "SELECT id FROM folios WHERE order_id = :oid"
             . ($hasType ? " AND folio_type = 'master'" : "")
             . " ORDER BY id ASC LIMIT 1";
        $st = $pdo->prepare($sql);
        $st->execute([':oid' => $orderId]);
        $id = (int)($st->fetchColumn() ?: 0);
        if ($id > 0) {
            return $id;
        }
    } catch (Throwable $e) {
        return 0;
    }

    // None yet - create one.
    try {
        $cols = ['order_id'];
        $vals = [':order_id'];
        $p = [':order_id' => $orderId];
        if (bcc_column_exists($pdo, 'folios', 'property_id')) {
            $cols[] = 'property_id'; $vals[] = ':property_id'; $p[':property_id'] = $propertyId;
        }
        if (bcc_column_exists($pdo, 'folios', 'folio_no')) {
            $cols[] = 'folio_no'; $vals[] = ':folio_no'; $p[':folio_no'] = 'FOL-' . $orderId;
        }
        if (bcc_column_exists($pdo, 'folios', 'folio_name')) {
            $cols[] = 'folio_name'; $vals[] = ':folio_name'; $p[':folio_name'] = 'Booking Folio';
        }
        if ($hasType) { $cols[] = 'folio_type'; $vals[] = "'master'"; }
        if (bcc_column_exists($pdo, 'folios', 'status')) { $cols[] = 'status'; $vals[] = '1'; }
        $pdo->prepare("INSERT INTO folios (" . implode(', ', $cols) . ") VALUES (" . implode(', ', $vals) . ")")
            ->execute($p);
        return (int)$pdo->lastInsertId();
    } catch (Throwable $e) {
        return 0;
    }
}

/* Recompute a folio's stored totals after a charge changes. Best-effort. */
function bcc_recalc_folio(PDO $pdo, int $folioId): void
{
    if ($folioId <= 0 || !bcc_table_exists($pdo, 'folios') || !bcc_table_exists($pdo, 'folio_transactions')) {
        return;
    }
    try {
        $voidSql = bcc_column_exists($pdo, 'folio_transactions', 'is_voided')
            ? " AND COALESCE(is_voided,0) = 0" : "";
        $st = $pdo->prepare("SELECT COALESCE(SUM(debit),0) c, COALESCE(SUM(credit),0) p FROM folio_transactions WHERE folio_id = :fid{$voidSql}");
        $st->execute([':fid' => $folioId]);
        $r = $st->fetch(PDO::FETCH_ASSOC) ?: ['c' => 0, 'p' => 0];
        $charges = round((float)$r['c'], 2);
        $payments = round((float)$r['p'], 2);

        $sets = [];
        $p = [':fid' => $folioId];
        if (bcc_column_exists($pdo, 'folios', 'total_charges'))  { $sets[] = "total_charges = :c";  $p[':c'] = $charges; }
        if (bcc_column_exists($pdo, 'folios', 'total_payments')) { $sets[] = "total_payments = :p"; $p[':p'] = $payments; }
        if (bcc_column_exists($pdo, 'folios', 'balance'))        { $sets[] = "balance = :b";        $p[':b'] = round($charges - $payments, 2); }
        if ($sets) {
            $pdo->prepare("UPDATE folios SET " . implode(', ', $sets) . " WHERE id = :fid")->execute($p);
        }
    } catch (Throwable $e) {
        // non-fatal
    }
}

/*
   Service catalogue for the inline Extras editor: categories plus their
   services and unit prices. Mirrors what extras.php offers, so extras can be
   added from the Control Centre without navigating away.
*/
function bcc_fetch_service_catalogue(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'tblservices')) {
        return ['categories' => [], 'services' => []];
    }

    $categories = [];
    if (bcc_table_exists($pdo, 'tblservicecategory')) {
        try {
            $categories = $pdo->query("SELECT id, category FROM tblservicecategory ORDER BY category ASC")
                ->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) {
            $categories = [];
        }
    }

    $priceCol = null;
    foreach (['price_per_person', 'price', 'rate', 'amount', 'unit_price'] as $c) {
        if (bcc_column_exists($pdo, 'tblservices', $c)) { $priceCol = $c; break; }
    }
    $nameCol = null;
    foreach (['service', 'name', 'title', 'description'] as $c) {
        if (bcc_column_exists($pdo, 'tblservices', $c)) { $nameCol = $c; break; }
    }
    if (!$nameCol) {
        return ['categories' => $categories, 'services' => []];
    }

    $sel = ["s.id", "s.`{$nameCol}` AS service"];
    $sel[] = $priceCol ? "COALESCE(s.`{$priceCol}`,0) AS price" : "0 AS price";
    $sel[] = bcc_column_exists($pdo, 'tblservices', 'category') ? "s.category AS category_id" : "0 AS category_id";

    $where = [];
    foreach (['is_active', 'active', 'status'] as $activeCol) {
        if (bcc_column_exists($pdo, 'tblservices', $activeCol)) {
            if ($activeCol === 'status') {
                $where[] = "COALESCE(CAST(s.`{$activeCol}` AS CHAR),'1') NOT IN ('0','inactive','deleted')";
            } else {
                $where[] = "COALESCE(s.`{$activeCol}`,1) = 1";
            }
            break;
        }
    }

    try {
        $sql = "SELECT " . implode(', ', $sel) . " FROM tblservices s"
             . ($where ? " WHERE " . implode(' AND ', $where) : '')
             . " ORDER BY s.`{$nameCol}` ASC";
        $services = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        $services = [];
    }

    return ['categories' => $categories, 'services' => $services];
}

function bcc_fetch_rooms(PDO $pdo, int $propertyId): array
{
    if (!bcc_table_exists($pdo, 'rooms')) {
        return [];
    }
    $roomNoCol = null;
    foreach (['room_no', 'room_number', 'number', 'name', 'title'] as $c) {
        if (bcc_column_exists($pdo, 'rooms', $c)) { $roomNoCol = $c; break; }
    }
    if (!$roomNoCol) {
        return [];
    }
    $sel = ["rm.id", "rm.`{$roomNoCol}` AS room_no"];
    $sel[] = bcc_column_exists($pdo, 'rooms', 'room_type_id') ? "rm.room_type_id" : "0 AS room_type_id";
    $sel[] = (bcc_table_exists($pdo, 'room_types') && bcc_column_exists($pdo, 'rooms', 'room_type_id'))
        ? "rt.title AS room_type_title" : "'' AS room_type_title";
    $joins = (bcc_table_exists($pdo, 'room_types') && bcc_column_exists($pdo, 'rooms', 'room_type_id'))
        ? " LEFT JOIN room_types rt ON rt.id = rm.room_type_id " : "";
    $where = [];
    $params = [];
    if ($propertyId > 0 && bcc_column_exists($pdo, 'rooms', 'property_id')) {
        $where[] = "rm.property_id = :pid";
        $params[':pid'] = $propertyId;
    }
    foreach (['is_active', 'active', 'status'] as $activeCol) {
        if (bcc_column_exists($pdo, 'rooms', $activeCol)) {
            if ($activeCol === 'status') {
                $where[] = "COALESCE(CAST(rm.`{$activeCol}` AS CHAR),'1') NOT IN ('0','inactive','deleted')";
            } else {
                $where[] = "COALESCE(rm.`{$activeCol}`,1) = 1";
            }
            break;
        }
    }
    try {
        $sql = "SELECT " . implode(', ', $sel) . " FROM rooms rm {$joins}"
             . ($where ? " WHERE " . implode(' AND ', $where) : '')
             . " ORDER BY CAST(rm.`{$roomNoCol}` AS UNSIGNED), rm.`{$roomNoCol}`";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

function bcc_rooming_capabilities(PDO $pdo): array
{
    $cols = ['room_id','room_no','room_type_id','check_in','check_out','adults','children','guest_name','first_name','last_name','remarks','notes'];
    $out = [];
    foreach ($cols as $c) {
        $out[$c] = bcc_column_exists($pdo, 'order_rooming', $c);
    }
    return $out;
}

function bcc_fetch_nightly(PDO $pdo, int $orderId): array
{
    if (!bcc_table_exists($pdo, 'rel_orders_prices')) {
        return [];
    }
    $hasRoomId   = bcc_column_exists($pdo, 'rel_orders_prices', 'room_id');
    $hasRoomType = bcc_column_exists($pdo, 'rel_orders_prices', 'room_type_id');

    $extra = [];
    foreach (['rate_plan_title', 'adults_count', 'kids_count', 'room_id', 'room_type_id',
              'applied_rate_type', 'rate_overridden', 'override_total', 'original_total',
              'override_reason'] as $c) {
        if (bcc_column_exists($pdo, 'rel_orders_prices', $c)) {
            $extra[] = "rop.`{$c}`";
        }
    }
    $extraSel = $extra ? ', ' . implode(', ', $extra) : '';

    /*
       Build the room/type join from ONLY the columns that actually exist.

       Bug this fixes: the type join used COALESCE(rm.room_type_id, rop.room_type_id)
       unconditionally. On installs where rel_orders_prices has no room_type_id the
       whole SELECT threw "no such column", the catch below swallowed it, and the
       Rates tab rendered EMPTY even though the reservation form showed the rates
       perfectly. A silent catch turning a schema mismatch into "no data" is the
       worst of both worlds, so the type expression is now assembled defensively.
    */
    $joinRoom = '';
    $roomSel  = '';
    if ($hasRoomId && bcc_table_exists($pdo, 'rooms')) {
        $joinRoom .= " LEFT JOIN rooms rm ON rm.id = rop.room_id ";
        $roomSel  .= ", rm.room_no AS room_no";

        if (bcc_table_exists($pdo, 'room_types')) {
            // Prefer the physical room's type; fall back to the row's own type only if that column exists.
            $typeExpr = bcc_column_exists($pdo, 'rooms', 'room_type_id')
                ? ($hasRoomType ? "COALESCE(rm.room_type_id, rop.room_type_id)" : "rm.room_type_id")
                : ($hasRoomType ? "rop.room_type_id" : null);
            if ($typeExpr !== null) {
                $joinRoom .= " LEFT JOIN room_types rt ON rt.id = {$typeExpr} ";
                $roomSel  .= ", rt.title AS room_type_title";
            } else {
                $roomSel  .= ", NULL AS room_type_title";
            }
        } else {
            $roomSel .= ", NULL AS room_type_title";
        }
    } elseif ($hasRoomType && bcc_table_exists($pdo, 'room_types')) {
        $joinRoom = " LEFT JOIN room_types rt ON rt.id = rop.room_type_id ";
        $roomSel  = ", NULL AS room_no, rt.title AS room_type_title";
    } else {
        $roomSel = ", NULL AS room_no, NULL AS room_type_title";
    }

    $orderBy = "rop.date ASC" . ($hasRoomId ? ", rop.room_id ASC" : "") . ", rop.id ASC";

    try {
        $stmt = $pdo->prepare("
            SELECT rop.id AS price_id, rop.date, rop.total {$extraSel} {$roomSel},
                   (
                       SELECT rmg_slot.room_no
                       FROM order_rooming rmg_slot
                       WHERE rmg_slot.order_id = rop.order_id
                         AND rop.room_id IS NOT NULL
                         AND rop.room_id > 0
                         AND rmg_slot.room_id = rop.room_id
                         AND DATE(rmg_slot.check_in) <= DATE(rop.date)
                         AND DATE(rmg_slot.check_out) > DATE(rop.date)
                       ORDER BY rmg_slot.id ASC
                       LIMIT 1
                   ) AS slot_no
            FROM rel_orders_prices rop
            {$joinRoom}
            WHERE rop.order_id = :id
            ORDER BY {$orderBy}
        ");
        $stmt->execute([':id' => $orderId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        /*
           Never fail silently: an empty Rates tab is indistinguishable from a
           booking with no rates. Log it and let the caller know why.
        */
        error_log('bcc_fetch_nightly failed for order ' . $orderId . ': ' . $e->getMessage());
        return [];
    }
}

function bcc_fetch_folios(PDO $pdo, int $orderId): array
{
    if (!bcc_table_exists($pdo, 'folios')) {
        return [];
    }
    $stmt = $pdo->prepare("
        SELECT f.*,
            (SELECT COALESCE(SUM(ft.debit),0) FROM folio_transactions ft WHERE ft.folio_id = f.id AND COALESCE(ft.is_voided,0)=0) AS charges,
            (SELECT COALESCE(SUM(ft.credit),0) FROM folio_transactions ft WHERE ft.folio_id = f.id AND COALESCE(ft.is_voided,0)=0) AS payments
        FROM folios f
        WHERE f.order_id = :id
        ORDER BY (f.folio_type = 'master') DESC, f.id ASC
    ");
    $stmt->execute([':id' => $orderId]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}

function bcc_fetch_folio_lines(PDO $pdo, int $orderId): array
{
    if (!bcc_table_exists($pdo, 'folio_transactions')) {
        return [];
    }

    /*
       booking_extra_agent_payers exists in two compatible layouts:
         legacy: transaction_id / agent_id
         newer : folio_transaction_id / payer_agent_id

       The previous fixed JOIN used only the legacy names. Rows saved by the
       newer Extras/Reservation pages therefore loaded with payer_agent_id=0,
       so the UI correctly rendered the charge as an Other folio but had no
       agent id with which to build the proforma button.
    */
    $hasPayerTable = bcc_table_exists($pdo, 'booking_extra_agent_payers');
    $txJoinParts = [];
    $agentParts = [];

    if ($hasPayerTable && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id')) {
        $txJoinParts[] = 'ep.transaction_id = ft.id';
    }
    if ($hasPayerTable && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'folio_transaction_id')) {
        $txJoinParts[] = 'ep.folio_transaction_id = ft.id';
    }
    if ($hasPayerTable && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'agent_id')) {
        $agentParts[] = 'ep.agent_id';
    }
    if ($hasPayerTable && bcc_column_exists($pdo, 'booking_extra_agent_payers', 'payer_agent_id')) {
        $agentParts[] = 'ep.payer_agent_id';
    }

    $payerIdExpr = $agentParts ? 'COALESCE(' . implode(', ', $agentParts) . ', 0)' : '0';
    $payerJoin = ($hasPayerTable && $txJoinParts)
        ? ' LEFT JOIN booking_extra_agent_payers ep ON (' . implode(' OR ', $txJoinParts) . ') '
        : '';

    $agentNameParts = [];
    if (bcc_table_exists($pdo, 'agents')) {
        foreach (['company', 'Agent', 'RegisteredName', 'registered_name', 'agent_name', 'agency_name', 'name'] as $col) {
            if (bcc_column_exists($pdo, 'agents', $col)) {
                $agentNameParts[] = "NULLIF(TRIM(pa.`" . str_replace('`', '``', $col) . "`),'')";
            }
        }
        if (bcc_column_exists($pdo, 'agents', 'firstname') || bcc_column_exists($pdo, 'agents', 'lastname')) {
            $first = bcc_column_exists($pdo, 'agents', 'firstname') ? "COALESCE(pa.firstname,'')" : "''";
            $last  = bcc_column_exists($pdo, 'agents', 'lastname') ? "COALESCE(pa.lastname,'')" : "''";
            $agentNameParts[] = "NULLIF(TRIM(CONCAT({$first},' ',{$last})),'')";
        }
        if (bcc_column_exists($pdo, 'agents', 'email')) {
            $agentNameParts[] = "NULLIF(TRIM(pa.email),'')";
        }
    }
    $payerNameExpr = $agentNameParts
        ? 'COALESCE(' . implode(', ', $agentNameParts) . ", CONCAT('Agent #', {$payerIdExpr}))"
        : "CONCAT('Agent #', {$payerIdExpr})";

    $agentJoin = bcc_table_exists($pdo, 'agents')
        ? " LEFT JOIN agents pa ON pa.id = {$payerIdExpr} "
        : '';

    $stmt = $pdo->prepare("
        SELECT ft.id, ft.tx_date, ft.tx_type, ft.description, ft.debit, ft.credit,
               ft.source_table, ft.source_id, ft.folio_id, f.folio_name, f.folio_type,
               {$payerIdExpr} AS payer_agent_id,
               {$payerNameExpr} AS payer_agent_name,
               COALESCE(ft.is_voided,0) AS is_voided
        FROM folio_transactions ft
        LEFT JOIN folios f ON f.id = ft.folio_id
        {$payerJoin}
        {$agentJoin}
        WHERE ft.order_id = :id
        ORDER BY ft.tx_date ASC, ft.id ASC
    ");
    $stmt->execute([':id' => $orderId]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}

/* ---------------------------------------------------------------------
   Payments
   ---------------------------------------------------------------------
   The folio tab could always SHOW money owed but never take any, so staff
   had to leave the Control Centre, find the booking again in payments.php,
   and post there. Everything below feeds the Payments tab so it happens in
   one place. The posting itself is payment_support.php's job.
   --------------------------------------------------------------------- */

function bcc_fetch_payments(PDO $pdo, int $orderId): array
{
    if (!bcc_table_exists($pdo, 'payment')) {
        return [];
    }
    $rows = get_booking_payments($pdo, $orderId);
    foreach ($rows as $i => $r) {
        // Reversal state drives the button; work it out once, server-side,
        // rather than letting the client guess from the void flags.
        $rows[$i]['is_reversed'] = payment_is_reversed($pdo, (int)$r['id']) ? 1 : 0;
    }
    return $rows;
}

function bcc_payment_targets(PDO $pdo, int $orderId): array
{
    /*
       The "Post To" list: master, then each room folio, then each individual
       guest folio. The value of a room/guest option is a JSON payload, not an
       id - that is the contract get_payment_target_folio() already expects, so
       it is passed through untouched.
    */
    $targets = [];
    if (!bcc_table_exists($pdo, 'folios')) {
        return $targets;
    }

    try {
        $masterId = get_or_create_master_folio($pdo, $orderId);
        $bal = 0.00;
        $stmt = $pdo->prepare("SELECT balance FROM folios WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $masterId]);
        $bal = round((float)($stmt->fetchColumn() ?: 0), 2);

        $targets[] = [
            'value'   => 'master',
            'label'   => 'Master Folio (Agent / Master Account)',
            'group'   => '',
            'balance' => $bal,
        ];
    } catch (Throwable $e) {
        $targets[] = ['value' => 'master', 'label' => 'Master Folio (Agent / Master Account)', 'group' => '', 'balance' => 0.00];
    }

    try {
        foreach (get_room_folios($pdo, $orderId) as $r) {
            $payload = $r;
            $payload['target_type'] = 'room';
            $label = 'Room ' . (trim((string)($r['room_no'] ?? '')) !== '' ? $r['room_no'] : 'N/A');
            if (!empty($r['room_type_title'])) {
                $label .= ' - ' . $r['room_type_title'];
            }
            $guest = trim((string)($r['guest'] ?? ''));
            $label .= ' - ' . ($guest !== '' ? $guest : 'Guest');
            $targets[] = [
                'value'   => json_encode($payload),
                'label'   => $label,
                'group'   => 'Room Folios',
                'balance' => round((float)($r['balance'] ?? 0), 2),
            ];
        }
    } catch (Throwable $e) { /* room folios are optional */ }

    try {
        foreach (get_guest_folios($pdo, $orderId) as $gf) {
            $payload = [
                'target_type' => 'guest',
                'folio_id'    => (int)($gf['folio_id'] ?? 0),
                'rooming_id'  => (int)($gf['rooming_id'] ?? 0),
                'occupant_id' => (int)($gf['occupant_id'] ?? 0),
            ];
            $name = trim((string)($gf['occupant_name'] ?? ''));
            if ($name === '') {
                $name = trim((string)($gf['folio_name'] ?? 'Guest Folio'));
            }
            $label = 'Guest: ' . $name;
            if (!empty($gf['room_no'])) {
                $label .= ' - Room ' . $gf['room_no'];
            }
            $targets[] = [
                'value'   => json_encode($payload),
                'label'   => $label,
                'group'   => 'Individual Guest Folios',
                'balance' => round((float)($gf['balance'] ?? 0), 2),
            ];
        }
    } catch (Throwable $e) { /* guest folios are optional */ }

    return $targets;
}

function bcc_fetch_currencies(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'tblcurrency')) {
        // No currency table: USD at parity keeps the modal usable.
        return [['id' => 1, 'code' => 'USD', 'name' => 'United States Dollars', 'roe' => 1.0]];
    }
    try {
        $stmt = $pdo->query("
            SELECT CurrencyID, CurrencyCode, Currency, ROE
            FROM tblcurrency
            WHERE status = 1
            ORDER BY CASE WHEN order_no IS NULL THEN 999999 ELSE order_no END ASC, CurrencyCode ASC
        ");
        $out = [];
        foreach (($stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []) as $c) {
            $out[] = [
                'id'   => (int)$c['CurrencyID'],
                'code' => trim((string)$c['CurrencyCode']),
                'name' => trim((string)$c['Currency']),
                // ROE always means: currency per 1 USD.
                'roe'  => (float)$c['ROE'],
            ];
        }
        return $out ?: [['id' => 1, 'code' => 'USD', 'name' => 'United States Dollars', 'roe' => 1.0]];
    } catch (Throwable $e) {
        return [['id' => 1, 'code' => 'USD', 'name' => 'United States Dollars', 'roe' => 1.0]];
    }
}

function bcc_fetch_bank_accounts(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'tblbankdetails')) return [];
    try {
        $stmt = $pdo->query("SELECT ID, Bank, AccountName, AccountNo FROM tblbankdetails WHERE bank_status = 1 ORDER BY Bank ASC");
        return $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
    } catch (Throwable $e) { return []; }
}

function bcc_fetch_pos_machines(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'pos_machines') || !bcc_table_exists($pdo, 'tblbankdetails')) return [];
    try {
        $stmt = $pdo->query("
            SELECT pm.id, pm.machine_name, pm.terminal_id, b.Bank
            FROM pos_machines pm
            INNER JOIN tblbankdetails b ON b.ID = pm.bank_detail_id
            WHERE pm.status = 1 AND b.bank_status = 1
            ORDER BY pm.machine_name ASC
        ");
        return $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
    } catch (Throwable $e) { return []; }
}

function bcc_payment_methods(): array
{
    // Mirrors the list in payments.php. Card/POS attract the 3% surcharge;
    // is_card_surcharge_method() in payment_support.php is the authority.
    return ['Cash', 'Bank Transfer', 'Card', 'POS', 'Mobile Money','Intercompany','Bank Charge'];
}

function bcc_fetch_colours(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'booking_color_codes')) {
        return [];
    }
    $cc = bcc_colour_cols($pdo);
    if (!$cc['hex']) {
        return []; // no usable colour column
    }
    $sel = ["id"];
    $sel[] = "`{$cc['hex']}` AS hex_color";
    $sel[] = $cc['text'] ? "`{$cc['text']}` AS text_color" : "'#111827' AS text_color";
    $sel[] = $cc['label'] ? "`{$cc['label']}` AS status_label" : "id AS status_label";
    $sel[] = $cc['key'] ? "`{$cc['key']}` AS status_key" : "id AS status_key";
    $where = $cc['active'] ? "WHERE `{$cc['active']}` = 1" : "";
    $order = $cc['priority'] ? "ORDER BY `{$cc['priority']}` ASC" : "ORDER BY id ASC";
    try {
        $stmt = $pdo->query("SELECT " . implode(', ', $sel) . " FROM booking_color_codes {$where} {$order}");
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

function bcc_fetch_agents(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'agents')) {
        return [];
    }

    try {
        /*
           Agent records are not always stored as firstname + lastname.
           Tour operators such as Wilderness Safaris are commonly stored in
           company / agency / Agent / RegisteredName columns. The old query
           displayed only firstname + lastname and also stopped after the first
           1,000 rows, so valid company agents could be blank or omitted.
        */
        $nameParts = [];
        foreach (['company', 'company_name', 'agency_name', 'agent_name', 'Agent', 'RegisteredName', 'name'] as $column) {
            if (bcc_column_exists($pdo, 'agents', $column)) {
                $safe = str_replace('`', '``', $column);
                $nameParts[] = "NULLIF(TRIM(`{$safe}`),'')";
            }
        }

        if (bcc_column_exists($pdo, 'agents', 'firstname') || bcc_column_exists($pdo, 'agents', 'lastname')) {
            $first = bcc_column_exists($pdo, 'agents', 'firstname') ? "COALESCE(firstname,'')" : "''";
            $last  = bcc_column_exists($pdo, 'agents', 'lastname')  ? "COALESCE(lastname,'')"  : "''";
            $nameParts[] = "NULLIF(TRIM(CONCAT({$first},' ',{$last})),'')";
        }

        if (bcc_column_exists($pdo, 'agents', 'email')) {
            $nameParts[] = "NULLIF(TRIM(email),'')";
        }

        $nameExpr = $nameParts
            ? "COALESCE(" . implode(',', $nameParts) . ", CONCAT('Agent #',id))"
            : "CONCAT('Agent #',id)";

        $sql = "SELECT id, {$nameExpr} AS name FROM agents ORDER BY name ASC, id ASC";
        $stmt = $pdo->query($sql);
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

function bcc_fetch_properties(PDO $pdo): array
{
    if (!bcc_table_exists($pdo, 'properties')) {
        return [];
    }
    try {
        $stmt = $pdo->query("SELECT id, name FROM properties ORDER BY name ASC");
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

function bcc_fetch_timeline(PDO $pdo, int $orderId): array
{
    $events = [];

    /* Resolve a user-name expression once, if a users table exists. */
    $userTable = null;
    foreach (['users', 'user', 'staff', 'employees'] as $t) {
        if (bcc_table_exists($pdo, $t)) { $userTable = $t; break; }
    }
    $userNameCol = null;
    if ($userTable) {
        foreach (['name', 'fullname', 'full_name', 'username', 'display_name', 'firstname'] as $c) {
            if (bcc_column_exists($pdo, $userTable, $c)) { $userNameCol = $c; break; }
        }
    }
    $nameCache = [];
    $resolveUser = function ($uid) use ($pdo, $userTable, $userNameCol, &$nameCache) {
        $uid = (int)$uid;
        if ($uid <= 0) return '';
        if (array_key_exists($uid, $nameCache)) return $nameCache[$uid];
        if (!$userTable || !$userNameCol) { $nameCache[$uid] = 'User #' . $uid; return $nameCache[$uid]; }
        try {
            $st = $pdo->prepare("SELECT `{$userNameCol}` FROM `{$userTable}` WHERE id = :id LIMIT 1");
            $st->execute([':id' => $uid]);
            $n = (string)$st->fetchColumn();
            $nameCache[$uid] = $n !== '' ? $n : ('User #' . $uid);
        } catch (Throwable $e) {
            $nameCache[$uid] = 'User #' . $uid;
        }
        return $nameCache[$uid];
    };

    /* 1) The system action log — the primary audit source. */
    if (bcc_table_exists($pdo, 'order_action_log')) {
        try {
            $detailCol = null;
            foreach (['detail', 'details', 'description', 'note', 'message'] as $c) {
                if (bcc_column_exists($pdo, 'order_action_log', $c)) { $detailCol = $c; break; }
            }
            $actionCol = null;
            foreach (['action', 'action_type', 'event', 'activity'] as $c) {
                if (bcc_column_exists($pdo, 'order_action_log', $c)) { $actionCol = $c; break; }
            }
            $whenCol = null;
            foreach (['created_at', 'logged_at', 'created_on', 'action_at', 'timestamp'] as $c) {
                if (bcc_column_exists($pdo, 'order_action_log', $c)) { $whenCol = $c; break; }
            }
            $userCol = null;
            foreach (['user_id', 'created_by', 'actioned_by', 'staff_id'] as $c) {
                if (bcc_column_exists($pdo, 'order_action_log', $c)) { $userCol = $c; break; }
            }
            if ($actionCol && $whenCol) {
                $sel = "`{$actionCol}` AS action, `{$whenCol}` AS when_at";
                $sel .= $detailCol ? ", `{$detailCol}` AS detail" : ", '' AS detail";
                $sel .= $userCol ? ", `{$userCol}` AS uid" : ", 0 AS uid";
                $st = $pdo->prepare("SELECT {$sel} FROM order_action_log WHERE order_id = :id ORDER BY `{$whenCol}` DESC LIMIT 200");
                $st->execute([':id' => $orderId]);
                foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
                    $events[] = [
                        'when'   => (string)$r['when_at'],
                        'action' => (string)$r['action'],
                        'detail' => (string)$r['detail'],
                        'who'    => $resolveUser($r['uid']),
                        'source' => 'Audit log',
                    ];
                }
            }
        } catch (Throwable $e) { /* ignore */ }
    }

    /* 2) Financial events from the folio ledger (charges, payments, voids). */
    if (bcc_table_exists($pdo, 'folio_transactions')) {
        try {
            $hasVoid = bcc_column_exists($pdo, 'folio_transactions', 'is_voided');
            $hasCreated = bcc_column_exists($pdo, 'folio_transactions', 'created_at');
            $whenExpr = $hasCreated ? 'ft.created_at' : 'ft.tx_date';
            $userSel = bcc_column_exists($pdo, 'folio_transactions', 'created_by') ? 'ft.created_by' : '0';
            $st = $pdo->prepare("
                SELECT {$whenExpr} AS when_at, ft.description, ft.debit, ft.credit,
                       {$userSel} AS uid" . ($hasVoid ? ", COALESCE(ft.is_voided,0) AS voided" : ", 0 AS voided") . "
                FROM folio_transactions ft
                WHERE ft.order_id = :id
                ORDER BY {$whenExpr} DESC
                LIMIT 200
            ");
            $st->execute([':id' => $orderId]);
            foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $debit = (float)$r['debit'];
                $credit = (float)$r['credit'];
                $voided = ((int)$r['voided'] === 1);
                if ($voided) {
                    $act = 'transaction_voided';
                } elseif ($credit > 0) {
                    $act = 'payment_received';
                } else {
                    $act = 'charge_posted';
                }
                $amt = $credit > 0 ? $credit : $debit;
                $events[] = [
                    'when'   => (string)$r['when_at'],
                    'action' => $act,
                    'detail' => trim((string)$r['description']) . ' — ' . number_format($amt, 2),
                    'who'    => $resolveUser($r['uid']),
                    'source' => 'Folio',
                ];
            }
        } catch (Throwable $e) { /* ignore */ }
    }

    /* 3) The booking's own lifecycle stamps. */
    try {
        $cols = [];
        foreach (['ordered_on' => 'booking_created', 'created_at' => 'booking_created', 'updated_at' => 'booking_updated'] as $c => $label) {
            if (bcc_column_exists($pdo, 'orders', $c)) { $cols[$c] = $label; }
        }
        $userCols = [];
        foreach (['created_by', 'updated_by'] as $c) {
            if (bcc_column_exists($pdo, 'orders', $c)) { $userCols[] = $c; }
        }
        if ($cols) {
            $sel = implode(', ', array_map(function ($c) { return "`{$c}`"; }, array_keys($cols)));
            if ($userCols) {
                $sel .= ', ' . implode(', ', array_map(function ($c) { return "`{$c}`"; }, $userCols));
            }
            $st = $pdo->prepare("SELECT {$sel} FROM orders WHERE id = :id LIMIT 1");
            $st->execute([':id' => $orderId]);
            $row = $st->fetch(PDO::FETCH_ASSOC);
            if ($row) {
                foreach ($cols as $c => $label) {
                    if (!empty($row[$c])) {
                        $uid = 0;
                        if ($label === 'booking_created' && isset($row['created_by'])) { $uid = (int)$row['created_by']; }
                        if ($label === 'booking_updated' && isset($row['updated_by'])) { $uid = (int)$row['updated_by']; }
                        $events[] = [
                            'when'   => (string)$row[$c],
                            'action' => $label,
                            'detail' => $label === 'booking_created' ? 'Booking created.' : 'Booking last updated.',
                            'who'    => $resolveUser($uid),
                            'source' => 'Booking',
                        ];
                    }
                }
            }
        }
    } catch (Throwable $e) { /* ignore */ }

    /* Merge chronologically, newest first. */
    usort($events, function ($a, $b) {
        return strcmp((string)$b['when'], (string)$a['when']);
    });

    return array_slice($events, 0, 250);
}

/* Build a compact financial summary for a booking. */
function bcc_financials(PDO $pdo, int $orderId, array $order): array
{
    $accommodation = 0.0;
    if (bcc_table_exists($pdo, 'rel_orders_prices')) {
        $stmt = $pdo->prepare("SELECT COALESCE(SUM(total),0) FROM rel_orders_prices WHERE order_id = :id");
        $stmt->execute([':id' => $orderId]);
        $accommodation = (float)$stmt->fetchColumn();
    }
    if ($accommodation <= 0) {
        $accommodation = (float)($order['totalamount'] ?? 0);
    }

    $charges = 0.0;
    $payments = 0.0;
    if (bcc_table_exists($pdo, 'folio_transactions')) {
        $stmt = $pdo->prepare("
            SELECT COALESCE(SUM(debit),0) AS c, COALESCE(SUM(credit),0) AS p
            FROM folio_transactions
            WHERE order_id = :id AND COALESCE(is_voided,0) = 0
        ");
        $stmt->execute([':id' => $orderId]);
        $r = $stmt->fetch(PDO::FETCH_ASSOC) ?: ['c' => 0, 'p' => 0];
        $charges = (float)$r['c'];
        $payments = (float)$r['p'];
    }

    // Extras = folio charges that are not accommodation (rough split).
    $extras = 0.0;
    if (bcc_table_exists($pdo, 'folio_transactions')) {
        $stmt = $pdo->prepare("
            SELECT COALESCE(SUM(debit),0)
            FROM folio_transactions
            WHERE order_id = :id AND COALESCE(is_voided,0) = 0
              AND LOWER(COALESCE(source_table,'')) IN ('tblservices','restaurant_menu_items','inventory_items','booking_extras')
        ");
        $stmt->execute([':id' => $orderId]);
        $extras = (float)$stmt->fetchColumn();
    }

    $grand = $charges > 0 ? $charges : $accommodation;
    if ($charges > 0 && $accommodation > 0) {
        // If accommodation isn't yet posted, add it on top of extras.
        $accomPosted = ($charges - $extras) > 0.5;
        if (!$accomPosted) {
            $grand = $accommodation + $extras;
        }
    }
    $balance = max(0, $grand - $payments);

    return [
        'accommodation' => round($accommodation, 2),
        'extras'        => round($extras, 2),
        'charges'       => round($grand, 2),
        'payments'      => round($payments, 2),
        'balance'       => round($balance, 2),
    ];
}

/* --------------------------------------------------------------------------
   AJAX API router
-------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Full cancellation accounting + safe reinstatement inside Control Centre
-------------------------------------------------------------------------- */
function bcc_try_add_column(PDO $pdo, string $table, string $column, string $definition): void
{
    if (bcc_column_exists($pdo, $table, $column)) return;
    try { $pdo->exec("ALTER TABLE `" . str_replace('`','``',$table) . "` ADD COLUMN " . $definition); } catch (Throwable $e) {}
}

function bcc_ensure_cancellation_columns(PDO $pdo): void
{
    bcc_try_add_column($pdo,'orders','cancellation_reason','`cancellation_reason` TEXT NULL');
    bcc_try_add_column($pdo,'orders','cancellation_policy_action','`cancellation_policy_action` VARCHAR(40) NULL');
    bcc_try_add_column($pdo,'orders','cancellation_original_balance','`cancellation_original_balance` DECIMAL(14,2) NOT NULL DEFAULT 0');
    bcc_try_add_column($pdo,'orders','cancellation_adjustment','`cancellation_adjustment` DECIMAL(14,2) NOT NULL DEFAULT 0');
    bcc_try_add_column($pdo,'orders','cancellation_policy_charge','`cancellation_policy_charge` DECIMAL(14,2) NOT NULL DEFAULT 0');
    bcc_try_add_column($pdo,'orders','cancellation_at','`cancellation_at` DATETIME NULL');
    bcc_try_add_column($pdo,'orders','cancellation_by','`cancellation_by` INT UNSIGNED NULL');
    bcc_try_add_column($pdo,'orders','cancellation_inventory_released','`cancellation_inventory_released` TINYINT(1) NOT NULL DEFAULT 0');
    bcc_try_add_column($pdo,'order_rooming','released_status','`released_status` TINYINT(1) NOT NULL DEFAULT 0');
    bcc_try_add_column($pdo,'order_rooming','released_at','`released_at` DATETIME NULL');
    bcc_try_add_column($pdo,'order_rooming','release_reason','`release_reason` VARCHAR(255) NULL');
}

function bcc_live_order_balance(PDO $pdo, int $orderId): float
{
    $sql = "SELECT COALESCE(SUM(COALESCE(debit,0)-COALESCE(credit,0)),0) FROM folio_transactions WHERE order_id=:id";
    if (bcc_column_exists($pdo,'folio_transactions','is_voided')) $sql .= " AND COALESCE(is_voided,0)=0";
    $st=$pdo->prepare($sql); $st->execute([':id'=>$orderId]);
    return round((float)$st->fetchColumn(),2);
}

function bcc_insert_cancellation_line(PDO $pdo, array $order, int $folioId, string $type, string $description, float $debit, float $credit): void
{
    $cols=['folio_id','order_id','tx_date','tx_type','description','debit','credit','source_table','source_id'];
    $vals=[':fid',':oid','NOW()',':typ',':des',':deb',':cre',"'orders'",':sid'];
    $par=[':fid'=>$folioId,':oid'=>(int)$order['id'],':typ'=>$type,':des'=>$description,':deb'=>round(max(0,$debit),2),':cre'=>round(max(0,$credit),2),':sid'=>(int)$order['id']];
    if (bcc_column_exists($pdo,'folio_transactions','property_id')) { array_unshift($cols,'property_id'); array_unshift($vals,':pid'); $par[':pid']=(int)($order['property_id']??0); }
    if (bcc_column_exists($pdo,'folio_transactions','created_at')) { $cols[]='created_at'; $vals[]='NOW()'; }
    if (bcc_column_exists($pdo,'folio_transactions','created_by')) { $cols[]='created_by'; $vals[]=':uid'; $par[':uid']=bcc_current_user_id()?:null; }
    $pdo->prepare('INSERT INTO folio_transactions (`'.implode('`,`',$cols).'`) VALUES ('.implode(',',$vals).')')->execute($par);
}

function bcc_recalc_order_folios(PDO $pdo, int $orderId): void
{
    $st=$pdo->prepare("SELECT id FROM folios WHERE order_id=:id"); $st->execute([':id'=>$orderId]);
    foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $fid) bcc_recalc_folio($pdo,(int)$fid);
}

function bcc_release_cancelled_inventory(PDO $pdo, int $orderId): void
{
    bcc_ensure_cancellation_columns($pdo);
    $roomIds=[];
    $st=$pdo->prepare("SELECT DISTINCT room_id FROM order_rooming WHERE order_id=:id AND room_id IS NOT NULL AND room_id>0");
    $st->execute([':id'=>$orderId]); foreach($st->fetchAll(PDO::FETCH_COLUMN) as $rid) $roomIds[(int)$rid]=1;
    $sets=[];
    if (bcc_column_exists($pdo,'order_rooming','released_status')) $sets[]='released_status=1';
    if (bcc_column_exists($pdo,'order_rooming','released_at')) $sets[]='released_at=NOW()';
    if (bcc_column_exists($pdo,'order_rooming','release_reason')) $sets[]="release_reason='Cancelled reservation'";
    if (bcc_column_exists($pdo,'order_rooming','checked_in_status')) $sets[]='checked_in_status=0';
    if (bcc_column_exists($pdo,'order_rooming','checked_out_status')) $sets[]='checked_out_status=1';
    if ($sets) $pdo->prepare('UPDATE order_rooming SET '.implode(',',$sets).' WHERE order_id=:id')->execute([':id'=>$orderId]);
    if ($roomIds && bcc_table_exists($pdo,'rooms') && bcc_column_exists($pdo,'rooms','status')) {
        foreach(array_keys($roomIds) as $rid) $pdo->prepare("UPDATE rooms SET status='available' WHERE id=:id")->execute([':id'=>$rid]);
    }
}

function bcc_check_reinstate_availability(PDO $pdo, int $orderId): array
{
    $st=$pdo->prepare("SELECT id,check_in,check_out FROM orders WHERE id=:id LIMIT 1"); $st->execute([':id'=>$orderId]);
    $o=$st->fetch(PDO::FETCH_ASSOC); if(!$o) return ['ok'=>false,'available'=>false,'message'=>'Booking not found.','rooms'=>[]];
    $cin=substr((string)$o['check_in'],0,10); $cout=substr((string)$o['check_out'],0,10);
    $st=$pdo->prepare("SELECT rmg.id rooming_id,rmg.room_id,COALESCE(rm.room_no,'') room_no,COALESCE(rt.title,'') room_type FROM order_rooming rmg LEFT JOIN rooms rm ON rm.id=rmg.room_id LEFT JOIN room_types rt ON rt.id=rmg.room_type_id WHERE rmg.order_id=:id ORDER BY rmg.id");
    $st->execute([':id'=>$orderId]); $rows=$st->fetchAll(PDO::FETCH_ASSOC)?:[]; $out=[]; $all=true;
    foreach($rows as $r){
        $rid=(int)$r['room_id'];
        if($rid<=0){$out[]=['room_no'=>'Holding','room_type'=>$r['room_type'],'available'=>true,'clash'=>null];continue;}
        $released = bcc_column_exists($pdo,'order_rooming','released_status') ? ' AND COALESCE(x.released_status,0)=0 ' : '';
        $bookingStatus = bcc_column_exists($pdo,'orders','booking_status') ? " AND LOWER(COALESCE(ob.booking_status,'')) NOT IN ('no_show','noshow') " : '';
        $noShow = bcc_column_exists($pdo,'orders','no_show_at') ? ' AND ob.no_show_at IS NULL ' : '';
        $q=$pdo->prepare("SELECT ob.order_no,TRIM(CONCAT(COALESCE(g.firstname,''),' ',COALESCE(g.lastname,''))) guest,ob.check_in,ob.check_out FROM order_rooming x INNER JOIN orders ob ON ob.id=x.order_id LEFT JOIN guests g ON g.id=ob.guest_id WHERE x.room_id=:rid AND x.order_id<>:oid {$released} AND LOWER(TRIM(CAST(COALESCE(ob.status,1) AS CHAR))) NOT IN ('0','cancelled','canceled','void','deleted','no_show','noshow') {$bookingStatus} {$noShow} AND DATE(ob.check_in)<DATE(:cout) AND DATE(ob.check_out)>DATE(:cin) LIMIT 1");
        $q->execute([':rid'=>$rid,':oid'=>$orderId,':cout'=>$cout,':cin'=>$cin]); $cl=$q->fetch(PDO::FETCH_ASSOC)?:null;
        if($cl)$all=false;
        $out[]=['room_no'=>$r['room_no']!==''?$r['room_no']:'#'.$rid,'room_type'=>$r['room_type'],'available'=>!$cl,'clash'=>$cl?['order_no'=>$cl['order_no'],'guest'=>$cl['guest'],'check_in'=>substr($cl['check_in'],0,10),'check_out'=>substr($cl['check_out'],0,10)]:null];
    }
    return ['ok'=>true,'available'=>$all,'check_in'=>$cin,'check_out'=>$cout,'rooms'=>$out];
}

$api = isset($_GET['api']) ? (string)$_GET['api'] : (isset($_POST['api']) ? (string)$_POST['api'] : '');

if ($api === 'diag') {
    // Quick schema diagnostic: reveals real column names so mappings can be verified.
    header('Content-Type: text/plain; charset=utf-8');

    /*
       Per-order data dump: ?api=diag&order_id=NNN shows exactly what is stored
       in order_rooming and rel_orders_prices for one booking, so a pricing
       mismatch (grid says 3 adults, invoice charges 2) can be traced to the
       actual persisted values rather than guessed at.
    */
    $diagOrder = (int)($_GET['order_id'] ?? $_GET['id'] ?? 0);
    if ($diagOrder > 0) {
        echo "==================================================\n";
        echo " ORDER {$diagOrder} - stored rooming vs priced rows\n";
        echo "==================================================\n\n";

        echo "== order_rooming (what the grid edits / reprice reads) ==\n";
        if (bcc_table_exists($pdo, 'order_rooming')) {
            try {
                $rr = $pdo->prepare("SELECT * FROM order_rooming WHERE order_id = :o ORDER BY room_no ASC, id ASC");
                $rr->execute([':o' => $diagOrder]);
                $rows = $rr->fetchAll(PDO::FETCH_ASSOC) ?: [];
                if (!$rows) { echo "  (no rooming rows)\n"; }
                foreach ($rows as $r) {
                    echo "  id=" . ($r['id'] ?? '?')
                       . "  room_no=" . ($r['room_no'] ?? '-')
                       . "  room_id=" . ($r['room_id'] ?? '-')
                       . "  room_type_id=" . ($r['room_type_id'] ?? '-')
                       . "  adults=" . (array_key_exists('adults', $r) ? $r['adults'] : '(no col)')
                       . "  children=" . (array_key_exists('children', $r) ? $r['children'] : '(no col)')
                       . "  in=" . substr((string)($r['check_in'] ?? ''), 0, 10)
                       . "  out=" . substr((string)($r['check_out'] ?? ''), 0, 10)
                       . "\n";
                }
                echo "\n  columns present: " . implode(', ', array_keys($rows[0] ?? [])) . "\n";
            } catch (Throwable $e) {
                echo "  error: " . $e->getMessage() . "\n";
            }
        } else {
            echo "  (table not found)\n";
        }

        echo "\n== rel_orders_prices (what the proforma/folio bill from) ==\n";
        if (bcc_table_exists($pdo, 'rel_orders_prices')) {
            try {
                $pr = $pdo->prepare("SELECT * FROM rel_orders_prices WHERE order_id = :o ORDER BY date ASC, id ASC");
                $pr->execute([':o' => $diagOrder]);
                $prows = $pr->fetchAll(PDO::FETCH_ASSOC) ?: [];
                if (!$prows) { echo "  (no priced rows - booking has never been priced)\n"; }
                foreach ($prows as $r) {
                    echo "  date=" . substr((string)($r['date'] ?? ''), 0, 10)
                       . "  room_id=" . ($r['room_id'] ?? '-')
                       . "  type=" . ($r['room_type_id'] ?? '-')
                       . "  adults_count=" . (array_key_exists('adults_count', $r) ? $r['adults_count'] : '(no col)')
                       . "  sharing_rate=" . (array_key_exists('sharing_rate', $r) ? $r['sharing_rate'] : '-')
                       . "  applied=" . ($r['applied_rate_type'] ?? '-')
                       . "  overridden=" . (array_key_exists('rate_overridden', $r) ? $r['rate_overridden'] : '-')
                       . "  total=" . ($r['total'] ?? '-')
                       . "\n";
                }
                if ($prows) {
                    $sum = 0.0; foreach ($prows as $r) { $sum += (float)($r['total'] ?? 0); }
                    echo "\n  SUM(total) = " . number_format($sum, 2) . "\n";
                }
            } catch (Throwable $e) {
                echo "  error: " . $e->getMessage() . "\n";
            }
        } else {
            echo "  (table not found)\n";
        }

        echo "\n== live reprice preview (adults the engine WOULD use, no write) ==\n";
        if (bcc_table_exists($pdo, 'order_rooming')) {
            try {
                $rr = $pdo->prepare("SELECT * FROM order_rooming WHERE order_id = :o ORDER BY room_no ASC, id ASC");
                $rr->execute([':o' => $diagOrder]);
                foreach (($rr->fetchAll(PDO::FETCH_ASSOC) ?: []) as $slot) {
                    $a = max(0, (int)($slot['adults'] ?? 0));
                    $c = max(0, (int)($slot['children'] ?? 0));
                    echo "  room_no=" . ($slot['room_no'] ?? '-')
                       . "  -> reprice would read adults=" . $a . " children=" . $c . "\n";
                }
            } catch (Throwable $e) {
                echo "  error: " . $e->getMessage() . "\n";
            }
        }
        exit;
    }

    foreach (['orders', 'booking_color_codes', 'properties', 'agents', 'guests'] as $t) {
        echo "== {$t} ==\n";
        if (!bcc_table_exists($pdo, $t)) { echo "  (table not found)\n\n"; continue; }
        try {
            $safe = str_replace('`', '``', $t);
            $rows = $pdo->query("SHOW COLUMNS FROM `{$safe}`")->fetchAll(PDO::FETCH_ASSOC);
            foreach ($rows as $r) {
                echo "  " . ($r['Field'] ?? '') . " (" . ($r['Type'] ?? '') . ")\n";
            }
        } catch (Throwable $e) {
            echo "  error: " . $e->getMessage() . "\n";
        }
        echo "\n";
    }
    echo "== colour column resolution ==\n";
    var_export(bcc_colour_cols($pdo));
    exit;
}


if ($api !== '') {

    try {
        switch ($api) {

            /* ---- Search ---- */
            case 'search': {
                $filters = [
                    'q'           => $_GET['q'] ?? '',
                    'agent_id'    => $_GET['agent_id'] ?? 0,
                    'status'      => $_GET['status'] ?? '',
                    'property_id' => $_GET['property_id'] ?? 0,
                    'date_field'  => $_GET['date_field'] ?? 'check_in',
                    'date_from'   => $_GET['date_from'] ?? '',
                    'date_to'     => $_GET['date_to'] ?? '',
                ];
                $rows = bcc_search_orders($pdo, $filters);
                bcc_json(['ok' => true, 'results' => $rows]);
                break;
            }

            /* ---- Full booking payload for the control panel ---- */
            case 'booking': {
                $orderId = (int)($_GET['order_id'] ?? 0);
                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }
                $currency = trim((string)($order['property_currency_code'] ?? 'USD')) ?: 'USD';
                $symbol = bcc_currency_symbol($currency);

                bcc_json([
                    'ok'         => true,
                    'order'      => $order,
                    'currency'   => $currency,
                    'symbol'     => $symbol,
                    'today'      => date('Y-m-d'),
                    'rooming'    => bcc_fetch_rooming($pdo, $orderId),
                    'rooms'      => bcc_fetch_rooms($pdo, (int)($order['property_id'] ?? 0)),
                    'room_types' => bcc_fetch_room_types($pdo, (int)($order['property_id'] ?? 0)),
                    'catalogue'  => bcc_fetch_service_catalogue($pdo),
                    'rooming_capabilities' => bcc_rooming_capabilities($pdo),
                    'nightly'    => bcc_fetch_nightly($pdo, $orderId),
                    'folios'     => bcc_fetch_folios($pdo, $orderId),
                    'folio_lines'=> bcc_fetch_folio_lines($pdo, $orderId),
                    'financials' => bcc_financials($pdo, $orderId, $order),
                    'colours'    => bcc_fetch_colours($pdo),
                    'timeline'   => bcc_fetch_timeline($pdo, $orderId),
                    /* Payments tab */
                    'payments'         => bcc_fetch_payments($pdo, $orderId),
                    'payment_targets'  => bcc_payment_targets($pdo, $orderId),
                    'currencies'       => bcc_fetch_currencies($pdo),
                    'bank_accounts'    => bcc_fetch_bank_accounts($pdo),
                    'pos_machines'     => bcc_fetch_pos_machines($pdo),
                    'payment_methods'  => bcc_payment_methods(),
                    'cc_surcharge_rate'=> CC_SURCHARGE_RATE,
                    'can_reverse_payment' => (bool)payment_reversal_authorised($pdo)['ok'],
                ]);
                break;
            }


            /* ---- Live availability probe for the Add Room panel ---- */
            case 'check_room_availability': {
                $orderId    = (int)($_GET['order_id'] ?? $_POST['order_id'] ?? 0);
                $roomTypeId = (int)($_GET['room_type_id'] ?? $_POST['room_type_id'] ?? 0);
                $checkIn    = substr(trim((string)($_GET['check_in'] ?? $_POST['check_in'] ?? '')), 0, 10);
                $checkOut   = substr(trim((string)($_GET['check_out'] ?? $_POST['check_out'] ?? '')), 0, 10);

                if ($orderId <= 0 || $roomTypeId <= 0
                    || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $checkIn)
                    || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $checkOut)
                    || $checkOut <= $checkIn) {
                    bcc_json(['ok' => false, 'error' => 'Enter a room type and a valid date range.']);
                }

                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }
                $propertyId = (int)($order['property_id'] ?? 0);

                $avail = bcc_room_type_availability($pdo, $propertyId, $roomTypeId, $checkIn, $checkOut);

                // Which physical rooms of this type are free for the WHOLE range?
                $freeRooms = [];
                $allRooms = bcc_fetch_rooms($pdo, $propertyId);
                $hasOrders = bcc_table_exists($pdo, 'orders');
                $statusSql = ($hasOrders && bcc_column_exists($pdo, 'orders', 'status'))
                    ? ' AND ' . bcc_active_status_sql('o') : '';
                $joinOrders = $hasOrders ? ' INNER JOIN orders o ON o.id = rmg.order_id ' : '';

                if (bcc_column_exists($pdo, 'order_rooming', 'room_id')) {
                    try {
                        $busy = $pdo->prepare("
                            SELECT COUNT(*) FROM order_rooming rmg {$joinOrders}
                            WHERE rmg.room_id = :room_id
                              AND DATE(rmg.check_in) < :checkout
                              AND DATE(rmg.check_out) > :checkin
                              {$statusSql}
                        ");
                        foreach ($allRooms as $rm) {
                            if ((int)$rm['room_type_id'] !== $roomTypeId) {
                                continue;
                            }
                            $busy->execute([':room_id' => (int)$rm['id'], ':checkout' => $checkOut, ':checkin' => $checkIn]);
                            if ((int)$busy->fetchColumn() === 0) {
                                $freeRooms[] = ['id' => (int)$rm['id'], 'room_no' => (string)$rm['room_no']];
                            }
                        }
                    } catch (Throwable $e) {
                        // fall through with whatever we have
                    }
                }

                /*
                   Reconcile the two views before returning.

                   'bookable' is a TYPE-level figure (stock maths) while
                   'free_rooms' is the list of physical rooms actually free for
                   the whole range. These must never contradict each other on
                   screen ("1 room free" next to "Assign Room (0 free)").

                   When every room of the type is assignable, the physical list
                   is the ground truth, so cap the headline figure by it. We only
                   keep a higher type-level number if there are genuinely
                   unassigned rows in play (room_id NULL), which legitimately
                   consume stock without occupying a specific room.
                */
                $freeCount = count($freeRooms);
                $reported = (int)$avail['bookable'];
                if ($reported > $freeCount) {
                    $reported = $freeCount;
                }

                /*
                   Nothing free for the whole stay? Look for a single-move juggle
                   that would create the space, rather than just saying "no".
                */
                $juggles = [];
                if ($freeCount <= 0) {
                    $juggles = bcc_juggle_suggestions($pdo, $propertyId, $roomTypeId, $checkIn, $checkOut, 5, $orderId);
                }

                bcc_json([
                    'ok'         => true,
                    'total'      => $avail['total'],
                    'bookable'   => $reported,
                    'type_bookable' => (int)$avail['bookable'],
                    'nights'     => $avail['nights'],
                    'free_rooms' => $freeRooms,
                    'juggles'    => $juggles,
                ]);
                break;
            }

            /* ---- Apply a juggle: move a blocking booking to another room ---- */
            case 'apply_juggle': {
                $orderId    = (int)($_POST['order_id'] ?? 0);
                $blockRowId = (int)($_POST['blocker_row_id'] ?? 0);
                $destRoomId = (int)($_POST['move_to_room_id'] ?? 0);

                if ($blockRowId <= 0 || $destRoomId <= 0 || !bcc_table_exists($pdo, 'order_rooming')) {
                    bcc_json(['ok' => false, 'error' => 'Invalid juggle request.']);
                }

                $st = $pdo->prepare("SELECT id, order_id, room_id, DATE(check_in) cin, DATE(check_out) cout FROM order_rooming WHERE id = :id LIMIT 1");
                $st->execute([':id' => $blockRowId]);
                $blk = $st->fetch(PDO::FETCH_ASSOC);
                if (!$blk) {
                    bcc_json(['ok' => false, 'error' => 'The booking to move no longer exists.']);
                }

                // Re-verify the destination is still free for the blocker's stay.
                $hasOrdersTbl = bcc_table_exists($pdo, 'orders');
                $statusSql = ($hasOrdersTbl && bcc_column_exists($pdo, 'orders', 'status'))
                    ? ' AND ' . bcc_active_status_sql('o') : '';
                $joinOrders = $hasOrdersTbl ? ' INNER JOIN orders o ON o.id = rmg.order_id ' : '';
                $chk = $pdo->prepare("
                    SELECT COUNT(*) FROM order_rooming rmg {$joinOrders}
                    WHERE rmg.room_id = :room_id AND rmg.id <> :ignore_id
                      AND DATE(rmg.check_in) < :checkout AND DATE(rmg.check_out) > :checkin
                      {$statusSql}
                ");
                $chk->execute([
                    ':room_id' => $destRoomId,
                    ':ignore_id' => $blockRowId,
                    ':checkout' => $blk['cout'],
                    ':checkin' => $blk['cin'],
                ]);
                if ((int)$chk->fetchColumn() !== 0) {
                    bcc_json(['ok' => false, 'error' => 'That room was taken while you were deciding. Re-run the availability check.']);
                }

                $pdo->prepare("UPDATE order_rooming SET room_id = :rid WHERE id = :id")
                    ->execute([':rid' => $destRoomId, ':id' => $blockRowId]);

                bcc_log($pdo, (int)$blk['order_id'], 'rooming_juggled',
                    'Moved to room #' . $destRoomId . ' (from #' . (int)$blk['room_id'] . ') to free space for booking #' . $orderId . '.');
                if ($orderId > 0) {
                    bcc_log($pdo, $orderId, 'juggle_applied',
                        'Freed a room by moving booking #' . (int)$blk['order_id'] . ' to room #' . $destRoomId . '.');
                }

                bcc_mark_nb_dirty((int)$blk['order_id']);
                bcc_json(['ok' => true, 'message' => 'Booking #' . (int)$blk['order_id'] . ' moved. The room is now free — re-check availability.']);
                break;
            }

            /* ---- Add a room (rooming row) to an existing booking ---- */
            case 'add_rooming_row': {
                $orderId    = (int)($_POST['order_id'] ?? 0);
                $roomTypeId = (int)($_POST['room_type_id'] ?? 0);
                $roomId     = (int)($_POST['room_id'] ?? 0);
                $adults     = max(0, (int)($_POST['adults'] ?? 0));
                $children   = max(0, (int)($_POST['children'] ?? 0));
                $checkIn    = substr(trim((string)($_POST['check_in'] ?? '')), 0, 10);
                $checkOut   = substr(trim((string)($_POST['check_out'] ?? '')), 0, 10);

                if ($orderId <= 0 || !bcc_table_exists($pdo, 'order_rooming')) {
                    bcc_json(['ok' => false, 'error' => 'Invalid booking.']);
                }
                if ($roomTypeId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Choose a room type.']);
                }
                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $checkIn) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $checkOut) || $checkOut <= $checkIn) {
                    bcc_json(['ok' => false, 'error' => 'Enter valid check-in and check-out dates.']);
                }
                if ($adults <= 0) {
                    bcc_json(['ok' => false, 'error' => 'A room needs at least one adult.']);
                }

                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }
                $propertyId = (int)($order['property_id'] ?? 0);

                // Validate an explicitly chosen room and derive its type/number.
                $roomNo = '';
                if ($roomId > 0 && bcc_table_exists($pdo, 'rooms')) {
                    $roomNoCol = null;
                    foreach (['room_no', 'room_number', 'number', 'name', 'title'] as $c) {
                        if (bcc_column_exists($pdo, 'rooms', $c)) { $roomNoCol = $c; break; }
                    }
                    $sel = $roomNoCol ? "`{$roomNoCol}` AS room_no" : "id AS room_no";
                    $sel .= bcc_column_exists($pdo, 'rooms', 'room_type_id') ? ", room_type_id" : ", 0 AS room_type_id";
                    $rs = $pdo->prepare("SELECT {$sel} FROM rooms WHERE id = :id LIMIT 1");
                    $rs->execute([':id' => $roomId]);
                    $room = $rs->fetch(PDO::FETCH_ASSOC);
                    if (!$room) {
                        bcc_json(['ok' => false, 'error' => 'Selected room does not exist.']);
                    }
                    $roomNo = (string)$room['room_no'];
                    if ((int)$room['room_type_id'] > 0 && (int)$room['room_type_id'] !== $roomTypeId) {
                        bcc_json(['ok' => false, 'error' => 'Room ' . $roomNo . ' is not of the selected room type.']);
                    }

                    /* HARD BLOCK: never place a room on top of another live booking. */
                    $hasOrdersTbl = bcc_table_exists($pdo, 'orders');
                    $sql = "SELECT r.order_id FROM order_rooming r";
                    if ($hasOrdersTbl) $sql .= " LEFT JOIN orders o ON o.id = r.order_id";
                    $sql .= " WHERE r.room_id = :room_id"
                          . " AND r.order_id <> :current_order_id"
                          . " AND DATE(r.check_in) < :checkout AND DATE(r.check_out) > :checkin";
                    if ($hasOrdersTbl && bcc_column_exists($pdo, 'orders', 'status')) {
                        $sql .= " AND " . bcc_active_status_sql('o');
                    }
                    $sql .= " LIMIT 1";
                    $cs = $pdo->prepare($sql);
                    $cs->execute([
                        ':room_id' => $roomId,
                        ':current_order_id' => $orderId,
                        ':checkout' => $checkOut,
                        ':checkin' => $checkIn,
                    ]);
                    $clash = $cs->fetch(PDO::FETCH_ASSOC);
                    if ($clash) {
                        bcc_json([
                            'ok' => false,
                            'error' => 'Room ' . ($roomNo !== '' ? $roomNo : ('#' . $roomId))
                                     . ' is already occupied for part of ' . $checkIn . ' to ' . $checkOut
                                     . ' by booking #' . (int)$clash['order_id'] . '.',
                        ]);
                    }
                }

                /* SOFT WARN: adding this room may oversell the type on some nights. */
                if (empty($_POST['confirm_oversell'])) {
                    $oversold = bcc_room_type_oversold_nights($pdo, $propertyId, $roomTypeId, $checkIn, $checkOut, 0);
                    if (!empty($oversold)) {
                        bcc_json([
                            'ok' => false,
                            'needs_confirm' => true,
                            'oversold' => $oversold,
                            'error' => 'Adding this room oversells the room type on ' . count($oversold) . ' night(s).',
                        ]);
                    }
                }

                // Next room slot number for this booking.
                $nextNo = 1;
                if (bcc_column_exists($pdo, 'order_rooming', 'room_no')) {
                    try {
                        $ns = $pdo->prepare("SELECT COALESCE(MAX(CAST(room_no AS UNSIGNED)),0)+1 FROM order_rooming WHERE order_id = :oid");
                        $ns->execute([':oid' => $orderId]);
                        $nextNo = max(1, (int)$ns->fetchColumn());
                    } catch (Throwable $e) {
                        $nextNo = 1;
                    }
                }

                $cols = ['order_id'];
                $vals = [':order_id'];
                $ins  = [':order_id' => $orderId];
                $add = function ($col, $param, $value) use (&$cols, &$vals, &$ins, $pdo) {
                    if (bcc_column_exists($pdo, 'order_rooming', $col)) {
                        $cols[] = "`{$col}`";
                        $vals[] = $param;
                        $ins[$param] = $value;
                    }
                };
                $add('property_id', ':property_id', $propertyId);
                $add('room_no', ':room_no', $nextNo);
                $add('room_type_id', ':room_type_id', $roomTypeId);
                $add('room_id', ':room_id', $roomId > 0 ? $roomId : null);
                $add('adults', ':adults', $adults);
                $add('children', ':children', $children);
                $add('check_in', ':check_in', $checkIn);
                $add('check_out', ':check_out', $checkOut);
                foreach (['guest_name', 'first_name', 'last_name'] as $c) {
                    if (array_key_exists($c, $_POST)) {
                        $add($c, ':' . $c, trim((string)$_POST[$c]));
                    }
                }

                $pdo->beginTransaction();
                $pdo->prepare("INSERT INTO order_rooming (" . implode(', ', $cols) . ") VALUES (" . implode(', ', $vals) . ")")
                    ->execute($ins);
                $newRowId = (int)$pdo->lastInsertId();

                // Realign the booking envelope + totals with the room rows.
                $agg = $pdo->prepare("SELECT MIN(DATE(check_in)) min_in, MAX(DATE(check_out)) max_out, COUNT(*) rooms, COALESCE(SUM(adults),0) adults, COALESCE(SUM(children),0) children FROM order_rooming WHERE order_id = :oid");
                $agg->execute([':oid' => $orderId]);
                $a = $agg->fetch(PDO::FETCH_ASSOC) ?: [];
                $osets = []; $op = [':oid' => $orderId];
                foreach ([['check_in','min_in'],['check_out','max_out'],['totalrooms','rooms'],['adults','adults'],['children','children']] as $pair) {
                    if (bcc_column_exists($pdo, 'orders', $pair[0]) && isset($a[$pair[1]])) {
                        $k = ':o_' . $pair[0];
                        $osets[] = "`{$pair[0]}` = {$k}";
                        $op[$k] = $a[$pair[1]];
                    }
                }
                if (bcc_column_exists($pdo, 'orders', 'updated_at')) $osets[] = 'updated_at = NOW()';
                if ($osets) {
                    $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :oid")->execute($op);
                }
                $pdo->commit();

                bcc_log($pdo, $orderId, 'rooming_room_added',
                    'Room added from Control Centre: slot ' . $nextNo . ', '
                    . ($roomNo !== '' ? 'room ' . $roomNo : 'unassigned')
                    . ', ' . $checkIn . ' to ' . $checkOut . ', ' . $adults . 'A ' . $children . 'C.');

                /* Price the new room's nights immediately. */
                $rp = bcc_reprice_order($pdo, $orderId);
                if ($rp['ok']) {
                    bcc_rebuild_room_charges($pdo, $orderId);
                    bcc_log($pdo, $orderId, 'repriced', 'Rates rebuilt after adding a room: ' . $rp['nights'] . ' night(s).');
                }

                bcc_mark_nb_dirty($orderId);
                bcc_json([
                    'ok' => true,
                    'message' => 'Room added to booking.'
                        . ($roomId > 0 ? '' : ' It is unassigned - allocate a room number when ready.')
                        . ($rp['ok'] ? ' Rates rebuilt.' : ' Note: rates not rebuilt (' . $rp['reason'] . ').'),
                    'row_id' => $newRowId,
                ]);
                break;
            }

            /* ---- Release multiple rooms from a booking ---- */
            case 'release_rooms': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $releaseCount = (int)($_POST['release_count'] ?? 0);
                if ($orderId <= 0 || $releaseCount <= 0 || !bcc_table_exists($pdo, 'order_rooming')) {
                    bcc_json(['ok' => false, 'error' => 'Enter a valid number of rooms to release.']);
                }

                /* Lock the rooming rows so two users cannot release the same rooms concurrently. */
                $pdo->beginTransaction();
                try {
                    $st = $pdo->prepare("SELECT * FROM order_rooming WHERE order_id = :oid ORDER BY CASE WHEN COALESCE(room_id,0)=0 THEN 0 ELSE 1 END ASC, CAST(room_no AS UNSIGNED) DESC, id DESC FOR UPDATE");
                    $st->execute([':oid' => $orderId]);
                    $allRows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
                    $currentCount = count($allRows);
                    $maxRelease = max(0, $currentCount - 1);

                    if ($currentCount <= 1) {
                        throw new RuntimeException('A booking must keep at least one room. Cancel the booking instead.');
                    }
                    if ($releaseCount > $maxRelease) {
                        throw new RuntimeException('You can release at most ' . $maxRelease . ' room(s). At least one room must remain.');
                    }

                    /* Prefer unallocated rooms first, then the highest-numbered room slots. */
                    $toRelease = array_slice($allRows, 0, $releaseCount);
                    $ids = [];
                    $labels = [];
                    foreach ($toRelease as $r) {
                        $ids[] = (int)$r['id'];
                        $label = 'slot ' . (string)($r['room_no'] ?? $r['id']);
                        if (!empty($r['room_id'])) {
                            $label .= ' (room_id ' . (int)$r['room_id'] . ')';
                        } else {
                            $label .= ' (unallocated)';
                        }
                        $labels[] = $label;
                    }

                    $ph = implode(',', array_fill(0, count($ids), '?'));
                    $del = $pdo->prepare("DELETE FROM order_rooming WHERE order_id = ? AND id IN ($ph)");
                    $del->execute(array_merge([$orderId], $ids));
                    if ($del->rowCount() !== count($ids)) {
                        throw new RuntimeException('Not all selected rooms could be released. No changes were saved.');
                    }

                    $agg = $pdo->prepare("SELECT MIN(DATE(check_in)) min_in, MAX(DATE(check_out)) max_out, COUNT(*) rooms, COALESCE(SUM(adults),0) adults, COALESCE(SUM(children),0) children FROM order_rooming WHERE order_id = :oid");
                    $agg->execute([':oid' => $orderId]);
                    $a = $agg->fetch(PDO::FETCH_ASSOC) ?: [];
                    $osets = []; $op = [':oid' => $orderId];
                    foreach ([['check_in','min_in'],['check_out','max_out'],['totalrooms','rooms'],['adults','adults'],['children','children']] as $pair) {
                        if (bcc_column_exists($pdo, 'orders', $pair[0]) && isset($a[$pair[1]])) {
                            $k = ':o_' . $pair[0];
                            $osets[] = "`{$pair[0]}` = {$k}";
                            $op[$k] = $a[$pair[1]];
                        }
                    }
                    if (bcc_column_exists($pdo, 'orders', 'updated_at')) $osets[] = 'updated_at = NOW()';
                    if ($osets) {
                        $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :oid")->execute($op);
                    }
                    $pdo->commit();

                    $userId = bcc_current_user_id();
                    $userName = bcc_user_display_name($pdo, $userId);
                    bcc_log($pdo, $orderId, 'rooming_rooms_released',
                        $releaseCount . ' room(s) released by ' . $userName . ' (user #' . $userId . '). Released: ' . implode(', ', $labels)
                        . '. Rooms before: ' . $currentCount . '; rooms remaining: ' . ($currentCount - $releaseCount) . '.');

                    $rp = bcc_reprice_order($pdo, $orderId);
                    if ($rp['ok']) {
                        bcc_rebuild_room_charges($pdo, $orderId);
                        bcc_log($pdo, $orderId, 'repriced', 'Rates rebuilt after releasing ' . $releaseCount . ' room(s): ' . $rp['nights'] . ' night(s).');
                    }

                    bcc_mark_nb_dirty($orderId);
                    bcc_json([
                        'ok' => true,
                        'message' => $releaseCount . ' room(s) released. ' . ($currentCount - $releaseCount) . ' room(s) remain.'
                            . ($rp['ok'] ? ' Rates and accommodation charges were rebuilt.' : ' Note: rates were not rebuilt (' . $rp['reason'] . ').'),
                        'released' => $releaseCount,
                        'remaining' => $currentCount - $releaseCount,
                    ]);
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) $pdo->rollBack();
                    bcc_json(['ok' => false, 'error' => $e->getMessage()]);
                }
                break;
            }

            /* ---- Remove a room (rooming row) from a booking ---- */
            case 'delete_rooming_row': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $rowId   = (int)($_POST['row_id'] ?? 0);
                if ($orderId <= 0 || $rowId <= 0 || !bcc_table_exists($pdo, 'order_rooming')) {
                    bcc_json(['ok' => false, 'error' => 'Invalid rooming row.']);
                }

                $cnt = $pdo->prepare("SELECT COUNT(*) FROM order_rooming WHERE order_id = :oid");
                $cnt->execute([':oid' => $orderId]);
                if ((int)$cnt->fetchColumn() <= 1) {
                    bcc_json(['ok' => false, 'error' => 'A booking must keep at least one room. Cancel the booking instead.']);
                }

                $st = $pdo->prepare("SELECT * FROM order_rooming WHERE id = :rid AND order_id = :oid LIMIT 1");
                $st->execute([':rid' => $rowId, ':oid' => $orderId]);
                $existing = $st->fetch(PDO::FETCH_ASSOC);
                if (!$existing) {
                    bcc_json(['ok' => false, 'error' => 'Rooming row not found for this booking.']);
                }

                /*
                   Capture the room type being removed BEFORE the DELETE, so the
                   Nightsbridge push still knows which type freed up even if this
                   was the booking's only room of that type (after deletion the
                   row is gone and the push could not otherwise resolve it).
                */
                $deletedTypeId = 0;
                if (isset($existing['room_id']) && (int)$existing['room_id'] > 0) {
                    try {
                        $rtq = $pdo->prepare("SELECT room_type_id FROM rooms WHERE id = :rid LIMIT 1");
                        $rtq->execute([':rid' => (int)$existing['room_id']]);
                        $deletedTypeId = (int)$rtq->fetchColumn();
                    } catch (Throwable $e) { $deletedTypeId = 0; }
                }
                if ($deletedTypeId <= 0 && isset($existing['room_type_id'])) {
                    $deletedTypeId = (int)$existing['room_type_id'];
                }

                $pdo->beginTransaction();
                $pdo->prepare("DELETE FROM order_rooming WHERE id = :rid AND order_id = :oid")
                    ->execute([':rid' => $rowId, ':oid' => $orderId]);

                $agg = $pdo->prepare("SELECT MIN(DATE(check_in)) min_in, MAX(DATE(check_out)) max_out, COUNT(*) rooms, COALESCE(SUM(adults),0) adults, COALESCE(SUM(children),0) children FROM order_rooming WHERE order_id = :oid");
                $agg->execute([':oid' => $orderId]);
                $a = $agg->fetch(PDO::FETCH_ASSOC) ?: [];
                $osets = []; $op = [':oid' => $orderId];
                foreach ([['check_in','min_in'],['check_out','max_out'],['totalrooms','rooms'],['adults','adults'],['children','children']] as $pair) {
                    if (bcc_column_exists($pdo, 'orders', $pair[0]) && isset($a[$pair[1]])) {
                        $k = ':o_' . $pair[0];
                        $osets[] = "`{$pair[0]}` = {$k}";
                        $op[$k] = $a[$pair[1]];
                    }
                }
                if (bcc_column_exists($pdo, 'orders', 'updated_at')) $osets[] = 'updated_at = NOW()';
                if ($osets) {
                    $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :oid")->execute($op);
                }
                $pdo->commit();

                bcc_log($pdo, $orderId, 'rooming_room_removed', 'Room slot removed from Control Centre (row #' . $rowId . ').');

                /* Drop the removed room's nights from pricing and the folio. */
                $rp = bcc_reprice_order($pdo, $orderId);
                if ($rp['ok']) {
                    bcc_rebuild_room_charges($pdo, $orderId);
                    bcc_log($pdo, $orderId, 'repriced', 'Rates rebuilt after removing a room: ' . $rp['nights'] . ' night(s).');
                }

                if ($deletedTypeId > 0) {
                    $GLOBALS['bcc_nb_extra_type_ids'][] = $deletedTypeId;
                }
                bcc_mark_nb_dirty($orderId);
                bcc_json(['ok' => true, 'message' => 'Room removed from booking.'
                    . ($rp['ok'] ? ' Rates rebuilt.' : '')]);
                break;
            }

            /* ---- Add an extra inline (no navigation to extras.php) ---- */
            case 'add_extra': {
                $orderId      = (int)($_POST['order_id'] ?? 0);
                $categoryId   = (int)($_POST['category_id'] ?? 0);
                $serviceId    = (int)($_POST['service_id'] ?? 0);
                $qty          = (float)($_POST['quantity'] ?? 0);
                $rate         = (float)($_POST['unit_price'] ?? 0);
                $txDate       = substr(trim((string)($_POST['tx_date'] ?? '')), 0, 10);
                $note         = trim((string)($_POST['note'] ?? ''));
                $payerAgentId = (int)($_POST['payer_agent_id'] ?? 0);
                $chargeTo     = strtolower(trim((string)($_POST['charge_to'] ?? 'master')));

                if ($categoryId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Category is compulsory. Choose a category first.']);
                }
                if ($orderId <= 0 || $serviceId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Choose a service.']);
                }
                if ($qty <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Quantity must be greater than zero.']);
                }
                if ($txDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $txDate)) {
                    $txDate = date('Y-m-d');
                }
                if (!in_array($chargeTo, ['master', 'room', 'other'], true)) {
                    $chargeTo = 'master';
                }
                if ($chargeTo === 'other' && $payerAgentId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Select the agent responsible for paying this extra.']);
                }

                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }

                // Resolve service fields once. Related services use their own saved prices.
                $nameCol = null;
                foreach (['service', 'name', 'title'] as $c) {
                    if (bcc_column_exists($pdo, 'tblservices', $c)) { $nameCol = $c; break; }
                }
                $priceCol = null;
                foreach (['price_per_person', 'price', 'rate', 'amount', 'unit_price'] as $c) {
                    if (bcc_column_exists($pdo, 'tblservices', $c)) { $priceCol = $c; break; }
                }

                $serviceSelect = "s.id"
                    . ($nameCol ? ", s.`{$nameCol}` AS service" : ", CAST(s.id AS CHAR) AS service")
                    . ($priceCol ? ", COALESCE(s.`{$priceCol}`,0) AS price" : ", 0 AS price")
                    . (bcc_column_exists($pdo, 'tblservices', 'category') ? ", COALESCE(s.category,0) AS category_id" : ", 0 AS category_id")
                    . (bcc_column_exists($pdo, 'tblservices', 'requires_extra') ? ", COALESCE(s.requires_extra,0) AS requires_extra" : ", 0 AS requires_extra")
                    . (bcc_column_exists($pdo, 'tblservices', 'related_service_id_1') ? ", COALESCE(s.related_service_id_1,0) AS related_service_id_1" : ", 0 AS related_service_id_1")
                    . (bcc_column_exists($pdo, 'tblservices', 'related_service_id_2') ? ", COALESCE(s.related_service_id_2,0) AS related_service_id_2" : ", 0 AS related_service_id_2");

                $fetchService = $pdo->prepare("SELECT {$serviceSelect} FROM tblservices s WHERE s.id = :id LIMIT 1");
                $fetchService->execute([':id' => $serviceId]);
                $svc = $fetchService->fetch(PDO::FETCH_ASSOC);
                if (!$svc) {
                    bcc_json(['ok' => false, 'error' => 'Service not found.']);
                }
                if ((int)$svc['category_id'] !== $categoryId) {
                    bcc_json(['ok' => false, 'error' => 'The selected service does not belong to the chosen category.']);
                }

                if ($rate <= 0) {
                    $rate = (float)$svc['price'];
                }
                $parentAmount = round($qty * $rate, 2);
                if ($parentAmount <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Amount must be greater than zero.']);
                }

                /*
                   Build the selected service plus up to two linked service fees.
                   This mirrors extras.php:
                   - linked services are posted automatically;
                   - each uses the SAME quantity as the selected service;
                   - each uses its OWN price_per_person/current service price;
                   - each follows the same Charge To destination as the parent.
                */
                $serviceLines = [[
                    'id'          => (int)$svc['id'],
                    'service'     => trim((string)$svc['service']),
                    'category_id' => (int)$svc['category_id'],
                    'rate'        => $rate,
                    'amount'      => $parentAmount,
                    'is_related'  => false,
                ]];

                $seenServiceIds = [$serviceId => true];
                $relatedIds = [
                    (int)($svc['related_service_id_1'] ?? 0),
                    (int)($svc['related_service_id_2'] ?? 0),
                ];
                foreach ($relatedIds as $relatedId) {
                    if ($relatedId <= 0 || isset($seenServiceIds[$relatedId])) {
                        continue;
                    }
                    $seenServiceIds[$relatedId] = true;
                    $fetchService->execute([':id' => $relatedId]);
                    $related = $fetchService->fetch(PDO::FETCH_ASSOC);
                    if (!$related) {
                        throw new RuntimeException('Linked service #' . $relatedId . ' was not found.');
                    }
                    $relatedRate = (float)$related['price'];
                    $relatedAmount = round($qty * $relatedRate, 2);
                    if ($relatedAmount <= 0) {
                        throw new RuntimeException('Linked service "' . trim((string)$related['service']) . '" has no valid price.');
                    }
                    $serviceLines[] = [
                        'id'          => (int)$related['id'],
                        'service'     => trim((string)$related['service']),
                        'category_id' => (int)$related['category_id'],
                        'rate'        => $relatedRate,
                        'amount'      => $relatedAmount,
                        'is_related'  => true,
                    ];
                }

                $masterFolioId = bcc_get_or_create_master_folio($pdo, $orderId, (int)($order['property_id'] ?? 0));
                if ($masterFolioId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Could not resolve the booking folio.']);
                }

                $roomTargets = [];
                $otherFolioId = 0;
                if ($chargeTo === 'room') {
                    $roomTargets = bcc_fetch_room_folio_targets($pdo, $orderId, (int)($order['property_id'] ?? 0));
                    if (!$roomTargets) {
                        bcc_json([
                            'ok' => false,
                            'error' => 'This booking has no rooms to charge, so a room account cannot be created. '
                                     . 'Add a room on the Rooming tab first, or charge to the master folio.',
                        ]);
                    }
                } elseif ($chargeTo === 'other') {
                    $otherFolioId = bcc_get_or_create_other_agent_folio(
                        $pdo, $orderId, (int)($order['property_id'] ?? 0), $payerAgentId
                    );
                }

                $cols = ['folio_id', 'order_id', 'tx_date', 'tx_type', 'description', 'debit', 'credit', 'source_table', 'source_id'];
                $vals = [':folio_id', ':order_id', ':tx_date', ':tx_type', ':description', ':debit', '0', "'tblservices'", ':source_id'];
                if (bcc_column_exists($pdo, 'folio_transactions', 'property_id')) {
                    array_unshift($cols, 'property_id');
                    array_unshift($vals, ':property_id');
                }
                if (bcc_column_exists($pdo, 'folio_transactions', 'created_by')) {
                    $cols[] = 'created_by'; $vals[] = ':created_by';
                }
                if (bcc_column_exists($pdo, 'folio_transactions', 'created_at')) {
                    $cols[] = 'created_at'; $vals[] = 'NOW()';
                }
                $insStmt = $pdo->prepare(
                    "INSERT INTO folio_transactions (" . implode(', ', $cols) . ") VALUES (" . implode(', ', $vals) . ")"
                );

                $categoryNameStmt = bcc_table_exists($pdo, 'tblservicecategory')
                    ? $pdo->prepare("SELECT category FROM tblservicecategory WHERE id = :id LIMIT 1")
                    : null;

                $touchedFolios = [];
                $postedDescriptions = [];
                $pdo->beginTransaction();
                try {
                    foreach ($serviceLines as $line) {
                        $catName = 'Extra';
                        if ($categoryNameStmt && (int)$line['category_id'] > 0) {
                            $categoryNameStmt->execute([':id' => (int)$line['category_id']]);
                            $catName = trim((string)($categoryNameStmt->fetchColumn() ?: 'Extra')) ?: 'Extra';
                        }
                        $txType = trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $catName)), '_');
                        if ($txType === '') { $txType = 'extra'; }

                        $desc = $line['service'] . ' x ' . number_format($qty, 2) . ' @ ' . number_format((float)$line['rate'], 2);
                        if (!$line['is_related'] && $note !== '') {
                            $desc .= ' - ' . $note;
                        }
                        $postedDescriptions[] = $desc;

                        $postings = [];
                        if ($chargeTo === 'room') {
                            $n = count($roomTargets);
                            $per = floor((((float)$line['amount']) / $n) * 100) / 100;
                            $remainder = round((float)$line['amount'] - round($per * $n, 2), 2);
                            foreach ($roomTargets as $i => $target) {
                                $share = round($per + ($i === 0 ? $remainder : 0), 2);
                                if ($share > 0) {
                                    $postings[] = [(int)$target['folio_id'], $desc, $share];
                                }
                            }
                        } elseif ($chargeTo === 'other') {
                            $postings[] = [$otherFolioId, $desc, (float)$line['amount']];
                        } else {
                            $postings[] = [$masterFolioId, $desc, (float)$line['amount']];
                        }

                        foreach ($postings as $post) {
                            $params = [
                                ':folio_id'   => (int)$post[0],
                                ':order_id'   => $orderId,
                                ':tx_date'    => $txDate . ' 00:00:00',
                                ':tx_type'    => $txType,
                                ':description'=> $post[1],
                                ':debit'      => (float)$post[2],
                                ':source_id'  => (int)$line['id'],
                            ];
                            if (bcc_column_exists($pdo, 'folio_transactions', 'property_id')) {
                                $params[':property_id'] = (int)($order['property_id'] ?? 0);
                            }
                            if (bcc_column_exists($pdo, 'folio_transactions', 'created_by')) {
                                $params[':created_by'] = bcc_current_user_id();
                            }
                            $insStmt->execute($params);
                            $newTxId = (int)$pdo->lastInsertId();
                            if ($chargeTo === 'other') {
                                bcc_save_extra_agent_payer(
                                    $pdo, $newTxId, $orderId, $payerAgentId, (int)$post[0], (int)$line['id']
                                );
                            }
                            $touchedFolios[(int)$post[0]] = true;
                        }
                    }
                    $pdo->commit();
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) { $pdo->rollBack(); }
                    throw $e;
                }

                foreach (array_keys($touchedFolios) as $fid) {
                    bcc_recalc_folio($pdo, (int)$fid);
                }

                $relatedCount = max(0, count($serviceLines) - 1);
                $allAmount = 0.0;
                foreach ($serviceLines as $line) { $allAmount += (float)$line['amount']; }
                bcc_log(
                    $pdo,
                    $orderId,
                    'extra_added',
                    'Added extra(s): ' . implode('; ', $postedDescriptions)
                    . ' = ' . number_format($allAmount, 2)
                    . ' charged to ' . ($chargeTo === 'room'
                        ? 'room account(s)'
                        : ($chargeTo === 'other'
                            ? 'other payer agent: ' . bcc_agent_name($pdo, $payerAgentId)
                            : 'master (booking agent)'))
                    . ' (Control Centre).'
                );

                $destinationText = $chargeTo === 'room'
                    ? 'the guest room account(s)' . (count($roomTargets) > 1 ? ' (split across ' . count($roomTargets) . ' rooms)' : '')
                    : ($chargeTo === 'other'
                        ? 'the account of ' . bcc_agent_name($pdo, $payerAgentId)
                        : 'the master (booking agent) folio');

                bcc_json([
                    'ok' => true,
                    'message' => 'Extra added to ' . $destinationText . '.'
                        . ($relatedCount > 0
                            ? ' ' . $relatedCount . ' linked fee' . ($relatedCount === 1 ? ' was' : 's were')
                              . ' added automatically using the same quantity and each service\'s correct price.'
                            : ''),
                ]);
                break;
            }
            /* ---- Edit an extra inline ---- */
            case 'update_extra': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $txId    = (int)($_POST['line_id'] ?? 0);
                $qty     = (float)($_POST['quantity'] ?? 0);
                $rate    = (float)($_POST['unit_price'] ?? 0);
                $txDate  = substr(trim((string)($_POST['tx_date'] ?? '')), 0, 10);
                $chargeTo = strtolower(trim((string)($_POST['charge_to'] ?? 'master')));
                $payerAgentId = (int)($_POST['payer_agent_id'] ?? 0);
                if (!in_array($chargeTo, ['master','room','other'], true)) { $chargeTo = 'master'; }
                if ($chargeTo === 'other' && $payerAgentId <= 0) {
                    bcc_json(['ok'=>false,'error'=>'Select the agent responsible for paying this extra.']);
                }

                if ($orderId <= 0 || $txId <= 0 || $qty <= 0 || $rate < 0) {
                    bcc_json(['ok' => false, 'error' => 'Quantity and rate must be valid.']);
                }
                if ($txDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $txDate)) {
                    $txDate = date('Y-m-d');
                }

                $cs = $pdo->prepare("SELECT ft.*, f.folio_type, ep.agent_id AS payer_agent_id FROM folio_transactions ft LEFT JOIN folios f ON f.id=ft.folio_id LEFT JOIN booking_extra_agent_payers ep ON ep.transaction_id=ft.id WHERE ft.id = :id AND ft.order_id = :oid LIMIT 1");
                $cs->execute([':id' => $txId, ':oid' => $orderId]);
                $line = $cs->fetch(PDO::FETCH_ASSOC);
                if (!$line) {
                    bcc_json(['ok' => false, 'error' => 'Extra not found on this booking.']);
                }
                if (strtolower(trim((string)$line['source_table'])) !== 'tblservices') {
                    bcc_json(['ok' => false, 'error' => 'This charge was not added as a booking extra and must be edited where it was raised.']);
                }

                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }

                $nameCol = null;
                foreach (['service', 'name', 'title'] as $c) {
                    if (bcc_column_exists($pdo, 'tblservices', $c)) { $nameCol = $c; break; }
                }
                $svcName = 'Extra';
                if ($nameCol) {
                    $s2 = $pdo->prepare("SELECT `{$nameCol}` FROM tblservices WHERE id = :id LIMIT 1");
                    $s2->execute([':id' => (int)$line['source_id']]);
                    $svcName = (string)($s2->fetchColumn() ?: 'Extra');
                }

                $amount = round($qty * $rate, 2);
                if ($amount <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Amount must be greater than zero.']);
                }
                $newDesc = $svcName . ' x ' . number_format($qty, 2) . ' @ ' . number_format($rate, 2);

                /*
                   Changing Charge To is a MOVE, not a second posting.
                   Find the logical rows represented by the selected line, remove
                   those rows, then recreate the same extra once at the requested
                   destination. This also repairs the old duplicate pattern where
                   a full master charge and an equal room split both exist.
                */
                $stripRoomTag = function ($v) {
                    return trim((string)preg_replace('/\s*\[[^\]]*\]\s*$/', '', (string)$v));
                };
                $oldBaseDesc = $stripRoomTag($line['description'] ?? '');
                $oldDate = substr((string)($line['tx_date'] ?? ''), 0, 10);

                $cand = $pdo->prepare("\n                    SELECT ft.id, ft.folio_id, ft.debit, ft.description, ft.tx_date, ft.tx_type, f.folio_type, ep.agent_id AS payer_agent_id\n                    FROM folio_transactions ft\n                    LEFT JOIN folios f ON f.id=ft.folio_id\n                    LEFT JOIN booking_extra_agent_payers ep ON ep.transaction_id=ft.id\n                    WHERE ft.order_id=:oid\n                      AND LOWER(COALESCE(ft.source_table,''))='tblservices'\n                      AND ft.source_id=:sid\n                      AND COALESCE(ft.is_voided,0)=0\n                      AND DATE(ft.tx_date)=:d\n                    ORDER BY ft.id ASC\n                    FOR UPDATE\n                ");

                $pdo->beginTransaction();
                try {
                    $cand->execute([':oid'=>$orderId, ':sid'=>(int)$line['source_id'], ':d'=>$oldDate]);
                    $candidates = [];
                    foreach ($cand->fetchAll(PDO::FETCH_ASSOC) ?: [] as $r) {
                        if ($stripRoomTag($r['description'] ?? '') === $oldBaseDesc) {
                            $candidates[] = $r;
                        }
                    }

                    $anchorIsRoom = strtolower(trim((string)($line['folio_type'] ?? ''))) === 'room';
                    $roomRows = []; $masterRows = [];
                    foreach ($candidates as $r) {
                        if (strtolower(trim((string)($r['folio_type'] ?? ''))) === 'room') $roomRows[] = $r;
                        else $masterRows[] = $r;
                    }
                    $roomTotal = round(array_sum(array_map(function($r){ return (float)$r['debit']; }, $roomRows)), 2);
                    $anchorAmount = round((float)$line['debit'], 2);

                    $removeIds = [$txId];
                    if ($anchorIsRoom) {
                        /* One room line represents the whole split extra. */
                        foreach ($roomRows as $r) $removeIds[] = (int)$r['id'];
                        /* Repair old double-post: matching master total equals the room split total. */
                        foreach ($masterRows as $r) {
                            if (abs(round((float)$r['debit'],2) - $roomTotal) < 0.01) $removeIds[] = (int)$r['id'];
                        }
                    } else {
                        /* Keep genuinely separate identical master extras separate. */
                        if ($roomTotal > 0 && abs($roomTotal - $anchorAmount) < 0.01) {
                            foreach ($roomRows as $r) $removeIds[] = (int)$r['id'];
                        }
                    }
                    $removeIds = array_values(array_unique(array_filter(array_map('intval', $removeIds))));

                    $affectedFolios = [];
                    foreach ($candidates as $r) {
                        if (in_array((int)$r['id'], $removeIds, true)) $affectedFolios[(int)$r['folio_id']] = true;
                    }
                    $in = implode(',', array_fill(0, count($removeIds), '?'));
                    $pdo->prepare("DELETE FROM booking_extra_agent_payers WHERE transaction_id IN ($in)")
                        ->execute($removeIds);
                    $pdo->prepare("DELETE FROM folio_transactions WHERE order_id=? AND id IN ($in)")
                        ->execute(array_merge([$orderId], $removeIds));

                    $masterFolioId = bcc_get_or_create_master_folio($pdo, $orderId, (int)($order['property_id'] ?? 0));
                    if ($masterFolioId <= 0) throw new RuntimeException('Could not resolve the booking master folio.');

                    $postings = [];
                    if ($chargeTo === 'room') {
                        $targets = bcc_fetch_room_folio_targets($pdo, $orderId, (int)($order['property_id'] ?? 0));
                        if (!$targets) throw new RuntimeException('This booking has no room accounts. Add/allocate a room first.');
                        $n = count($targets);
                        $per = floor(($amount / $n) * 100) / 100;
                        $remainder = round($amount - round($per * $n, 2), 2);
                        foreach ($targets as $i => $t) {
                            $share = round($per + ($i === 0 ? $remainder : 0), 2);
                            if ($share > 0) $postings[] = [(int)$t['folio_id'], $share];
                        }
                    } elseif ($chargeTo === 'other') {
                        $otherFolioId = bcc_get_or_create_other_agent_folio(
                            $pdo, $orderId, (int)($order['property_id'] ?? 0), $payerAgentId
                        );
                        $postings[] = [$otherFolioId, $amount];
                    } else {
                        $postings[] = [$masterFolioId, $amount];
                    }

                    $cols = ['folio_id','order_id','tx_date','tx_type','description','debit','credit','source_table','source_id'];
                    $vals = [':folio_id',':order_id',':tx_date',':tx_type',':description',':debit','0',"'tblservices'",':source_id'];
                    if (bcc_column_exists($pdo,'folio_transactions','property_id')) { array_unshift($cols,'property_id'); array_unshift($vals,':property_id'); }
                    if (bcc_column_exists($pdo,'folio_transactions','created_by')) { $cols[]='created_by'; $vals[]=':created_by'; }
                    if (bcc_column_exists($pdo,'folio_transactions','created_at')) { $cols[]='created_at'; $vals[]='NOW()'; }
                    $ins = $pdo->prepare("INSERT INTO folio_transactions (".implode(',',$cols).") VALUES (".implode(',',$vals).")");

                    foreach ($postings as $p) {
                        $args = [
                            ':folio_id'=>$p[0], ':order_id'=>$orderId, ':tx_date'=>$txDate.' 00:00:00',
                            ':tx_type'=>(string)($line['tx_type'] ?? 'extra'), ':description'=>$newDesc,
                            ':debit'=>$p[1], ':source_id'=>(int)$line['source_id'],
                        ];
                        if (bcc_column_exists($pdo,'folio_transactions','property_id')) $args[':property_id']=(int)($order['property_id']??0);
                        if (bcc_column_exists($pdo,'folio_transactions','created_by')) $args[':created_by']=bcc_current_user_id();
                        $ins->execute($args);
                        $newTxId = (int)$pdo->lastInsertId();
                        if ($chargeTo === 'other') {
                            bcc_save_extra_agent_payer($pdo, $newTxId, $orderId, $payerAgentId, (int)$p[0], (int)$line['source_id']);
                        }
                        $affectedFolios[$p[0]] = true;
                    }

                    foreach (array_keys($affectedFolios) as $fid) bcc_recalc_folio($pdo, (int)$fid);
                    $pdo->commit();

                    bcc_log($pdo, $orderId, 'extra_updated',
                        'Extra moved/updated to ' . ($chargeTo === 'room' ? 'room account(s)' : ($chargeTo === 'other' ? 'other payer agent: '.bcc_agent_name($pdo,$payerAgentId) : 'master account'))
                        . ': ' . $newDesc . ' = ' . number_format($amount,2) . '. Removed ' . count($removeIds) . ' previous posting row(s).');

                    bcc_json(['ok'=>true, 'message'=>'Extra updated and moved without duplication.']);
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) $pdo->rollBack();
                    bcc_json(['ok'=>false, 'error'=>$e->getMessage()]);
                }
                break;
            }

            /* ---- Per-room nightly override (mirrors the reservation form) ---- */
            case 'override_night': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $date    = substr(trim((string)($_POST['date'] ?? '')), 0, 10);
                $reason  = trim((string)($_POST['reason'] ?? ''));
                $clear   = !empty($_POST['clear']);
                $whole   = !empty($_POST['whole_stay']);
                $roomsIn = isset($_POST['rooms']) ? json_decode((string)$_POST['rooms'], true) : [];

                if ($orderId <= 0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
                    bcc_json(['ok' => false, 'error' => 'Invalid night.']);
                }
                if (!bcc_table_exists($pdo, 'rel_orders_prices')) {
                    bcc_json(['ok' => false, 'error' => 'Nightly pricing is not available.']);
                }
                if (!$clear && (!is_array($roomsIn) || !$roomsIn)) {
                    bcc_json(['ok' => false, 'error' => 'Set at least one room to Per person or Flat total.']);
                }

                // Which nights are we touching?
                $dates = [$date];
                if ($whole || $clear) {
                    $ds = $pdo->prepare("SELECT DISTINCT DATE(date) d FROM rel_orders_prices WHERE order_id = :oid ORDER BY d");
                    $ds->execute([':oid' => $orderId]);
                    $all = $ds->fetchAll(PDO::FETCH_COLUMN) ?: [];
                    if ($whole && $all) { $dates = $all; }
                    if ($clear && !$whole) { $dates = [$date]; }
                }

                $hasComp = bcc_column_exists($pdo, 'rel_orders_prices', 'override_components');
                $touched = 0;

                /*
                   Map every physical room back to its booking room slot. The UI
                   sends overrides keyed by order_rooming.room_no (Room 1, Room 2,
                   ...), not by the arbitrary position of a rel_orders_prices row.
                   Ordering by price-row id/room_id and using $idx + 1 can target
                   the wrong room, especially after room moves or mixed room types.
                */
                $slotByRoomId = [];
                try {
                    $slotStmt = $pdo->prepare("SELECT room_id, room_no FROM order_rooming WHERE order_id = :oid ORDER BY room_no ASC, id ASC");
                    $slotStmt->execute([':oid' => $orderId]);
                    foreach ($slotStmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $slotRow) {
                        $rid = (int)($slotRow['room_id'] ?? 0);
                        $sno = (string)($slotRow['room_no'] ?? '');
                        if ($rid > 0 && $sno !== '') {
                            $slotByRoomId[$rid] = $sno;
                        }
                    }
                } catch (Throwable $e) {
                    $slotByRoomId = [];
                }

                foreach ($dates as $day) {
                    // All room rows priced on this night.
                    $rs = $pdo->prepare("SELECT * FROM rel_orders_prices WHERE order_id = :oid AND DATE(date) = :d ORDER BY id ASC");
                    $rs->execute([':oid' => $orderId, ':d' => $day]);
                    $nightRows = $rs->fetchAll(PDO::FETCH_ASSOC) ?: [];
                    if (!$nightRows) { continue; }

                    foreach ($nightRows as $idx => $nr) {
                        $rowId = (int)$nr['id'];
                        $roomIdForRow = (int)($nr['room_id'] ?? 0);
                        $slotNo = ($roomIdForRow > 0 && isset($slotByRoomId[$roomIdForRow]))
                            ? (string)$slotByRoomId[$roomIdForRow]
                            : (string)($idx + 1);

                        $curTotal = (float)($nr['total'] ?? 0);
                        $orig = ($nr['original_total'] ?? null) !== null
                            ? (float)$nr['original_total'] : $curTotal;

                        if ($clear) {
                            $restore = $orig > 0 ? $orig : $curTotal;
                            $sets = ["total = :t"];
                            $p = [':t' => round($restore, 2), ':id' => $rowId];
                            if (bcc_column_exists($pdo, 'rel_orders_prices', 'rate_overridden')) $sets[] = "rate_overridden = 0";
                            foreach (['override_total','override_value','override_reason','override_type','original_total'] as $c) {
                                if (bcc_column_exists($pdo, 'rel_orders_prices', $c)) $sets[] = "`{$c}` = NULL";
                            }
                            if ($hasComp) $sets[] = "override_components = NULL";
                            $pdo->prepare("UPDATE rel_orders_prices SET " . implode(', ', $sets) . " WHERE id = :id")->execute($p);
                            $touched++;
                            continue;
                        }

                        // Does this room have an instruction? Keep = leave alone.
                        if (!isset($roomsIn[$slotNo]) || !is_array($roomsIn[$slotNo])) {
                            continue;
                        }
                        $mode = (string)($roomsIn[$slotNo]['mode'] ?? 'keep');
                        if ($mode !== 'flat' && $mode !== 'perPerson') {
                            continue;
                        }
                        $val = (float)($roomsIn[$slotNo]['value'] ?? 0);
                        if ($val < 0) { $val = 0; }

                        $adults = max(1, (int)($nr['adults_count'] ?? 1));
                        $newTotal = ($mode === 'perPerson')
                            ? round($val * $adults, 2)
                            : round($val, 2);

                        $detail = ($mode === 'perPerson')
                            ? ('Room ' . $slotNo . ' ' . number_format($val, 2) . '/pp')
                            : ('Room ' . $slotNo . ($val == 0.0 ? ' waived' : ' ' . number_format($val, 2)));

                        $sets = ["total = :t"];
                        $p = [':t' => $newTotal, ':id' => $rowId];
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'original_total')) {
                            $sets[] = "original_total = :o"; $p[':o'] = round($orig, 2);
                        }
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_total')) {
                            $sets[] = "override_total = :ot"; $p[':ot'] = $newTotal;
                        }
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_value')) {
                            $sets[] = "override_value = :ov"; $p[':ov'] = round($val, 2);
                        }
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'rate_overridden')) $sets[] = "rate_overridden = 1";
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_type')) $sets[] = "override_type = 'per_room'";
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_reason')) {
                            $sets[] = "override_reason = :r"; $p[':r'] = $reason;
                        }
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_by')) {
                            $sets[] = "override_by = :ub"; $p[':ub'] = bcc_current_user_id();
                        }
                        if (bcc_column_exists($pdo, 'rel_orders_prices', 'override_at')) $sets[] = "override_at = NOW()";
                        if ($hasComp) {
                            // Store in the SAME shape the reservation form reads.
                            $sets[] = "override_components = :oc";
                            $p[':oc'] = json_encode(['per_room' => [$slotNo => ['mode' => $mode, 'value' => round($val, 2)]]], JSON_UNESCAPED_SLASHES);
                        }

                        $pdo->prepare("UPDATE rel_orders_prices SET " . implode(', ', $sets) . " WHERE id = :id")->execute($p);
                        $touched++;

                        bcc_log($pdo, $orderId, 'rate_overridden',
                            $day . ' ' . $detail . ': ' . number_format($curTotal, 2) . ' -> ' . number_format($newTotal, 2)
                            . ($reason !== '' ? ' - ' . $reason : '') . ' (Control Centre).');
                    }
                }

                if ($touched === 0) {
                    bcc_json(['ok' => false, 'error' => 'Nothing to change — every room was left on Keep.']);
                }

                // Keep the order header in step with the nights.
                try {
                    $sum = $pdo->prepare("SELECT COALESCE(SUM(total),0) FROM rel_orders_prices WHERE order_id = :oid");
                    $sum->execute([':oid' => $orderId]);
                    $grand = round((float)$sum->fetchColumn(), 2);
                    $osets = [];
                    $oparams = [':oid' => $orderId];
                    $gi = 0;
                    foreach (['base_price', 'amount', 'totalamount'] as $c) {
                        if (bcc_column_exists($pdo, 'orders', $c)) {
                            $k = ':g' . $gi++;
                            $osets[] = "`{$c}` = {$k}";
                            $oparams[$k] = $grand;
                        }
                    }
                    if ($osets) {
                        $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :oid")
                            ->execute($oparams);
                    }
                } catch (Throwable $e) { /* non-fatal */ }

                /*
                   rel_orders_prices and the folio ledger must move together.
                   Previously this endpoint changed the nightly rows and order
                   total, but left the existing room_charge transactions at the
                   old amounts. That made the Rates tab look updated while the
                   folio/proforma still showed stale accommodation charges.
                */
                bcc_rebuild_room_charges($pdo, $orderId);

                if ($clear) {
                    bcc_log($pdo, $orderId, 'rate_override_cleared',
                        'Cleared overrides on ' . count($dates) . ' night(s) (Control Centre).');
                }

                bcc_json([
                    'ok' => true,
                    'message' => $clear
                        ? 'Overrides cleared — calculated rates restored.'
                        : ($touched . ' room-night(s) updated' . ($whole ? ' across the whole stay.' : '.')),
                ]);
                break;
            }

            /* ---- Excel-style inline rooming update ---- */
            case 'save_rooming_row': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $rowId   = (int)($_POST['row_id'] ?? 0);
                if ($orderId <= 0 || $rowId <= 0 || !bcc_table_exists($pdo, 'order_rooming')) {
                    bcc_json(['ok' => false, 'error' => 'Invalid rooming row.']);
                }

                $st = $pdo->prepare("SELECT * FROM order_rooming WHERE id = :rid AND order_id = :oid LIMIT 1");
                $st->execute([':rid' => $rowId, ':oid' => $orderId]);
                $existing = $st->fetch(PDO::FETCH_ASSOC);
                if (!$existing) {
                    bcc_json(['ok' => false, 'error' => 'Rooming row not found for this booking.']);
                }

                $checkIn  = trim((string)($_POST['check_in'] ?? ($existing['check_in'] ?? '')));
                $checkOut = trim((string)($_POST['check_out'] ?? ($existing['check_out'] ?? '')));
                if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', substr($checkIn, 0, 10)) ||
                    !preg_match('/^\d{4}-\d{2}-\d{2}$/', substr($checkOut, 0, 10))) {
                    bcc_json(['ok' => false, 'error' => 'Enter valid check-in and check-out dates.']);
                }
                $checkIn = substr($checkIn, 0, 10);
                $checkOut = substr($checkOut, 0, 10);
                if ($checkOut <= $checkIn) {
                    bcc_json(['ok' => false, 'error' => 'Check-out must be after check-in.']);
                }

                $adults = max(0, (int)($_POST['adults'] ?? ($existing['adults'] ?? 0)));
                $children = max(0, (int)($_POST['children'] ?? ($existing['children'] ?? 0)));
                if (($adults + $children) <= 0) {
                    bcc_json(['ok' => false, 'error' => 'A room must contain at least one adult or child.']);
                }

                /*
                   Room id resolution.
                   Only treat the room as changed when the client actually sent
                   the field. Previously a missing/blank room_id silently set the
                   row to NULL - unassigning a real room AND skipping the clash
                   check below, which is one way dates could be moved onto an
                   occupied room without any warning.
                */
                $roomIdProvided = array_key_exists('room_id', $_POST) && trim((string)$_POST['room_id']) !== '';
                $roomId = $roomIdProvided
                    ? (int)$_POST['room_id']
                    : (int)($existing['room_id'] ?? 0);

                $roomNo = '';
                $roomTypeId = (int)($existing['room_type_id'] ?? 0);
                $propertyId = (int)($existing['property_id'] ?? 0);
                if ($propertyId <= 0 && bcc_column_exists($pdo, 'orders', 'property_id')) {
                    $ps = $pdo->prepare("SELECT property_id FROM orders WHERE id = :oid LIMIT 1");
                    $ps->execute([':oid' => $orderId]);
                    $propertyId = (int)$ps->fetchColumn();
                }

                if ($roomId > 0 && bcc_table_exists($pdo, 'rooms')) {
                    $roomNoCol = null;
                    foreach (['room_no', 'room_number', 'number', 'name', 'title'] as $c) {
                        if (bcc_column_exists($pdo, 'rooms', $c)) { $roomNoCol = $c; break; }
                    }
                    $sel = $roomNoCol ? "`{$roomNoCol}` AS room_no" : "id AS room_no";
                    $sel .= bcc_column_exists($pdo, 'rooms', 'room_type_id') ? ", room_type_id" : ", 0 AS room_type_id";
                    $rs = $pdo->prepare("SELECT {$sel} FROM rooms WHERE id = :id LIMIT 1");
                    $rs->execute([':id' => $roomId]);
                    $room = $rs->fetch(PDO::FETCH_ASSOC);
                    if (!$room) {
                        bcc_json(['ok' => false, 'error' => 'Selected room does not exist.']);
                    }
                    $roomNo = (string)$room['room_no'];
                    if ((int)$room['room_type_id'] > 0) $roomTypeId = (int)$room['room_type_id'];
                }

                /*
                   HARD BLOCK - physical room double-booking.
                   Runs whenever the row has a physical room, whether the room was
                   just changed or was already assigned. This catches pulling a
                   check-in earlier or pushing a check-out later into another live
                   booking's stay: the new window is re-tested from scratch, so
                   any overlap is found regardless of the previous dates.
                */
                if ($roomId > 0
                    && bcc_column_exists($pdo, 'order_rooming', 'room_id')
                    && bcc_column_exists($pdo, 'order_rooming', 'check_in')
                    && bcc_column_exists($pdo, 'order_rooming', 'check_out')) {

                    $sql = "SELECT r.id, r.order_id FROM order_rooming r";
                    $hasOrdersTbl = bcc_table_exists($pdo, 'orders');
                    if ($hasOrdersTbl) $sql .= " LEFT JOIN orders o ON o.id = r.order_id";
                    $sql .= " WHERE r.room_id = :room_id AND r.id <> :row_id"
                          . " AND r.order_id <> :current_order_id"
                          . " AND DATE(r.check_in) < :checkout AND DATE(r.check_out) > :checkin";
                    if ($hasOrdersTbl && bcc_column_exists($pdo, 'orders', 'status')) {
                        // Full status filter: a cancelled / no-show / void booking
                        // must not block a legitimate move.
                        $sql .= " AND " . bcc_active_status_sql('o');
                    }
                    $sql .= " LIMIT 1";
                    $cs = $pdo->prepare($sql);
                    $cs->execute([
                        ':room_id' => $roomId,
                        ':row_id' => $rowId,
                        ':current_order_id' => $orderId,
                        ':checkout' => $checkOut,
                        ':checkin' => $checkIn,
                    ]);
                    $clash = $cs->fetch(PDO::FETCH_ASSOC);
                    if ($clash) {
                        bcc_json([
                            'ok' => false,
                            'error' => 'Room ' . ($roomNo !== '' ? $roomNo : ('#' . $roomId))
                                     . ' is already occupied for part of ' . $checkIn . ' to ' . $checkOut
                                     . ' by booking #' . (int)$clash['order_id'] . '. Pick different dates or another room.',
                        ]);
                    }
                }

                /*
                   SOFT WARN - room-type inventory.
                   A physical room being free does not mean the TYPE has stock on
                   every night. Extending a stay can quietly oversell the type.
                   We surface this and let the user decide (consistent with the
                   "don't lose a booking to strict rules - juggle the calendar"
                   approach), rather than blocking outright.
                */
                $datesChanged = (substr((string)($existing['check_in'] ?? ''), 0, 10) !== $checkIn)
                             || (substr((string)($existing['check_out'] ?? ''), 0, 10) !== $checkOut);

                if ($roomTypeId > 0 && $propertyId > 0 && $datesChanged && empty($_POST['confirm_oversell'])) {
                    $oversold = bcc_room_type_oversold_nights($pdo, $propertyId, $roomTypeId, $checkIn, $checkOut, $rowId);
                    if (!empty($oversold)) {
                        $nights = [];
                        foreach ($oversold as $o) {
                            $nights[] = $o['date'] . ' (' . $o['occupied'] . '/' . $o['total'] . ' rooms already held)';
                        }
                        bcc_json([
                            'ok' => false,
                            'needs_confirm' => true,
                            'oversold' => $oversold,
                            'error' => 'These dates oversell this room type on '
                                     . count($oversold) . ' night(s): ' . implode(', ', $nights)
                                     . '. The physical room is free, but the type has no spare stock. Continue anyway?',
                        ]);
                    }
                }

                $sets = [];
                $params = [':rid'=>$rowId, ':oid'=>$orderId];
                $assign = function ($col, $value, $param) use (&$sets, &$params, $pdo) {
                    if (bcc_column_exists($pdo, 'order_rooming', $col)) {
                        $sets[] = "`{$col}` = {$param}";
                        $params[$param] = $value;
                    }
                };
                // Only touch room_id when the client actually sent it.
                if ($roomIdProvided) {
                    $assign('room_id', $roomId > 0 ? $roomId : null, ':room_id');
                }
                if ($roomNo !== '') $assign('room_no', $roomNo, ':room_no');
                if ($roomTypeId > 0) $assign('room_type_id', $roomTypeId, ':room_type_id');
                $assign('check_in', $checkIn, ':check_in');
                $assign('check_out', $checkOut, ':check_out');
                $assign('adults', $adults, ':adults');
                $assign('children', $children, ':children');
                foreach (['guest_name','first_name','last_name','remarks','notes'] as $c) {
                    if (array_key_exists($c, $_POST)) $assign($c, trim((string)$_POST[$c]), ':' . $c);
                }
                if (bcc_column_exists($pdo, 'order_rooming', 'updated_at')) $sets[] = "updated_at = NOW()";
                if (!$sets) bcc_json(['ok'=>false,'error'=>'No editable rooming columns were found.']);

                $pdo->beginTransaction();
                $pdo->prepare("UPDATE order_rooming SET " . implode(', ', $sets) . " WHERE id = :rid AND order_id = :oid")->execute($params);

                // Keep the booking envelope and group totals aligned with the edited room rows.
                $agg = $pdo->prepare("SELECT MIN(DATE(check_in)) min_in, MAX(DATE(check_out)) max_out, COUNT(*) rooms, COALESCE(SUM(adults),0) adults, COALESCE(SUM(children),0) children FROM order_rooming WHERE order_id = :oid");
                $agg->execute([':oid'=>$orderId]);
                $a = $agg->fetch(PDO::FETCH_ASSOC) ?: [];
                $osets=[]; $op=[':oid'=>$orderId];
                foreach ([['check_in','min_in'],['check_out','max_out'],['totalrooms','rooms'],['adults','adults'],['children','children']] as $pair) {
                    if (bcc_column_exists($pdo,'orders',$pair[0]) && isset($a[$pair[1]])) {
                        $k=':o_'.$pair[0]; $osets[]="`{$pair[0]}` = {$k}"; $op[$k]=$a[$pair[1]];
                    }
                }
                if (bcc_column_exists($pdo,'orders','updated_at')) $osets[]='updated_at = NOW()';
                if ($osets) $pdo->prepare("UPDATE orders SET " . implode(', ', $osets) . " WHERE id = :oid")->execute($op);
                $pdo->commit();

                bcc_log($pdo, $orderId, 'rooming_inline_edit', 'Rooming row #' . $rowId . ' updated: room ' . ($roomNo ?: $roomId) . ', ' . $checkIn . ' to ' . $checkOut . ', ' . $adults . ' adults, ' . $children . ' children.');

                /*
                   Reprice from the new rooming rows. Without this, moving a stay
                   from the 24th to the 25th left the 24th priced and posted, so
                   it kept showing on the folio and proforma for a night nobody
                   was staying.
                */
                $rp = bcc_reprice_order($pdo, $orderId);
                if ($rp['ok']) {
                    bcc_rebuild_room_charges($pdo, $orderId);
                    bcc_log($pdo, $orderId, 'repriced',
                        'Nightly rates rebuilt after rooming change: ' . $rp['nights'] . ' night(s)'
                        . ($rp['kept_overrides'] > 0 ? ', ' . $rp['kept_overrides'] . ' override(s) preserved' : '') . '.');
                }

                /*
                   Push both the NEW type (resolved post-commit from the row) and
                   the pre-edit type, so moving a room from one type to another
                   re-opens the old type and closes the new one on the channel.
                */
                $oldSaveType = (int)($existing['room_type_id'] ?? 0);
                if ($oldSaveType <= 0 && (int)($existing['room_id'] ?? 0) > 0) {
                    try {
                        $rtq = $pdo->prepare("SELECT room_type_id FROM rooms WHERE id = :rid LIMIT 1");
                        $rtq->execute([':rid' => (int)$existing['room_id']]);
                        $oldSaveType = (int)$rtq->fetchColumn();
                    } catch (Throwable $e) { /* ignore */ }
                }
                if ($oldSaveType > 0) {
                    $GLOBALS['bcc_nb_extra_type_ids'][] = $oldSaveType;
                }
                bcc_mark_nb_dirty($orderId);
                bcc_json([
                    'ok' => true,
                    'message' => 'Rooming row saved.'
                        . ($rp['ok']
                            ? ' Rates rebuilt (' . $rp['nights'] . ' night(s)'
                              . ($rp['kept_overrides'] > 0 ? ', ' . $rp['kept_overrides'] . ' override(s) kept' : '') . ').'
                            : ' Note: rates were not rebuilt (' . $rp['reason'] . ').'),
                ]);
                break;
            }

            /* ---- Force a full reprice + folio rebuild from the current rooming rows ---- *
               Rebuilds rel_orders_prices and the accommodation folio charges without
               requiring a rooming edit. Useful when a booking was priced by an older
               run and the stored amounts have drifted from the live rooming pax/dates
               (e.g. an adult count was changed but a reprice failed at the time).
               Overrides are preserved by bcc_reprice_order per night+room. */
            case 'reprice_order': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                if ($orderId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid booking.']);
                }

                /*
                   Optional: wipe existing rate overrides first so the reprice
                   recomputes every night from the live rooming pax/dates instead
                   of preserving stale (or corrupt) override totals. Used when a
                   booking's nightly rows were left flagged rate_overridden = 1
                   with figures that no longer match the rooming (e.g. a whole-
                   booking total stamped onto every per-room row).
                */
                $clearOverrides = !empty($_POST['clear_overrides']);
                $cleared = 0;
                if ($clearOverrides && bcc_table_exists($pdo, 'rel_orders_prices')) {
                    $osets = [];
                    if (bcc_column_exists($pdo, 'rel_orders_prices', 'rate_overridden')) {
                        $osets[] = "rate_overridden = 0";
                    }
                    foreach (['override_total','override_value','override_reason',
                              'override_type','override_components','original_total'] as $c) {
                        if (bcc_column_exists($pdo, 'rel_orders_prices', $c)) {
                            $osets[] = "`{$c}` = NULL";
                        }
                    }
                    if ($osets) {
                        $cs = $pdo->prepare(
                            "UPDATE rel_orders_prices SET " . implode(', ', $osets) . " WHERE order_id = :oid"
                        );
                        $cs->execute([':oid' => $orderId]);
                        $cleared = $cs->rowCount();
                        bcc_log($pdo, $orderId, 'overrides_cleared',
                            'Cleared rate overrides on ' . $cleared . ' nightly row(s) before reprice (Rates tab).');
                    }
                }

                $rp = bcc_reprice_order($pdo, $orderId);
                if (!$rp['ok']) {
                    bcc_json(['ok' => false, 'error' => 'Reprice failed: ' . $rp['reason']]);
                }
                bcc_rebuild_room_charges($pdo, $orderId);
                bcc_log($pdo, $orderId, 'repriced',
                    'Manual reprice from Rates tab: ' . $rp['nights'] . ' night(s)'
                    . ($rp['kept_overrides'] > 0 ? ', ' . $rp['kept_overrides'] . ' override(s) preserved' : '') . '.');

                bcc_json([
                    'ok' => true,
                    'message' => ($cleared > 0 ? 'Cleared ' . $cleared . ' override(s), then repriced' : 'Booking repriced')
                        . ' from current rooming: ' . $rp['nights'] . ' night(s)'
                        . ($rp['kept_overrides'] > 0 ? ', ' . $rp['kept_overrides'] . ' override(s) kept' : '')
                        . '. Folio and proforma rebuilt.',
                ]);
                break;
            }

            /*
             * Update guest name, meal plan, agent and agent consultant.
             *
             * Repricing is explicit:
             * - reprice_for_agent = 0 keeps every existing nightly rate unchanged.
             * - reprice_for_agent = 1 rebuilds rates from the newly selected
             *   agent's contract and refreshes Master Folio accommodation charges.
             */
            case 'update_basic_details': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $guestName = trim(preg_replace('/\s+/', ' ', (string)($_POST['guest_name'] ?? '')));
                $mealPlan = strtoupper(trim((string)($_POST['meal_plan'] ?? '')));
                $agentId = (int)($_POST['agent_id'] ?? 0);
                $contactId = (int)($_POST['contact_id'] ?? 0);
                $repriceForAgent = !empty($_POST['reprice_for_agent']) ? 1 : 0;

                if ($orderId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid booking.']);
                }

                if ($mealPlan !== '' && !in_array($mealPlan, ['BB', 'HB', 'FB', 'RO'], true)) {
                    bcc_json(['ok' => false, 'error' => 'Meal plan must be BB, HB, FB or RO.']);
                }

                $orderStmt = $pdo->prepare("SELECT * FROM orders WHERE id = :id LIMIT 1");
                $orderStmt->execute([':id' => $orderId]);
                $existingOrder = $orderStmt->fetch(PDO::FETCH_ASSOC);

                if (!$existingOrder) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }

                $oldAgentId = (int)($existingOrder['agent_id'] ?? 0);
                $oldContactId = (int)($existingOrder['contact_id'] ?? 0);

                if ($agentId <= 0) {
                    $agentId = $oldAgentId;
                }

                if ($contactId > 0) {
                    if (!bcc_table_exists($pdo, 'consultants')) {
                        bcc_json(['ok' => false, 'error' => 'Consultants table is unavailable.']);
                    }

                    $contactCheck = $pdo->prepare("
                        SELECT COUNT(*)
                        FROM consultants
                        WHERE id = :contact_id
                          AND agent_id = :agent_id
                    ");
                    $contactCheck->execute([
                        ':contact_id' => $contactId,
                        ':agent_id' => $agentId,
                    ]);

                    if ((int)$contactCheck->fetchColumn() === 0) {
                        bcc_json([
                            'ok' => false,
                            'error' => 'The selected consultant is not linked to the selected agent.',
                        ]);
                    }
                }

                $agentChanged = $agentId > 0 && $agentId !== $oldAgentId;
                $contactChanged = $contactId !== $oldContactId;
                $changes = [];

                $pdo->beginTransaction();

                try {
                    if ($guestName !== '') {
                        $parts = explode(' ', $guestName, 2);
                        $first = trim($parts[0]);
                        $last = trim($parts[1] ?? '');

                        $pdo->prepare("
                            INSERT INTO guests (firstname, lastname)
                            VALUES (:firstname, :lastname)
                        ")->execute([
                            ':firstname' => $first,
                            ':lastname' => $last,
                        ]);

                        $guestId = (int)$pdo->lastInsertId();

                        $pdo->prepare("
                            UPDATE orders
                            SET guest_id = :guest_id
                            WHERE id = :id
                        ")->execute([
                            ':guest_id' => $guestId,
                            ':id' => $orderId,
                        ]);

                        if (bcc_table_exists($pdo, 'order_rooming')) {
                            $roomingSets = [];
                            $roomingParams = [':id' => $orderId];

                            if (bcc_column_exists($pdo, 'order_rooming', 'guest_name')) {
                                $roomingSets[] = 'guest_name = :guest_name';
                                $roomingParams[':guest_name'] = $guestName;
                            }
                            if (bcc_column_exists($pdo, 'order_rooming', 'first_name')) {
                                $roomingSets[] = 'first_name = :first_name';
                                $roomingParams[':first_name'] = $first;
                            }
                            if (bcc_column_exists($pdo, 'order_rooming', 'last_name')) {
                                $roomingSets[] = 'last_name = :last_name';
                                $roomingParams[':last_name'] = $last;
                            }

                            if ($roomingSets) {
                                $pdo->prepare(
                                    'UPDATE order_rooming SET '
                                    . implode(', ', $roomingSets)
                                    . ' WHERE order_id = :id'
                                )->execute($roomingParams);
                            }
                        }

                        $changes[] = 'guest name';
                    }

                    $orderSets = [];
                    $orderParams = [':id' => $orderId];

                    if ($mealPlan !== '' && bcc_column_exists($pdo, 'orders', 'meal_plan')) {
                        $orderSets[] = 'meal_plan = :meal_plan';
                        $orderParams[':meal_plan'] = $mealPlan;
                        $changes[] = 'meal plan';
                    }

                    if ($agentId > 0 && bcc_column_exists($pdo, 'orders', 'agent_id')) {
                        $orderSets[] = 'agent_id = :agent_id';
                        $orderParams[':agent_id'] = $agentId;

                        if ($agentChanged) {
                            $changes[] = 'agent';
                        }
                    }

                    if (bcc_column_exists($pdo, 'orders', 'contact_id')) {
                        $orderSets[] = 'contact_id = :contact_id';
                        $orderParams[':contact_id'] = $contactId > 0 ? $contactId : null;

                        if ($contactChanged) {
                            $changes[] = 'agent consultant';
                        }
                    }

                    if (bcc_column_exists($pdo, 'orders', 'updated_by')) {
                        $orderSets[] = 'updated_by = :updated_by';
                        $orderParams[':updated_by'] = bcc_current_user_id() ?: null;
                    }

                    if (bcc_column_exists($pdo, 'orders', 'updated_at')) {
                        $orderSets[] = 'updated_at = NOW()';
                    }

                    if ($orderSets) {
                        $pdo->prepare(
                            'UPDATE orders SET '
                            . implode(', ', $orderSets)
                            . ' WHERE id = :id'
                        )->execute($orderParams);
                    }

                    $pdo->commit();
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) {
                        $pdo->rollBack();
                    }
                    throw $e;
                }

                $rateMessage = 'Existing nightly rates were kept unchanged.';

                if ($repriceForAgent && $agentChanged) {
                    $reprice = bcc_reprice_order($pdo, $orderId);

                    if (!$reprice['ok']) {
                        bcc_log(
                            $pdo,
                            $orderId,
                            'agent_changed_reprice_failed',
                            'Agent and consultant were updated, but repricing failed: '
                            . $reprice['reason']
                        );

                        bcc_json([
                            'ok' => false,
                            'saved' => true,
                            'error' => 'Agent and consultant were updated, but the new agent rates could not be applied: '
                                . $reprice['reason'],
                        ]);
                    }

                    bcc_rebuild_room_charges($pdo, $orderId);

                    $rateMessage = 'Room rates and Master Folio accommodation charges were rebuilt from the new agent contract for '
                        . $reprice['nights'] . ' night(s).';
                } elseif ($repriceForAgent && !$agentChanged) {
                    $rateMessage = 'The agent did not change, so existing nightly rates were kept unchanged.';
                }

                $changeLabel = $changes
                    ? ucfirst(implode(', ', array_values(array_unique($changes)))) . ' updated.'
                    : 'Booking details checked.';

                bcc_log(
                    $pdo,
                    $orderId,
                    'basic_details_updated',
                    $changeLabel . ' ' . $rateMessage
                );

                bcc_json([
                    'ok' => true,
                    'message' => $changeLabel . ' ' . $rateMessage,
                    'repriced' => ($repriceForAgent && $agentChanged),
                ]);
                break;
            }

            /* ---- Full cancellation accounting policy ---- */
            case 'cancel_booking_policy': {
                $orderId=(int)($_POST['order_id']??0); $reason=trim((string)($_POST['reason']??''));
                $action=trim((string)($_POST['policy_action']??'keep_balance'));
                $adjust=round(max(0,(float)($_POST['adjustment']??0)),2); $charge=round(max(0,(float)($_POST['policy_charge']??0)),2);
                if($orderId<=0||$reason==='') bcc_json(['ok'=>false,'error'=>'Cancellation reason is required.']);
                if(!in_array($action,['keep_balance','waive_all','waive_and_charge_policy'],true)) bcc_json(['ok'=>false,'error'=>'Invalid accounting policy.']);
                bcc_ensure_cancellation_columns($pdo);
                $st=$pdo->prepare("SELECT * FROM orders WHERE id=:id LIMIT 1");$st->execute([':id'=>$orderId]);$o=$st->fetch(PDO::FETCH_ASSOC);
                if(!$o) bcc_json(['ok'=>false,'error'=>'Booking not found.']);
                $pdo->beginTransaction();
                try{
                    $original=bcc_live_order_balance($pdo,$orderId); $adj=0.0;$posted=0.0;
                    if($action==='waive_all')$adj=max(0,$original);
                    elseif($action==='waive_and_charge_policy'){$adj=min(max(0,$original),$adjust);$posted=$charge;}
                    $fid=bcc_get_or_create_master_folio($pdo,$orderId,(int)($o['property_id']??0));
                    $label=(string)($o['order_no']??('#'.$orderId));
                    if($adj>0.00001) bcc_insert_cancellation_line($pdo,$o,$fid,'cancellation_adjustment','Cancellation Adjustment - '.$label.' | '.$reason,0,$adj);
                    if($posted>0.00001) bcc_insert_cancellation_line($pdo,$o,$fid,'cancellation_policy_charge','Cancellation Policy Charge - '.$label.' | '.$reason,$posted,0);
                    $sets=['status=0'];$par=[':id'=>$orderId];
                    foreach([['cancellation_reason',':reason',$reason],['cancellation_policy_action',':pa',$action],['cancellation_original_balance',':ob',$original],['cancellation_adjustment',':adj',$adj],['cancellation_policy_charge',':pc',$posted],['cancellation_by',':uid',bcc_current_user_id()?:null]] as $x){if(bcc_column_exists($pdo,'orders',$x[0])){$sets[]=$x[0].'='.$x[1];$par[$x[1]]=$x[2];}}
                    if(bcc_column_exists($pdo,'orders','cancellation_at'))$sets[]='cancellation_at=NOW()';
                    if(bcc_column_exists($pdo,'orders','cancellation_inventory_released'))$sets[]='cancellation_inventory_released=1';
                    if(bcc_column_exists($pdo,'orders','booking_status'))$sets[]="booking_status='cancelled'";
                    if(bcc_column_exists($pdo,'orders','updated_at'))$sets[]='updated_at=NOW()';
                    $pdo->prepare('UPDATE orders SET '.implode(',',$sets).' WHERE id=:id')->execute($par);
                    bcc_release_cancelled_inventory($pdo,$orderId); bcc_recalc_order_folios($pdo,$orderId);
                    $pdo->commit();
                    bcc_log($pdo,$orderId,'booking_cancelled','Cancelled with policy '.$action.'. Original balance '.number_format($original,2).', adjustment '.number_format($adj,2).', policy charge '.number_format($posted,2).'. Reason: '.$reason);
                    bcc_mark_nb_dirty($orderId);
                    bcc_json(['ok'=>true,'message'=>'Booking cancelled, inventory released and folios recalculated.','original_balance'=>$original,'adjustment'=>$adj,'policy_charge'=>$posted]);
                }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
                break;
            }

            case 'reinstate_check': {
                $orderId=(int)($_REQUEST['order_id']??0);
                if($orderId<=0) bcc_json(['ok'=>false,'available'=>false,'error'=>'Invalid booking.','rooms'=>[]]);
                bcc_json(bcc_check_reinstate_availability($pdo,$orderId));
                break;
            }

            case 'reinstate_booking': {
                $orderId=(int)($_POST['order_id']??0); if($orderId<=0) bcc_json(['ok'=>false,'error'=>'Invalid booking.']);
                $st=$pdo->prepare("SELECT * FROM orders WHERE id=:id LIMIT 1");$st->execute([':id'=>$orderId]);$o=$st->fetch(PDO::FETCH_ASSOC);
                if(!$o) bcc_json(['ok'=>false,'error'=>'Booking not found.']);
                $av=bcc_check_reinstate_availability($pdo,$orderId);
                if(empty($av['available'])){ $busy=[];foreach(($av['rooms']??[]) as $r)if(empty($r['available']))$busy[]=$r['room_no']; bcc_json(['ok'=>false,'error'=>'Cannot reinstate. Room(s) '.implode(', ',$busy).' are occupied for these dates.','availability'=>$av]); }
                $pdo->beginTransaction();
                try{
                    $sets=['status=1'];
                    foreach(['cancellation_at','cancellation_by'] as $c)if(bcc_column_exists($pdo,'orders',$c))$sets[]="$c=NULL";
                    if(bcc_column_exists($pdo,'orders','cancellation_inventory_released'))$sets[]='cancellation_inventory_released=0';
                    if(bcc_column_exists($pdo,'orders','booking_status'))$sets[]='booking_status=NULL';
                    if(bcc_column_exists($pdo,'orders','no_show_at'))$sets[]='no_show_at=NULL';
                    if(bcc_column_exists($pdo,'orders','updated_at'))$sets[]='updated_at=NOW()';
                    $pdo->prepare('UPDATE orders SET '.implode(',',$sets).' WHERE id=:id')->execute([':id'=>$orderId]);
                    $rs=[];$rp=[':id'=>$orderId];
                    if(bcc_column_exists($pdo,'order_rooming','released_status'))$rs[]='released_status=0';
                    if(bcc_column_exists($pdo,'order_rooming','released_at'))$rs[]='released_at=NULL';
                    if(bcc_column_exists($pdo,'order_rooming','release_reason'))$rs[]='release_reason=NULL';
                    if(bcc_column_exists($pdo,'order_rooming','checked_out_status'))$rs[]='checked_out_status=0';
                    if(bcc_column_exists($pdo,'order_rooming','check_in')){$rs[]='check_in=:cin';$rp[':cin']=$o['check_in'];}
                    if(bcc_column_exists($pdo,'order_rooming','check_out')){$rs[]='check_out=:cout';$rp[':cout']=$o['check_out'];}
                    if($rs)$pdo->prepare('UPDATE order_rooming SET '.implode(',',$rs).' WHERE order_id=:id')->execute($rp);
                    if(bcc_table_exists($pdo,'rooms')&&bcc_column_exists($pdo,'rooms','status')){$q=$pdo->prepare('SELECT DISTINCT room_id FROM order_rooming WHERE order_id=:id AND room_id>0');$q->execute([':id'=>$orderId]);foreach($q->fetchAll(PDO::FETCH_COLUMN) as $rid)$pdo->prepare("UPDATE rooms SET status='booked' WHERE id=:id")->execute([':id'=>(int)$rid]);}
                    $pdo->commit(); bcc_log($pdo,$orderId,'booking_reinstated','Booking reinstated after successful room availability check.');
                    bcc_mark_nb_dirty($orderId);
                    bcc_json(['ok'=>true,'message'=>'Booking reinstated and rooms re-held successfully.']);
                }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
                break;
            }

            /* ---- Change status (cancel / reinstate / check-in / no-show / confirm) ---- */
            case 'set_status': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $status  = trim((string)($_POST['status'] ?? ''));
                $allowed = ['confirmed', 'provisional', 'cancelled', 'checked_in', 'checked_out', 'no_show'];
                if ($orderId <= 0 || !in_array($status, $allowed, true)) {
                    bcc_json(['ok' => false, 'error' => 'Invalid request.']);
                }

                /*
                 * Reservation Form stores Confirmed as status 1 and Provisional
                 * as status 2. The Control Centre must use the same values so a
                 * direct confirmation is exactly equivalent to selecting
                 * "Confirmed" in reservation_form.php.
                 */
                $storedStatus = $status === 'confirmed'
                    ? 1
                    : ($status === 'provisional' ? 2 : $status);

                $sets = "status = :status";
                $params = [':status' => $storedStatus, ':id' => $orderId];

                if ($status === 'confirmed') {
                    bcc_ensure_confirmation_audit_columns($pdo);

                    if (bcc_column_exists($pdo, 'orders', 'confirmed_at')) {
                        $sets .= ", confirmed_at = NOW()";
                    }
                    if (bcc_column_exists($pdo, 'orders', 'confirmed_by')) {
                        $sets .= ", confirmed_by = :confirmed_by";
                        $params[':confirmed_by'] = bcc_current_user_id() > 0
                            ? bcc_current_user_id()
                            : null;
                    }
                }

                if (bcc_column_exists($pdo, 'orders', 'updated_at')) {
                    $sets .= ", updated_at = NOW()";
                }
                if (bcc_column_exists($pdo, 'orders', 'updated_by')) {
                    $sets .= ", updated_by = :uid";
                    $params[':uid'] = bcc_current_user_id() > 0
                        ? bcc_current_user_id()
                        : null;
                }

                $pdo->prepare("UPDATE orders SET {$sets} WHERE id = :id")->execute($params);

                if ($status === 'confirmed') {
                    $confirmedBy = bcc_user_display_name($pdo, bcc_current_user_id());
                    bcc_log(
                        $pdo,
                        $orderId,
                        'booking_confirmed',
                        'Booking confirmed from Booking Control Centre by ' . $confirmedBy . '.'
                    );
                    bcc_mark_nb_dirty($orderId);
                    bcc_json([
                        'ok' => true,
                        'message' => 'Booking confirmed successfully by ' . $confirmedBy . '.'
                    ]);
                }

                bcc_log($pdo, $orderId, 'status_' . $status, 'Status set to ' . $status . ' from Control Centre.');
                bcc_mark_nb_dirty($orderId);
                bcc_json(['ok' => true, 'message' => 'Status updated to ' . $status . '.']);
                break;
            }

            /* ---- Change booking colour ---- */
            case 'set_colour': {
                $orderId  = (int)($_POST['order_id'] ?? 0);
                $colourId = (int)($_POST['colour_id'] ?? 0);
                if ($orderId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid booking.']);
                }
                if (!bcc_column_exists($pdo, 'orders', 'booking_color_code_id')) {
                    bcc_json(['ok' => false, 'error' => 'Colour coding not available on this system.']);
                }
                $val = $colourId > 0 ? $colourId : null;
                $pdo->prepare("UPDATE orders SET booking_color_code_id = :cid WHERE id = :id")
                    ->execute([':cid' => $val, ':id' => $orderId]);
                bcc_log($pdo, $orderId, 'colour_change', 'Booking colour set (#' . $colourId . ').');
                bcc_json(['ok' => true, 'message' => 'Booking colour updated.']);
                break;
            }

            /* ---- Void a folio transaction (extra removal) ---- */
            case 'void_line': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $lineId  = (int)($_POST['line_id'] ?? 0);
                if ($orderId <= 0 || $lineId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid request.']);
                }
                if (!bcc_column_exists($pdo, 'folio_transactions', 'is_voided')) {
                    bcc_json(['ok' => false, 'error' => 'This system does not support voiding lines here.']);
                }
                $sets = "is_voided = 1";
                $params = [':lid' => $lineId, ':oid' => $orderId];
                if (bcc_column_exists($pdo, 'folio_transactions', 'voided_at')) {
                    $sets .= ", voided_at = NOW()";
                }
                if (bcc_column_exists($pdo, 'folio_transactions', 'voided_by')) {
                    $sets .= ", voided_by = :uid";
                    $params[':uid'] = bcc_current_user_id();
                }
                $pdo->prepare("UPDATE folio_transactions SET {$sets} WHERE id = :lid AND order_id = :oid")->execute($params);
                bcc_log($pdo, $orderId, 'void_line', 'Voided folio line #' . $lineId . ' from Control Centre.');
                bcc_json(['ok' => true, 'message' => 'Line voided.']);
                break;
            }

            /* ---- Quick note / remark ---- */
            /* ---- Record a payment ----------------------------------------
               Mirrors the save_payment handler in payments.php exactly, minus
               the page redirect. The maths is NOT re-implemented: entered ->
               USD conversion, the 3% principal-only surcharge and the gratuity
               all come from payment_support.php, so a change there moves both
               screens together.
            ------------------------------------------------------------------ */
            case 'save_payment': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $order = bcc_fetch_order($pdo, $orderId);
                if (!$order) {
                    bcc_json(['ok' => false, 'error' => 'Booking not found.']);
                }
                $bookingPropertyId = (int)($order['property_id'] ?? 0);
                if ($bookingPropertyId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Booking property not found.']);
                }

                /*
                   ENTERED amount is in the selected currency.
                   BASE accounting is USD.
                   Locked formula: Effective USD = entered / ROE,
                   where ROE = selected currency per 1 USD.
                */
                $enteredAmount   = (float)($_POST['amount'] ?? 0);
                $gratuityEntered = max(0, round((float)($_POST['gratuity_amount'] ?? 0), 2));
                $paymentMethod   = trim((string)($_POST['payment_method'] ?? ''));
                $paymentDate     = trim((string)($_POST['payment_date'] ?? date('Y-m-d')));
                $currency        = trim((string)($_POST['currency'] ?? '1'));
                $roeAtm          = trim((string)($_POST['roe_atm'] ?? ''));
                $roeValue        = (float)($roeAtm !== '' ? $roeAtm : 0);
                $idUpload        = trim((string)($_POST['id_upload'] ?? ''));
                $postTo          = trim((string)($_POST['post_to'] ?? 'master'));
                $bankAccountId   = (int)($_POST['bank_account_id'] ?? 0);
                $posMachineId    = (int)($_POST['pos_machine_id'] ?? 0);
                $invoice         = (int)($_POST['invoice'] ?? 0);

                if ($roeValue <= 0) { $roeValue = 0; }

                $applyCardSurcharge = is_card_surcharge_method($paymentMethod);
                // Only the principal is surcharged. Gratuity never is.
                $surchargeEntered = $applyCardSurcharge ? round($enteredAmount * CC_SURCHARGE_RATE, 2) : 0.00;
                $amountToDebitEntered = round($enteredAmount + $gratuityEntered + $surchargeEntered, 2);
                $surchargeUsd = $roeValue > 0 ? round($surchargeEntered / $roeValue, 2) : 0.00;
                $gratuityUsd  = $roeValue > 0 ? round($gratuityEntered / $roeValue, 2) : 0.00;
                $amount       = $roeValue > 0 ? round($amountToDebitEntered / $roeValue, 2) : 0.00;

                if (!is_bank_payment_method($paymentMethod)) { $bankAccountId = 0; }
                if (!is_card_surcharge_method($paymentMethod)) { $posMachineId = 0; }

                $payErrors = [];
                if ($enteredAmount <= 0) $payErrors[] = 'Payment amount must be greater than zero.';
                if ($roeValue <= 0)      $payErrors[] = 'Invalid ROE. ROE must be greater than zero and must mean Currency per 1 USD.';
                if ($amount <= 0)        $payErrors[] = 'Effective USD payment amount must be greater than zero. Check amount, gratuity, surcharge and ROE.';
                if ($paymentMethod === '') $payErrors[] = 'Please select a payment method.';
                if ($paymentDate === '')   $payErrors[] = 'Please enter a payment date.';

                if (is_bank_payment_method($paymentMethod) && $bankAccountId > 0) {
                    $chk = $pdo->prepare("SELECT COUNT(*) FROM tblbankdetails WHERE ID = :id AND bank_status = 1");
                    $chk->execute([':id' => $bankAccountId]);
                    if ((int)$chk->fetchColumn() <= 0) $payErrors[] = 'Selected bank account is not active.';
                }
                if (is_card_surcharge_method($paymentMethod) && $posMachineId > 0) {
                    $chk = $pdo->prepare("
                        SELECT COUNT(*) FROM pos_machines pm
                        INNER JOIN tblbankdetails b ON b.ID = pm.bank_detail_id
                        WHERE pm.id = :id AND pm.status = 1 AND b.bank_status = 1
                    ");
                    $chk->execute([':id' => $posMachineId]);
                    if ((int)$chk->fetchColumn() <= 0) $payErrors[] = 'Selected POS machine is not active.';
                }

                if ($payErrors) {
                    bcc_json(['ok' => false, 'error' => implode(' ', $payErrors)]);
                }

                try {
                    $pdo->beginTransaction();

                    $masterFolioId = get_or_create_master_folio($pdo, $orderId);
                    ensure_room_charges_posted_to_folio($pdo, $orderId, $masterFolioId);
                    $targetFolioId = get_payment_target_folio($pdo, $orderId, $postTo, $masterFolioId);

                    if ($gratuityUsd > 0) {
                        post_optional_gratuity_to_folio(
                            $pdo, $orderId, $bookingPropertyId, $targetFolioId,
                            $paymentDate, $gratuityUsd, $gratuityEntered, $roeValue
                        );
                    }
                    if ($applyCardSurcharge && $surchargeUsd > 0) {
                        post_cc_surcharge_to_folio(
                            $pdo, $orderId, $bookingPropertyId, $targetFolioId,
                            $paymentDate, $surchargeUsd, $surchargeEntered, $roeValue, $paymentMethod
                        );
                    }

                    $refValue = $idUpload !== '' ? $idUpload : (
                        'Principal ' . number_format($enteredAmount, 2, '.', '')
                        . ($gratuityEntered > 0 ? (' + Gratuity ' . number_format($gratuityEntered, 2, '.', '')) : '')
                        . ($applyCardSurcharge ? (' + 3% Surcharge ' . number_format($surchargeEntered, 2, '.', '') . ' = ' . number_format($amountToDebitEntered, 2, '.', '')) : '')
                        . ' @ ROE ' . number_format($roeValue, 5, '.', '')
                        . ' = USD ' . number_format($amount, 2, '.', '')
                    );
                    $maxLen = pay_column_max_length($pdo, 'payment', 'id_upload', 100);
                    if (mb_strlen($refValue) > $maxLen) {
                        $refValue = mb_substr($refValue, 0, $maxLen);
                    }

                    $ins = $pdo->prepare("
                        INSERT INTO payment
                            (property_id, order_id, date_time, amount, invoice, is_main_amount,
                             payment_method, added_date, id_upload, currency, roe_atm,
                             bank_account_id, pos_machine_id)
                        VALUES
                            (:property_id, :order_id, :date_time, :amount, :invoice, 1,
                             :payment_method, :added_date, :id_upload, :currency, :roe_atm,
                             :bank_account_id, :pos_machine_id)
                    ");
                    $ins->execute([
                        ':property_id'     => $bookingPropertyId,
                        ':order_id'        => $orderId,
                        ':date_time'       => $paymentDate . ' 00:00:00',
                        // USD base, and for POS/Card this INCLUDES the surcharge.
                        ':amount'          => round($amount, 2),
                        ':invoice'         => $invoice,
                        ':payment_method'  => $paymentMethod,
                        ':added_date'      => $paymentDate,
                        ':id_upload'       => $refValue,
                        ':currency'        => $currency !== '' ? (int)$currency : null,
                        ':roe_atm'         => $roeValue,
                        ':bank_account_id' => $bankAccountId > 0 ? $bankAccountId : null,
                        ':pos_machine_id'  => $posMachineId > 0 ? $posMachineId : null,
                    ]);
                    $paymentId = (int)$pdo->lastInsertId();

                    post_payment_to_folio($pdo, $paymentId, $targetFolioId);
                    recalculate_folio_totals($pdo, $targetFolioId);
                    if ($masterFolioId !== $targetFolioId) {
                        recalculate_folio_totals($pdo, $masterFolioId);
                    }
                    sync_order_payment_status($pdo, $orderId);

                    $pdo->commit();
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) { $pdo->rollBack(); }
                    bcc_json(['ok' => false, 'error' => 'Payment failed: ' . $e->getMessage()]);
                }

                bcc_log($pdo, $orderId, 'payment', 'Payment of USD ' . number_format($amount, 2)
                    . ' recorded (' . $paymentMethod . ').');

                bcc_json([
                    'ok'      => true,
                    'message' => 'Payment of USD ' . number_format($amount, 2) . ' recorded.',
                    'payment_id' => $paymentId,
                ]);
                break;
            }

            /* ---- Reverse a payment ---------------------------------------
               Posts a reversal row; nothing is deleted. A folio is a legal
               record, so the original payment and the fact it was reversed
               both stay visible. Role-gated, and the acting user is written
               to both the folio line and payment_audit_log.
            ------------------------------------------------------------------ */
            case 'reverse_payment': {
                $orderId   = (int)($_POST['order_id'] ?? 0);
                $paymentId = (int)($_POST['payment_id'] ?? 0);
                $reason    = trim((string)($_POST['reason'] ?? ''));

                if ($orderId <= 0 || $paymentId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid reversal request.']);
                }
                if ($reason === '') {
                    bcc_json(['ok' => false, 'error' => 'A reason is required to reverse a payment.']);
                }

                $auth = payment_reversal_authorised($pdo);
                if (empty($auth['ok'])) {
                    bcc_json(['ok' => false, 'error' => $auth['error']]);
                }

                /*
                   Create the audit table BEFORE the transaction opens. It is
                   DDL, and DDL implicitly commits in MySQL - doing it inside
                   would end the transaction early and the commit below would
                   throw "There is no active transaction" on a reversal that
                   had actually succeeded.
                */
                ensure_payment_audit_log_table($pdo);

                try {
                    $pdo->beginTransaction();
                    $res = reverse_payment($pdo, $orderId, $paymentId, $reason);
                    $pdo->commit();
                } catch (Throwable $e) {
                    if ($pdo->inTransaction()) { $pdo->rollBack(); }
                    bcc_json(['ok' => false, 'error' => $e->getMessage()]);
                }

                bcc_log($pdo, $orderId, 'payment', 'Payment #' . $paymentId . ' reversed ('
                    . number_format((float)$res['amount'], 2) . ') - ' . $reason);

                bcc_json([
                    'ok'      => true,
                    'message' => 'Payment #' . $paymentId . ' reversed.',
                ]);
                break;
            }

            case 'save_note': {
                $orderId = (int)($_POST['order_id'] ?? 0);
                $note    = trim((string)($_POST['note'] ?? ''));
                if ($orderId <= 0) {
                    bcc_json(['ok' => false, 'error' => 'Invalid booking.']);
                }
                $noteCol = '';
                foreach (['res_notes', 'reservation_notes', 'notes', 'remarks', 'internal_notes'] as $c) {
                    if (bcc_column_exists($pdo, 'orders', $c)) { $noteCol = $c; break; }
                }
                if ($noteCol === '') {
                    bcc_json(['ok' => false, 'error' => 'No notes field on this system.']);
                }
                $pdo->prepare("UPDATE orders SET `{$noteCol}` = :n WHERE id = :id")
                    ->execute([':n' => $note, ':id' => $orderId]);
                bcc_log($pdo, $orderId, 'note', 'Note updated from Control Centre.');
                bcc_json(['ok' => true, 'message' => 'Note saved.']);
                break;
            }

            default:
                bcc_json(['ok' => false, 'error' => 'Unknown action.']);
        }
    } catch (Throwable $e) {
        bcc_json(['ok' => false, 'error' => 'Server error: ' . $e->getMessage()]);
    }
    exit;
}

/* --------------------------------------------------------------------------
   Page render
-------------------------------------------------------------------------- */
$agents = bcc_fetch_agents($pdo);

$consultantsByAgent = [];
if (bcc_table_exists($pdo, 'consultants')) {
    try {
        $consultantRows = $pdo->query("
            SELECT
                id,
                agent_id,
                firstname,
                lastname,
                email,
                mobile,
                phone
            FROM consultants
            ORDER BY agent_id ASC, firstname ASC, lastname ASC, id ASC
        ")->fetchAll(PDO::FETCH_ASSOC);

        foreach ($consultantRows as $consultantRow) {
            $agentKey = (string)(int)($consultantRow['agent_id'] ?? 0);
            if ($agentKey === '0') {
                continue;
            }

            if (!isset($consultantsByAgent[$agentKey])) {
                $consultantsByAgent[$agentKey] = [];
            }

            $consultantsByAgent[$agentKey][] = [
                'id'        => (int)($consultantRow['id'] ?? 0),
                'firstname' => (string)($consultantRow['firstname'] ?? ''),
                'lastname'  => (string)($consultantRow['lastname'] ?? ''),
                'email'     => (string)($consultantRow['email'] ?? ''),
                'mobile'    => (string)($consultantRow['mobile'] ?? ''),
                'phone'     => (string)($consultantRow['phone'] ?? ''),
            ];
        }
    } catch (Throwable $e) {
        $consultantsByAgent = [];
    }
}
$properties = bcc_fetch_properties($pdo);
$preselectId = (int)($_GET['id'] ?? $_GET['order_id'] ?? 0);

require_once __DIR__ . '/../../includes/header.php';
?>
<style>
    :root {
        --bcc-navy: #0f172a;
        --bcc-accent: #2563eb;
        --bcc-line: #e5e7eb;
        --bcc-muted: #6b7280;
    }
    .bcc-hero {
        background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
        color: #fff; border-radius: 16px; padding: 20px 24px; margin-bottom: 18px;
    }
    .bcc-hero h3 { margin: 0; font-weight: 700; letter-spacing: -.02em; }
    .bcc-hero small { color: #94a3b8; }
    .bcc-search-card { border: 1px solid var(--bcc-line); border-radius: 14px; }
    .bcc-result {
        border: 1px solid var(--bcc-line); border-radius: 12px; padding: 12px 14px;
        margin-bottom: 8px; cursor: pointer; transition: all .12s ease; background: #fff;
        display: flex; align-items: center; gap: 12px;
    }
    .bcc-result:hover { border-color: var(--bcc-accent); box-shadow: 0 2px 10px rgba(37,99,235,.08); transform: translateY(-1px); }
    .bcc-result.active { border-color: var(--bcc-accent); box-shadow: 0 0 0 2px rgba(37,99,235,.15); }
    .bcc-colourdot { width: 12px; height: 12px; border-radius: 50%; flex: 0 0 auto; }
    .bcc-panel { border: 1px solid var(--bcc-line); border-radius: 16px; min-height: 480px; background: #fff; }
    .bcc-tabs { border-bottom: 1px solid var(--bcc-line); padding: 6px 8px 0; display: flex; flex-wrap: wrap; gap: 2px; }
    .bcc-tab {
        border: none; background: transparent; padding: 10px 14px; font-size: .86rem; font-weight: 600;
        color: var(--bcc-muted); border-radius: 10px 10px 0 0; cursor: pointer; white-space: nowrap;
    }
    .bcc-tab:hover { color: var(--bcc-navy); background: #f8fafc; }
    .bcc-tab.active { color: var(--bcc-accent); background: #eff6ff; }
    .bcc-tabpane { padding: 18px 20px; display: none; }
    .bcc-tabpane.active { display: block; }
    .bcc-stat { border: 1px solid var(--bcc-line); border-radius: 12px; padding: 12px 14px; }
    .bcc-stat .lbl { font-size: .7rem; text-transform: uppercase; letter-spacing: .06em; color: var(--bcc-muted); }
    .bcc-stat .val { font-size: 1.15rem; font-weight: 700; color: var(--bcc-navy); }
    .bcc-actionbtn { border-radius: 10px; font-weight: 600; }
    .bcc-empty { color: var(--bcc-muted); text-align: center; padding: 60px 20px; }
    .bcc-swatch { width: 26px; height: 26px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; }
    .bcc-swatch.sel { border-color: #111827; box-shadow: 0 0 0 2px #fff, 0 0 0 4px #111827; }
    .bcc-table { font-size: .84rem; }
    .bcc-table th { font-size: .68rem; text-transform: uppercase; letter-spacing: .05em; color: var(--bcc-muted); }
    .bcc-badge { font-size: .68rem; padding: 3px 8px; border-radius: 999px; font-weight: 700; }
    .bcc-quick a, .bcc-quick button { text-decoration: none; }
    .bcc-rooming-grid input, .bcc-rooming-grid select { min-width: 78px; border-color:#d7dee8; background:#fff; }
    .bcc-rooming-grid td { vertical-align: middle; }
    .bcc-rooming-grid .table-warning > td { background:#fff8db !important; }
    .bcc-rooming-grid .table-success > td { background:#eafaf1 !important; }
    @media (max-width: 991px) { .bcc-panel { min-height: auto; } }
</style>


<style>
:root{
    --bcc-bg:#f4f7fb;
    --bcc-navy:#0b1220;
    --bcc-navy2:#12233f;
    --bcc-blue:#2563eb;
    --bcc-cyan:#06b6d4;
    --bcc-green:#10b981;
    --bcc-gold:#f59e0b;
    --bcc-text:#162033;
    --bcc-muted:#6b7280;
    --bcc-line:#e3e9f1;
    --bcc-card:#ffffff;
    --bcc-radius:20px;
    --bcc-shadow:0 18px 48px rgba(15,23,42,.10);
    --bcc-shadow-sm:0 8px 24px rgba(15,23,42,.07);
}
body{
    background:
        radial-gradient(circle at 5% 0%,rgba(37,99,235,.09),transparent 28%),
        radial-gradient(circle at 95% 0%,rgba(16,185,129,.07),transparent 25%),
        var(--bcc-bg);
}
.bcc-shell{max-width:1800px;margin:0 auto}
.bcc-hero{
    position:relative;
    overflow:hidden;
    border-radius:26px!important;
    padding:24px 28px!important;
    background:
        linear-gradient(135deg,rgba(255,255,255,.07),rgba(255,255,255,0)),
        linear-gradient(120deg,#08111f 0%,#10213f 52%,#173b68 100%)!important;
    box-shadow:0 24px 60px rgba(8,17,31,.27);
    border:1px solid rgba(255,255,255,.08);
}
.bcc-hero:before{
    content:"";position:absolute;width:360px;height:360px;border-radius:50%;
    right:-125px;top:-205px;
    background:radial-gradient(circle,rgba(59,130,246,.34),transparent 68%);
}
.bcc-hero:after{
    content:"";position:absolute;width:280px;height:280px;border-radius:50%;
    left:42%;bottom:-210px;
    background:radial-gradient(circle,rgba(16,185,129,.22),transparent 70%);
}
.bcc-hero>div{position:relative;z-index:2}
.bcc-hero h3{font-size:1.55rem!important;font-weight:850!important;letter-spacing:-.03em!important}
.bcc-hero small{font-size:.86rem!important;color:#cbd5e1!important}
.bcc-eyebrow{
    color:#93c5fd;text-transform:uppercase;letter-spacing:.14em;
    font-size:.66rem;font-weight:850;margin-bottom:4px;
}
.bcc-live-chip{
    display:inline-flex;align-items:center;padding:8px 12px;border-radius:999px;
    background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.15);
    color:#e2e8f0;font-size:.72rem;font-weight:800;backdrop-filter:blur(12px);
}
.bcc-search-card,.bcc-panel{
    background:rgba(255,255,255,.94)!important;
    border:1px solid rgba(226,232,240,.96)!important;
    box-shadow:var(--bcc-shadow-sm)!important;
    backdrop-filter:blur(16px);
}
.bcc-search-card{border-radius:20px!important;overflow:hidden}
.bcc-search-card .card-body{padding:18px!important}
.bcc-search-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.bcc-search-heading h6{font-weight:850;color:var(--bcc-navy)}
.bcc-search-heading>span{
    background:#eef2f7;color:#64748b;border-radius:999px;
    padding:5px 9px;font-size:.67rem;font-weight:800;
}
.bcc-mini-label{
    color:#94a3b8;text-transform:uppercase;letter-spacing:.1em;
    font-size:.61rem;font-weight:800;margin-bottom:2px;
}
.bcc-search-wrap{position:relative}
.bcc-search-wrap>i{
    position:absolute;left:13px;top:50%;transform:translateY(-50%);
    color:#94a3b8;z-index:2;
}
.bcc-search-wrap input{padding-left:38px}
.bcc-search-card .form-control,.bcc-search-card .form-select{
    border-radius:12px!important;border-color:#dbe3ed!important;
    min-height:40px;background:#fff;box-shadow:none!important;
}
.bcc-search-card .form-control:focus,.bcc-search-card .form-select:focus{
    border-color:#60a5fa!important;box-shadow:0 0 0 4px rgba(59,130,246,.10)!important;
}
.bcc-search-btn{
    border:none!important;border-radius:12px!important;padding:10px 15px!important;
    font-weight:850!important;
    background:linear-gradient(135deg,#2563eb,#1d4ed8)!important;
    box-shadow:0 10px 22px rgba(37,99,235,.22);
    transition:.18s ease;
}
.bcc-search-btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px rgba(37,99,235,.28)}
#bccResults{max-height:calc(100vh - 255px);overflow:auto;padding-right:3px}
#bccResults::-webkit-scrollbar{width:7px}
#bccResults::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:999px}
.bcc-result{
    position:relative;overflow:hidden;border-radius:16px!important;
    padding:14px 14px 14px 17px!important;margin-bottom:10px!important;
    background:rgba(255,255,255,.97)!important;
    box-shadow:0 4px 14px rgba(15,23,42,.04);
    transition:.18s ease!important;
}
.bcc-result:before{
    content:"";position:absolute;left:0;top:0;bottom:0;width:4px;
    background:var(--bcc-accent,#64748b);
}
.bcc-result:hover{
    border-color:#bfdbfe!important;transform:translateY(-2px)!important;
    box-shadow:0 12px 28px rgba(37,99,235,.11)!important;
}
.bcc-result.active{
    background:linear-gradient(135deg,#eff6ff,#fff 72%)!important;
    border-color:#60a5fa!important;
    box-shadow:0 0 0 3px rgba(59,130,246,.10),0 14px 30px rgba(37,99,235,.14)!important;
}
.bcc-panel{border-radius:24px!important;overflow:hidden;min-height:650px!important}
.bcc-booking-head{
    padding:18px 22px 14px;
    background:linear-gradient(180deg,#f8fafc,#fff);
    border-bottom:1px solid var(--bcc-line);
}
.bcc-booking-name{font-size:1.35rem;color:var(--bcc-navy);letter-spacing:-.02em}
.bcc-order-chip{
    min-width:150px;padding:10px 13px;border-radius:14px;
    background:#f8fafc;border:1px solid #e2e8f0;
}
.bcc-tabs{
    position:sticky!important;top:0;z-index:20;
    padding:8px 12px 0!important;gap:4px!important;flex-wrap:nowrap!important;
    overflow-x:auto;background:rgba(255,255,255,.97);
    backdrop-filter:blur(15px);
}
.bcc-tab{
    position:relative;padding:11px 13px 12px!important;
    font-size:.76rem!important;font-weight:780!important;
    transition:.16s ease;
}
.bcc-tab.active{background:#eff6ff!important;color:var(--bcc-blue)!important}
.bcc-tab.active:after{
    content:"";position:absolute;left:18px;right:18px;bottom:0;height:3px;
    border-radius:999px 999px 0 0;
    background:linear-gradient(90deg,var(--bcc-blue),var(--bcc-cyan));
}
.bcc-tabpane{padding:22px!important;animation:bccFade .18s ease}
@keyframes bccFade{from{opacity:.35;transform:translateY(4px)}to{opacity:1;transform:none}}
.bcc-stat{
    position:relative;overflow:hidden;border-radius:16px!important;
    padding:14px 15px!important;
    background:linear-gradient(145deg,#fff,#f8fafc)!important;
    box-shadow:0 6px 18px rgba(15,23,42,.045);
    transition:.17s ease;
}
.bcc-stat:hover{transform:translateY(-2px);box-shadow:0 12px 26px rgba(15,23,42,.08)}
.bcc-stat:after{
    content:"";position:absolute;right:-22px;top:-22px;width:68px;height:68px;border-radius:50%;
    background:radial-gradient(circle,rgba(59,130,246,.11),transparent 72%);
}
.bcc-stat .lbl{font-size:.62rem!important;letter-spacing:.1em!important;font-weight:850!important}
.bcc-stat .val{font-size:1.18rem!important;font-weight:850!important;letter-spacing:-.02em}
.bcc-actionbtn{border-radius:11px!important;font-weight:760!important;padding:8px 12px!important}
.bcc-table{font-size:.8rem!important}
.bcc-table thead th{
    background:#f8fafc!important;font-size:.63rem!important;
    letter-spacing:.08em!important;padding:11px 12px!important;
}
.bcc-table tbody td{padding:11px 12px!important;border-color:#edf1f5!important}
.bcc-table tbody tr:hover{background:#f8fbff}
.bcc-badge{
    font-size:.63rem!important;padding:5px 9px!important;
    text-transform:uppercase;letter-spacing:.03em;
}
.bcc-swatch{
    width:34px!important;height:34px!important;border-radius:11px!important;
    box-shadow:0 5px 14px rgba(15,23,42,.10);transition:.16s ease;
}
.bcc-swatch:hover{transform:scale(1.08)}
.bcc-empty{padding:70px 20px!important}
.bcc-empty i{
    width:58px;height:58px;border-radius:18px;display:grid!important;
    place-items:center;margin-left:auto;margin-right:auto;
    background:#eef2f7;color:#64748b;
}
.bcc-panel .form-control,.bcc-panel .form-select{
    border-radius:12px!important;border-color:#dce3ec!important;
}
@media(max-width:991px){
    .bcc-panel{min-height:auto!important}
    .bcc-hero{padding:20px!important}
    .bcc-tabpane{padding:16px!important}
    #bccResults{max-height:none}
}
@media(max-width:575px){
    .bcc-hero h3{font-size:1.2rem!important}
    .bcc-booking-head{padding:15px}
    .bcc-order-chip{width:100%;text-align:left!important}
}
</style>

<div class="container-fluid py-3 bcc-shell">

    <div class="bcc-hero">
        <div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
            <div>
                <div class="bcc-eyebrow">Guest Journey Command Suite</div>
                <h3><i class="bi bi-command me-2"></i>Booking Control Centre</h3>
                <small>Operations, revenue, rooming, folios, documents and communications in one premium workspace.</small>
            </div>
            <div class="text-end">
                <span class="bcc-live-chip"><i class="bi bi-shield-check me-1"></i>Live Operations</span>
            </div>
        </div>
    </div>

    <div class="row g-3">
        <!-- LEFT: search + results -->
        <div class="col-lg-4">
            <div class="card bcc-search-card mb-3">
                <div class="card-body">
                    <div class="bcc-search-heading">
                        <div>
                            <div class="bcc-mini-label">Reservation Finder</div>
                            <h6 class="mb-0"><i class="bi bi-search me-2 text-primary"></i>Search bookings</h6>
                        </div>
                        <span>Smart Search</span>
                    </div>
                    <div class="bcc-search-wrap mb-2">
                        <i class="bi bi-search"></i>
                        <input type="text" id="bccQ" class="form-control" placeholder="Guest, agent, booking ID or order number…" autocomplete="off">
                    </div>
                    <div class="row g-2">
                        <div class="col-6">
                            <select id="bccAgent" class="form-select form-select-sm">
                                <option value="0">Any agent</option>
                                <?php foreach ($agents as $a): ?>
                                    <option value="<?php echo (int)$a['id']; ?>"><?php echo bcc_h($a['name'] ?: ('Agent #' . $a['id'])); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                        <div class="col-6">
                            <select id="bccStatus" class="form-select form-select-sm">
                                <option value="all">Any status</option>
                                <option value="confirmed">Confirmed</option>
                                <option value="provisional">Provisional</option>
                                <option value="checked_in">Checked In</option>
                                <option value="checked_out">Checked Out</option>
                                <option value="no_show">No Show</option>
                                <option value="cancelled">Cancelled</option>
                            </select>
                        </div>
                        <?php if (count($properties) > 1): ?>
                        <div class="col-12">
                            <select id="bccProperty" class="form-select form-select-sm">
                                <option value="0">All properties</option>
                                <?php foreach ($properties as $p): ?>
                                    <option value="<?php echo (int)$p['id']; ?>"><?php echo bcc_h($p['name']); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                        <?php endif; ?>
                        <div class="col-5">
                            <select id="bccDateField" class="form-select form-select-sm">
                                <option value="check_in">Arrival</option>
                                <option value="check_out">Departure</option>
                            </select>
                        </div>
                        <div class="col-4">
                            <input type="date" id="bccFrom" class="form-control form-control-sm" title="From">
                        </div>
                        <div class="col-3">
                            <input type="date" id="bccTo" class="form-control form-control-sm" title="To">
                        </div>
                    </div>
                    <div class="d-grid mt-2">
                        <button id="bccSearchBtn" class="btn btn-primary btn-sm bcc-actionbtn bcc-search-btn"><i class="bi bi-search me-2"></i>Search Bookings</button>
                    </div>
                </div>
            </div>

            <div id="bccResults">
                <div class="bcc-empty"><i class="bi bi-inbox fs-3 d-block mb-2"></i>Search to load bookings.</div>
            </div>
        </div>

        <!-- RIGHT: control panel -->
        <div class="col-lg-8">
            <div class="bcc-panel">
                <div id="bccPanelEmpty" class="bcc-empty">
                    <i class="bi bi-hand-index-thumb fs-1 d-block mb-3"></i>
                    Select a booking on the left to open its control panel.
                </div>

                <div id="bccPanel" style="display:none;">
                    <!-- header strip -->
                    <div class="bcc-booking-head">
                        <div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
                            <div>
                                <div class="d-flex align-items-center gap-2">
                                    <span id="bccColourDot" class="bcc-colourdot" style="width:16px;height:16px;"></span>
                                    <h4 class="mb-0 fw-bold bcc-booking-name" id="bccGuestName">—</h4>
                                    <span class="bcc-badge" id="bccStatusBadge"></span>
                                </div>
                                <div class="text-muted small mt-1" id="bccSubline">—</div>
                                <div class="small mt-1 fw-semibold" id="bccCreatedBy" style="color:#334155;"><i class="bi bi-person-check me-1"></i>Created by —</div>
                            </div>
                            <div class="text-end bcc-order-chip">
                                <div class="text-muted small">Booking</div>
                                <div class="fw-bold" id="bccOrderNo">#—</div>
                            </div>
                        </div>
                    </div>

                    <!-- tabs -->
                    <div class="bcc-tabs mt-2" id="bccTabs">
                        <button class="bcc-tab active" data-tab="overview"><i class="bi bi-grid-1x2 me-1"></i>Overview</button>
                        <button class="bcc-tab" data-tab="rooming"><i class="bi bi-door-open me-1"></i>Rooming</button>
                        <button class="bcc-tab" data-tab="rates"><i class="bi bi-cash-stack me-1"></i>Rates</button>
                        <button class="bcc-tab" data-tab="extras"><i class="bi bi-plus-square me-1"></i>Extras</button>
                        <button class="bcc-tab" data-tab="folios"><i class="bi bi-journal-text me-1"></i>Folios</button>
                        <button class="bcc-tab" data-tab="payments"><i class="bi bi-credit-card me-1"></i>Payments</button>
                        <button class="bcc-tab" data-tab="invoicing"><i class="bi bi-receipt me-1"></i>Invoicing</button>
                        <button class="bcc-tab" data-tab="colour"><i class="bi bi-palette me-1"></i>Colour &amp; Status</button>
                        <button class="bcc-tab" data-tab="comms"><i class="bi bi-chat-dots me-1"></i>Comms</button>
                        <button class="bcc-tab" data-tab="timeline"><i class="bi bi-clock-history me-1"></i>Timeline</button>
                    </div>

                    <div class="bcc-tabpane active" data-pane="overview" id="paneOverview"></div>
                    <div class="bcc-tabpane" data-pane="rooming" id="paneRooming"></div>
                    <div class="bcc-tabpane" data-pane="rates" id="paneRates"></div>
                    <div class="bcc-tabpane" data-pane="extras" id="paneExtras"></div>
                    <div class="bcc-tabpane" data-pane="folios" id="paneFolios"></div>
                    <div class="bcc-tabpane" data-pane="payments" id="panePayments"></div>
                    <div class="bcc-tabpane" data-pane="invoicing" id="paneInvoicing"></div>
                    <div class="bcc-tabpane" data-pane="colour" id="paneColour"></div>
                    <div class="bcc-tabpane" data-pane="comms" id="paneComms"></div>
                    <div class="bcc-tabpane" data-pane="timeline" id="paneTimeline"></div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
(function () {
    "use strict";

    const API = 'booking_control_centre.php';
    const EXTRA_PAYER_AGENTS = <?php echo json_encode($agents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
    const BOOKING_CONSULTANTS_BY_AGENT = <?php echo json_encode($consultantsByAgent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
    let currentBooking = null;

    /* Compact searchable agent picker used by Extras.
       It renders a small dropdown directly beneath the field, limits visible
       results and never expands the page with the full agent list. */
    function attachAgentSearch(input) {
        if (!input || input.dataset.agentSearchReady === '1') return;
        input.dataset.agentSearchReady = '1';
        input.setAttribute('autocomplete', 'off');

        /*
           Put the result menu directly under <body>, not inside the Extras card.
           The tab/card containers use overflow rules which clipped the old
           absolutely-positioned menu after roughly three agents.
        */
        const menu = document.createElement('div');
        menu.className = 'bcc-agent-search-menu shadow-lg';
        menu.style.cssText = [
            'display:none',
            'position:fixed',
            'z-index:2147483000',
            'max-height:320px',
            'overflow-y:auto',
            'overscroll-behavior:contain',
            'background:#fff',
            'border:1px solid #ced4da',
            'border-radius:.5rem',
            'padding:5px',
            'box-shadow:0 14px 36px rgba(15,23,42,.22)'
        ].join(';');
        document.body.appendChild(menu);

        function selectedName() {
            const id = String(input.dataset.agentId || '');
            const found = EXTRA_PAYER_AGENTS.find(function(a){ return String(a.id) === id; });
            return found ? String(found.name || ('Agent #' + found.id)) : '';
        }

        function positionMenu() {
            if (menu.style.display === 'none') return;
            const rect = input.getBoundingClientRect();
            const gap = 4;
            const viewportGap = 10;
            const desiredHeight = Math.min(320, Math.max(120, menu.scrollHeight));
            const roomBelow = window.innerHeight - rect.bottom - viewportGap;
            const roomAbove = rect.top - viewportGap;
            const openAbove = roomBelow < 170 && roomAbove > roomBelow;
            const available = Math.max(120, openAbove ? roomAbove - gap : roomBelow - gap);

            menu.style.left = Math.max(viewportGap, rect.left) + 'px';
            menu.style.width = Math.max(280, rect.width) + 'px';
            menu.style.maxWidth = 'calc(100vw - 20px)';
            menu.style.maxHeight = Math.min(320, available) + 'px';
            menu.style.top = openAbove
                ? Math.max(viewportGap, rect.top - Math.min(desiredHeight, available) - gap) + 'px'
                : Math.min(window.innerHeight - viewportGap, rect.bottom + gap) + 'px';
        }

        function hideMenu() {
            menu.style.display = 'none';
        }

        function draw() {
            const q = String(input.value || '').trim().toLowerCase();
            const matches = EXTRA_PAYER_AGENTS.filter(function(a){
                return String(a.name || ('Agent #' + a.id)).toLowerCase().indexOf(q) !== -1;
            }).slice(0, 50);

            if (!matches.length) {
                menu.innerHTML = '<div class="small text-muted px-2 py-2">No matching agents</div>';
            } else {
                menu.innerHTML = matches.map(function(a){
                    const name = String(a.name || ('Agent #' + a.id));
                    return '<button type="button" class="btn btn-sm btn-light text-start w-100 bcc-agent-option mb-1" '
                        + 'style="white-space:normal;line-height:1.25;" data-agent-id="' + h(a.id)
                        + '" data-agent-name="' + h(name) + '">' + h(name) + '</button>';
                }).join('');

                menu.querySelectorAll('.bcc-agent-option').forEach(function(btn){
                    /* mousedown fires before the input blur, so lower entries
                       remain selectable even when the page itself is scrollable. */
                    btn.addEventListener('mousedown', function(e){
                        e.preventDefault();
                        input.value = btn.getAttribute('data-agent-name') || '';
                        input.dataset.agentId = btn.getAttribute('data-agent-id') || '';
                        hideMenu();
                        input.dispatchEvent(new Event('change', {bubbles:true}));
                    });
                });
            }
            menu.style.display = 'block';
            positionMenu();
        }

        input.addEventListener('focus', draw);
        input.addEventListener('input', function(){ input.dataset.agentId = ''; draw(); });
        input.addEventListener('keydown', function(e){
            if (e.key === 'Escape') hideMenu();
        });
        input.addEventListener('blur', function(){
            setTimeout(function(){
                hideMenu();
                if (!input.dataset.agentId && input.value.trim() !== '') {
                    const exact = EXTRA_PAYER_AGENTS.find(function(a){
                        return String(a.name || ('Agent #' + a.id)).toLowerCase() === input.value.trim().toLowerCase();
                    });
                    if (exact) input.dataset.agentId = String(exact.id);
                    else input.value = selectedName();
                }
            }, 180);
        });

        window.addEventListener('resize', positionMenu);
        document.addEventListener('scroll', positionMenu, true);
        document.addEventListener('mousedown', function(e){
            if (e.target !== input && !menu.contains(e.target)) hideMenu();
        });
    }

    function payerAgentId(input) {
        return input ? String(input.dataset.agentId || '') : '';
    }

    function h(s) {
        return String(s == null ? '' : s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }
    // Safe string coercion — guards .replace()/.toLowerCase() against numbers/null.
    function S(v) { return v == null ? '' : String(v); }
    function pretty(v) { return S(v).replace(/_/g, ' '); }
    function money(n, sym) {
        n = parseFloat(n || 0);
        return (sym || '$') + n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    }
    function fmtDate(d) {
        if (!d) return '—';
        const t = String(d).substring(0, 10);
        const dt = new Date(t + 'T00:00:00');
        if (isNaN(dt.getTime())) return t;
        return dt.toLocaleDateString(undefined, { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' });
    }
    function normaliseBookingStatus(s) {
        s = String(s == null ? '' : s).trim().toLowerCase();
        s = s.replace(/[-\s]+/g, '_');

        const aliases = {
            '0': 'cancelled',
            '1': 'confirmed',
            '2': 'provisional',
            '3': 'checked_in',
            '4': 'checked_out',
            'confirm': 'confirmed',
            'confirmed_booking': 'confirmed',
            'provisional_booking': 'provisional',
            'in_house': 'checked_in',
            'inhouse': 'checked_in',
            'checkedin': 'checked_in',
            'checkedout': 'checked_out',
            'noshow': 'no_show',
            'active_out': 'active_out_balance',
            'active_out_balance': 'active_out_balance',
            'checked_out_paid': 'checked_out_paid',
            'pay_on_arrival': 'pay_on_arrival',
            'chase_payment': 'chase_payment',
            'online_payment': 'online_payment'
        };

        return aliases[s] || s;
    }

    function statusColour(s) {
        s = normaliseBookingStatus(s);

        /*
         * Same palette used by the RoomZ booking calendar.
         */
        const map = {
            active_out_balance: '#DC2626',
            chase_payment: '#EC4899',
            pay_on_arrival: '#8B5CF6',
            checked_in: '#22C55E',
            online_payment: '#F97316',
            confirmed: '#FACC15',
            invoiced: '#FDBA74',
            provisional: '#9CA3AF',
            checked_out: '#3B82F6',
            checked_out_paid: '#3B82F6',
            cancelled: '#6B7280',
            no_show: '#92400E'
        };

        return map[s] || '#64748B';
    }

    function statusTextColour(s) {
        s = normaliseBookingStatus(s);

        return ['confirmed', 'invoiced'].indexOf(s) >= 0
            ? '#111827'
            : '#FFFFFF';
    }
    function toast(msg, ok) {
        const el = document.createElement('div');
        el.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;padding:12px 18px;border-radius:10px;font-weight:600;color:#fff;box-shadow:0 4px 20px rgba(0,0,0,.2);'
            + 'background:' + (ok === false ? '#dc2626' : '#16a34a') + ';';
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(function () { el.style.opacity = '0'; el.style.transition = 'opacity .4s'; }, 2200);
        setTimeout(function () { el.remove(); }, 2700);
    }

    async function apiGet(params) {
        const url = API + '?' + new URLSearchParams(params).toString();
        const r = await fetch(url, { credentials: 'same-origin' });
        return r.json();
    }
    async function apiPost(params) {
        const body = new URLSearchParams(params);
        const r = await fetch(API, { method: 'POST', credentials: 'same-origin', body: body });
        return r.json();
    }

    /* ---------- Search ---------- */
    async function runSearch() {
        const box = document.getElementById('bccResults');
        box.innerHTML = '<div class="bcc-empty"><div class="spinner-border spinner-border-sm"></div><div class="mt-2">Searching…</div></div>';
        const params = {
            api: 'search',
            q: document.getElementById('bccQ').value.trim(),
            agent_id: document.getElementById('bccAgent').value,
            status: document.getElementById('bccStatus').value,
            date_field: document.getElementById('bccDateField').value,
            date_from: document.getElementById('bccFrom').value,
            date_to: document.getElementById('bccTo').value
        };
        const propEl = document.getElementById('bccProperty');
        if (propEl) params.property_id = propEl.value;

        try {
            const data = await apiGet(params);
            if (!data.ok) { box.innerHTML = '<div class="bcc-empty"><i class="bi bi-exclamation-triangle fs-3 d-block mb-2 text-warning"></i>' + h(data.error || 'Search failed.') + '</div>'; return; }
            if (!data.results.length) { box.innerHTML = '<div class="bcc-empty"><i class="bi bi-search fs-3 d-block mb-2"></i>No bookings match.</div>'; return; }
            box.innerHTML = data.results.map(renderResult).join('');
            box.querySelectorAll('.bcc-result').forEach(function (el) {
                el.addEventListener('click', function () {
                    box.querySelectorAll('.bcc-result').forEach(function (x) { x.classList.remove('active'); });
                    el.classList.add('active');
                    openBooking(parseInt(el.getAttribute('data-id'), 10));
                });
            });
        } catch (e) {
            box.innerHTML = '<div class="bcc-empty">Error: ' + h(e.message) + '</div>';
        }
    }

    function renderResult(r) {
        const dot = statusColour(r.status) || r.colour_hex || '#64748B';
        const stay = fmtDate(r.check_in) + ' → ' + fmtDate(r.check_out);
        return '<div class="bcc-result" data-id="' + r.id + '" style="--bcc-accent:' + h(dot) + '">'
            + '<span class="bcc-colourdot" style="background:' + h(dot) + '"></span>'
            + '<div class="flex-grow-1 min-w-0">'
            + '<div class="fw-semibold text-truncate">' + h(r.guest_name || 'Guest') + ' <span class="text-muted small">#' + h(r.order_no) + '</span></div>'
            + '<div class="small text-muted text-truncate">' + stay + '</div>'
            + '<div class="small text-muted text-truncate">' + h(r.agent_name || 'Direct') + ' · ' + h(r.property_name || '') + '</div>'
            + '</div>'
            + '<span class="bcc-badge" style="background:' + statusColour(r.status) + ';color:' + statusTextColour(r.status) + ';box-shadow:0 5px 12px ' + statusColour(r.status) + '35">' + h(pretty(normaliseBookingStatus(r.status))) + '</span>'
            + '</div>';
    }

    /* ---------- Open booking ---------- */
    async function openBooking(orderId) {
        document.getElementById('bccPanelEmpty').style.display = 'none';
        document.getElementById('bccPanel').style.display = 'block';
        setActiveTab('overview');
        document.getElementById('paneOverview').innerHTML = '<div class="text-center py-5"><div class="spinner-border"></div></div>';

        const data = await apiGet({ api: 'booking', order_id: orderId });
        if (!data.ok) { toast(data.error || 'Failed to load booking', false); return; }
        currentBooking = data;

        const o = data.order;
        document.getElementById('bccGuestName').textContent = o.guest_name || 'Guest';
        document.getElementById('bccOrderNo').textContent = '#' + (o.order_no || o.id);
        document.getElementById('bccSubline').innerHTML =
            fmtDate(o.check_in) + ' → ' + fmtDate(o.check_out)
            + ' · ' + h(o.agent_name || 'Direct')
            + ' · ' + h(o.property_name || '')
            + ' · ' + (o.totalrooms || 1) + ' room(s)';
        const creatorLine = document.getElementById('bccCreatedBy');
        if (creatorLine) {
            creatorLine.innerHTML = '<i class="bi bi-person-check me-1"></i>Created by <strong>'
                + h(o.creator_name || 'Unknown user') + '</strong>'
                + (o.booking_created_at ? ' <span class="text-muted fw-normal">on ' + h(String(o.booking_created_at)) + '</span>' : '');
        }
        const sb = document.getElementById('bccStatusBadge');
        sb.textContent = pretty(o.status);
        sb.style.background = statusColour(o.status) + '22';
        sb.style.color = statusColour(o.status);
        document.getElementById('bccColourDot').style.background = o.colour_hex || statusColour(o.status);

        renderOverview(data);
        renderRooming(data);
        renderRates(data);
        renderExtras(data);
        renderFolios(data);
        renderPayments(data);
        renderInvoicing(data);
        renderColour(data);
        renderComms(data);
        renderTimeline(data);
    }

    /* Refresh only the booking data needed after an Extras action.
       This is still an AJAX request, but it does not rebuild or navigate the page,
       so the user remains on the Extras tab and keeps their current position. */
    async function refreshExtrasTab() {
        if (!currentBooking || !currentBooking.order) return false;

        const orderId = currentBooking.order.id;
        const data = await apiGet({ api: 'booking', order_id: orderId });
        if (!data.ok) {
            throw new Error(data.error || 'Failed to refresh extras.');
        }

        currentBooking = data;
        renderExtras(data);
        renderFolios(data);
        renderOverview(data);
        setActiveTab('extras');
        return true;
    }

    /* ---------- Tabs ---------- */
    function setActiveTab(name) {
        document.querySelectorAll('.bcc-tab').forEach(function (t) {
            t.classList.toggle('active', t.getAttribute('data-tab') === name);
        });
        document.querySelectorAll('.bcc-tabpane').forEach(function (p) {
            p.classList.toggle('active', p.getAttribute('data-pane') === name);
        });
    }
    document.getElementById('bccTabs').addEventListener('click', function (e) {
        const btn = e.target.closest('.bcc-tab');
        if (btn) setActiveTab(btn.getAttribute('data-tab'));
    });

    /* ---------- Renderers ---------- */
    function renderOverview(d) {
        const f = d.financials, sym = d.symbol, o = d.order;
        const bal = parseFloat(f.balance || 0);
        const rf = '/rooms/modules/reservations/reservation_form.php?id=' + o.id;
        const pf = '/rooms/modules/payments/proforma_invoice.php?order_id=' + o.id;
        document.getElementById('paneOverview').innerHTML =
            '<div class="row g-2 mb-3">'
            + statCard('Grand Total', money(f.charges, sym))
            + statCard('Accommodation', money(f.accommodation, sym))
            + statCard('Extras', money(f.extras, sym))
            + statCard('Payments', money(f.payments, sym))
            + statCard('Balance Due', money(f.balance, sym), bal > 0 ? '#dc2626' : '#16a34a')
            + '</div>'
            + '<div class="bcc-quick d-flex flex-wrap gap-2">'
            + '<a class="btn btn-outline-dark btn-sm bcc-actionbtn" href="' + pf + '" target="_blank"><i class="bi bi-file-earmark-text me-1"></i>Proforma</a>'
            + '<a class="btn btn-outline-secondary btn-sm bcc-actionbtn" href="/rooms/modules/payments/extras.php?order_id=' + o.id + '"><i class="bi bi-plus-circle me-1"></i>Extras</a>'
            + '<button type="button" class="btn btn-outline-primary btn-sm bcc-actionbtn bcc-open-basic-details"><i class="bi bi-person-vcard me-1"></i>Names / Meal Plan / Agent</button>'
            + quickStatusButtons(o, d.today)
            + '</div>'
            + '<hr>'
            + '<div class="row g-3">'
            + '<div class="col-md-6"><div class="bcc-stat"><div class="lbl">Guest</div><div class="val">' + h(o.guest_name || 'Guest') + '</div></div></div>'
            + '<div class="col-md-6"><div class="bcc-stat"><div class="lbl">Agent</div><div class="val">' + h(o.agent_name || 'Direct') + '</div></div></div>'
            + '<div class="col-md-6"><div class="bcc-stat"><div class="lbl">Room Type</div><div class="val">' + h(o.room_type_title || '—') + '</div></div></div>'
            + '<div class="col-md-6"><div class="bcc-stat"><div class="lbl">Meal Plan</div><div class="val">' + h(o.meal_plan || '—') + '</div></div></div>'
            + '</div>';
    }
    function statCard(lbl, val, colour) {
        return '<div class="col-6 col-md">'
            + '<div class="bcc-stat"><div class="lbl">' + h(lbl) + '</div>'
            + '<div class="val" ' + (colour ? 'style="color:' + colour + '"' : '') + '>' + val + '</div></div></div>';
    }
    /*
       Date-aware action buttons.

       Check In      — only on the arrival day, and only if not already in-house.
       Late Check In — after the arrival day has passed (still not checked in).
       Check Out     — only on the departure day, and only if currently in-house.
       Late Check Out— after the departure day has passed (still in-house).
       Cancel        — only when the booking is not already cancelled; routes to
                       cancel_reservation.php so the system's real cancel flow
                       (releasing availability) runs.
       Reinstate     — only when the booking IS cancelled.
       Confirm       — opens the reservation form so the user confirms there.
    */
    function dayDiff(fromISO, toISO) {
        // whole days from fromISO -> toISO (positive = toISO is later)
        const a = new Date(String(fromISO).substring(0, 10) + 'T00:00:00');
        const b = new Date(String(toISO).substring(0, 10) + 'T00:00:00');
        if (isNaN(a.getTime()) || isNaN(b.getTime())) return null;
        return Math.round((b - a) / 86400000);
    }

    function quickStatusButtons(o, today) {
        const s = normaliseBookingStatus(o.status);
        const t = String(today || '').substring(0, 10);
        let html = '';

        // Cancelled bookings are reinstated here after a live room-by-room availability check.
        if (s === 'cancelled') {
            html += '<button type="button" class="btn btn-outline-success btn-sm bcc-actionbtn bcc-open-reinstate">'
                 + '<i class="bi bi-arrow-counterclockwise me-1"></i>Reinstate Booking</button>';
            return html;
        }

        const isIn  = (s === 'checked_in');
        const isOut = (s === 'checked_out');

        // --- Arrival-day logic ---
        if (!isIn && !isOut) {
            const dIn = (o.check_in && t) ? dayDiff(t, o.check_in) : null; // >0 future, 0 today, <0 past
            if (dIn === 0) {
                html += actBtn('Check In', 'checked_in', 'success', 'bi-box-arrow-in-right');
            } else if (dIn !== null && dIn < 0) {
                // Arrival day has passed and they're still not in-house.
                html += '<button class="btn btn-warning btn-sm bcc-actionbtn bcc-status-btn" data-status="checked_in">'
                     + '<i class="bi bi-clock-history me-1"></i>Late Check In <span class="badge text-bg-dark ms-1">'
                     + Math.abs(dIn) + 'd late</span></button>';
            }
            // If arrival is still in the future, no check-in button at all.
        }

        // --- Departure-day logic (only meaningful once in-house) ---
        if (isIn) {
            const dOut = (o.check_out && t) ? dayDiff(t, o.check_out) : null;
            if (dOut === 0) {
                html += actBtn('Check Out', 'checked_out', 'primary', 'bi-box-arrow-right');
            } else if (dOut !== null && dOut < 0) {
                html += '<button class="btn btn-warning btn-sm bcc-actionbtn bcc-status-btn" data-status="checked_out">'
                     + '<i class="bi bi-clock-history me-1"></i>Late Check Out <span class="badge text-bg-dark ms-1">'
                     + Math.abs(dOut) + 'd over</span></button>';
            }
            // Departure still in the future → guest is simply in-house, no button.
        }

        // --- No Show: only sensible once arrival day has arrived/passed and not in-house ---
        if (!isIn && !isOut && s !== 'no_show') {
            const dIn = (o.check_in && t) ? dayDiff(t, o.check_in) : null;
            if (dIn !== null && dIn <= 0) {
                html += actBtn('No Show', 'no_show', 'warning', 'bi-person-x');
            }
        }

        // --- Confirm directly here; identical to choosing Confirmed in Reservation Form ---
        if (s !== 'confirmed' && s !== 'no_show' && !isIn && !isOut) {
            html += '<button type="button" class="btn btn-outline-success btn-sm bcc-actionbtn bcc-status-btn" '
                 + 'data-status="confirmed" data-confirm-booking="1">'
                 + '<i class="bi bi-check-circle me-1"></i>Confirm Booking</button>';
        }

        // Full cancellation policy is handled inside this Control Centre.
        html += '<button type="button" class="btn btn-outline-danger btn-sm bcc-actionbtn bcc-open-cancel">'
             + '<i class="bi bi-x-circle me-1"></i>Cancel Booking</button>';

        return html;
    }
    function actBtn(label, status, variant, icon) {
        return '<button class="btn btn-outline-' + variant + ' btn-sm bcc-actionbtn bcc-status-btn" data-status="' + status + '">'
            + '<i class="bi ' + icon + ' me-1"></i>' + label + '</button>';
    }

    function renderRooming(d) {
        const rows = d.rooming || [], rooms = d.rooms || [], caps = d.rooming_capabilities || {};
        let html = '<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">'
            + '<div><h6 class="mb-0">Excel-style Rooming Grid</h6><div class="small text-muted">Click into any cell, type the real allocation, then save the row. Children may occupy their own room with Adults set to 0.</div></div>'
            + '<div class="d-flex gap-2"><button class="btn btn-primary btn-sm bcc-actionbtn" id="bccSaveAllRooming"><i class="bi bi-save me-1"></i>Save All Changes</button>'
            + (rows.length > 1 ? '<button class="btn btn-outline-danger btn-sm bcc-actionbtn" id="bccReleaseRooms"><i class="bi bi-door-open me-1"></i>Release Rooms</button>' : '')
            + '<a class="btn btn-outline-primary btn-sm bcc-actionbtn" href="/rooms/modules/reservations/reservation_form.php?id=' + d.order.id + '#rooming"><i class="bi bi-box-arrow-up-right me-1"></i>Full Rooming Setup</a></div></div>';
        if (!rows.length) { html += '<div class="text-muted">No rooming rows.</div>'; }
        else {
            html += '<div class="alert alert-info py-2 small"><i class="bi bi-info-circle me-1"></i>Dates and pax are per room. For a children-only room, set Adults to 0 and enter the children count. Nightly rates will use the child percentage without a single supplement. The booking dates automatically follow the earliest check-in and latest check-out.</div>';
            html += '<div class="table-responsive"><table class="table bcc-table align-middle bcc-rooming-grid"><thead><tr>'
                + '<th style="min-width:145px">Actual Room</th><th>Type</th>'
                + (caps.first_name ? '<th style="min-width:135px">First Name</th>' : '')
                + (caps.last_name ? '<th style="min-width:135px">Last Name</th>' : '')
                + (caps.guest_name && !caps.first_name && !caps.last_name ? '<th style="min-width:170px">Guest</th>' : '<th>Guest</th>')
                + '<th style="min-width:135px">Check-in</th><th style="min-width:135px">Check-out</th>'
                + '<th style="width:88px">Adults</th><th style="width:88px">Children</th>'
                + (caps.remarks || caps.notes ? '<th style="min-width:180px">Remarks</th>' : '')
                + '<th style="width:90px"></th></tr></thead><tbody>';
            rows.forEach(function (r) {
                let roomOptions = '<option value="0">Unallocated</option>';
                rooms.forEach(function (rm) {
                    roomOptions += '<option value="' + rm.id + '" ' + (String(r.room_id || '') === String(rm.id) ? 'selected' : '') + '>'
                        + h(rm.room_no) + (rm.room_type_title ? ' — ' + h(rm.room_type_title) : '') + '</option>';
                });
                html += '<tr class="bcc-rooming-row" data-row-id="' + r.id + '">'
                    + '<td><select class="form-select form-select-sm bcc-r-room">' + roomOptions + '</select></td>'
                    + '<td class="small"><span class="bcc-r-type">' + h(r.room_type_title || '—') + '</span></td>'
                    + (caps.first_name ? '<td><input class="form-control form-control-sm bcc-r-first" value="' + h(r.first_name || '') + '"></td>' : '')
                    + (caps.last_name ? '<td><input class="form-control form-control-sm bcc-r-last" value="' + h(r.last_name || '') + '"></td>' : '')
                    + (caps.guest_name && !caps.first_name && !caps.last_name ? '<td><input class="form-control form-control-sm bcc-r-guest" value="' + h(r.guest_name || '') + '"></td>' : '<td class="small text-muted">' + h(r.guest_name || r.guest || '—') + '</td>')
                    + '<td><input type="date" class="form-control form-control-sm bcc-r-in" value="' + h(S(r.check_in).substring(0,10)) + '"></td>'
                    + '<td><input type="date" class="form-control form-control-sm bcc-r-out" value="' + h(S(r.check_out).substring(0,10)) + '"></td>'
                    + '<td><input type="number" min="0" max="20" class="form-control form-control-sm bcc-r-adults text-center" value="' + parseInt(r.adults || 0,10) + '"></td>'
                    + '<td><input type="number" min="0" max="20" class="form-control form-control-sm bcc-r-children text-center" value="' + parseInt(r.children || 0,10) + '"></td>'
                    + (caps.remarks || caps.notes ? '<td><input class="form-control form-control-sm bcc-r-remarks" value="' + h(r.remarks || r.notes || '') + '"></td>' : '')
                    + '<td class="text-nowrap">'
                    + '<button class="btn btn-outline-primary btn-sm bcc-save-rooming-row"><i class="bi bi-check2"></i> Save</button> '
                    + '<button class="btn btn-outline-danger btn-sm bcc-del-rooming-row" title="Remove this room from the booking"><i class="bi bi-trash"></i></button>'
                    + '</td>'
                    + '</tr>';
            });
            html += '</tbody></table></div>';
        }

        /* ---------- Add a room to this booking ---------- */
        const types = d.room_types || [];
        if (types.length) {
            const typeOpts = types.map(function (t) {
                return '<option value="' + t.id + '">' + h(t.title) + '</option>';
            }).join('');
            html += '<div class="bcc-stat mt-3" style="background:#f8fafc;">'
                + '<div class="d-flex justify-content-between align-items-center mb-2">'
                + '<h6 class="mb-0"><i class="bi bi-plus-square me-1"></i>Add a Room to this Booking</h6>'
                + '<button class="btn btn-sm btn-outline-secondary" id="bccToggleAddRoom">Show</button>'
                + '</div>'
                + '<div id="bccAddRoomBody" style="display:none;">'
                + '<div class="row g-2 align-items-end">'
                + '<div class="col-md-3"><label class="form-label small mb-1">Room Type</label>'
                + '<select id="bccAddType" class="form-select form-select-sm">' + typeOpts + '</select></div>'
                + '<div class="col-md-2"><label class="form-label small mb-1">Check In</label>'
                + '<input type="date" id="bccAddIn" class="form-control form-control-sm" value="' + h(S(d.order.check_in).substring(0,10)) + '"></div>'
                + '<div class="col-md-2"><label class="form-label small mb-1">Check Out</label>'
                + '<input type="date" id="bccAddOut" class="form-control form-control-sm" value="' + h(S(d.order.check_out).substring(0,10)) + '"></div>'
                + '<div class="col-md-1"><label class="form-label small mb-1">Adults</label>'
                + '<input type="number" id="bccAddAdults" class="form-control form-control-sm" min="0" value="2"></div>'
                + '<div class="col-md-1"><label class="form-label small mb-1">Kids</label>'
                + '<input type="number" id="bccAddKids" class="form-control form-control-sm" min="0" value="0"></div>'
                + '<div class="col-md-3"><button class="btn btn-sm btn-outline-primary w-100" id="bccCheckAvail">'
                + '<i class="bi bi-search me-1"></i>Check Availability</button></div>'
                + '</div>'
                + '<div id="bccAvailResult" class="mt-2"></div>'
                + '</div></div>';
        }

        document.getElementById('paneRooming').innerHTML = html;

        const roomMap = {};
        rooms.forEach(function (rm) { roomMap[String(rm.id)] = rm; });
        document.querySelectorAll('.bcc-r-room').forEach(function (sel) {
            sel.addEventListener('change', function () {
                const row = sel.closest('.bcc-rooming-row'), rm = roomMap[String(sel.value)];
                const type = row ? row.querySelector('.bcc-r-type') : null;
                if (type) type.textContent = rm && rm.room_type_title ? rm.room_type_title : '—';
                if (row) row.classList.add('table-warning');
            });
        });
        document.querySelectorAll('.bcc-rooming-row input').forEach(function (el) {
            el.addEventListener('input', function () { el.closest('tr').classList.add('table-warning'); });
        });
        const saveAll = document.getElementById('bccSaveAllRooming');
        if (saveAll) saveAll.addEventListener('click', async function () {
            const dirty = Array.from(document.querySelectorAll('.bcc-rooming-row.table-warning'));
            if (!dirty.length) { toast('There are no unsaved rooming changes.', true); return; }
            saveAll.disabled = true;
            for (let i=0;i<dirty.length;i++) {
                const ok = await saveRoomingRow(dirty[i], false);
                if (!ok) { saveAll.disabled = false; return; }
            }
            saveAll.disabled = false;
            toast('All rooming changes saved.', true);
            openBooking(currentBooking.order.id);
        });

        const releaseRoomsBtn = document.getElementById('bccReleaseRooms');
        if (releaseRoomsBtn) releaseRoomsBtn.addEventListener('click', async function () {
            const totalRooms = document.querySelectorAll('.bcc-rooming-row').length;
            const maxRelease = Math.max(0, totalRooms - 1);
            if (maxRelease < 1) {
                alert('This booking must keep at least one room.');
                return;
            }

            const entered = prompt('How many rooms do you want to release?\n\nCurrent rooms: ' + totalRooms + '\nMaximum you may release: ' + maxRelease + '\nAt least 1 room will remain.', '1');
            if (entered === null) return;
            const releaseCount = parseInt(String(entered).trim(), 10);
            if (!Number.isFinite(releaseCount) || releaseCount < 1 || releaseCount > maxRelease) {
                alert('Enter a whole number between 1 and ' + maxRelease + '.');
                return;
            }

            const remaining = totalRooms - releaseCount;
            if (!confirm('Release ' + releaseCount + ' room(s) from this booking?\n\n'
                + remaining + ' room(s) will remain. Unallocated rooms are released first, followed by the highest-numbered room slots. '
                + 'Rates and accommodation charges will be recalculated. This action will be audit logged and cannot be undone.')) return;

            releaseRoomsBtn.disabled = true;
            const originalHtml = releaseRoomsBtn.innerHTML;
            releaseRoomsBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Releasing...';
            try {
                const res = await apiPost({
                    api: 'release_rooms',
                    order_id: currentBooking.order.id,
                    release_count: releaseCount
                });
                toast(res.message || res.error, res.ok);
                if (res.ok) await openBooking(currentBooking.order.id);
            } finally {
                releaseRoomsBtn.disabled = false;
                releaseRoomsBtn.innerHTML = originalHtml;
            }
        });

        /* ---------- Add Room: toggle, availability probe, commit ---------- */
        const toggleAdd = document.getElementById('bccToggleAddRoom');
        if (toggleAdd) {
            toggleAdd.addEventListener('click', function () {
                const body = document.getElementById('bccAddRoomBody');
                const showing = body.style.display !== 'none';
                body.style.display = showing ? 'none' : 'block';
                toggleAdd.textContent = showing ? 'Show' : 'Hide';
            });
        }

        const checkBtn = document.getElementById('bccCheckAvail');
        if (checkBtn) {
            checkBtn.addEventListener('click', async function () {
                const box = document.getElementById('bccAvailResult');
                const typeId = document.getElementById('bccAddType').value;
                const cin = document.getElementById('bccAddIn').value;
                const cout = document.getElementById('bccAddOut').value;
                if (!cin || !cout || cout <= cin) {
                    box.innerHTML = '<div class="alert alert-warning py-2 small mb-0">Enter a valid date range (check-out after check-in).</div>';
                    return;
                }
                box.innerHTML = '<div class="text-muted small"><span class="spinner-border spinner-border-sm me-1"></span>Checking availability…</div>';

                const res = await apiGet({
                    api: 'check_room_availability',
                    order_id: currentBooking.order.id,
                    room_type_id: typeId,
                    check_in: cin,
                    check_out: cout
                });
                if (!res.ok) {
                    box.innerHTML = '<div class="alert alert-danger py-2 small mb-0">' + h(res.error || 'Check failed.') + '</div>';
                    return;
                }

                const free = res.free_rooms || [];
                const juggles = res.juggles || [];
                const bookable = parseInt(res.bookable || 0, 10);
                const tight = (res.nights || []).filter(function (n) { return parseInt(n.free, 10) <= 0; });

                let out = '';
                if (free.length > 0) {
                    out += '<div class="alert alert-success py-2 small mb-2">'
                         + '<strong>' + free.length + '</strong> room(s) of this type free for the whole stay '
                         + '(' + res.total + ' total).</div>';
                } else {
                    /*
                       Distinguish "someone else has it" from "you already have it".
                       If this booking's own rooming rows hold every room of the
                       type, saying only "not available" is confusing - the user
                       can see the room on their own grid.
                    */
                    const typeId = String(document.getElementById('bccAddType').value);
                    const mine = (currentBooking.rooming || []).filter(function (r) {
                        return String(r.room_type_id) === typeId && parseInt(r.room_id || 0, 10) > 0;
                    });
                    const total = parseInt(res.total || 0, 10);

                    if (mine.length > 0 && mine.length >= total) {
                        out += '<div class="alert alert-info py-2 small mb-2">'
                             + '<i class="bi bi-info-circle me-1"></i>'
                             + '<strong>This booking already holds ' + (total === 1 ? 'the only' : 'all ' + total)
                             + ' room(s) of this type.</strong> '
                             + 'There is nothing further to allocate — the room is already on this booking.'
                             + '</div>';
                    } else {
                        out += '<div class="alert alert-danger py-2 small mb-2">'
                             + '<i class="bi bi-x-octagon me-1"></i><strong>No room of this type is free</strong> for the whole stay'
                             + (tight.length ? ' — ' + tight.length + ' night(s) are fully committed.' : '.')
                             + (mine.length > 0 ? ' (' + mine.length + ' of the ' + total + ' is already on this booking.)' : '')
                             + '</div>';
                    }
                }

                // Per-night detail
                if ((res.nights || []).length) {
                    out += '<div class="d-flex flex-wrap gap-1 mb-2">';
                    res.nights.forEach(function (n) {
                        const f = parseInt(n.free, 10);
                        const col = f > 0 ? '#16a34a' : '#dc2626';
                        out += '<span class="badge" style="background:' + col + '18;color:' + col + ';border:1px solid ' + col + '40;">'
                             + fmtDate(n.date) + ': ' + f + ' free</span>';
                    });
                    out += '</div>';
                }

                if (free.length > 0) {
                    /* Rooms are genuinely available — offer the add. */
                    let roomOpts = '<option value="0">Leave unassigned (allocate later)</option>';
                    free.forEach(function (r) {
                        roomOpts += '<option value="' + r.id + '">Room ' + h(r.room_no) + '</option>';
                    });
                    out += '<div class="row g-2 align-items-end">'
                         + '<div class="col-md-5"><label class="form-label small mb-1">Assign Room ('
                         + free.length + ' free)</label>'
                         + '<select id="bccAddRoomSel" class="form-select form-select-sm">' + roomOpts + '</select></div>'
                         + '<div class="col-md-4"><button class="btn btn-sm btn-success w-100" id="bccCommitAddRoom">'
                         + '<i class="bi bi-plus-lg me-1"></i>Add Room to Booking</button></div>'
                         + '</div>';
                } else {
                    /*
                       Nothing free: do NOT offer Add (it would only oversell).
                       Instead show juggles that would genuinely create space.
                    */
                    if (juggles.length) {
                        const sameType = juggles.filter(function (j) { return j.same_type; });
                        const crossType = juggles.filter(function (j) { return !j.same_type; });

                        out += '<div class="bcc-stat" style="background:#fffbeb;border-color:#fcd34d;">'
                             + '<div class="fw-semibold small mb-2"><i class="bi bi-shuffle me-1"></i>'
                             + 'Juggle the calendar to create space</div>';

                        if (sameType.length) {
                            out += '<div class="small text-muted mb-1">Same room type — clean like-for-like moves:</div>';
                            sameType.forEach(function (j) { out += juggleCard(j, true); });
                        }
                        if (crossType.length) {
                            out += '<div class="small text-muted mt-2 mb-1">'
                                 + '<i class="bi bi-exclamation-triangle me-1"></i>'
                                 + 'Only possible if you <strong>ignore room types</strong> — the guest moves to a different type:</div>';
                            crossType.forEach(function (j) { out += juggleCard(j, false); });
                        }
                        out += '</div>';
                    } else {
                        out += '<div class="alert alert-secondary py-2 small mb-0">'
                             + 'No single-move juggle can free a room for these dates either. '
                             + 'Try different dates, another room type, or the waitlist.</div>';
                    }
                }
                box.innerHTML = out;

                const commit = document.getElementById('bccCommitAddRoom');
                if (commit) commit.addEventListener('click', function () { addRoomToBooking(); });

                box.querySelectorAll('.bcc-apply-juggle').forEach(function (b) {
                    b.addEventListener('click', async function () {
                        const same = b.getAttribute('data-same') === '1';
                        const msg = same
                            ? 'Move booking #' + b.getAttribute('data-bkg') + ' to Room '
                              + b.getAttribute('data-tono') + '?\n\nThis frees Room '
                              + b.getAttribute('data-fromno') + ' for your new stay.'
                            : 'IGNORE ROOM TYPES?\n\nBooking #' + b.getAttribute('data-bkg')
                              + ' would move to Room ' + b.getAttribute('data-tono')
                              + ' (' + b.getAttribute('data-totype') + ') — a DIFFERENT room type '
                              + 'to what that guest booked.\n\nThis frees Room ' + b.getAttribute('data-fromno')
                              + ' for your new stay. Continue?';
                        if (!confirm(msg)) return;
                        b.disabled = true;
                        const r = await apiPost({
                            api: 'apply_juggle',
                            order_id: currentBooking.order.id,
                            blocker_row_id: b.getAttribute('data-row'),
                            move_to_room_id: b.getAttribute('data-toid')
                        });
                        toast(r.message || r.error, r.ok);
                        if (r.ok) {
                            checkBtn.click(); // re-run availability; the room should now be free
                        } else {
                            b.disabled = false;
                        }
                    });
                });
            });
        }
    }

    /* One juggle suggestion, rendered as an actionable card. */
    function juggleCard(j, sameType) {
        const badge = sameType
            ? '<span class="badge text-bg-success">same type</span>'
            : '<span class="badge text-bg-warning text-dark">different type</span>';
        return '<div class="d-flex justify-content-between align-items-center gap-2 py-1 border-top">'
            + '<div class="small">'
            + 'Move booking <strong>#' + h(j.blocker_order_id) + '</strong> '
            + '(' + fmtDate(j.blocker_in) + ' → ' + fmtDate(j.blocker_out) + ')<br>'
            + 'from <strong>Room ' + h(j.room_no) + '</strong> '
            + '→ <strong>Room ' + h(j.move_to_room_no) + '</strong> '
            + (j.move_to_type_title ? '<span class="text-muted">(' + h(j.move_to_type_title) + ')</span> ' : '')
            + badge
            + '<div class="text-success">This frees Room ' + h(j.room_no) + ' for your stay.</div>'
            + '</div>'
            + '<button class="btn btn-sm ' + (sameType ? 'btn-success' : 'btn-warning') + ' bcc-apply-juggle text-nowrap"'
            + ' data-row="' + h(j.blocker_row_id) + '"'
            + ' data-toid="' + h(j.move_to_room_id) + '"'
            + ' data-tono="' + h(j.move_to_room_no) + '"'
            + ' data-fromno="' + h(j.room_no) + '"'
            + ' data-bkg="' + h(j.blocker_order_id) + '"'
            + ' data-totype="' + h(j.move_to_type_title || 'other type') + '"'
            + ' data-same="' + (sameType ? '1' : '0') + '">'
            + '<i class="bi bi-shuffle me-1"></i>Apply</button>'
            + '</div>';
    }

    async function addRoomToBooking(confirmOversell) {
        if (!currentBooking) return;
        const sel = document.getElementById('bccAddRoomSel');
        if (!sel) {
            toast('Run Check Availability first — no free room is currently offered.', false);
            return;
        }
        const params = {
            api: 'add_rooming_row',
            order_id: currentBooking.order.id,
            room_type_id: document.getElementById('bccAddType').value,
            room_id: sel ? sel.value : 0,
            check_in: document.getElementById('bccAddIn').value,
            check_out: document.getElementById('bccAddOut').value,
            adults: document.getElementById('bccAddAdults').value,
            children: document.getElementById('bccAddKids').value
        };
        if (confirmOversell) params.confirm_oversell = 1;

        const res = await apiPost(params);

        if (!res.ok && res.needs_confirm) {
            let detail = '';
            if (Array.isArray(res.oversold) && res.oversold.length) {
                detail = res.oversold.map(function (o) {
                    return '  • ' + fmtDate(o.date) + ' — ' + o.occupied + ' of ' + o.total
                         + ' rooms already held (over by ' + o.over + ')';
                }).join('\n');
            }
            const proceed = confirm(
                'ROOM TYPE OVERSELL WARNING\n\n'
                + 'Adding this room oversells the type on '
                + (res.oversold ? res.oversold.length : 0) + ' night(s):\n\n'
                + detail + '\n\nAdd the room anyway?'
            );
            if (!proceed) return;
            return addRoomToBooking(true);
        }

        toast(res.ok ? (res.message || 'Room added.') : (res.error || 'Could not add room.'), res.ok);
        if (res.ok) openBooking(currentBooking.order.id);
    }

    async function saveRoomingRow(row, reloadAfter) {
        if (!currentBooking || !row) return false;
        const btn = row.querySelector('.bcc-save-rooming-row');
        if (btn) btn.disabled = true;
        const params = {
            api: 'save_rooming_row', order_id: currentBooking.order.id,
            row_id: row.getAttribute('data-row-id'),
            room_id: row.querySelector('.bcc-r-room').value,
            check_in: row.querySelector('.bcc-r-in').value,
            check_out: row.querySelector('.bcc-r-out').value,
            adults: row.querySelector('.bcc-r-adults').value,
            children: row.querySelector('.bcc-r-children').value
        };
        const q = function (sel) { const x=row.querySelector(sel); return x ? x.value : null; };
        if (q('.bcc-r-first') !== null) params.first_name = q('.bcc-r-first');
        if (q('.bcc-r-last') !== null) params.last_name = q('.bcc-r-last');
        if (q('.bcc-r-guest') !== null) params.guest_name = q('.bcc-r-guest');
        if (q('.bcc-r-remarks') !== null) {
            if (currentBooking.rooming_capabilities.remarks) params.remarks = q('.bcc-r-remarks');
            else params.notes = q('.bcc-r-remarks');
        }
        try {
            let res = await apiPost(params);

            /*
               Room-type oversell: the physical room is free but the type has no
               spare stock on one or more nights. Surface the detail and let the
               user decide, then re-send with an explicit confirmation flag.
            */
            if (!res.ok && res.needs_confirm) {
                let detail = '';
                if (Array.isArray(res.oversold) && res.oversold.length) {
                    detail = res.oversold.map(function (o) {
                        return '  • ' + fmtDate(o.date) + ' — ' + o.occupied + ' of ' + o.total
                             + ' rooms already held (over by ' + o.over + ')';
                    }).join('\n');
                }
                const proceed = confirm(
                    'ROOM TYPE OVERSELL WARNING\n\n'
                    + 'These dates oversell this room type on '
                    + (res.oversold ? res.oversold.length : 0) + ' night(s):\n\n'
                    + detail + '\n\n'
                    + 'The specific room is free, but the room type has no spare stock on those nights.\n\n'
                    + 'Save anyway?'
                );
                if (!proceed) {
                    if (btn) btn.disabled = false;
                    return false;
                }
                params.confirm_oversell = 1;
                res = await apiPost(params);
            }

            if (!res.ok) { toast(res.error || 'Could not save rooming row.', false); if (btn) btn.disabled=false; return false; }
            row.classList.remove('table-warning'); row.classList.add('table-success');
            setTimeout(function(){ row.classList.remove('table-success'); }, 900);
            if (reloadAfter !== false) { toast(res.message || 'Rooming row saved.', true); openBooking(currentBooking.order.id); }
            return true;
        } catch (err) {
            toast(err.message || 'Could not save rooming row.', false); if (btn) btn.disabled=false; return false;
        }
    }

    function renderRates(d) {
        const rows = d.nightly || [], sym = d.symbol;
        let html = '<div class="d-flex justify-content-between align-items-center mb-2">'
            + '<div><h6 class="mb-0">Nightly Rates</h6>'
            + '<div class="small text-muted">Click Edit on any night to override its price without leaving this page.</div></div>'
            + '<div class="d-flex gap-2">'
            + '<button class="btn btn-outline-primary btn-sm bcc-actionbtn" id="bccReprice" title="Rebuild every night from the current rooming pax and dates. Manual overrides are kept.">'
            + '<i class="bi bi-arrow-repeat me-1"></i>Reprice from Rooming</button>'
            + '<button class="btn btn-outline-danger btn-sm bcc-actionbtn" id="bccRepriceClear" title="Wipe every manual rate override on this booking, then recompute all nights from the current rooming pax and dates.">'
            + '<i class="bi bi-eraser me-1"></i>Clear overrides &amp; reprice</button>'
            + '<button class="btn btn-primary btn-sm bcc-actionbtn" id="bccApplyRate">'
            + '<i class="bi bi-cash-coin me-1"></i>Apply Rate</button>'
            + '<a class="btn btn-outline-secondary btn-sm bcc-actionbtn" href="/rooms/modules/reservations/reservation_form.php?id=' + d.order.id + '" target="_blank">'
            + '<i class="bi bi-box-arrow-up-right me-1"></i>Full Rate Setup</a></div></div>';
        if (!rows.length) { html += '<div class="text-muted">No nightly pricing rows.</div>'; }
        else {
            html += '<div class="table-responsive"><table class="table bcc-table align-middle"><thead><tr>'
                + '<th>Date</th><th>Room</th><th>Type</th><th>Pax</th><th>Rate</th>'
                + '<th class="text-end">Total</th><th style="width:80px"></th></tr></thead><tbody>';
            let sum = 0;
            const seenNights = {};
            rows.forEach(function (r) {
                sum += parseFloat(r.total || 0);
                const ov = (parseInt(r.rate_overridden || 0, 10) === 1);
                const pax = (r.adults_count || 0) + 'A' + ((r.kids_count || 0) > 0 ? ' ' + r.kids_count + 'C' : '');
                const day = S(r.date).substring(0, 10);
                let totalCell = money(r.total, sym);
                if (ov && r.original_total && parseFloat(r.original_total) !== parseFloat(r.total)) {
                    totalCell = '<span class="text-muted text-decoration-line-through small">'
                              + money(r.original_total, sym) + '</span> '
                              + '<span class="text-danger fw-semibold">' + money(r.total, sym) + '</span>';
                }
                /*
                   The modal edits every room of a night at once, so only the
                   first row of each date carries the Edit button.
                */
                const first = !seenNights[day];
                seenNights[day] = true;
                html += '<tr>'
                    + '<td>' + fmtDate(r.date) + '</td>'
                    + '<td>' + h(r.room_no || '—') + '</td>'
                    + '<td>' + h(r.room_type_title || '—') + '</td>'
                    + '<td>' + pax + '</td>'
                    + '<td class="small text-muted">' + h(pretty(r.applied_rate_type)) + (ov ? ' <span class="badge text-bg-warning">override</span>' : '') + '</td>'
                    + '<td class="text-end fw-semibold">' + totalCell + '</td>'
                    + '<td class="text-end">'
                    + (first
                        ? '<button class="btn btn-sm btn-outline-primary bcc-rate-edit"'
                          + ' data-date="' + h(day) + '"'
                          + ' data-reason="' + h(r.override_reason || '') + '">'
                          + '<i class="bi bi-pencil"></i> Edit</button>'
                        : '')
                    + '</td>'
                    + '</tr>';
            });
            html += '</tbody><tfoot><tr><th colspan="5" class="text-end">Total</th><th class="text-end">' + money(sum, sym) + '</th><th></th></tr></tfoot>';
            html += '</table></div>';
        }
        document.getElementById('paneRates').innerHTML = html;

        /*
           Group the room-rows by night so the override modal can present every
           room of that date together, exactly like the reservation form.
        */
        const byNight = {};
        rows.forEach(function (r) {
            const day = S(r.date).substring(0, 10);
            if (!byNight[day]) byNight[day] = [];
            byNight[day].push(r);
        });

        document.querySelectorAll('.bcc-rate-edit').forEach(function (b) {
            const day = b.getAttribute('data-date');
            b.setAttribute('data-rooms', JSON.stringify(byNight[day] || []));
            b.addEventListener('click', function () { openRateModal(b, sym); });
        });

        async function doReprice(btn, clearOverrides) {
            if (!currentBooking) return;
            const msg = clearOverrides
                ? 'Wipe EVERY manual rate override on this booking, then recompute all nights '
                  + 'from the current rooming pax and dates?\n\n'
                  + 'Use this when the stored nightly totals are wrong or corrupt and should be '
                  + 'rebuilt cleanly. This cannot be undone.'
                : 'Rebuild every night from the current rooming pax and dates?\n\n'
                  + 'Manual price overrides are preserved. This corrects nightly totals '
                  + 'that drifted from the live rooming (for example after an adult count change).';
            if (!confirm(msg)) return;

            const original = btn.innerHTML;
            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>'
                + (clearOverrides ? 'Clearing…' : 'Repricing…');
            const params = { api: 'reprice_order', order_id: currentBooking.order.id };
            if (clearOverrides) params.clear_overrides = 1;
            try {
                const res = await apiPost(params);
                if (!res.ok) { toast(res.error || 'Reprice failed.', false); btn.disabled = false; btn.innerHTML = original; return; }
                toast(res.message || 'Booking repriced.', true);
                openBooking(currentBooking.order.id);
            } catch (err) {
                toast(err.message || 'Reprice failed.', false);
                btn.disabled = false; btn.innerHTML = original;
            }
        }

        const applyRateBtn = document.getElementById('bccApplyRate');
        if (applyRateBtn) {
            applyRateBtn.addEventListener('click', function () {
                if (typeof window.openApplyRate !== 'function') {
                    toast('Apply Rate tool is unavailable. Check apply_rate_widget.php.', false);
                    return;
                }
                window.openApplyRate(d.order.id, {
                    onDone: function () {
                        toast('Contract rate applied.', true);
                        openBooking(d.order.id);
                    }
                });
            });
        }

        const reBtn = document.getElementById('bccReprice');
        if (reBtn) reBtn.addEventListener('click', function () { doReprice(reBtn, false); });

        const reClearBtn = document.getElementById('bccRepriceClear');
        if (reClearBtn) reClearBtn.addEventListener('click', function () { doReprice(reClearBtn, true); });
    }

    /*
       Nightly rate override modal — mirrors the reservation form's editor:
       one line per room with Keep / Per person / Flat total, a live recalculated
       night total, an apply-to-whole-stay option and a reason for the audit log.
    */
    function openRateModal(btn, sym) {
        const date = btn.getAttribute('data-date');
        const nightRooms = JSON.parse(btn.getAttribute('data-rooms') || '[]');
        const reason = btn.getAttribute('data-reason') || '';
        const anyOv = nightRooms.some(function (r) { return parseInt(r.rate_overridden || 0, 10) === 1; });

        let calcTotal = 0;
        nightRooms.forEach(function (r) {
            calcTotal += parseFloat((r.original_total !== null && r.original_total !== undefined && r.original_total !== '')
                ? r.original_total : r.total) || 0;
        });

        let rowsHtml = '';
        nightRooms.forEach(function (r, i) {
            const slot = (r.slot_no !== null && r.slot_no !== undefined && String(r.slot_no) !== '')
                ? String(r.slot_no)
                : String(i + 1);
            const pax = (r.adults_count || 0) + 'A' + ((r.kids_count || 0) > 0 ? ' ' + r.kids_count + 'C' : '');
            rowsHtml +=
                '<tr class="bcc-ovr-row" data-slot="' + slot + '" data-adults="' + (r.adults_count || 1) + '"'
                + ' data-calc="' + h((r.original_total !== null && r.original_total !== undefined && r.original_total !== '') ? r.original_total : r.total) + '">'
                + '<td><strong>Room ' + slot + '</strong> <span class="text-muted small">' + pax
                + (r.room_type_title ? ' · ' + h(r.room_type_title) : '') + '</span></td>'
                + '<td style="width:150px"><select class="form-select form-select-sm bcc-ovr-mode">'
                + '<option value="keep">Keep</option>'
                + '<option value="perPerson">Per person</option>'
                + '<option value="flat">Flat total</option>'
                + '</select></td>'
                + '<td style="width:130px"><input type="number" class="form-control form-control-sm bcc-ovr-val" step="0.01" min="0" value="0.00" disabled></td>'
                + '<td class="text-end fw-semibold bcc-ovr-total" style="width:110px">' + money(r.total, sym) + '</td>'
                + '</tr>';
        });

        let el = document.getElementById('bccRateModal');
        if (el) el.remove();
        el = document.createElement('div');
        el.id = 'bccRateModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content">'
            + '<div class="modal-header"><div>'
            + '<h5 class="modal-title"><i class="bi bi-pencil-square me-2"></i>Override Night Price</h5>'
            + '<div class="text-muted small">' + fmtDate(date) + '</div></div>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>'
            + '<div class="modal-body">'
            + '<div class="small text-muted mb-2">' + nightRooms.length + ' room(s) this night. Edit any of them below.</div>'
            + '<table class="table table-sm align-middle mb-3"><thead><tr>'
            + '<th>Room</th><th>Mode</th><th>Rate / Amount</th><th class="text-end">Room total</th>'
            + '</tr></thead><tbody>' + rowsHtml + '</tbody></table>'
            + '<div class="bcc-stat mb-3" style="background:#f8fafc;">'
            + '<div class="d-flex justify-content-between"><span class="text-muted">Calculated total (current rates)</span>'
            + '<strong id="bccOvrCalc">' + money(calcTotal, sym) + '</strong></div>'
            + '<div class="d-flex justify-content-between mt-1"><span>New night total</span>'
            + '<strong id="bccOvrNew" style="font-size:1.15rem">' + money(calcTotal, sym) + '</strong></div>'
            + '</div>'
            + '<div class="form-check mb-3"><input class="form-check-input" type="checkbox" id="bccOvrWhole">'
            + '<label class="form-check-label small fw-semibold text-uppercase" for="bccOvrWhole">'
            + 'Apply these room rates to <strong>every night</strong> of the stay</label></div>'
            + '<label class="form-label small fw-semibold text-uppercase">Reason (optional)</label>'
            + '<textarea id="bccOvrReason" class="form-control" rows="3" placeholder="Example: tour leader room waived / negotiated per-person rate">' + h(reason) + '</textarea>'
            + '<div class="alert alert-info small mt-3 mb-0">'
            + 'Set each room individually. <strong>Per person</strong> = rate x that room\'s adults. '
            + '<strong>Flat total</strong> = the exact room amount (type 0 to waive, e.g. a tour leader). '
            + 'Leave a room on <strong>Keep</strong> to use its calculated rate.</div>'
            + '</div>'
            + '<div class="modal-footer justify-content-between">'
            + (anyOv ? '<button class="btn btn-outline-danger" id="bccOvrClear">Clear Overrides</button>' : '<span></span>')
            + '<div><button class="btn btn-light me-2" data-bs-dismiss="modal">Cancel</button>'
            + '<button class="btn btn-primary" id="bccOvrApply">Apply</button></div>'
            + '</div></div></div>';
        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        /* Live recalculation as modes/values change. */
        function recalc() {
            let total = 0;
            el.querySelectorAll('.bcc-ovr-row').forEach(function (tr) {
                const mode = tr.querySelector('.bcc-ovr-mode').value;
                const valEl = tr.querySelector('.bcc-ovr-val');
                const adults = Math.max(1, parseInt(tr.getAttribute('data-adults'), 10) || 1);
                const calc = parseFloat(tr.getAttribute('data-calc')) || 0;
                let rt = calc;
                if (mode === 'keep') {
                    valEl.disabled = true;
                } else {
                    valEl.disabled = false;
                    const v = parseFloat(valEl.value) || 0;
                    rt = (mode === 'perPerson') ? v * adults : v;
                }
                tr.querySelector('.bcc-ovr-total').textContent = money(rt, sym);
                total += rt;
            });
            document.getElementById('bccOvrNew').textContent = money(total, sym);
        }
        el.querySelectorAll('.bcc-ovr-mode').forEach(function (s) { s.addEventListener('change', recalc); });
        el.querySelectorAll('.bcc-ovr-val').forEach(function (i) { i.addEventListener('input', recalc); });
        recalc();

        document.getElementById('bccOvrApply').addEventListener('click', async function () {
            const rooms = {};
            let any = false;
            el.querySelectorAll('.bcc-ovr-row').forEach(function (tr) {
                const mode = tr.querySelector('.bcc-ovr-mode').value;
                if (mode === 'keep') return;
                rooms[tr.getAttribute('data-slot')] = {
                    mode: mode,
                    value: parseFloat(tr.querySelector('.bcc-ovr-val').value) || 0
                };
                any = true;
            });
            if (!any) { toast('Set at least one room to Per person or Flat total.', false); return; }
            this.disabled = true;
            const res = await apiPost({
                api: 'override_night',
                order_id: currentBooking.order.id,
                date: date,
                rooms: JSON.stringify(rooms),
                whole_stay: document.getElementById('bccOvrWhole').checked ? 1 : 0,
                reason: document.getElementById('bccOvrReason').value
            });
            toast(res.message || res.error, res.ok);
            if (res.ok) { modal.hide(); openBooking(currentBooking.order.id); }
            else { this.disabled = false; }
        });

        const clr = document.getElementById('bccOvrClear');
        if (clr) clr.addEventListener('click', async function () {
            if (!confirm('Clear overrides and restore the calculated rates for this night?')) return;
            this.disabled = true;
            const res = await apiPost({
                api: 'override_night',
                order_id: currentBooking.order.id,
                date: date,
                clear: 1
            });
            toast(res.message || res.error, res.ok);
            if (res.ok) { modal.hide(); openBooking(currentBooking.order.id); }
            else { this.disabled = false; }
        });
    }

    function renderExtras(d) {
        /*
           Room-account extras are stored as one debit row per room folio. That
           is correct for the ledgers, but showing every split row here makes a
           single extra look duplicated and makes each row's amount look as if
           quantity was not multiplied. Collapse those physical posting rows
           back into one logical extra for this screen and sum their debits.

           Example: 4 x 198.00 = 792.00 split over two rooms is stored as two
           396.00 rows. The Extras tab must display one 792.00 line, not two
           apparently duplicated 396.00 lines.
        */
        const rawLines = (d.folio_lines || []).filter(function (l) {
            const st = String(l.source_table || '').toLowerCase();
            return ['tblservices', 'restaurant_menu_items', 'inventory_items', 'booking_extras'].indexOf(st) >= 0;
        });
        const groupedRoomExtras = {};
        const lines = [];
        rawLines.forEach(function (l) {
            const isBookingExtra = String(l.source_table || '').toLowerCase() === 'tblservices';
            const isRoomPosting = String(l.folio_type || '').toLowerCase() === 'room';
            if (!isBookingExtra || !isRoomPosting) {
                lines.push(l);
                return;
            }

            const key = [
                String(l.source_id || ''),
                String(l.tx_date || '').substring(0, 10),
                String(l.description || ''),
                String(parseInt(l.is_voided || 0, 10) || 0)
            ].join('|');

            if (!groupedRoomExtras[key]) {
                groupedRoomExtras[key] = Object.assign({}, l, {
                    debit: 0,
                    split_count: 0,
                    split_transaction_ids: []
                });
                lines.push(groupedRoomExtras[key]);
            }
            groupedRoomExtras[key].debit = Math.round(
                (parseFloat(groupedRoomExtras[key].debit || 0) + parseFloat(l.debit || 0)) * 100
            ) / 100;
            groupedRoomExtras[key].split_count++;
            groupedRoomExtras[key].split_transaction_ids.push(parseInt(l.id || 0, 10));
            if (groupedRoomExtras[key].split_count > 1) {
                groupedRoomExtras[key].folio_name = groupedRoomExtras[key].split_count + ' room accounts';
            }
        });
        const sym = d.symbol;
        const cat = d.catalogue || { categories: [], services: [] };

        let html = '<div class="d-flex justify-content-between align-items-center mb-2">'
            + '<div><h6 class="mb-0">Extras &amp; Add-ons</h6>'
            + '<div class="small text-muted">Add and edit extras right here — no need to leave this page.</div></div>'
            + '<a class="btn btn-outline-secondary btn-sm bcc-actionbtn" href="/rooms/modules/payments/extras.php?order_id=' + d.order.id + '" target="_blank">'
            + '<i class="bi bi-box-arrow-up-right me-1"></i>Full Extras Page</a></div>';

        /* ----- Inline add row ----- */
        if ((cat.services || []).length) {
            let catOpts = '<option value="">Select category *</option>';
            (cat.categories || []).forEach(function (c) {
                catOpts += '<option value="' + c.id + '">' + h(c.category) + '</option>';
            });
            html += '<div class="bcc-stat mb-3" style="background:#f8fafc;">'
                + '<div class="row g-2 align-items-end">'
                + '<div class="col-md-2"><label class="form-label small mb-1 fw-semibold">Category <span class="text-danger">*</span></label>'
                + '<select id="bccExCat" class="form-select form-select-sm">' + catOpts + '</select></div>'
                + '<div class="col-md-3"><label class="form-label small mb-1 fw-semibold">Service <span class="text-danger">*</span></label>'
                + '<select id="bccExSvc" class="form-select form-select-sm"></select></div>'
                + '<div class="col-md-2"><label class="form-label small mb-1">Date</label>'
                + '<input type="date" id="bccExDate" class="form-control form-control-sm" value="' + h(S(d.order.check_in).substring(0,10)) + '"></div>'
                + '<div class="col-md-1"><label class="form-label small mb-1">Qty</label>'
                + '<input type="number" id="bccExQty" class="form-control form-control-sm" min="0.01" step="0.01" value="1"></div>'
                + '<div class="col-md-2"><label class="form-label small mb-1">Unit Price</label>'
                + '<input type="number" id="bccExRate" class="form-control form-control-sm" min="0" step="0.01" value="0"></div>'
                + '<div class="col-md-2"><label class="form-label small mb-1">Charge To</label>'
                + '<select id="bccExChargeTo" class="form-select form-select-sm">'
                + '<option value="master">Master (Agent)</option>'
                + '<option value="room">Room Account (Guest)</option>'
                + '<option value="other">Others (Select Paying Agent)</option>'
                + '</select></div>'
                + '<div class="col-md-3 d-none" id="bccExPayerWrap"><label class="form-label small mb-1">Paying Agent</label>'
                + '<input type="search" id="bccExPayerAgent" class="form-control form-control-sm bcc-agent-search" placeholder="Type to search agent…"></div>'
                + '<div class="col-md-2"><button class="btn btn-sm btn-success w-100" id="bccExAdd">'
                + '<i class="bi bi-plus-lg me-1"></i>Add Extra</button></div>'
                + '<div class="col-md-10"><input type="text" id="bccExNote" class="form-control form-control-sm" placeholder="Optional note (e.g. pickup time)"></div>'
                + '</div>'
                + '<div class="small text-muted mt-1" id="bccExTotal"></div>'
                + '</div>';
        }

        if (!lines.length) {
            html += '<div class="text-muted">No extras posted yet.</div>';
        } else {
            html += '<div class="table-responsive"><table class="table bcc-table align-middle"><thead><tr>'
                + '<th style="min-width:130px">Date</th><th>Description</th>'
                + '<th style="width:130px">Charged To</th>'
                + '<th style="width:90px">Qty</th><th style="width:110px">Rate</th>'
                + '<th class="text-end">Amount</th><th style="width:120px"></th></tr></thead><tbody>';
            lines.forEach(function (l) {
                const voided = parseInt(l.is_voided || 0, 10) === 1;
                const own = String(l.source_table || '').toLowerCase() === 'tblservices';
                // Recover qty/rate from the description so they can be edited.
                let q = 1, r = parseFloat(l.debit || 0);
                const m = String(l.description || '').match(/x\s*([0-9.]+)\s*@\s*([0-9.,]+)/i);
                if (m) { q = parseFloat(m[1]); r = parseFloat(String(m[2]).replace(/,/g, '')); }

                /* Where is this charge sitting - the agent's master folio or a guest room account? */
                const isRoom = String(l.folio_type || '').toLowerCase() === 'room';
                const isOther = parseInt(l.payer_agent_id || 0, 10) > 0 || String(l.folio_type || '').toLowerCase() === 'other';
                const chargedTo = isRoom
                    ? '<span class="badge text-bg-info-subtle text-info-emphasis border" title="' + h(l.folio_name || '') + '">'
                      + '<i class="bi bi-door-open me-1"></i>Room Account</span>'
                    : (isOther
                      ? '<span class="badge text-bg-warning-subtle text-warning-emphasis border" title="' + h(l.folio_name || '') + '"><i class="bi bi-building me-1"></i>Other: ' + h(l.payer_agent_name || 'Agent') + '</span>'
                      : '<span class="badge text-bg-light border" title="' + h(l.folio_name || 'Master folio') + '"><i class="bi bi-briefcase me-1"></i>Master</span>');

                if (voided) {
                    html += '<tr class="text-muted text-decoration-line-through">'
                        + '<td>' + fmtDate(l.tx_date) + '</td><td>' + h(l.description || '') + '</td>'
                        + '<td>' + chargedTo + '</td>'
                        + '<td>' + q + '</td><td>' + money(r, sym) + '</td>'
                        + '<td class="text-end">' + money(l.debit, sym) + '</td>'
                        + '<td class="text-end"><span class="badge text-bg-secondary">voided</span></td></tr>';
                    return;
                }
                if (!own) {
                    // Raised elsewhere (restaurant, stock...) — show, don't let this screen edit it.
                    html += '<tr><td>' + fmtDate(l.tx_date) + '</td><td>' + h(l.description || '')
                        + ' <span class="badge text-bg-light border" title="Added outside the booking extras">'
                        + h(l.source_table) + '</span></td>'
                        + '<td>' + chargedTo + '</td>'
                        + '<td>' + q + '</td><td>' + money(r, sym) + '</td>'
                        + '<td class="text-end">' + money(l.debit, sym) + '</td>'
                        + '<td class="text-end text-nowrap">' + (isOther && parseInt(l.payer_agent_id || 0, 10) > 0
                          ? '<a class="btn btn-outline-dark btn-sm" target="_blank" title="Other payer extras proforma" href="/rooms/modules/payments/proforma_other_agent_extras.php?order_id=' + d.order.id + '&agent_id=' + encodeURIComponent(l.payer_agent_id) + '"><i class="bi bi-file-earmark-text"></i></a>'
                          : '<span class="small text-muted">edit on source</span>') + '</td></tr>';
                    return;
                }
                html += '<tr class="bcc-extra-row" data-line="' + l.id + '">'
                    + '<td><input type="date" class="form-control form-control-sm bcc-ex-date" value="' + h(S(l.tx_date).substring(0,10)) + '"></td>'
                    + '<td>' + h(l.description || '') + '</td>'
                    + '<td><select class="form-select form-select-sm bcc-ex-charge-to">'
                    + '<option value="master"' + (!isRoom && !isOther ? ' selected' : '') + '>Master</option>'
                    + '<option value="room"' + (isRoom ? ' selected' : '') + '>Room Account</option>'
                    + '<option value="other"' + (isOther ? ' selected' : '') + '>Others</option>'
                    + '</select><input type="search" class="form-control form-control-sm mt-1 bcc-ex-payer-agent bcc-agent-search' + (isOther ? '' : ' d-none') + '" placeholder="Type to search agent…" value="' + (isOther ? h(l.payer_agent_name || '') : '') + '" data-agent-id="' + (isOther ? h(l.payer_agent_id || '') : '') + '"></td>'
                    + '<td><input type="number" class="form-control form-control-sm bcc-ex-qty" min="0.01" step="0.01" value="' + q + '"></td>'
                    + '<td><input type="number" class="form-control form-control-sm bcc-ex-rate" min="0" step="0.01" value="' + r.toFixed(2) + '"></td>'
                    + '<td class="text-end fw-semibold">' + money(l.debit, sym) + '</td>'
                    + '<td class="text-end text-nowrap">'
                    + (isOther && parseInt(l.payer_agent_id || 0, 10) > 0
                      ? '<a class="btn btn-outline-dark btn-sm" target="_blank" title="Other payer extras proforma" href="/rooms/modules/payments/proforma_other_agent_extras.php?order_id=' + d.order.id + '&agent_id=' + encodeURIComponent(l.payer_agent_id) + '"><i class="bi bi-file-earmark-text"></i></a> '
                      : '')
                    + '<button class="btn btn-outline-primary btn-sm bcc-ex-save" title="Save this line"><i class="bi bi-check2"></i></button> '
                    + '<button class="btn btn-outline-danger btn-sm bcc-void-btn" data-line="' + l.id + '" title="Remove"><i class="bi bi-trash"></i></button>'
                    + '</td></tr>';
            });
            html += '</tbody></table></div>';
        }
        document.getElementById('paneExtras').innerHTML = html;

        /* ----- Wire the inline add row ----- */
        const svcSel = document.getElementById('bccExSvc');
        if (svcSel) {
            const all = cat.services || [];
            const fillServices = function () {
                const c = String(document.getElementById('bccExCat').value);
                const list = c ? all.filter(function (s) { return String(s.category_id) === c; }) : [];
                svcSel.innerHTML = list.length
                    ? '<option value="">Select service</option>' + list.map(function (s) { return '<option value="' + s.id + '" data-price="' + s.price + '">' + h(s.service) + '</option>'; }).join('')
                    : '<option value="">Select a category first</option>';
                syncRate();
            };
            const syncRate = function () {
                const opt = svcSel.options[svcSel.selectedIndex];
                const price = opt ? parseFloat(opt.getAttribute('data-price') || 0) : 0;
                document.getElementById('bccExRate').value = (price || 0).toFixed(2);
                calcTotal();
            };
            const calcTotal = function () {
                const q = parseFloat(document.getElementById('bccExQty').value || 0);
                const r = parseFloat(document.getElementById('bccExRate').value || 0);
                document.getElementById('bccExTotal').textContent = (q > 0 && r >= 0)
                    ? 'Line total: ' + money(q * r, sym) : '';
            };
            document.getElementById('bccExCat').addEventListener('change', fillServices);
            svcSel.addEventListener('change', syncRate);
            const chargeToSel = document.getElementById('bccExChargeTo');
            const payerWrap = document.getElementById('bccExPayerWrap');
            const togglePayer = function(){ payerWrap.classList.toggle('d-none', chargeToSel.value !== 'other'); };
            chargeToSel.addEventListener('change', togglePayer); togglePayer();
            attachAgentSearch(document.getElementById('bccExPayerAgent'));
            document.getElementById('bccExQty').addEventListener('input', calcTotal);
            document.getElementById('bccExRate').addEventListener('input', calcTotal);
            fillServices();

            document.getElementById('bccExAdd').addEventListener('click', async function () {
                const btn = this;
                const categoryId = parseInt(document.getElementById('bccExCat').value, 10);
                const sid = parseInt(svcSel.value, 10);
                if (!categoryId) { toast('Category is compulsory. Choose a category first.', false); return; }
                if (!sid) { toast('Choose a service first.', false); return; }
                btn.disabled = true;
                const res = await apiPost({
                    api: 'add_extra',
                    order_id: currentBooking.order.id,
                    category_id: categoryId,
                    service_id: sid,
                    quantity: document.getElementById('bccExQty').value,
                    unit_price: document.getElementById('bccExRate').value,
                    tx_date: document.getElementById('bccExDate').value,
                    charge_to: document.getElementById('bccExChargeTo').value,
                    payer_agent_id: payerAgentId(document.getElementById('bccExPayerAgent')),
                    note: document.getElementById('bccExNote').value
                });
                toast(res.message || res.error, res.ok);
                if (res.ok) {
                    try {
                        await refreshExtrasTab();
                    } catch (refreshError) {
                        toast(refreshError.message || 'Extra saved, but the list could not be refreshed.', false);
                    }
                }
                btn.disabled = false;
            });
        }

        /* ----- Inline edit: save a line ----- */
        document.querySelectorAll('.bcc-ex-save').forEach(function (b) {
            b.addEventListener('click', async function () {
                const row = b.closest('.bcc-extra-row');
                b.disabled = true;
                const res = await apiPost({
                    api: 'update_extra',
                    order_id: currentBooking.order.id,
                    line_id: row.getAttribute('data-line'),
                    quantity: row.querySelector('.bcc-ex-qty').value,
                    unit_price: row.querySelector('.bcc-ex-rate').value,
                    tx_date: row.querySelector('.bcc-ex-date').value,
                    charge_to: row.querySelector('.bcc-ex-charge-to').value,
                    payer_agent_id: payerAgentId(row.querySelector('.bcc-ex-payer-agent'))
                });
                toast(res.message || res.error, res.ok);
                if (res.ok) {
                    try {
                        await refreshExtrasTab();
                    } catch (refreshError) {
                        toast(refreshError.message || 'Extra updated, but the list could not be refreshed.', false);
                        b.disabled = false;
                    }
                } else {
                    b.disabled = false;
                }
            });
        });
        document.querySelectorAll('.bcc-extra-row .bcc-ex-payer-agent').forEach(attachAgentSearch);
        document.querySelectorAll('.bcc-extra-row .bcc-ex-charge-to').forEach(function(sel){
            const row=sel.closest('tr'), payer=row.querySelector('.bcc-ex-payer-agent');
            const sync=function(){ payer.classList.toggle('d-none', sel.value!=='other'); row.classList.add('table-warning'); };
            sel.addEventListener('change',sync);
        });
        document.querySelectorAll('.bcc-extra-row input, .bcc-extra-row .bcc-ex-payer-agent').forEach(function (el) {
            el.addEventListener('input', function () { el.closest('tr').classList.add('table-warning'); });
            el.addEventListener('change', function () { el.closest('tr').classList.add('table-warning'); });
        });
    }

    function renderFolios(d) {
        const folios = d.folios || [], sym = d.symbol;
        let html = '<div class="d-flex justify-content-between align-items-center mb-2"><h6 class="mb-0">Folios</h6></div>';
        if (!folios.length) { html += '<div class="text-muted">No folios yet.</div>'; }
        else {
            html += '<div class="row g-2 mb-3">';
            folios.forEach(function (f) {
                const bal = parseFloat(f.charges || 0) - parseFloat(f.payments || 0);
                html += '<div class="col-md-6"><div class="bcc-stat">'
                    + '<div class="d-flex justify-content-between"><span class="fw-semibold">' + h(f.folio_name || ('Folio #' + f.id)) + '</span>'
                    + '<span class="badge text-bg-light">' + h(f.folio_type || '') + '</span></div>'
                    + '<div class="small text-muted mt-1">Charges ' + money(f.charges, sym) + ' · Paid ' + money(f.payments, sym) + '</div>'
                    + '<div class="fw-bold mt-1" style="color:' + (bal > 0 ? '#dc2626' : '#16a34a') + '">Balance ' + money(bal, sym) + '</div>'
                    + '</div></div>';
            });
            html += '</div>';
        }
        // Full ledger
        const lines = d.folio_lines || [];
        if (lines.length) {
            html += '<h6 class="mt-2">Ledger</h6><div class="table-responsive"><table class="table bcc-table align-middle"><thead><tr>'
                + '<th>Date</th><th>Folio</th><th>Description</th><th class="text-end">Debit</th><th class="text-end">Credit</th></tr></thead><tbody>';
            lines.forEach(function (l) {
                const voided = parseInt(l.is_voided || 0, 10) === 1;
                html += '<tr' + (voided ? ' class="text-muted text-decoration-line-through"' : '') + '>'
                    + '<td>' + fmtDate(l.tx_date) + '</td>'
                    + '<td class="small">' + h(l.folio_type || '') + '</td>'
                    + '<td>' + h(l.description || '') + '</td>'
                    + '<td class="text-end">' + (parseFloat(l.debit || 0) ? money(l.debit, sym) : '') + '</td>'
                    + '<td class="text-end">' + (parseFloat(l.credit || 0) ? money(l.credit, sym) : '') + '</td>'
                    + '</tr>';
            });
            html += '</tbody></table></div>';
        }
        document.getElementById('paneFolios').innerHTML = html;
    }

    /* ---------- Payments ----------
       Payment history for the booking, plus the Record Payment action. The
       Folios tab shows what is OWED; this shows what has been TAKEN and lets
       you take more without leaving the Control Centre.
    */
    function renderPayments(d) {
        const pays = d.payments || [], sym = d.symbol;
        const canReverse = !!d.can_reverse_payment;

        let paid = 0, reversed = 0;
        pays.forEach(function (p) {
            const amt = parseFloat(p.amount || 0);
            if (parseInt(p.is_reversed || 0, 10) === 1) { reversed += amt; } else { paid += amt; }
        });

        let html = '<div class="d-flex justify-content-between align-items-center mb-3">'
            + '<h6 class="mb-0">Payments</h6>'
            + '<button type="button" class="btn btn-sm btn-primary" id="bccAddPaymentBtn">'
            + '<i class="bi bi-credit-card me-1"></i>Record Payment</button>'
            + '</div>';

        html += '<div class="row g-2 mb-3">'
            + '<div class="col-md-4"><div class="bcc-stat"><div class="small text-muted">Net Received</div>'
            + '<div class="fw-bold" style="color:#16a34a">' + money(paid, sym) + '</div></div></div>'
            + '<div class="col-md-4"><div class="bcc-stat"><div class="small text-muted">Reversed</div>'
            + '<div class="fw-bold" style="color:' + (reversed > 0 ? '#dc2626' : '#6b7280') + '">' + money(reversed, sym) + '</div></div></div>'
            + '<div class="col-md-4"><div class="bcc-stat"><div class="small text-muted">Payments Recorded</div>'
            + '<div class="fw-bold">' + pays.length + '</div></div></div>'
            + '</div>';

        if (!pays.length) {
            html += '<div class="text-muted">No payments recorded against this booking yet.</div>';
        } else {
            html += '<div class="table-responsive"><table class="table bcc-table align-middle"><thead><tr>'
                + '<th>Date</th><th>Method</th><th>Folio</th><th>Reference</th>'
                + '<th class="text-end">Amount (USD)</th><th></th></tr></thead><tbody>';
            pays.forEach(function (p) {
                const isRev = parseInt(p.is_reversed || 0, 10) === 1;
                const src = [];
                if (p.bank_name) src.push(h(p.bank_name));
                if (p.pos_machine_name) src.push(h(p.pos_machine_name));

                html += '<tr' + (isRev ? ' class="table-light text-muted"' : '') + '>'
                    + '<td>' + fmtDate((p.date_time || '').substring(0, 10)) + '</td>'
                    + '<td>' + h(p.payment_method || '')
                    + (src.length ? '<div class="small text-muted">' + src.join(' · ') + '</div>' : '')
                    + '</td>'
                    + '<td class="small">' + h(p.folio_no || '-') + '</td>'
                    + '<td class="small">' + h(p.id_upload || '') + '</td>'
                    + '<td class="text-end fw-semibold' + (isRev ? ' text-decoration-line-through' : '') + '">'
                    + money(p.amount, sym) + '</td>'
                    + '<td class="text-end">';

                if (isRev) {
                    html += '<span class="badge text-bg-secondary">Reversed</span>';
                } else if (canReverse) {
                    html += '<button type="button" class="btn btn-sm btn-outline-danger bcc-reverse-pay" '
                         + 'data-id="' + parseInt(p.id, 10) + '" data-amount="' + h(p.amount) + '">'
                         + '<i class="bi bi-arrow-counterclockwise"></i> Reverse</button>';
                } else {
                    // Role gate: the button is not rendered at all rather than
                    // shown-and-rejected, so nobody clicks it expecting it to work.
                    html += '<span class="small text-muted">-</span>';
                }
                html += '</td></tr>';
            });
            html += '</tbody></table></div>';
        }

        document.getElementById('panePayments').innerHTML = html;

        const addBtn = document.getElementById('bccAddPaymentBtn');
        if (addBtn) { addBtn.addEventListener('click', function () { openPaymentModal(d); }); }

        document.querySelectorAll('.bcc-reverse-pay').forEach(function (b) {
            b.addEventListener('click', function () {
                openReversePayment(parseInt(b.getAttribute('data-id'), 10), b.getAttribute('data-amount'), d);
            });
        });
    }

    /* ---------- Record Payment modal ----------
       Mirrors the modal in payments.php: same fields, same locked ROE
       direction, same 3%-on-principal-only surcharge, same gratuity rules.
       Built dynamically and appended to document.body, like the rate override
       modal, so it cannot be trapped by a CSS stacking context on an ancestor.
    */
    function openPaymentModal(d) {
        const o = d.order;
        const targets = d.payment_targets || [];
        const currencies = d.currencies || [];
        const methods = d.payment_methods || [];
        const banks = d.bank_accounts || [];
        const posMachines = d.pos_machines || [];
        const surchargeRate = parseFloat(d.cc_surcharge_rate || 0.03);

        let targetOpts = '', lastGroup = null;
        targets.forEach(function (t) {
            if (t.group !== lastGroup) {
                if (lastGroup !== null && lastGroup !== '') targetOpts += '</optgroup>';
                if (t.group) targetOpts += '<optgroup label="' + h(t.group) + '">';
                lastGroup = t.group;
            }
            targetOpts += '<option value="' + h(t.value) + '" data-balance="' + h(String(t.balance)) + '">'
                        + h(t.label) + '</option>';
        });
        if (lastGroup) targetOpts += '</optgroup>';

        let curOpts = '';
        currencies.forEach(function (c) {
            curOpts += '<option value="' + parseInt(c.id, 10) + '" data-roe="' + h(String(c.roe))
                    + '" data-code="' + h(c.code) + '">' + h(c.code) + ' - ' + h(c.name) + '</option>';
        });

        let methodOpts = '<option value="">Select method</option>';
        methods.forEach(function (m) { methodOpts += '<option value="' + h(m) + '">' + h(m) + '</option>'; });

        let bankOpts = '<option value="0">No specific bank account</option>';
        banks.forEach(function (b) {
            bankOpts += '<option value="' + parseInt(b.ID, 10) + '">'
                     + h((b.Bank || '') + ' - ' + (b.AccountName || '') + ' (' + (b.AccountNo || '') + ')')
                     + '</option>';
        });

        let posOpts = '<option value="0">No specific POS machine</option>';
        posMachines.forEach(function (p) {
            posOpts += '<option value="' + parseInt(p.id, 10) + '">'
                    + h((p.machine_name || '') + (p.terminal_id ? ' (' + p.terminal_id + ')' : '') + ' - ' + (p.Bank || ''))
                    + '</option>';
        });

        const firstBal = targets.length ? parseFloat(targets[0].balance || 0) : 0;

        let el = document.getElementById('bccPayModal');
        if (el) el.remove();
        el = document.createElement('div');
        el.id = 'bccPayModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"><div class="modal-content">'
            + '<div class="modal-header"><div>'
            + '<h5 class="modal-title"><i class="bi bi-credit-card me-2"></i>Record Payment</h5>'
            + '<div class="text-muted small">Booking ' + h(o.order_no || ('#' + o.id)) + '</div></div>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>'
            + '<div class="modal-body">'
            + '<div id="bccPayError" class="alert alert-danger d-none py-2 small"></div>'

            + '<label class="form-label small fw-semibold text-uppercase">Post To</label>'
            + '<select id="bccPayTarget" class="form-select mb-1">' + targetOpts + '</select>'
            + '<div class="form-text mb-3">Post payment to Master, Room, or Individual Guest Folio.</div>'

            + '<div class="bcc-stat mb-3" style="background:#f8fafc;">'
            + '<div class="small"><strong>Selected:</strong> <span id="bccPayTargetName">'
            + h(targets.length ? targets[0].label : '') + '</span></div>'
            + '<div class="small"><strong>Balance Due:</strong> <span id="bccPayTargetBal">USD '
            + firstBal.toFixed(2) + '</span></div>'
            + '</div>'

            + '<div class="row g-3 mb-3">'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">Amount Entered</label>'
            + '<input type="number" step="0.01" min="0" id="bccPayAmount" class="form-control" value="' + firstBal.toFixed(2) + '">'
            + '<div class="form-text">Auto-filled from the selected folio balance. Override it for partial payments.</div></div>'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">Optional Gratuity</label>'
            + '<input type="number" step="0.01" min="0" id="bccPayGratuity" class="form-control" value="0.00">'
            + '<div class="form-text">Never surcharged, even on Card/POS.</div></div>'
            + '</div>'

            + '<div class="row g-3 mb-3">'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">Payment Date</label>'
            + '<input type="date" id="bccPayDate" class="form-control" value="' + h(d.today) + '"></div>'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">Payment Method</label>'
            + '<select id="bccPayMethod" class="form-select">' + methodOpts + '</select></div>'
            + '</div>'

            + '<div class="row g-3 mb-3 d-none" id="bccPayBankWrap">'
            + '<div class="col-12"><label class="form-label small fw-semibold text-uppercase">Bank Account</label>'
            + '<select id="bccPayBank" class="form-select">' + bankOpts + '</select></div></div>'

            + '<div class="row g-3 mb-3 d-none" id="bccPayPosWrap">'
            + '<div class="col-12"><label class="form-label small fw-semibold text-uppercase">POS Machine</label>'
            + '<select id="bccPayPos" class="form-select">' + posOpts + '</select></div></div>'

            + '<div class="row g-3 mb-3">'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">Currency</label>'
            + '<select id="bccPayCurrency" class="form-select">' + curOpts + '</select></div>'
            + '<div class="col-md-6"><label class="form-label small fw-semibold text-uppercase">ROE / Rate</label>'
            + '<input type="number" step="0.00001" min="0" id="bccPayRoe" class="form-control" value="1.00000">'
            + '<div class="form-text">Locked: Currency per 1 USD. Do not enter USD per currency.</div></div>'
            + '</div>'

            + '<div class="bcc-stat mb-3" style="background:#f1f5f9;">'
            + '<div class="fw-semibold mb-1">Exchange Preview</div>'
            + '<div class="small" id="bccPayPrev"></div>'
            + '</div>'

            + '<div class="alert alert-warning small py-2">'
            + '<strong>POS/Card surcharge:</strong> when the method is POS or Card the system posts a '
            + 'CC Charge at ' + (surchargeRate * 100).toFixed(0) + '% on the <strong>principal only</strong>. '
            + 'Optional gratuity is never surcharged.</div>'

            + '<label class="form-label small fw-semibold text-uppercase">Reference / Upload ID</label>'
            + '<input type="text" id="bccPayRef" class="form-control" placeholder="Receipt ref, bank ref or upload id">'
            + '</div>'
            + '<div class="modal-footer">'
            + '<button class="btn btn-light" data-bs-dismiss="modal">Cancel</button>'
            + '<button class="btn btn-primary" id="bccPaySave"><i class="bi bi-save me-1"></i>Save Payment</button>'
            + '</div></div></div>';
        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        const $ = function (id) { return document.getElementById(id); };

        function selectedCurrencyCode() {
            const opt = $('bccPayCurrency').options[$('bccPayCurrency').selectedIndex];
            return opt ? (opt.getAttribute('data-code') || 'USD') : 'USD';
        }

        /* The exchange preview. Same formula as the server:
           surcharge = principal x rate (Card/POS only)
           debit     = principal + gratuity + surcharge      (entered currency)
           USD       = debit / ROE                            (ROE = currency per 1 USD)
           Deliberately duplicated here for LIVE feedback only - the server
           recomputes it all before anything is written. */
        function preview() {
            const amt = parseFloat($('bccPayAmount').value || 0);
            const grat = parseFloat($('bccPayGratuity').value || 0);
            const roe = parseFloat($('bccPayRoe').value || 0);
            const method = $('bccPayMethod').value;
            const isCard = (method === 'POS' || method === 'Card');
            const code = selectedCurrencyCode();

            const sur = isCard ? Math.round(amt * surchargeRate * 100) / 100 : 0;
            const debit = Math.round((amt + grat + sur) * 100) / 100;
            const usd = roe > 0 ? Math.round((debit / roe) * 100) / 100 : 0;

            $('bccPayPrev').innerHTML =
                '1 USD = ' + (roe || 0).toFixed(5) + ' ' + h(code) + '<br>'
                + '<strong>Optional Gratuity:</strong> ' + grat.toFixed(2) + '<br>'
                + '<strong>CC Surcharge:</strong> ' + sur.toFixed(2) + '<br>'
                + '<strong>Amount to Debit Customer:</strong> ' + debit.toFixed(2) + ' ' + h(code) + '<br>'
                + '<strong>Effective USD Payment:</strong> ' + usd.toFixed(2);

            $('bccPayBankWrap').classList.toggle('d-none', method !== 'Bank Transfer');
            $('bccPayPosWrap').classList.toggle('d-none', !isCard);
        }

        $('bccPayTarget').addEventListener('change', function () {
            const opt = this.options[this.selectedIndex];
            const bal = parseFloat(opt.getAttribute('data-balance') || 0);
            $('bccPayTargetName').textContent = opt.textContent.trim();
            $('bccPayTargetBal').textContent = 'USD ' + bal.toFixed(2);
            $('bccPayAmount').value = bal.toFixed(2);
            preview();
        });
        $('bccPayCurrency').addEventListener('change', function () {
            const opt = this.options[this.selectedIndex];
            const roe = parseFloat(opt.getAttribute('data-roe') || 1);
            $('bccPayRoe').value = (roe > 0 ? roe : 1).toFixed(5);
            preview();
        });
        ['bccPayAmount', 'bccPayGratuity', 'bccPayRoe', 'bccPayMethod'].forEach(function (id) {
            $(id).addEventListener('input', preview);
            $(id).addEventListener('change', preview);
        });
        preview();

        $('bccPaySave').addEventListener('click', function () {
            const btn = this;
            const err = $('bccPayError');
            err.classList.add('d-none');

            const params = {
                api: 'save_payment',
                order_id: o.id,
                post_to: $('bccPayTarget').value,
                amount: $('bccPayAmount').value,
                gratuity_amount: $('bccPayGratuity').value,
                payment_date: $('bccPayDate').value,
                payment_method: $('bccPayMethod').value,
                currency: $('bccPayCurrency').value,
                roe_atm: $('bccPayRoe').value,
                bank_account_id: $('bccPayBank') ? $('bccPayBank').value : 0,
                pos_machine_id: $('bccPayPos') ? $('bccPayPos').value : 0,
                id_upload: $('bccPayRef').value
            };

            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving...';

            apiPost(params).then(function (r) {
                if (!r.ok) {
                    err.textContent = r.error || 'Payment failed.';
                    err.classList.remove('d-none');
                    btn.disabled = false;
                    btn.innerHTML = '<i class="bi bi-save me-1"></i>Save Payment';
                    return;
                }
                modal.hide();
                toast(r.message || 'Payment recorded.');
                openBooking(o.id);
            }).catch(function (e) {
                err.textContent = 'Payment failed: ' + e;
                err.classList.remove('d-none');
                btn.disabled = false;
                btn.innerHTML = '<i class="bi bi-save me-1"></i>Save Payment';
            });
        });
    }

    /* Reversal. A reason is mandatory - it lands on the folio line and in
       payment_audit_log alongside the user who did it. */
    function openReversePayment(paymentId, amount, d) {
        let el = document.getElementById('bccRevModal');
        if (el) el.remove();
        el = document.createElement('div');
        el.id = 'bccRevModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-dialog-centered"><div class="modal-content">'
            + '<div class="modal-header"><h5 class="modal-title text-danger">'
            + '<i class="bi bi-arrow-counterclockwise me-2"></i>Reverse Payment</h5>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>'
            + '<div class="modal-body">'
            + '<div id="bccRevError" class="alert alert-danger d-none py-2 small"></div>'
            + '<p class="mb-2">Reverse payment <strong>#' + paymentId + '</strong> of '
            + '<strong>USD ' + parseFloat(amount || 0).toFixed(2) + '</strong>?</p>'
            + '<div class="alert alert-info small py-2">Nothing is deleted. A reversal line is posted to the '
            + 'folio, so the original payment and its reversal both stay on the record. Your name is recorded '
            + 'against it.</div>'
            + '<label class="form-label small fw-semibold text-uppercase">Reason <span class="text-danger">*</span></label>'
            + '<textarea id="bccRevReason" class="form-control" rows="3" '
            + 'placeholder="Example: posted to the wrong booking / guest disputed the charge"></textarea>'
            + '</div>'
            + '<div class="modal-footer">'
            + '<button class="btn btn-light" data-bs-dismiss="modal">Cancel</button>'
            + '<button class="btn btn-danger" id="bccRevGo">Reverse Payment</button>'
            + '</div></div></div>';
        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        document.getElementById('bccRevGo').addEventListener('click', function () {
            const btn = this;
            const err = document.getElementById('bccRevError');
            const reason = document.getElementById('bccRevReason').value.trim();
            err.classList.add('d-none');
            if (!reason) {
                err.textContent = 'A reason is required to reverse a payment.';
                err.classList.remove('d-none');
                return;
            }
            const params = {
                api: 'reverse_payment',
                order_id: d.order.id,
                payment_id: paymentId,
                reason: reason
            };

            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Reversing...';

            apiPost(params).then(function (r) {
                if (!r.ok) {
                    err.textContent = r.error || 'Reversal failed.';
                    err.classList.remove('d-none');
                    btn.disabled = false;
                    btn.textContent = 'Reverse Payment';
                    return;
                }
                modal.hide();
                toast(r.message || 'Payment reversed.');
                openBooking(d.order.id);
            }).catch(function (e) {
                err.textContent = 'Reversal failed: ' + e;
                err.classList.remove('d-none');
                btn.disabled = false;
                btn.textContent = 'Reverse Payment';
            });
        });
    }

    /* ---------------------------------------------------------------------
       Cancel Booking. The "Cancel Booking" action button calls this. It opens
       a modal to capture the mandatory reason and the accounting policy, then
       POSTs to the cancel_booking_policy handler (which zeroes the booking,
       releases inventory, posts any cancellation folio lines and recalculates).
       Built in the same dynamic-modal style as openReversePayment().
       --------------------------------------------------------------------- */


    /* ---------------------------------------------------------------------
       Update guest name and meal plan WITHOUT recalculating saved rates.
       --------------------------------------------------------------------- */
    function openBasicDetailsModal() {
        if (!currentBooking || !currentBooking.order || !currentBooking.order.id) {
            toast('Open a booking first.', false);
            return;
        }

        const o = currentBooking.order;
        const orderId = parseInt(o.id, 10) || 0;
        const bookingLabel = o.order_no || ('#' + o.id);
        const currentAgentId = String(parseInt(o.agent_id || 0, 10) || 0);
        const currentContactId = String(parseInt(o.contact_id || 0, 10) || 0);

        let agentOptions = '<option value="0">Keep current agent</option>';
        EXTRA_PAYER_AGENTS.forEach(function (agent) {
            agentOptions += '<option value="' + h(agent.id) + '">'
                + h(agent.name || ('Agent #' + agent.id))
                + '</option>';
        });

        let el = document.getElementById('bccBasicDetailsModal');
        if (el) el.remove();

        el = document.createElement('div');
        el.id = 'bccBasicDetailsModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content">'
            + '<div class="modal-header">'
            + '<h5 class="modal-title"><i class="bi bi-person-vcard me-2"></i>Booking Details — '
            + h(String(bookingLabel)) + '</h5>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>'
            + '</div>'
            + '<div class="modal-body">'
            + '<div id="bccBasicDetailsError" class="alert alert-danger d-none py-2 small"></div>'
            + '<div class="row g-3">'
            + '<div class="col-md-7">'
            + '<label class="form-label fw-semibold">Guest name</label>'
            + '<input type="text" id="bccBasicGuestName" class="form-control" '
            + 'value="' + h(o.guest_name || '') + '" placeholder="Enter full guest name">'
            + '</div>'
            + '<div class="col-md-5">'
            + '<label class="form-label fw-semibold">Meal plan</label>'
            + '<select id="bccBasicMealPlan" class="form-select">'
            + '<option value="">Keep current meal plan</option>'
            + '<option value="RO">RO — Room Only</option>'
            + '<option value="BB">BB — Bed & Breakfast</option>'
            + '<option value="HB">HB — Half Board</option>'
            + '<option value="FB">FB — Full Board</option>'
            + '</select>'
            + '<div class="form-text">Current: ' + h(o.meal_plan || 'Not set') + '</div>'
            + '</div>'
            + '<div class="col-md-7">'
            + '<label class="form-label fw-semibold">Agent</label>'
            + '<select id="bccBasicAgent" class="form-select">' + agentOptions + '</select>'
            + '<div class="form-text">Current: ' + h(o.agent_name || 'Not assigned') + '</div>'
            + '</div>'
            + '<div class="col-md-5">'
            + '<label class="form-label fw-semibold">Agent consultant</label>'
            + '<select id="bccBasicConsultant" class="form-select">'
            + '<option value="0">No consultant</option>'
            + '</select>'
            + '</div>'
            + '<div class="col-12">'
            + '<div id="bccAgentRateChoice" class="border rounded-3 p-3 bg-light">'
            + '<div class="fw-semibold mb-2"><i class="bi bi-cash-coin me-1"></i>'
            + 'When the agent changes, should room rates change to the new agent contract?</div>'
            + '<div class="form-check">'
            + '<input class="form-check-input" type="radio" name="bccRepriceAgent" '
            + 'id="bccKeepAgentRates" value="0" checked>'
            + '<label class="form-check-label" for="bccKeepAgentRates">'
            + '<strong>No — keep the currently saved room rates</strong>'
            + '<div class="small text-muted">Only the agent and consultant will change.</div>'
            + '</label>'
            + '</div>'
            + '<div class="form-check mt-2">'
            + '<input class="form-check-input" type="radio" name="bccRepriceAgent" '
            + 'id="bccApplyAgentRates" value="1">'
            + '<label class="form-check-label" for="bccApplyAgentRates">'
            + '<strong>Yes — apply the new agent contract rates</strong>'
            + '<div class="small text-muted">Nightly rates and Master Folio accommodation charges will be rebuilt.</div>'
            + '</label>'
            + '</div>'
            + '</div>'
            + '</div>'
            + '</div>'
            + '</div>'
            + '<div class="modal-footer justify-content-between">'
            + '<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>'
            + '<button type="button" class="btn btn-primary" id="bccBasicDetailsSave">'
            + '<i class="bi bi-save me-1"></i>Save Changes</button>'
            + '</div></div></div>';

        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        const mealSelect = document.getElementById('bccBasicMealPlan');
        const agentSelect = document.getElementById('bccBasicAgent');
        const consultantSelect = document.getElementById('bccBasicConsultant');
        const rateChoice = document.getElementById('bccAgentRateChoice');

        const currentMeal = String(o.meal_plan || '').toUpperCase();
        if (['RO', 'BB', 'HB', 'FB'].indexOf(currentMeal) !== -1) {
            mealSelect.value = currentMeal;
        }

        if (currentAgentId !== '0') {
            agentSelect.value = currentAgentId;
        }

        function consultantLabel(contact) {
            const name = String(
                (contact.firstname || '') + ' ' + (contact.lastname || '')
            ).trim();
            const detail = contact.email || contact.mobile || contact.phone || '';
            return name !== ''
                ? (detail !== '' ? name + ' — ' + detail : name)
                : (detail !== '' ? detail : 'Consultant #' + contact.id);
        }

        function loadConsultants(agentId, selectedId) {
            const key = String(parseInt(agentId || 0, 10) || 0);
            const contacts = BOOKING_CONSULTANTS_BY_AGENT[key] || [];

            consultantSelect.innerHTML = '<option value="0">No consultant</option>';

            contacts.forEach(function (contact) {
                const option = document.createElement('option');
                option.value = String(contact.id);
                option.textContent = consultantLabel(contact);
                consultantSelect.appendChild(option);
            });

            consultantSelect.value = String(selectedId || '0');
            if (consultantSelect.value !== String(selectedId || '0')) {
                consultantSelect.value = '0';
            }
        }

        loadConsultants(currentAgentId, currentContactId);

        function updateRateChoice() {
            const changed = String(agentSelect.value || '0') !== currentAgentId
                && String(agentSelect.value || '0') !== '0';

            rateChoice.style.opacity = changed ? '1' : '.58';
            rateChoice.querySelectorAll('input').forEach(function (input) {
                input.disabled = !changed;
            });

            if (!changed) {
                document.getElementById('bccKeepAgentRates').checked = true;
            }
        }

        agentSelect.addEventListener('change', function () {
            const selectedAgentId = String(this.value || '0');
            loadConsultants(
                selectedAgentId === '0' ? currentAgentId : selectedAgentId,
                selectedAgentId === currentAgentId ? currentContactId : '0'
            );
            updateRateChoice();
        });

        updateRateChoice();

        document.getElementById('bccBasicDetailsSave').addEventListener('click', async function () {
            const btn = this;
            const err = document.getElementById('bccBasicDetailsError');
            const guestName = document.getElementById('bccBasicGuestName').value.trim();
            const mealPlan = mealSelect.value;
            const selectedAgentId = String(agentSelect.value || '0');
            const effectiveAgentId = selectedAgentId === '0'
                ? currentAgentId
                : selectedAgentId;
            const contactId = String(consultantSelect.value || '0');
            const repriceInput = document.querySelector(
                'input[name="bccRepriceAgent"]:checked'
            );
            const repriceForAgent = repriceInput ? repriceInput.value : '0';

            err.classList.add('d-none');
            err.textContent = '';

            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving...';

            try {
                const res = await apiPost({
                    api: 'update_basic_details',
                    order_id: orderId,
                    guest_name: guestName,
                    meal_plan: mealPlan,
                    agent_id: effectiveAgentId,
                    contact_id: contactId,
                    reprice_for_agent: repriceForAgent
                });

                if (!res.ok) {
                    err.textContent = res.error || 'Unable to update booking details.';
                    err.classList.remove('d-none');

                    if (res.saved) {
                        btn.innerHTML = '<i class="bi bi-arrow-clockwise me-1"></i>Retry Rate Update';
                    } else {
                        btn.innerHTML = '<i class="bi bi-save me-1"></i>Save Changes';
                    }

                    btn.disabled = false;
                    return;
                }

                modal.hide();
                toast(res.message || 'Booking details updated.');
                openBooking(orderId);
            } catch (e) {
                err.textContent = 'Update failed: ' + e;
                err.classList.remove('d-none');
                btn.disabled = false;
                btn.innerHTML = '<i class="bi bi-save me-1"></i>Save Changes';
            }
        });
    }

    /* ---------------------------------------------------------------------
       Reinstate Booking.

       First performs the server-side room-by-room availability check. The modal
       clearly lists every original room and any conflicting booking. Reinstate
       is enabled only when all original rooms are available.
       --------------------------------------------------------------------- */
    async function openReinstateModal() {
        if (!currentBooking || !currentBooking.order || !currentBooking.order.id) {
            toast('Open a booking first.', false);
            return;
        }

        const o = currentBooking.order;
        const orderId = parseInt(o.id, 10) || 0;
        const label = '#' + (o.order_no || o.id);

        let el = document.getElementById('bccReinstateModal');
        if (el) el.remove();

        el = document.createElement('div');
        el.id = 'bccReinstateModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content">'
            + '<div class="modal-header">'
            + '<h5 class="modal-title text-success"><i class="bi bi-arrow-counterclockwise me-2"></i>'
            + 'Reinstate Booking ' + h(String(label)) + '</h5>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>'
            + '</div>'
            + '<div class="modal-body">'
            + '<div id="bccReinstateError" class="alert alert-danger d-none py-2 small"></div>'
            + '<div class="alert alert-info py-2 small">'
            + '<i class="bi bi-search me-1"></i>Checking whether the original rooms are still available for the booking dates...'
            + '</div>'
            + '<div id="bccReinstateAvailability">'
            + '<div class="text-center py-4"><div class="spinner-border text-primary"></div>'
            + '<div class="small text-muted mt-2">Checking room availability...</div></div>'
            + '</div>'
            + '</div>'
            + '<div class="modal-footer justify-content-between">'
            + '<button type="button" class="btn btn-light" data-bs-dismiss="modal">Keep Cancelled</button>'
            + '<button type="button" class="btn btn-success" id="bccReinstateGo" disabled>'
            + '<i class="bi bi-arrow-counterclockwise me-1"></i>Reinstate Booking</button>'
            + '</div></div></div>';

        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        const errorBox = document.getElementById('bccReinstateError');
        const availabilityBox = document.getElementById('bccReinstateAvailability');
        const goBtn = document.getElementById('bccReinstateGo');

        let availability;
        try {
            availability = await apiGet({
                api: 'reinstate_check',
                order_id: orderId
            });
        } catch (e) {
            errorBox.textContent = 'Availability check failed: ' + e;
            errorBox.classList.remove('d-none');
            availabilityBox.innerHTML = '';
            return;
        }

        if (!availability || availability.ok === false) {
            errorBox.textContent = (availability && (availability.error || availability.message))
                ? (availability.error || availability.message)
                : 'Could not check room availability.';
            errorBox.classList.remove('d-none');
            availabilityBox.innerHTML = '';
            return;
        }

        const rooms = Array.isArray(availability.rooms) ? availability.rooms : [];
        let html = '<div class="mb-3"><strong>Stay:</strong> '
            + h(availability.check_in || String(o.check_in || '').substring(0, 10))
            + ' to '
            + h(availability.check_out || String(o.check_out || '').substring(0, 10))
            + '</div>';

        if (!rooms.length) {
            html += '<div class="alert alert-warning py-2 small">'
                + 'No rooming rows were found. The booking can be reinstated, but it will remain in the holding area.'
                + '</div>';
        } else {
            html += '<div class="table-responsive"><table class="table table-sm align-middle">'
                + '<thead><tr><th>Room</th><th>Room Type</th><th>Status</th><th>Conflict</th></tr></thead><tbody>';

            rooms.forEach(function (room) {
                const available = !!room.available;
                const clash = room.clash || null;
                let conflict = '';

                if (clash) {
                    conflict = h(clash.order_no || '')
                        + (clash.guest ? ' — ' + h(clash.guest) : '')
                        + '<div class="small text-muted">'
                        + h(clash.check_in || '') + ' to ' + h(clash.check_out || '')
                        + '</div>';
                } else {
                    conflict = '<span class="text-muted">None</span>';
                }

                html += '<tr>'
                    + '<td class="fw-semibold">' + h(room.room_no || 'Holding') + '</td>'
                    + '<td>' + h(room.room_type || '') + '</td>'
                    + '<td>'
                    + (available
                        ? '<span class="badge text-bg-success">Available</span>'
                        : '<span class="badge text-bg-danger">Occupied</span>')
                    + '</td>'
                    + '<td>' + conflict + '</td>'
                    + '</tr>';
            });

            html += '</tbody></table></div>';
        }

        if (availability.available) {
            html += '<div class="alert alert-success py-2 small mb-0">'
                + '<i class="bi bi-check-circle me-1"></i>'
                + 'All original rooms are available. The booking can be safely reinstated.'
                + '</div>';
            goBtn.disabled = false;
        } else {
            html += '<div class="alert alert-danger py-2 small mb-0">'
                + '<i class="bi bi-exclamation-triangle me-1"></i>'
                + 'The booking cannot be reinstated because one or more original rooms have been resold.'
                + '</div>';
            goBtn.disabled = true;
        }

        availabilityBox.innerHTML = html;

        goBtn.addEventListener('click', async function () {
            const btn = this;
            errorBox.classList.add('d-none');
            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Reinstating...';

            try {
                const res = await apiPost({
                    api: 'reinstate_booking',
                    order_id: orderId
                });

                if (!res.ok) {
                    errorBox.textContent = res.error || 'Reinstatement failed.';
                    errorBox.classList.remove('d-none');
                    btn.disabled = false;
                    btn.innerHTML = '<i class="bi bi-arrow-counterclockwise me-1"></i>Reinstate Booking';
                    return;
                }

                modal.hide();
                toast(res.message || 'Booking reinstated.');
                openBooking(orderId);
            } catch (e) {
                errorBox.textContent = 'Reinstatement failed: ' + e;
                errorBox.classList.remove('d-none');
                btn.disabled = false;
                btn.innerHTML = '<i class="bi bi-arrow-counterclockwise me-1"></i>Reinstate Booking';
            }
        });
    }

    function openCancellationModal() {
        if (!currentBooking || !currentBooking.order || !currentBooking.order.id) {
            toast('Open a booking first.', false);
            return;
        }
        const o = currentBooking.order;
        const orderId = o.id;
        const label = '#' + (o.order_no || o.id);

        let el = document.getElementById('bccCancelModal');
        if (el) el.remove();
        el = document.createElement('div');
        el.id = 'bccCancelModal';
        el.className = 'modal fade';
        el.innerHTML =
            '<div class="modal-dialog modal-dialog-centered"><div class="modal-content">'
            + '<div class="modal-header"><h5 class="modal-title text-danger">'
            + '<i class="bi bi-x-circle me-2"></i>Cancel Booking ' + h(String(label)) + '</h5>'
            + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>'
            + '<div class="modal-body">'
            + '<div id="bccCancelError" class="alert alert-danger d-none py-2 small"></div>'
            + '<div class="alert alert-warning small py-2"><i class="bi bi-exclamation-triangle me-1"></i>'
            + 'This cancels the booking and releases its rooms back to availability. Nothing is deleted - '
            + 'the booking is retained as cancelled and can be reinstated later.</div>'
            + '<label class="form-label small fw-semibold text-uppercase">Reason <span class="text-danger">*</span></label>'
            + '<textarea id="bccCancelReason" class="form-control mb-3" rows="2" '
            + 'placeholder="Example: guest cancelled / no-show / duplicate booking"></textarea>'
            + '<label class="form-label small fw-semibold text-uppercase">Accounting policy</label>'
            + '<select id="bccCancelPolicy" class="form-select mb-3">'
            + '<option value="keep_balance">Keep balance as-is (no adjustment)</option>'
            + '<option value="waive_all">Waive the full outstanding balance</option>'
            + '<option value="waive_and_charge_policy">Waive part &amp; charge a cancellation fee</option>'
            + '</select>'
            + '<div id="bccCancelPolicyFields" class="d-none">'
            + '<div class="row g-2">'
            + '<div class="col-6"><label class="form-label small">Waive amount</label>'
            + '<input type="number" step="0.01" min="0" id="bccCancelAdjust" class="form-control" value="0"></div>'
            + '<div class="col-6"><label class="form-label small">Cancellation fee</label>'
            + '<input type="number" step="0.01" min="0" id="bccCancelCharge" class="form-control" value="0"></div>'
            + '</div>'
            + '<div class="form-text">The fee is posted to the master folio; the waive amount reduces the outstanding balance.</div>'
            + '</div>'
            + '</div>'
            + '<div class="modal-footer justify-content-between">'
            + '<button class="btn btn-light" data-bs-dismiss="modal">Keep Booking</button>'
            + '<button class="btn btn-danger" id="bccCancelGo"><i class="bi bi-x-circle me-1"></i>Cancel Booking</button>'
            + '</div></div></div>';
        document.body.appendChild(el);

        const modal = new bootstrap.Modal(el);
        modal.show();

        // Show the waive/fee inputs only for the policy that needs them.
        const policySel = document.getElementById('bccCancelPolicy');
        const policyFields = document.getElementById('bccCancelPolicyFields');
        policySel.addEventListener('change', function () {
            if (policySel.value === 'waive_and_charge_policy') {
                policyFields.classList.remove('d-none');
            } else {
                policyFields.classList.add('d-none');
            }
        });

        document.getElementById('bccCancelGo').addEventListener('click', function () {
            const btn = this;
            const err = document.getElementById('bccCancelError');
            const reason = document.getElementById('bccCancelReason').value.trim();
            err.classList.add('d-none');
            if (!reason) {
                err.textContent = 'A cancellation reason is required.';
                err.classList.remove('d-none');
                return;
            }
            const params = {
                api: 'cancel_booking_policy',
                order_id: orderId,
                reason: reason,
                policy_action: policySel.value,
                adjustment: document.getElementById('bccCancelAdjust').value || 0,
                policy_charge: document.getElementById('bccCancelCharge').value || 0
            };

            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Cancelling...';

            apiPost(params).then(function (r) {
                if (!r.ok) {
                    err.textContent = r.error || 'Cancellation failed.';
                    err.classList.remove('d-none');
                    btn.disabled = false;
                    btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>Cancel Booking';
                    return;
                }
                modal.hide();
                toast(r.message || 'Booking cancelled.');
                openBooking(orderId);
            }).catch(function (e) {
                err.textContent = 'Cancellation failed: ' + e;
                err.classList.remove('d-none');
                btn.disabled = false;
                btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>Cancel Booking';
            });
        });
    }

    function renderInvoicing(d) {
        const o = d.order;
        const P = '/rooms/modules/payments/';
        document.getElementById('paneInvoicing').innerHTML =
            '<h6 class="mb-2">Proformas &amp; Invoices</h6>'
            + '<div class="d-flex flex-wrap gap-2 mb-3">'
            + '<a class="btn btn-outline-dark btn-sm bcc-actionbtn" href="' + P + 'proforma_invoice.php?order_id=' + o.id + '" target="_blank"><i class="bi bi-file-earmark-text me-1"></i>Proforma Invoice</a>'
            + '<a class="btn btn-outline-dark btn-sm bcc-actionbtn" href="' + P + 'proforma_invoice.php?order_id=' + o.id + '" target="_blank"><i class="bi bi-journal-text me-1"></i>Master Folio Proforma</a>'
            + '<a class="btn btn-outline-dark btn-sm bcc-actionbtn" href="' + P + 'proforma_room_account_extra.php?order_id=' + o.id + '" target="_blank"><i class="bi bi-door-open me-1"></i>Room Account Proforma</a>'
            + '</div>'
            + '<h6 class="mb-2">Email</h6>'
            + '<div class="d-flex flex-wrap gap-2 mb-3">'
            + '<a class="btn btn-outline-primary btn-sm bcc-actionbtn" href="' + P + 'email_proforma.php?order_id=' + o.id + '&type=standard" target="_blank"><i class="bi bi-envelope me-1"></i>Email Proforma</a>'
            + '<a class="btn btn-outline-primary btn-sm bcc-actionbtn" href="' + P + 'email_proforma.php?order_id=' + o.id + '&type=master" target="_blank"><i class="bi bi-envelope-paper me-1"></i>Email Master Folio</a>'
            + '</div>'
            + '<h6 class="mb-2">Money</h6>'
            + '<div class="d-flex flex-wrap gap-2">'
            + '<a class="btn btn-success btn-sm bcc-actionbtn" href="' + P + 'payments.php?order_id=' + o.id + '"><i class="bi bi-cash-coin me-1"></i>Payments</a>'
            + '<a class="btn btn-outline-secondary btn-sm bcc-actionbtn" href="' + P + 'extras.php?order_id=' + o.id + '"><i class="bi bi-plus-circle me-1"></i>Extras &amp; Folios</a>'
            + '</div>'
            + '<div class="text-muted small mt-3">Proformas break accommodation down per room per night. Documents open in a new tab.</div>';
    }

    function renderColour(d) {
        const o = d.order, colours = d.colours || [];
        let sw = colours.map(function (c) {
            const sel = (parseInt(o.booking_color_code_id || 0, 10) === parseInt(c.id, 10)) ? ' sel' : '';
            return '<div class="text-center">'
                + '<div class="bcc-swatch' + sw_(sel) + '" title="' + h(c.status_label) + '" data-cid="' + c.id + '" style="background:' + h(c.hex_color) + '"></div>'
                + '<div class="small text-muted mt-1" style="max-width:70px">' + h(c.status_label) + '</div></div>';
        }).join('');
        function sw_(x){return x;}
        let html = '<h6 class="mb-2">Booking Colour</h6>'
            + '<div class="d-flex flex-wrap gap-3 mb-3">' + (sw || '<span class="text-muted">No colour codes defined.</span>') + '</div>'
            + '<hr><h6 class="mb-2">Status</h6>'
            + '<div class="bcc-quick d-flex flex-wrap gap-2">' + quickStatusButtons(o, d.today) + '</div>';
        document.getElementById('paneColour').innerHTML = html;

        document.getElementById('paneColour').querySelectorAll('.bcc-swatch').forEach(function (el) {
            el.addEventListener('click', async function () {
                const cid = el.getAttribute('data-cid');
                const res = await apiPost({ api: 'set_colour', order_id: o.id, colour_id: cid });
                toast(res.message || res.error, res.ok);
                if (res.ok) openBooking(o.id);
            });
        });
    }

    function renderComms(d) {
        const o = d.order;
        document.getElementById('paneComms').innerHTML =
            '<h6 class="mb-2">Communications</h6>'
            + '<div class="d-flex flex-wrap gap-2 mb-3">'
            + '<a class="btn btn-outline-primary btn-sm bcc-actionbtn" href="/rooms/modules/payments/email_proforma.php?order_id=' + o.id + '&type=standard" target="_blank"><i class="bi bi-envelope me-1"></i>Email Proforma</a>'
            + (o.guest_email ? '<a class="btn btn-outline-dark btn-sm bcc-actionbtn" href="mailto:' + h(o.guest_email) + '"><i class="bi bi-at me-1"></i>Email Guest</a>' : '')
            + '</div>'
            + '<label class="form-label fw-semibold small">Internal note / remark</label>'
            + '<textarea id="bccNote" class="form-control mb-2" rows="14" style="min-height:320px;font-size:.95rem;line-height:1.6;" placeholder="Add an internal note for this booking…">' + h(o.res_notes || o.reservation_notes || o.notes || o.remarks || '') + '</textarea>'
            + '<button id="bccSaveNote" class="btn btn-primary btn-sm bcc-actionbtn"><i class="bi bi-save me-1"></i>Save Note</button>';

        const btn = document.getElementById('bccSaveNote');
        if (btn) btn.addEventListener('click', async function () {
            const res = await apiPost({ api: 'save_note', order_id: o.id, note: document.getElementById('bccNote').value });
            toast(res.message || res.error, res.ok);
        });
    }

    function auditIcon(action) {
        const a = S(action).toLowerCase();
        if (a.indexOf('payment') >= 0) return ['bi-cash-coin', '#16a34a'];
        if (a.indexOf('void') >= 0) return ['bi-slash-circle', '#dc2626'];
        if (a.indexOf('charge') >= 0) return ['bi-receipt', '#2563eb'];
        if (a.indexOf('cancel') >= 0) return ['bi-x-circle', '#dc2626'];
        if (a.indexOf('created') >= 0) return ['bi-plus-circle', '#0ea5e9'];
        if (a.indexOf('updated') >= 0) return ['bi-pencil', '#6b7280'];
        if (a.indexOf('checked_in') >= 0 || a.indexOf('check_in') >= 0) return ['bi-box-arrow-in-right', '#22c55e'];
        if (a.indexOf('checked_out') >= 0 || a.indexOf('check_out') >= 0) return ['bi-box-arrow-right', '#3b82f6'];
        if (a.indexOf('colour') >= 0 || a.indexOf('color') >= 0) return ['bi-palette', '#a855f7'];
        if (a.indexOf('status') >= 0) return ['bi-flag', '#f59e0b'];
        if (a.indexOf('note') >= 0) return ['bi-sticky', '#64748b'];
        return ['bi-dot', '#94a3b8'];
    }

    function renderTimeline(d) {
        const rows = d.timeline || [];
        let html = '<div class="d-flex justify-content-between align-items-center mb-3">'
            + '<h6 class="mb-0">Full Audit Trail</h6>'
            + '<span class="badge text-bg-light">' + rows.length + ' event' + (rows.length === 1 ? '' : 's') + '</span></div>';

        if (!rows.length) {
            html += '<div class="text-muted">No recorded activity for this booking.</div>';
        } else {
            html += '<div style="position:relative;padding-left:8px;">';
            rows.forEach(function (r) {
                const ic = auditIcon(r.action);
                const when = S(r.when);
                const day = when.substring(0, 10);
                const time = when.length > 11 ? when.substring(11, 16) : '';
                html += '<div class="d-flex gap-3 pb-3" style="border-left:2px solid #e5e7eb;margin-left:10px;padding-left:16px;position:relative;">'
                    + '<span style="position:absolute;left:-9px;top:2px;width:16px;height:16px;border-radius:50%;background:#fff;'
                    + 'border:2px solid ' + ic[1] + ';display:flex;align-items:center;justify-content:center;"></span>'
                    + '<div class="flex-grow-1 min-w-0">'
                    + '<div class="d-flex justify-content-between align-items-start gap-2">'
                    + '<span class="fw-semibold small" style="color:' + ic[1] + '"><i class="bi ' + ic[0] + ' me-1"></i>'
                    + h(pretty(r.action)) + '</span>'
                    + '<span class="small text-muted text-nowrap">' + h(fmtDate(day)) + (time ? ' · ' + h(time) : '') + '</span>'
                    + '</div>'
                    + '<div class="small text-muted">' + h(r.detail || '') + '</div>'
                    + '<div class="small" style="color:#9ca3af">'
                    + (r.who ? '<i class="bi bi-person me-1"></i>' + h(r.who) : '')
                    + (r.source ? ' <span class="badge text-bg-light ms-1" style="font-size:.62rem">' + h(r.source) + '</span>' : '')
                    + '</div>'
                    + '</div></div>';
            });
            html += '</div>';
        }
        document.getElementById('paneTimeline').innerHTML = html;
    }

    /* ---------- Delegated actions ---------- */
    document.addEventListener('click', async function (e) {
        const basicDetailsBtn = e.target.closest('.bcc-open-basic-details');
        if (basicDetailsBtn) {
            e.preventDefault();
            openBasicDetailsModal();
            return;
        }

        const reinstateBtn = e.target.closest('.bcc-open-reinstate');
        if (reinstateBtn) {
            e.preventDefault();
            openReinstateModal();
            return;
        }

        const cancelBtn = e.target.closest('.bcc-open-cancel');
        if (cancelBtn) {
            e.preventDefault();
            openCancellationModal();
            return;
        }
        const sbtn = e.target.closest('.bcc-status-btn');
        if (sbtn && currentBooking) {
            const status = sbtn.getAttribute('data-status');
            const label = S(sbtn.textContent).trim();

            if (status === 'confirmed') {
                const bookingNo = S(currentBooking.order.order_no || ('#' + currentBooking.order.id));
                const guestName = S(currentBooking.order.guest_name || 'this guest');
                if (!confirm(
                    'Confirm booking ' + bookingNo + ' for ' + guestName + '?\n\n'
                    + 'This will change the reservation status to Confirmed and record the confirmation date and user in the audit trail.'
                )) return;
            }

            if (label.toLowerCase().indexOf('late') === 0 && !confirm(label + '?\n\nThis is outside the scheduled day — continue?')) return;
            const res = await apiPost({ api: 'set_status', order_id: currentBooking.order.id, status: status });
            toast(res.message || res.error, res.ok);
            if (res.ok) openBooking(currentBooking.order.id);
            return;
        }
        const rbtn = e.target.closest('.bcc-save-rooming-row');
        if (rbtn && currentBooking) {
            await saveRoomingRow(rbtn.closest('.bcc-rooming-row'), true);
            return;
        }
        const delRoomBtn = e.target.closest('.bcc-del-rooming-row');
        if (delRoomBtn && currentBooking) {
            /*
               A booking must always keep at least one room. Catch it here too so
               the user gets the right guidance instantly rather than a rejection
               after the round trip. The server enforces this regardless.
            */
            const totalRooms = document.querySelectorAll('.bcc-rooming-row').length;
            if (totalRooms <= 1) {
                alert('You cannot remove the only room on this booking.\n\n'
                    + 'A booking must have at least one room. If this reservation is no longer required, '
                    + 'cancel the entire booking instead — use "Cancel Booking" on the Overview tab, '
                    + 'which applies your cancellation policy and releases the room.');
                return;
            }
            const row = delRoomBtn.closest('.bcc-rooming-row');
            const roomSel = row ? row.querySelector('.bcc-r-room') : null;
            const label = roomSel && roomSel.selectedIndex >= 0
                ? roomSel.options[roomSel.selectedIndex].text
                : 'this room';
            if (!confirm('Remove ' + label + ' from this booking?\n\nThe room slot and its allocation will be deleted, and the booking totals recalculated. This cannot be undone.')) return;
            const res = await apiPost({
                api: 'delete_rooming_row',
                order_id: currentBooking.order.id,
                row_id: row.getAttribute('data-row-id')
            });
            toast(res.message || res.error, res.ok);
            if (res.ok) openBooking(currentBooking.order.id);
            return;
        }
        const vbtn = e.target.closest('.bcc-void-btn');
        if (vbtn && currentBooking) {
            if (!confirm('Void this extra line?')) return;
            const res = await apiPost({ api: 'void_line', order_id: currentBooking.order.id, line_id: vbtn.getAttribute('data-line') });
            toast(res.message || res.error, res.ok);
            if (res.ok) {
                try {
                    await refreshExtrasTab();
                } catch (refreshError) {
                    toast(refreshError.message || 'Extra voided, but the list could not be refreshed.', false);
                }
            }
            return;
        }
    });

    /* ---------- Wire search ---------- */
    document.getElementById('bccSearchBtn').addEventListener('click', runSearch);
    document.getElementById('bccQ').addEventListener('keydown', function (e) { if (e.key === 'Enter') runSearch(); });
    ['bccAgent', 'bccStatus', 'bccDateField'].forEach(function (id) {
        const el = document.getElementById(id);
        if (el) el.addEventListener('change', runSearch);
    });

    /* Preselect if arriving with ?id= */
    const preId = <?php echo (int)$preselectId; ?>;
    if (preId > 0) {
        openBooking(preId);
        runSearch();
    } else {
        runSearch();
    }
})();
</script>

<?php
/* Same contract selector / rate application tool used by reservation_form.php. */
$applyRateWidget = __DIR__ . '/apply_rate_widget.php';
if (is_file($applyRateWidget)) {
    require_once $applyRateWidget;
}
?>

<?php require_once __DIR__ . '/../../includes/footer.php'; ?>
