<?php
//cal.php
require_once __DIR__ . '/../../includes/bootstrap.php';
require_login();

$pageTitle = 'Timeline Planner';

if (file_exists(__DIR__ . '/waitlist_support.php')) {
    require_once __DIR__ . '/waitlist_support.php';
}

if (!function_exists('planner_e')) {
    function planner_e($value)
    {
        return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
    }
}

if (!function_exists('planner_display_date')) {
    function planner_display_date($value, $format = 'd M Y')
    {
        if (!$value) {
            return '';
        }
        $ts = strtotime((string)$value);
        return $ts ? date($format, $ts) : (string)$value;
    }
}

if (!function_exists('planner_table_exists')) {
    function planner_table_exists(PDO $pdo, string $table): bool
    {
        static $cache = [];
        $key = strtolower($table);

        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }

        try {
            $stmt = $pdo->prepare("
                SELECT COUNT(*)
                FROM INFORMATION_SCHEMA.TABLES
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = :table_name
            ");
            $stmt->execute([':table_name' => $table]);
            $cache[$key] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) {
            $cache[$key] = false;
        }

        return $cache[$key];
    }
}

if (!function_exists('planner_column_exists')) {
    function planner_column_exists(PDO $pdo, string $table, string $column): bool
    {
        static $cache = [];
        $key = strtolower($table . '.' . $column);

        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }

        try {
            $stmt = $pdo->prepare("
                SELECT COUNT(*)
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = :table_name
                  AND COLUMN_NAME = :column_name
            ");
            $stmt->execute([
                ':table_name' => $table,
                ':column_name' => $column,
            ]);
            $cache[$key] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) {
            $cache[$key] = false;
        }

        return $cache[$key];
    }
}


if (!function_exists('planner_today_date')) {
    function planner_today_date(): string
    {
        return date('Y-m-d');
    }
}

if (!function_exists('planner_current_user_id')) {
    function planner_current_user_id(): ?int
    {
        foreach (['user_id','ss_user_id','id','uid','staffid'] as $key) {
            if (!empty($_SESSION[$key]) && (int)$_SESSION[$key] > 0) {
                return (int)$_SESSION[$key];
            }
        }
        return null;
    }
}

if (!function_exists('planner_recalc_folio')) {
    function planner_recalc_folio(PDO $pdo, int $folioId): void
    {
        if ($folioId <= 0 || !planner_table_exists($pdo, 'folios') || !planner_table_exists($pdo, 'folio_transactions')) {
            return;
        }

        $stmt = $pdo->prepare("
            SELECT
                COALESCE(SUM(CASE
                    WHEN COALESCE(is_voided,0)=0
                     AND LOWER(COALESCE(tx_type,'')) NOT IN ('payment','receipt','deposit','payment_received','folio_payment','guest_payment','payment_void','void_payment','payment_reversal','reversal','refund','payment_refund')
                    THEN COALESCE(debit,0) ELSE 0 END),0) AS charges,
                COALESCE(SUM(CASE
                    WHEN COALESCE(is_voided,0)=0
                     AND LOWER(COALESCE(tx_type,'')) IN ('payment','receipt','deposit','payment_received','folio_payment','guest_payment')
                    THEN COALESCE(credit,0) ELSE 0 END),0) AS payments
            FROM folio_transactions
            WHERE folio_id = :folio_id
        ");
        $stmt->execute([':folio_id' => $folioId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $charges = (float)($row['charges'] ?? 0);
        $payments = (float)($row['payments'] ?? 0);

        $sets = [];
        $params = [':folio_id' => $folioId];

        foreach ([
            'total_charges' => ':charges',
            'charges' => ':charges',
            'total_payments' => ':payments',
            'payments' => ':payments',
            'balance' => ':balance',
        ] as $col => $param) {
            if (planner_column_exists($pdo, 'folios', $col)) {
                $sets[] = "`{$col}` = {$param}";
            }
        }

        if (planner_column_exists($pdo, 'folios', 'updated_at')) {
            $sets[] = "updated_at = NOW()";
        }

        if ($sets) {
            $params[':charges'] = $charges;
            $params[':payments'] = $payments;
            $params[':balance'] = $charges - $payments;
            $pdo->prepare("UPDATE folios SET " . implode(', ', $sets) . " WHERE id = :folio_id")->execute($params);
        }
    }
}

if (!function_exists('planner_sync_order_payment_status')) {
    function planner_sync_order_payment_status(PDO $pdo, int $orderId): void
    {
        if ($orderId <= 0 || !planner_table_exists($pdo, 'folios') || !planner_table_exists($pdo, 'orders')) {
            return;
        }

        $chargesExpr = '0';
        if (planner_column_exists($pdo, 'folios', 'total_charges')) {
            $chargesExpr = 'COALESCE(total_charges,0)';
        } elseif (planner_column_exists($pdo, 'folios', 'charges')) {
            $chargesExpr = 'COALESCE(charges,0)';
        }

        $paymentsExpr = '0';
        if (planner_column_exists($pdo, 'folios', 'total_payments')) {
            $paymentsExpr = 'COALESCE(total_payments,0)';
        } elseif (planner_column_exists($pdo, 'folios', 'payments')) {
            $paymentsExpr = 'COALESCE(payments,0)';
        }

        $balanceExpr = '0';
        if (planner_column_exists($pdo, 'folios', 'balance')) {
            $balanceExpr = 'COALESCE(balance,0)';
        }

        $stmt = $pdo->prepare("
            SELECT
                COALESCE(SUM({$chargesExpr}), 0) AS total_charges,
                COALESCE(SUM({$paymentsExpr}), 0) AS total_payments,
                COALESCE(SUM({$balanceExpr}), 0) AS total_balance
            FROM folios
            WHERE order_id = :order_id
        ");
        $stmt->execute([':order_id' => $orderId]);

        $folio = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $charges  = (float)($folio['total_charges'] ?? 0);
        $payments = (float)($folio['total_payments'] ?? 0);
        $balance  = (float)($folio['total_balance'] ?? 0);

        if ($charges <= 0.00001 || $payments <= 0.00001) {
            $status = 0;
        } elseif ($balance <= 0.00001) {
            $status = 2;
        } else {
            $status = 1;
        }

        if (planner_column_exists($pdo, 'orders', 'payment_status')) {
            $stmt = $pdo->prepare("
                UPDATE orders
                SET payment_status = :payment_status
                WHERE id = :id
            ");
            $stmt->execute([
                ':payment_status' => $status,
                ':id' => $orderId,
            ]);
        }
    }
}

if (!function_exists('planner_write_payment_audit_log_safe')) {
    function planner_write_payment_audit_log_safe(PDO $pdo, int $orderId, string $actionType, string $reason, array $payload): void
    {
        if (!planner_table_exists($pdo, 'payment_audit_log')) {
            return;
        }

        $cols = ['order_id', 'action_type', 'action_reason', 'payload_json'];
        $vals = [':order_id', ':action_type', ':action_reason', ':payload_json'];
        $params = [
            ':order_id' => $orderId,
            ':action_type' => $actionType,
            ':action_reason' => $reason,
            ':payload_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
        ];

        if (planner_column_exists($pdo, 'payment_audit_log', 'action_by')) {
            $cols[] = 'action_by';
            $vals[] = ':action_by';
            $params[':action_by'] = planner_current_user_id();
        }
        if (planner_column_exists($pdo, 'payment_audit_log', 'action_at')) {
            $cols[] = 'action_at';
            $vals[] = 'NOW()';
        }

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

if (!function_exists('planner_sync_source_folio_reference_if_present')) {
    function planner_sync_source_folio_reference_if_present(PDO $pdo, array $tx, int $newFolioId): void
    {
        $sourceTable = trim((string)($tx['source_table'] ?? ''));
        $sourceId = (int)($tx['source_id'] ?? 0);
        if ($sourceTable === '' || $sourceId <= 0) {
            return;
        }

        $allowed = ['payment', 'payments', 'extras', 'folio_extras', 'order_extras', 'tblbookingdetail', 'pos_orders', 'pos_order_items', 'restaurant_orders', 'restaurant_order_items'];
        if (!in_array($sourceTable, $allowed, true)) {
            return;
        }
        if (!planner_table_exists($pdo, $sourceTable) || !planner_column_exists($pdo, $sourceTable, 'folio_id')) {
            return;
        }

        $stmt = $pdo->prepare("UPDATE `{$sourceTable}` SET folio_id = :folio_id WHERE id = :id");
        $stmt->execute([':folio_id' => $newFolioId, ':id' => $sourceId]);
    }
}

if (!function_exists('planner_move_folio_transaction')) {
if (!function_exists('planner_move_folio_transaction')) {
    function planner_move_folio_transaction(PDO $pdo, int $orderId, int $transactionId, int $targetFolioId, string $reason): void
    {
        if ($orderId <= 0) {
            throw new RuntimeException('Invalid booking selected.');
        }
        if ($transactionId <= 0) {
            throw new RuntimeException('Invalid transaction selected.');
        }
        if ($targetFolioId <= 0) {
            throw new RuntimeException('Please select the destination folio.');
        }
        if (trim($reason) === '') {
            throw new RuntimeException('Please enter a reason for moving this transaction.');
        }
        if (!planner_table_exists($pdo, 'folio_transactions') || !planner_table_exists($pdo, 'folios')) {
            throw new RuntimeException('Folio transaction tables were not found.');
        }

        $stmt = $pdo->prepare("
            SELECT
                ft.*,
                f.folio_name AS old_folio_name,
                f.folio_type AS old_folio_type,
                f.order_id AS old_order_id
            FROM folio_transactions ft
            INNER JOIN folios f ON f.id = ft.folio_id
            WHERE ft.id = :id
              AND f.order_id = :order_id
            LIMIT 1
            FOR UPDATE
        ");
        $stmt->execute([
            ':id' => $transactionId,
            ':order_id' => $orderId,
        ]);

        $tx = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$tx) {
            throw new RuntimeException('Selected folio transaction was not found for this booking.');
        }

        $oldFolioId = (int)($tx['folio_id'] ?? 0);

        if ($oldFolioId === $targetFolioId) {
            throw new RuntimeException('The selected transaction is already on that folio.');
        }

        if (!empty($tx['is_voided'])) {
            throw new RuntimeException('Voided transactions cannot be moved.');
        }

        $txType = strtolower(trim((string)($tx['tx_type'] ?? '')));
        if ($txType === 'room_charge') {
            throw new RuntimeException('Room charges cannot be moved here. Rebuild or correct room allocations instead.');
        }

        $stmt = $pdo->prepare("
            SELECT *
            FROM folios
            WHERE id = :folio_id
              AND order_id = :order_id
              AND COALESCE(status,1) = 1
            LIMIT 1
            FOR UPDATE
        ");
        $stmt->execute([
            ':folio_id' => $targetFolioId,
            ':order_id' => $orderId,
        ]);

        $targetFolio = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$targetFolio) {
            throw new RuntimeException('Destination folio does not belong to this booking or is inactive.');
        }

        /*
           Important HY093 fix:
           Do not reuse the same named placeholder twice in this UPDATE.
           Some PDO MySQL setups throw:
           SQLSTATE[HY093]: Invalid parameter number
           when :order_id appears in both SET and WHERE.
        */
        $sets = ['folio_id = :new_folio_id'];
        $params = [
            ':new_folio_id' => $targetFolioId,
            ':id' => $transactionId,
        ];

        if (planner_column_exists($pdo, 'folio_transactions', 'order_id')) {
            $sets[] = 'order_id = :set_order_id';
            $params[':set_order_id'] = $orderId;
        }

        $where = 'id = :id';

        if (planner_column_exists($pdo, 'folio_transactions', 'order_id')) {
            $where .= ' AND order_id = :where_order_id';
            $params[':where_order_id'] = $orderId;
        }

        $stmt = $pdo->prepare(
            'UPDATE folio_transactions SET ' . implode(', ', $sets) . ' WHERE ' . $where
        );
        $stmt->execute($params);

        planner_sync_source_folio_reference_if_present($pdo, $tx, $targetFolioId);

        planner_recalc_folio($pdo, $oldFolioId);
        planner_recalc_folio($pdo, $targetFolioId);
        planner_sync_order_payment_status($pdo, $orderId);

        planner_write_payment_audit_log_safe($pdo, $orderId, 'move_folio_transaction', $reason, [
            'transaction_id' => $transactionId,
            'tx_type' => $tx['tx_type'] ?? '',
            'description' => $tx['description'] ?? '',
            'debit' => $tx['debit'] ?? 0,
            'credit' => $tx['credit'] ?? 0,
            'source_table' => $tx['source_table'] ?? '',
            'source_id' => $tx['source_id'] ?? '',
            'from_folio_id' => $oldFolioId,
            'from_folio_name' => $tx['old_folio_name'] ?? '',
            'to_folio_id' => $targetFolioId,
            'to_folio_name' => $targetFolio['folio_name'] ?? '',
        ]);
    }
}
}


if (!function_exists('planner_value_matches_pin')) {
    function planner_value_matches_pin(string $stored, string $pin): bool
    {
        $stored = trim($stored);
        $pin = trim($pin);
        if ($stored === '' || $pin === '') return false;
        if (hash_equals($stored, $pin)) return true;
        if (strlen($stored) >= 50 && function_exists('password_verify') && password_verify($pin, $stored)) return true;
        if (hash_equals(strtolower($stored), md5($pin))) return true;
        if (hash_equals(strtolower($stored), sha1($pin))) return true;
        return false;
    }
}

if (!function_exists('planner_role_is_admin_or_above')) {
    function planner_role_is_admin_or_above(string $role): bool
    {
        $role = strtolower(trim($role));
        if ($role === '') return false;
        foreach (['super_admin','superadmin','administrator','admin','master','owner','director','manager'] as $allowed) {
            if ($role === $allowed || strpos($role, $allowed) !== false) return true;
        }
        return false;
    }
}

if (!function_exists('planner_current_role_key_safe')) {
    function planner_current_role_key_safe(): string
    {
        foreach (['role_key','user_role_key','ss_role_key','role','user_role','type','Role'] as $k) {
            if (!empty($_SESSION[$k])) return (string)$_SESSION[$k];
        }
        return '';
    }
}

if (!function_exists('planner_admin_pin_valid')) {
    function planner_admin_pin_valid(PDO $pdo, string $pin): bool
    {
        $pin = trim($pin);
        if ($pin === '') return false;

        $candidateTables = ['tblconsultant','consultants','users','system_users'];
        $nameCols = ['Username','username','user_name','email','Email','firstname','name'];
        $pinCols = ['admin_pin','pin','auth_pin','manager_pin','void_pin','password','Password','user_password','password_hash'];
        $roleCols = ['role_key','user_role_key','ss_role_key','role','user_role','type','Role'];
        $activeCols = ['is_active','active','status'];

        foreach ($candidateTables as $table) {
            if (!planner_table_exists($pdo, $table)) continue;

            $existingPinCols = [];
            foreach ($pinCols as $c) if (planner_column_exists($pdo, $table, $c)) $existingPinCols[] = $c;
            if (!$existingPinCols) continue;

            $select = ['*'];
            $sql = "SELECT " . implode(',', $select) . " FROM `{$table}` WHERE 1=1";
            foreach ($activeCols as $c) {
                if (planner_column_exists($pdo, $table, $c)) {
                    if ($c === 'status') $sql .= " AND LOWER(CAST(`{$c}` AS CHAR)) NOT IN ('0','inactive','disabled','deleted')";
                    else $sql .= " AND COALESCE(`{$c}`,1)=1";
                    break;
                }
            }
            $sql .= " LIMIT 500";

            try {
                foreach ($pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC) as $row) {
                    $roleOk = false;
                    foreach ($roleCols as $rc) {
                        if (array_key_exists($rc, $row) && planner_role_is_admin_or_above((string)$row[$rc])) { $roleOk = true; break; }
                    }
                    if (!$roleOk && isset($row['type']) && in_array((string)$row['type'], ['1','master'], true)) $roleOk = true;
                    if (!$roleOk) continue;

                    foreach ($existingPinCols as $pc) {
                        if (array_key_exists($pc, $row) && planner_value_matches_pin((string)$row[$pc], $pin)) return true;
                    }
                }
            } catch (Throwable $e) {
                continue;
            }
        }

        return false;
    }
}

if (!function_exists('planner_void_payment_transaction')) {
    function planner_void_payment_transaction(PDO $pdo, int $orderId, int $transactionId, string $adminPin, string $reason): void
    {
        if ($orderId <= 0 || $transactionId <= 0) throw new RuntimeException('Invalid payment void request.');
        if (trim($reason) === '') throw new RuntimeException('Please enter a reason for voiding this payment.');
        if (!planner_admin_pin_valid($pdo, $adminPin)) throw new RuntimeException('Invalid admin PIN or insufficient authority.');
        if (!planner_table_exists($pdo, 'folio_transactions') || !planner_table_exists($pdo, 'folios')) throw new RuntimeException('Folio transaction tables were not found.');
        if (!planner_column_exists($pdo, 'folio_transactions', 'is_voided')) throw new RuntimeException('folio_transactions.is_voided column is required before payments can be voided safely.');

        $stmt = $pdo->prepare("\n            SELECT ft.*, f.order_id AS folio_order_id\n            FROM folio_transactions ft\n            INNER JOIN folios f ON f.id = ft.folio_id\n            WHERE ft.id = :id\n              AND f.order_id = :order_id\n            LIMIT 1\n            FOR UPDATE\n        ");
        $stmt->execute([':id' => $transactionId, ':order_id' => $orderId]);
        $tx = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$tx) throw new RuntimeException('Payment transaction was not found for this booking.');
        if ((int)($tx['is_voided'] ?? 0) === 1) throw new RuntimeException('This payment is already voided.');

        $credit = (float)($tx['credit'] ?? 0);
        $txType = strtolower(trim((string)($tx['tx_type'] ?? '')));
        $paymentTypes = ['payment','receipt','deposit','payment_received','folio_payment','guest_payment'];
        if ($credit <= 0 && !in_array($txType, $paymentTypes, true)) {
            throw new RuntimeException('Only payment/receipt transactions can be voided from here. Charges must not be voided with this control.');
        }

        $sets = ['is_voided = 1'];
        $params = [':id' => $transactionId];
        if (planner_column_exists($pdo, 'folio_transactions', 'voided_at')) { $sets[] = 'voided_at = NOW()'; }
        if (planner_column_exists($pdo, 'folio_transactions', 'voided_by')) { $sets[] = 'voided_by = :voided_by'; $params[':voided_by'] = planner_current_user_id(); }
        if (planner_column_exists($pdo, 'folio_transactions', 'notes')) { $sets[] = "notes = CONCAT(COALESCE(notes,''), :void_note)"; $params[':void_note'] = "\nVOID PAYMENT: " . $reason; }
        elseif (planner_column_exists($pdo, 'folio_transactions', 'description')) { $sets[] = "description = CONCAT(COALESCE(description,''), :void_note)"; $params[':void_note'] = " | VOID PAYMENT: " . $reason; }

        $pdo->prepare('UPDATE folio_transactions SET ' . implode(', ', $sets) . ' WHERE id = :id')->execute($params);

        $folioId = (int)($tx['folio_id'] ?? 0);
        planner_recalc_folio($pdo, $folioId);
        planner_sync_order_payment_status($pdo, $orderId);
        planner_write_payment_audit_log_safe($pdo, $orderId, 'void_payment', $reason, [
            'transaction_id' => $transactionId,
            'folio_id' => $folioId,
            'tx_type' => $tx['tx_type'] ?? '',
            'description' => $tx['description'] ?? '',
            'credit' => $tx['credit'] ?? 0,
            'admin_user_id' => planner_current_user_id(),
        ]);
    }
}

if (!function_exists('planner_void_charge_transaction')) {
    /*
       TEMPORARY CONTROL - added at the user's request as a stop-gap for clearing
       mis-posted charges (e.g. a restaurant charge posted to the wrong folio).
       Mirrors planner_void_payment_transaction() but for charge/debit lines instead
       of payments. Flagged for removal once a proper correction workflow exists.
    */
    function planner_void_charge_transaction(PDO $pdo, int $orderId, int $transactionId, string $adminPin, string $reason): void
    {
        if ($orderId <= 0 || $transactionId <= 0) throw new RuntimeException('Invalid charge void request.');
        if (trim($reason) === '') throw new RuntimeException('Please enter a reason for voiding this charge.');
        if (!planner_admin_pin_valid($pdo, $adminPin)) throw new RuntimeException('Invalid admin PIN or insufficient authority.');
        if (!planner_table_exists($pdo, 'folio_transactions') || !planner_table_exists($pdo, 'folios')) throw new RuntimeException('Folio transaction tables were not found.');
        if (!planner_column_exists($pdo, 'folio_transactions', 'is_voided')) throw new RuntimeException('folio_transactions.is_voided column is required before charges can be voided safely.');

        $stmt = $pdo->prepare("\n            SELECT ft.*, f.order_id AS folio_order_id\n            FROM folio_transactions ft\n            INNER JOIN folios f ON f.id = ft.folio_id\n            WHERE ft.id = :id\n              AND f.order_id = :order_id\n            LIMIT 1\n            FOR UPDATE\n        ");
        $stmt->execute([':id' => $transactionId, ':order_id' => $orderId]);
        $tx = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$tx) throw new RuntimeException('Charge transaction was not found for this booking.');
        if ((int)($tx['is_voided'] ?? 0) === 1) throw new RuntimeException('This charge is already voided.');

        $credit = (float)($tx['credit'] ?? 0);
        $debit = (float)($tx['debit'] ?? 0);
        $txType = strtolower(trim((string)($tx['tx_type'] ?? '')));
        $paymentTypes = ['payment','receipt','deposit','payment_received','folio_payment','guest_payment'];

        if ($credit > 0 || in_array($txType, $paymentTypes, true)) {
            throw new RuntimeException('This is a payment/receipt line. Use Void Payment instead of Void Charge.');
        }
        if ($debit <= 0) {
            throw new RuntimeException('Only charge (debit) transactions can be voided with this control.');
        }
        if ($txType === 'room_charge') {
            throw new RuntimeException('Room charges cannot be voided here. Correct room allocations/rates instead.');
        }

        $sets = ['is_voided = 1'];
        $params = [':id' => $transactionId];
        if (planner_column_exists($pdo, 'folio_transactions', 'voided_at')) { $sets[] = 'voided_at = NOW()'; }
        if (planner_column_exists($pdo, 'folio_transactions', 'voided_by')) { $sets[] = 'voided_by = :voided_by'; $params[':voided_by'] = planner_current_user_id(); }
        if (planner_column_exists($pdo, 'folio_transactions', 'notes')) { $sets[] = "notes = CONCAT(COALESCE(notes,''), :void_note)"; $params[':void_note'] = "\nVOID CHARGE: " . $reason; }
        elseif (planner_column_exists($pdo, 'folio_transactions', 'description')) { $sets[] = "description = CONCAT(COALESCE(description,''), :void_note)"; $params[':void_note'] = " | VOID CHARGE: " . $reason; }

        $pdo->prepare('UPDATE folio_transactions SET ' . implode(', ', $sets) . ' WHERE id = :id')->execute($params);

        $folioId = (int)($tx['folio_id'] ?? 0);
        planner_recalc_folio($pdo, $folioId);
        planner_sync_order_payment_status($pdo, $orderId);
        planner_write_payment_audit_log_safe($pdo, $orderId, 'void_charge', $reason, [
            'transaction_id' => $transactionId,
            'folio_id' => $folioId,
            'tx_type' => $tx['tx_type'] ?? '',
            'description' => $tx['description'] ?? '',
            'debit' => $tx['debit'] ?? 0,
            'source_table' => $tx['source_table'] ?? '',
            'source_id' => $tx['source_id'] ?? '',
            'admin_user_id' => planner_current_user_id(),
        ]);
    }
}


if (!function_exists('planner_build_group_rooming_list')) {
    function planner_build_group_rooming_list(PDO $pdo, array $order, int $orderId): array
    {
        $rooming = [];
        $orderKidsNoCharge = 0;
        foreach (['kids_0_3_no_charge','kids_0_3','children_0_3_no_charge','children_0_3','kids_no_charge','infants','toddlers','babies'] as $kidsCol) {
            if (array_key_exists($kidsCol, $order)) {
                $orderKidsNoCharge = (int)($order[$kidsCol] ?? 0);
                break;
            }
        }

        $roomGuestSql = "''";

        if (planner_table_exists($pdo, 'order_rooming')) {
            if (planner_table_exists($pdo, 'order_rooming_guests')) {
                $roomingGuestLinkColumn = '';
                foreach (['order_rooming_id', 'rooming_id'] as $linkCol) {
                    if (planner_column_exists($pdo, 'order_rooming_guests', $linkCol)) {
                        $roomingGuestLinkColumn = $linkCol;
                        break;
                    }
                }

                if ($roomingGuestLinkColumn !== '') {
                    if (planner_column_exists($pdo, 'order_rooming_guests', 'first_name') && planner_column_exists($pdo, 'order_rooming_guests', 'last_name')) {
                        $nameExpr = "TRIM(CONCAT(COALESCE(org.first_name,''), ' ', COALESCE(org.last_name,'')))";
                    } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'firstname') && planner_column_exists($pdo, 'order_rooming_guests', 'lastname')) {
                        $nameExpr = "TRIM(CONCAT(COALESCE(org.firstname,''), ' ', COALESCE(org.lastname,'')))";
                    } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'guest_name')) {
                        $nameExpr = "org.guest_name";
                    } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'name')) {
                        $nameExpr = "org.name";
                    } else {
                        $nameExpr = "''";
                    }

                    $roomGuestSql = "(SELECT GROUP_CONCAT(NULLIF(TRIM({$nameExpr}), '') ORDER BY org.id SEPARATOR ', ') FROM order_rooming_guests org WHERE org.`{$roomingGuestLinkColumn}` = rmg.id)";
                }
            }

            $roomingNotesSelect = [];
            foreach (['notes','note','remarks','special_requests','housekeeping_notes'] as $col) {
                if (planner_column_exists($pdo, 'order_rooming', $col)) {
                    $roomingNotesSelect[] = "rmg.`{$col}` AS rmg_{$col}";
                }
            }
            $roomingNotesSql = $roomingNotesSelect ? (", " . implode(", ", $roomingNotesSelect)) : "";

            $kidsNoChargeRoomingSql = "0 AS kids_0_3_no_charge";
            foreach (['kids_0_3_no_charge','kids_0_3','children_0_3_no_charge','children_0_3','kids_no_charge','infants','toddlers','babies'] as $kidsCol) {
                if (planner_column_exists($pdo, 'order_rooming', $kidsCol)) {
                    $kidsNoChargeRoomingSql = "COALESCE(rmg.`{$kidsCol}`, 0) AS kids_0_3_no_charge";
                    break;
                }
            }

            $stmt = $pdo->prepare("
                SELECT
                    rmg.id,
                    rmg.room_no,
                    rmg.room_id,
                    rmg.room_type_id,
                    rmg.adults,
                    rmg.children,
                    {$kidsNoChargeRoomingSql},
                    rmg.check_in,
                    rmg.check_out,
                COALESCE(rmg.checked_in_status,0) AS room_checked_in_status,
                COALESCE(rmg.checked_out_status,0) AS room_checked_out_status,
                    rmg.checked_in_status,
                    rmg.checked_out_status,
                    r.room_no AS allocated_room_no,
                    rt.title AS room_type,
                    rt.higher_occupancy,
                    {$roomGuestSql} AS rooming_guests
                    {$roomingNotesSql}
                FROM order_rooming rmg
                LEFT JOIN rooms r ON r.id = rmg.room_id
                LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
                WHERE rmg.order_id = :order_id
                ORDER BY rmg.check_in ASC, rmg.room_no ASC, rmg.id ASC
            ");
            $stmt->execute([':order_id' => $orderId]);
            foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $rNotes = [];
                foreach ($r as $rk => $rv) {
                    if (strpos((string)$rk, 'rmg_') === 0 && trim((string)$rv) !== '') {
                        $rNotes[] = [
                            'label' => ucwords(str_replace('_', ' ', substr($rk, 4))),
                            'value' => (string)$rv,
                        ];
                    }
                }
                $rooming[] = [
                    'id' => (int)($r['id'] ?? 0),
                    'room_no' => (string)($r['allocated_room_no'] ?: $r['room_no'] ?: ''),
                    'room_type' => (string)($r['room_type'] ?? ''),
                    'adults' => (int)($r['adults'] ?? 0),
                    'children' => (int)($r['children'] ?? 0),
                    'kids_0_3_no_charge' => (int)(($r['kids_0_3_no_charge'] ?? 0) ?: $orderKidsNoCharge),
                    'check_in' => planner_display_date($r['check_in'] ?? ''),
                    'check_out' => planner_display_date($r['check_out'] ?? ''),
                    'raw_check_in' => (string)($r['check_in'] ?? ''),
                    'raw_check_out' => (string)($r['check_out'] ?? ''),
                    'checked_in' => (int)($r['checked_in_status'] ?? 0),
                    'checked_out' => (int)($r['checked_out_status'] ?? 0),
                    'guests' => (string)($r['rooming_guests'] ?? ''),
                    'notes' => $rNotes,
                    'order_id' => $orderId,
                    'order_no' => (string)($order['order_no'] ?? ('#' . $orderId)),
                    'is_current_order' => true,
                    'is_primary' => false,
                ];
            }
        }

        /* Mark the first room of the currently-open booking as its Primary Room. */
        foreach ($rooming as $idx => $rr) {
            if ((int)($rr['order_id'] ?? 0) === $orderId) {
                $rooming[$idx]['is_primary'] = true;
                break;
            }
        }

        /*
           Group bookings in this system are commonly modelled as several separate
           `orders` rows sharing the same group_name (see restaurant module group
           sales). If this booking has a group_name, pull in the other bookings in
           that same group too, so opening the Folio Control Centre from ANY room
           in the group shows the full group's Rooming List - not just this one
           booking's own rooms.
        */
        $groupNameForRooming = trim((string)($order['group_name'] ?? ''));
        if ($groupNameForRooming !== '' && planner_table_exists($pdo, 'order_rooming')) {
            try {
                /*
                   group_name is a free-text label staff type in - it is NOT a unique
                   group identifier. Two genuinely unrelated bookings (different guests,
                   different months) can easily share the same generic group name
                   (e.g. "Smith Family", "Conference Group"). Matching on group_name
                   alone pulled in those unrelated bookings' rooms/guests too.

                   A real single group event has all its rooms booked for the same
                   stay - so also require the sibling booking's dates to overlap with
                   this booking's own check_in/check_out before treating it as part
                   of the same group.
                */
                $siblingSql = "
                    SELECT o.id, o.order_no
                    FROM orders o
                    WHERE o.group_name = :group_name
                      AND o.id <> :order_id
                      AND o.property_id = :property_id
                      AND COALESCE(o.status,1) NOT IN (0,4)
                      AND o.check_in < :this_check_out
                      AND o.check_out > :this_check_in
                    ORDER BY o.id ASC
                    LIMIT 100
                ";
                $stmtSiblings = $pdo->prepare($siblingSql);
                $stmtSiblings->execute([
                    ':group_name' => $groupNameForRooming,
                    ':order_id' => $orderId,
                    ':property_id' => (int)($order['property_id'] ?? 0),
                    ':this_check_in' => (string)($order['check_in'] ?? ''),
                    ':this_check_out' => (string)($order['check_out'] ?? ''),
                ]);
                $siblingOrders = $stmtSiblings->fetchAll(PDO::FETCH_ASSOC) ?: [];

                foreach ($siblingOrders as $sib) {
                    $sibOrderId = (int)($sib['id'] ?? 0);
                    if ($sibOrderId <= 0) continue;

                    $stmtSibRooming = $pdo->prepare("
                        SELECT
                            rmg.id,
                            rmg.room_no,
                            COALESCE(r.room_no, rmg.room_no) AS allocated_room_no,
                            rt.title AS room_type,
                            rmg.adults,
                            rmg.children,
                            rmg.check_in,
                            rmg.check_out,
                            COALESCE(rmg.checked_in_status,0) AS checked_in_status,
                            COALESCE(rmg.checked_out_status,0) AS checked_out_status,
                            {$roomGuestSql} AS rooming_guests
                        FROM order_rooming rmg
                        LEFT JOIN rooms r ON r.id = rmg.room_id
                        LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
                        WHERE rmg.order_id = :order_id
                        ORDER BY rmg.check_in ASC, rmg.room_no ASC, rmg.id ASC
                    ");
                    $stmtSibRooming->execute([':order_id' => $sibOrderId]);
                    $sibRows = $stmtSibRooming->fetchAll(PDO::FETCH_ASSOC) ?: [];

                    foreach ($sibRows as $sidx => $sr) {
                        $rooming[] = [
                            'id' => (int)($sr['id'] ?? 0),
                            'room_no' => (string)($sr['allocated_room_no'] ?: $sr['room_no'] ?: ''),
                            'room_type' => (string)($sr['room_type'] ?? ''),
                            'adults' => (int)($sr['adults'] ?? 0),
                            'children' => (int)($sr['children'] ?? 0),
                            'kids_0_3_no_charge' => 0,
                            'check_in' => planner_display_date($sr['check_in'] ?? ''),
                            'check_out' => planner_display_date($sr['check_out'] ?? ''),
                            'raw_check_in' => (string)($sr['check_in'] ?? ''),
                            'raw_check_out' => (string)($sr['check_out'] ?? ''),
                            'checked_in' => (int)($sr['checked_in_status'] ?? 0),
                            'checked_out' => (int)($sr['checked_out_status'] ?? 0),
                            'guests' => (string)($sr['rooming_guests'] ?? ''),
                            'notes' => [],
                            'order_id' => $sibOrderId,
                            'order_no' => (string)($sib['order_no'] ?? ('#' . $sibOrderId)),
                            'is_current_order' => false,
                            'is_primary' => ($sidx === 0),
                        ];
                    }
                }
            } catch (Throwable $ignoreGroupRooming) {
                // If sibling lookup fails for any reason, still show this booking's own rooming list.
            }
        }

        if ($orderKidsNoCharge <= 0 && !empty($rooming)) {
            foreach ($rooming as $rr) {
                $orderKidsNoCharge += (int)($rr['kids_0_3_no_charge'] ?? 0);
            }
        }

        return [
            'rooming' => $rooming,
            'kids_no_charge_total' => $orderKidsNoCharge,
        ];
    }
}

if (!function_exists('planner_order_rooming_check_status')) {
    function planner_order_rooming_check_status(PDO $pdo, int $orderId): array
    {
        $today = planner_today_date();
        $out = [
            'can_checkin_today' => false,
            'can_checkout_today' => false,
            'can_delayed_checkin' => false,
        ];

        if ($orderId <= 0 || !planner_table_exists($pdo, 'order_rooming')) {
            return $out;
        }

        $checkedInExpr = planner_column_exists($pdo, 'order_rooming', 'checked_in_status') ? 'COALESCE(checked_in_status,0)' : '0';
        $checkedOutExpr = planner_column_exists($pdo, 'order_rooming', 'checked_out_status') ? 'COALESCE(checked_out_status,0)' : '0';

        $stmt = $pdo->prepare("
            SELECT
                SUM(CASE WHEN check_in = :today_checkin AND {$checkedInExpr} = 0 THEN 1 ELSE 0 END) AS due_checkin,
                SUM(CASE WHEN check_in < :today_delayed_from AND check_out > :today_delayed_to AND {$checkedInExpr} = 0 THEN 1 ELSE 0 END) AS delayed_checkin,
                SUM(CASE WHEN check_out = :today_checkout AND {$checkedOutExpr} = 0 THEN 1 ELSE 0 END) AS due_checkout
            FROM order_rooming
            WHERE order_id = :order_id
        ");
        $stmt->execute([
            ':today_checkin' => $today,
            ':today_delayed_from' => $today,
            ':today_delayed_to' => $today,
            ':today_checkout' => $today,
            ':order_id' => $orderId,
        ]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $out['can_checkin_today'] = ((int)($row['due_checkin'] ?? 0) > 0);
        $out['can_checkout_today'] = ((int)($row['due_checkout'] ?? 0) > 0);
        $out['can_delayed_checkin'] = ((int)($row['delayed_checkin'] ?? 0) > 0);

        return $out;
    }
}

if (!function_exists('planner_status_meta')) {
    function planner_status_meta($status): array
{
    $s = strtolower(trim((string)$status));

    switch ($s) {

        case '1':
        case 'confirmed':
        case 'confirm':
        case 'reserved':
            return ['Confirmed', 'confirmed', 'success'];

        case '2':
        case 'pending':
        case 'provisional':
        case 'draft':
            return ['Provisional', 'provisional', 'warning'];

        case '3':
        case 'checked_in':
        case 'checkin':
        case 'inhouse':
        case 'in-house':
            return ['In House', 'inhouse', 'primary'];

        case 'paid':
        case 'fully_paid':
            return ['Paid', 'paid', 'success'];

        case '4':
        case 'checked_out':
        case 'checkout':
        case 'completed':
            return ['Checked Out', 'checkedout', 'secondary'];

        case '0':
        case 'cancelled':
        case 'canceled':
            return ['Cancelled', 'cancelled', 'danger'];

        case 'noshow':
        case 'no_show':
            return ['No Show', 'noshow', 'danger'];

        default:
            return [ucwords($s !== '' ? str_replace('_', ' ', $s) : 'Open'), 'default', 'dark'];
    }
}
}

if (!function_exists('planner_normalize_code')) {
    function planner_normalize_code($value): string
    {
        $value = trim((string)$value);
        if ($value === '') {
            return '';
        }

        $value = strtoupper($value);
        $value = str_replace(['-', ' '], '_', $value);
        $value = preg_replace('/_+/', '_', $value);
        return trim($value, '_');
    }
}

if (!function_exists('planner_normalize_booking_status')) {
    function planner_normalize_booking_status($status): string
    {
        $s = planner_normalize_code($status);

        switch ($s) {
            case '1':
            case 'CONFIRM':
            case 'CONFIRMED':
            case 'RESERVED':
                return 'CONFIRMED';

            case '2':
            case 'PENDING':
            case 'PROVISIONAL':
            case 'DRAFT':
                return 'PROVISIONAL';

            case '3':
            case 'CHECKIN':
            case 'CHECKED_IN':
            case 'INHOUSE':
            case 'IN_HOUSE':
                return 'CHECKED_IN';

            case '4':
            case 'CHECKOUT':
            case 'CHECKED_OUT':
            case 'COMPLETED':
                return 'CHECKED_OUT';

            case '0':
            case 'CANCELLED':
            case 'CANCELED':
                return 'CANCELLED';

            case 'NOSHOW':
            case 'NO_SHOW':
                return 'NO_SHOW';

            default:
                return $s;
        }
    }
}

if (!function_exists('planner_normalize_payment_status')) {
    function planner_normalize_payment_status($status): string
    {
        $raw = trim((string)$status);
        $s = planner_normalize_code($raw);

        switch ($s) {
            case '0':
            case 'UNPAID':
            case 'NOT_PAID':
            case 'NO_PAYMENT':
                return 'UNPAID';

            case '1':
            case 'PART_PAID':
            case 'PARTLY_PAID':
            case 'PARTIAL':
            case 'PARTIAL_PAYMENT':
                return 'PART_PAID';

            case '2':
            case 'PAID':
            case 'FULLY_PAID':
            case 'SETTLED':
                return 'PAID';

            case 'INVOICED':
                return 'INVOICED';

            default:
                return $s;
        }
    }
}


if (!function_exists('planner_humanize_code')) {
    function planner_humanize_code($value): string
    {
        $code = planner_normalize_code($value);
        if ($code === '') {
            return 'Not set';
        }

        return ucwords(strtolower(str_replace('_', ' ', $code)));
    }
}

if (!function_exists('planner_payment_due_label')) {
    function planner_payment_due_label($value): string
    {
        $code = planner_normalize_code($value);
        switch ($code) {
            case 'PAY_ON_ARRIVAL': return 'Pay On Arrival';
            case 'CHASE_PAYMENT': return 'Chase Payment';
            case 'ONLINE_PAYMENT': return 'Online Payment';
            case 'ACCOUNT': return 'Account';
            case 'CASH': return 'Cash';
            case 'CARD': return 'Card';
            case 'BANK_TRANSFER': return 'Bank Transfer';
            case '': return 'Not set';
            default: return planner_humanize_code($code);
        }
    }
}

if (!function_exists('planner_payment_status_label')) {
    function planner_payment_status_label($value): string
    {
        $code = planner_normalize_payment_status($value);
        switch ($code) {
            case 'UNPAID': return 'Unpaid';
            case 'PART_PAID': return 'Part Paid';
            case 'PAID': return 'Fully Paid';
            case 'INVOICED': return 'Invoiced';
            case '': return 'Not set';
            default: return planner_humanize_code($code);
        }
    }
}


if (!function_exists('planner_clean_folio_numbers')) {
    function planner_clean_folio_numbers($value): string
    {
        $raw = trim((string)$value);
        if ($raw === '') {
            return '';
        }

        $parts = array_filter(array_map('trim', explode(',', $raw)), static function ($v) {
            return $v !== '';
        });

        $unique = [];
        foreach ($parts as $p) {
            $key = strtoupper($p);
            if (!isset($unique[$key])) {
                $unique[$key] = $p;
            }
        }

        return implode(', ', array_values($unique));
    }
}

if (!function_exists('planner_booking_finance_hint')) {
    function planner_booking_finance_hint(array $booking): string
    {
        $paymentDueLabel = planner_payment_due_label($booking['payment_due'] ?? '');
        $paymentStatusLabel = planner_payment_status_label($booking['payment_status'] ?? '');
        $balance = round((float)($booking['balance'] ?? 0), 2);
        $invoiceNumber = trim((string)($booking['invoice_number'] ?? ''));
        $isInvoiced = $invoiceNumber !== '' || ((int)($booking['invoiced'] ?? 0) === 1);

        $parts = [
            'Payment Due: ' . $paymentDueLabel,
            'Payment Status: ' . $paymentStatusLabel,
            'Balance: ' . number_format($balance, 2),
        ];

        if ($isInvoiced) {
            $parts[] = 'Invoiced' . ($invoiceNumber !== '' ? ': ' . $invoiceNumber : ': Yes');
        }

        return implode(' | ', $parts);
    }
}

if (!function_exists('planner_color_rule_value')) {
    function planner_color_rule_value(array $rule, array $keys): string
    {
        foreach ($keys as $key) {
            if (array_key_exists($key, $rule) && trim((string)$rule[$key]) !== '') {
                return trim((string)$rule[$key]);
            }
        }
        return '';
    }
}

if (!function_exists('planner_color_rule_specificity')) {
    function planner_color_rule_specificity(array $rule): int
    {
        $score = 0;
        foreach ([
            ['booking_status_code','status_code','booking_status','status','status_key'],
            ['payment_due_code','payment_due','payment_due_type','payment_due_key'],
            ['payment_status_code','payment_status','payment_status_key'],
            ['booking_source_code','booking_source','source_code','source_key'],
            ['invoiced_condition','invoiced'],
            ['balance_condition','balance']
        ] as $keys) {
            if (planner_color_rule_value($rule, $keys) !== '') {
                $score++;
            }
        }
        return $score;
    }
}

if (!function_exists('planner_booking_fallback_color')) {
    function planner_booking_fallback_color(array $booking): string
    {
        $status        = planner_normalize_booking_status($booking['status_code'] ?? $booking['status'] ?? '');
        $paymentDue    = planner_normalize_code($booking['payment_due'] ?? '');
        $paymentStatus = planner_normalize_payment_status($booking['payment_status'] ?? '');
        $source        = planner_normalize_code($booking['booking_source'] ?? '');
        $invoiced      = !empty($booking['invoice_number']) || ((int)($booking['invoiced'] ?? 0) === 1);
        $balance       = round((float)($booking['balance'] ?? 0), 2);

        /*
           Built-in fallback mirrors the PMS colour legend:
           - checked in green
           - checked out with balance red
           - invoiced peach
           - chase payment pink
           - pay on arrival purple
           - online orange
           - confirmed yellow
           - provisional grey
        */
        $roomCheckedIn  = ((int)($booking['room_checked_in_status'] ?? $booking['checked_in_status'] ?? 0) === 1);
        $roomCheckedOut = ((int)($booking['room_checked_out_status'] ?? $booking['checked_out_status'] ?? 0) === 1);

        // Checked-out must override checked-in because checked_out rooms often still have checked_in_status = 1.
        if ($roomCheckedOut) {
            return $balance > 0 ? '#DC2626' : '#FDBA74';
        }
        if ($roomCheckedIn) return '#22C55E';
        if ($status === 'CHECKED_OUT') return $balance > 0 ? '#DC2626' : '#FDBA74';
        if ($invoiced || $paymentStatus === 'INVOICED') return '#FDBA74';
        if ($paymentDue === 'CHASE_PAYMENT') return '#EC4899';
        if ($paymentDue === 'PAY_ON_ARRIVAL') return '#8B5CF6';
        if ($paymentDue === 'ONLINE_PAYMENT' || $source === 'ONLINE') return '#F97316';
        if ($status === 'PROVISIONAL') return '#94A3B8';
        if ($status === 'CANCELLED' || $status === 'NO_SHOW') return '#64748B';
        if ($status === 'CONFIRMED') return '#FACC15';

        return '#3B82F6';
    }
}

if (!function_exists('planner_booking_color')) {
function planner_booking_color(PDO $pdo, array $booking): string
{
    static $rules = null;

    if ($rules === null) {
        try {
            $stmt = $pdo->query("
                SELECT *
                FROM booking_color_codes
                WHERE is_active = 1
                ORDER BY priority_no ASC, id ASC
            ");
            $rules = $stmt->fetchAll(PDO::FETCH_ASSOC);

            /*
               Do not let a broad rule like "Confirmed = Yellow" override
               more specific rules like Pay On Arrival, Chase Payment,
               Invoiced, Online Payment or Active Out. Specific rules win first,
               then priority_no decides within the same specificity.
            */
            usort($rules, function ($a, $b) {
                $sa = planner_color_rule_specificity($a);
                $sb = planner_color_rule_specificity($b);
                if ($sa !== $sb) return $sb <=> $sa;

                $pa = (int)($a['priority_no'] ?? $a['priority'] ?? 999);
                $pb = (int)($b['priority_no'] ?? $b['priority'] ?? 999);
                if ($pa !== $pb) return $pa <=> $pb;

                return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
            });
        } catch (Throwable $e) {
            $rules = [];
        }
    }

    $status        = planner_normalize_booking_status($booking['status_code'] ?? $booking['status'] ?? '');

    // Room-level calendar status.
    // Checked-out must override checked-in because a checked-out room can still have checked_in_status = 1.
    $roomCheckedIn  = ((int)($booking['room_checked_in_status'] ?? $booking['checked_in_status'] ?? 0) === 1);
    $roomCheckedOut = ((int)($booking['room_checked_out_status'] ?? $booking['checked_out_status'] ?? 0) === 1);
    $balanceForRoom = round((float)($booking['balance'] ?? 0), 2);

    if ($roomCheckedOut) {
        return $balanceForRoom > 0 ? '#DC2626' : '#FDBA74';
    }

    if ($roomCheckedIn) {
        return '#22C55E';
    }

    $pinkAgentIds = [201, 161, 200];
    if (in_array((int)($booking['agent_id'] ?? 0), $pinkAgentIds, true)) {
        return '#FFFF00';
    }

    $paymentDue    = planner_normalize_code($booking['payment_due'] ?? '');
    $paymentStatus = planner_normalize_payment_status($booking['payment_status'] ?? '');
    $source        = planner_normalize_code($booking['booking_source'] ?? '');
    $invoiced      = !empty($booking['invoice_number']) || ((int)($booking['invoiced'] ?? 0) === 1);
    $balance       = round((float)($booking['balance'] ?? 0), 2);

    foreach ($rules as $r) {
        $ruleStatus        = planner_normalize_booking_status(planner_color_rule_value($r, ['booking_status_code','status_code','booking_status','status','status_key']));
        $rulePaymentDue    = planner_normalize_code(planner_color_rule_value($r, ['payment_due_code','payment_due','payment_due_type','payment_due_key']));
        $rulePaymentStatus = planner_normalize_payment_status(planner_color_rule_value($r, ['payment_status_code','payment_status','payment_status_key']));
        $ruleSource        = planner_normalize_code(planner_color_rule_value($r, ['booking_source_code','booking_source','source_code','source_key']));

        if ($ruleStatus !== '' && $ruleStatus !== $status) continue;
        if ($rulePaymentDue !== '' && $rulePaymentDue !== $paymentDue) continue;
        if ($rulePaymentStatus !== '' && $rulePaymentStatus !== $paymentStatus) continue;
        if ($ruleSource !== '' && $ruleSource !== $source) continue;

        $invoicedCondition = strtolower(trim(planner_color_rule_value($r, ['invoiced_condition','invoiced'])));
        $balanceCondition  = strtolower(trim(planner_color_rule_value($r, ['balance_condition','balance'])));

        if ($invoicedCondition === 'yes' && !$invoiced) continue;
        if ($invoicedCondition === 'no' && $invoiced) continue;
        if ($balanceCondition === 'has_balance' && $balance <= 0) continue;
        if ($balanceCondition === 'no_balance' && $balance > 0) continue;

        $bg = trim((string)($r['bg_color'] ?? $r['background_color'] ?? $r['color'] ?? ''));
        if ($bg !== '' && preg_match('/^#?[a-f0-9]{3,6}$/i', $bg)) {
            return $bg[0] === '#' ? $bg : ('#' . $bg);
        }
    }

    return planner_booking_fallback_color($booking);
}
}

if (!function_exists('planner_room_slot_meta')) {
    function planner_room_slot_meta($roomTypeTitle, int $adults, int $children, int $higherOccupancy = 0): array
    {
        $roomTypeTitle = trim((string)$roomTypeTitle);
        $titleLower = strtolower($roomTypeTitle);
        $totalPax = max(0, $adults) + max(0, $children);
        $isFamily = strpos($titleLower, 'family') !== false;

        if ($isFamily) {
            return [
                'label' => 'Family Room',
                'class' => 'family',
                'badge' => 'info',
                'icon'  => 'bi-people-fill',
                'is_triple' => false,
                'is_family' => true,
            ];
        }

        if ($totalPax >= 3 && ($higherOccupancy >= 3 || $higherOccupancy <= 0)) {
            return [
                'label' => 'Triple / Extra Bed',
                'class' => 'triple',
                'badge' => 'warning',
                'icon'  => 'bi-person-plus-fill',
                'is_triple' => true,
                'is_family' => false,
            ];
        }

        if ($totalPax === 2) {
            return [
                'label' => 'Double',
                'class' => 'double',
                'badge' => 'secondary',
                'icon'  => 'bi-person-fill',
                'is_triple' => false,
                'is_family' => false,
            ];
        }

        if ($totalPax === 1) {
            return [
                'label' => 'Single',
                'class' => 'single',
                'badge' => 'secondary',
                'icon'  => 'bi-person',
                'is_triple' => false,
                'is_family' => false,
            ];
        }

        return [
            'label' => 'Empty',
            'class' => 'empty',
            'badge' => 'secondary',
            'icon'  => 'bi-door-open',
            'is_triple' => false,
            'is_family' => false,
        ];
    }
}

if (!function_exists('planner_bar_geometry')) {
    function planner_bar_geometry(array $booking, array $days, int $dayWidth): ?array
    {
        $rangeStart = strtotime($days[0]['date']);
        $rangeEnd   = strtotime(end($days)['date']);

        $checkIn  = strtotime((string)$booking['check_in']);
        $checkOut = strtotime((string)$booking['check_out']);

        if (!$checkIn || !$checkOut) {
            return null;
        }

        $bookingStart = max($checkIn, $rangeStart);
        $bookingEnd   = min(strtotime('-1 day', $checkOut), $rangeEnd);

        if ($bookingEnd < $rangeStart || $bookingStart > $rangeEnd || $bookingEnd < $bookingStart) {
            return null;
        }

        $offsetDays = (int) floor(($bookingStart - $rangeStart) / 86400);
        $spanDays   = (int) floor(($bookingEnd - $bookingStart) / 86400) + 1;

        return [
            'left'  => $offsetDays * $dayWidth,
            'width' => max($dayWidth, ($spanDays * $dayWidth)),
            'span'  => $spanDays,
            'start' => $bookingStart,
            'end'   => $bookingEnd,
        ];
    }
}

if (!function_exists('planner_assign_lanes')) {
    function planner_assign_lanes(array $bookings, array $days, int $dayWidth): array
    {
        $prepared = [];

        foreach ($bookings as $idx => $booking) {
            $geo = planner_bar_geometry($booking, $days, $dayWidth);
            if (!$geo) {
                continue;
            }

            $prepared[] = [
                'booking' => $booking,
                'geo'     => $geo,
                'index'   => $idx,
            ];
        }

        usort($prepared, function ($a, $b) {
            if ($a['geo']['start'] === $b['geo']['start']) {
                if ($a['geo']['end'] === $b['geo']['end']) {
                    return $a['index'] <=> $b['index'];
                }
                return $a['geo']['end'] <=> $b['geo']['end'];
            }
            return $a['geo']['start'] <=> $b['geo']['start'];
        });

        $laneEndTimes = [];
        $output = [];

        foreach ($prepared as $item) {
            $assignedLane = null;

            foreach ($laneEndTimes as $lane => $laneEnd) {
                if ($item['geo']['start'] > $laneEnd) {
                    $assignedLane = $lane;
                    break;
                }
            }

            if ($assignedLane === null) {
                $assignedLane = count($laneEndTimes);
            }

            $laneEndTimes[$assignedLane] = $item['geo']['end'];

            $output[] = [
                'booking' => $item['booking'],
                'geo'     => $item['geo'],
                'lane'    => $assignedLane,
            ];
        }

        return [
            'items'      => $output,
            'lane_count' => max(1, count($laneEndTimes)),
        ];
    }
}

if (!function_exists('count_available_rooms_by_type')) {
    function count_available_rooms_by_type(PDO $pdo, int $propertyId, int $roomTypeId, string $checkIn, string $checkOut, int $excludeOrderId = 0): array
    {
        if ($propertyId <= 0 || $roomTypeId <= 0 || $checkIn === '' || $checkOut === '') {
            return [
                'total_rooms'     => 0,
                'occupied_rooms'  => 0,
                'available_rooms' => 0,
            ];
        }

        /*
           Correct PMS availability logic:
           - Count physical rooms of this room type.
           - For each NIGHT in the requested stay, count physical rooms occupied that night.
           - Checkout date is not occupied, so check_in <= night AND check_out > night.
           - Availability for a stay is the MINIMUM free rooms across the nights.
           This means a room checking out on 27 can be sold again to a guest checking in on 27.
        */

        $stmtTotal = $pdo->prepare("
            SELECT COUNT(*)
            FROM rooms
            WHERE property_id = :property_id
              AND room_type_id = :room_type_id
        ");
        $stmtTotal->execute([
            ':property_id'  => $propertyId,
            ':room_type_id' => $roomTypeId,
        ]);
        $totalRooms = (int)$stmtTotal->fetchColumn();

        $startTs = strtotime($checkIn);
        $endTs   = strtotime($checkOut);
        if (!$startTs || !$endTs || $endTs <= $startTs) {
            return [
                'total_rooms'     => $totalRooms,
                'occupied_rooms'  => 0,
                'available_rooms' => $totalRooms,
            ];
        }

        $days = [];
        for ($ts = $startTs; $ts < $endTs; $ts = strtotime('+1 day', $ts)) {
            $days[] = date('Y-m-d', $ts);
        }

        $dayUnion = [];
        $params = [
            ':property_id'  => $propertyId,
            ':room_type_id' => $roomTypeId,
        ];
        foreach ($days as $i => $day) {
            $dayUnion[] = "SELECT :night_{$i} AS day_date";
            $params[":night_{$i}"] = $day;
        }

        $excludeSql = '';
        if ($excludeOrderId > 0) {
            $excludeSql = ' AND o.id <> :exclude_order_id ';
            $params[':exclude_order_id'] = $excludeOrderId;
        }

        $activeStatusSql = "
            LOWER(TRIM(CAST(COALESCE(o.status, 1) AS CHAR))) NOT IN
            ('0','cancelled','canceled','void','deleted','no_show','noshow')
        ";

        $stmtOcc = $pdo->prepare("
            SELECT COALESCE(MAX(occupied_rooms), 0)
            FROM (
                SELECT
                    d.day_date,
                    COUNT(DISTINCT r.id) AS occupied_rooms
                FROM (" . implode(" UNION ALL ", $dayUnion) . ") d
                LEFT JOIN rooms r
                    ON r.property_id = :property_id
                   AND r.room_type_id = :room_type_id
                LEFT JOIN order_rooming rmg
                    ON rmg.room_id = r.id
                   AND rmg.check_in <= d.day_date
                   AND rmg.check_out > d.day_date
                LEFT JOIN orders o
                    ON o.id = rmg.order_id
                   AND o.property_id = r.property_id
                   AND {$activeStatusSql}
                   {$excludeSql}
                WHERE o.id IS NOT NULL
                GROUP BY d.day_date
            ) x
        ");
        $stmtOcc->execute($params);
        $occupiedRooms = (int)$stmtOcc->fetchColumn();

        return [
            'total_rooms'     => $totalRooms,
            'occupied_rooms'  => $occupiedRooms,
            'available_rooms' => max(0, $totalRooms - $occupiedRooms),
        ];
    }
}

if (!function_exists('build_waitlist_url')) {
    function build_waitlist_url(array $params = []): string
    {
        $base = '/rooms/modules/reservations/waitlist.php';
        $query = http_build_query(array_filter($params, static function ($v) {
            return $v !== null && $v !== '';
        }));

        return $base . ($query !== '' ? ('?' . $query) : '');
    }
}

if (!function_exists('fetch_waitlist_alert_summary')) {
    function fetch_waitlist_alert_summary(PDO $pdo): array
    {
        if (!function_exists('waitlist_table_exists') || !waitlist_table_exists($pdo)) {
            return [
                'waiting_count'  => 0,
                'notified_count' => 0,
            ];
        }

        $sql = "
            SELECT
                SUM(CASE WHEN status = 'waiting' THEN 1 ELSE 0 END)  AS waiting_count,
                SUM(CASE WHEN status = 'notified' THEN 1 ELSE 0 END) AS notified_count
            FROM reservation_waitlist
            WHERE status IN ('waiting', 'notified')
        ";
        $row = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];

        return [
            'waiting_count'  => (int)($row['waiting_count'] ?? 0),
            'notified_count' => (int)($row['notified_count'] ?? 0),
        ];
    }
}

$properties = $pdo->query("
    SELECT id, name, code
    FROM properties
    WHERE is_active = 1
    ORDER BY name ASC
")->fetchAll(PDO::FETCH_ASSOC);

$selectedPropertyId = (int)($_GET['property_id'] ?? 0);
if ($selectedPropertyId <= 0 && !empty($properties)) {
    $selectedPropertyId = (int)$properties[0]['id'];
}

$dateFrom  = trim((string)($_GET['from'] ?? date('Y-m-d', strtotime('-10 days'))));
$dateTo    = trim((string)($_GET['to'] ?? date('Y-m-d', strtotime('+20 days'))));
$focusRoom = (int)($_GET['room_id'] ?? 0);
$filterSearch = trim((string)($_GET['q'] ?? ''));
$filterStatus = planner_normalize_booking_status(trim((string)($_GET['booking_status'] ?? '')));
$filterAgentId = (int)($_GET['agent_id'] ?? 0);

$isFolioControlAjax = (isset($_GET['folio_control']) && (string)$_GET['folio_control'] === '1');
$isAjaxSearch = (isset($_GET['ajax_search']) && (string)$_GET['ajax_search'] === '1');
$isPlannerJsonRequest = $isFolioControlAjax || $isAjaxSearch;


$fromTs = strtotime($dateFrom);
$toTs   = strtotime($dateTo);

if (!$fromTs) {
    $fromTs = strtotime(date('Y-m-d'));
    $dateFrom = date('Y-m-d', $fromTs);
}
if (!$toTs || $toTs < $fromTs) {
    $toTs = strtotime('+13 days', $fromTs);
    $dateTo = date('Y-m-d', $toTs);
}

$days = [];
$cursor = $fromTs;
while ($cursor <= $toTs) {
    $days[] = [
        'date' => date('Y-m-d', $cursor),
        'day' => date('D', $cursor),
        'num' => date('d', $cursor),
        'mon' => date('M', $cursor),
        'is_today' => date('Y-m-d') === date('Y-m-d', $cursor),
    ];
    $cursor = strtotime('+1 day', $cursor);
}

$totalDays = count($days);
$defaultDayWidth = 42;
$dayWidth = $defaultDayWidth;
$roomLabelWidth = 190;
$fitToScreenRange = ($totalDays > 0 && $totalDays <= 31);
$rowMinWidth = $roomLabelWidth + ($totalDays * $dayWidth);
$calendarFitMode = $fitToScreenRange ? 'fit' : 'scroll';

$rooms = [];
if ($selectedPropertyId > 0 && !$isPlannerJsonRequest) {
    $stmtRooms = $pdo->prepare("
        SELECT
            r.id,
            r.property_id,
            r.room_no,
            r.status,
            r.room_type_id,
            rt.title AS room_type,
            rt.higher_occupancy AS room_higher_occupancy,
            p.name AS property_name,
            p.code AS property_code
        FROM rooms r
        LEFT JOIN room_types rt ON rt.id = r.room_type_id
        LEFT JOIN properties p ON p.id = r.property_id
        WHERE r.property_id = :property_id
        ORDER BY r.floor_id ASC, r.room_no ASC
    ");
    $stmtRooms->execute([':property_id' => $selectedPropertyId]);
    $rooms = $stmtRooms->fetchAll(PDO::FETCH_ASSOC);
}

$roomTypeSummaries = [];
if ($selectedPropertyId > 0 && !$isPlannerJsonRequest) {
    $stmtRoomTypes = $pdo->prepare("
        SELECT
            rt.id,
            rt.title,
            rt.higher_occupancy,
            COUNT(r.id) AS total_rooms
        FROM room_types rt
        LEFT JOIN rooms r
          ON r.room_type_id = rt.id
         AND r.property_id = rt.property_id
        WHERE rt.property_id = :property_id
        GROUP BY rt.id, rt.title, rt.higher_occupancy
        ORDER BY rt.title ASC
    ");
    $stmtRoomTypes->execute([':property_id' => $selectedPropertyId]);
    $roomTypeRows = $stmtRoomTypes->fetchAll(PDO::FETCH_ASSOC);

    foreach ($roomTypeRows as $typeRow) {
        $availability = count_available_rooms_by_type(
            $pdo,
            $selectedPropertyId,
            (int)$typeRow['id'],
            $dateFrom,
            $dateTo
        );

        $roomTypeSummaries[] = [
            'id'              => (int)$typeRow['id'],
            'title'           => (string)$typeRow['title'],
            'higher_occupancy'=> (int)($typeRow['higher_occupancy'] ?? 0),
            'total_rooms'     => (int)($availability['total_rooms'] ?? 0),
            'occupied_rooms'  => (int)($availability['occupied_rooms'] ?? 0),
            'available_rooms' => (int)($availability['available_rooms'] ?? 0),
            'book_url'        => '/rooms/modules/reservations/reservation_form.php?' . http_build_query([
                'property_id'  => $selectedPropertyId,
                'room_type_id' => (int)$typeRow['id'],
                'check_in'     => $dateFrom,
                'check_out'    => $dateTo,
            ]),
            'waitlist_url'    => build_waitlist_url([
                'property_id'  => $selectedPropertyId,
                'room_type_id' => (int)$typeRow['id'],
                'check_in'     => $dateFrom,
                'check_out'    => $dateTo,
                'totalrooms'   => 1,
                'adults'       => 2,
                'kids'         => 0,
                'source'       => 'availability_planner',
            ]),
        ];
    }
}

$bookings = [];

$orderBookingSourceSql  = planner_column_exists($pdo, 'orders', 'booking_source') ? "o.booking_source" : "NULL";
$orderPaymentDueSql     = planner_column_exists($pdo, 'orders', 'payment_due') ? "o.payment_due" : "NULL";
$orderPaymentStatusSql  = planner_column_exists($pdo, 'orders', 'payment_status') ? "o.payment_status" : "NULL";
$orderInvoiceNumberSql  = planner_column_exists($pdo, 'orders', 'invoice_number') ? "o.invoice_number" : "NULL";
$orderInvoicedSql       = planner_column_exists($pdo, 'orders', 'invoiced') ? "o.invoiced" : "0";

$hasAgentsTable = planner_table_exists($pdo, 'agents');
$hasOrderAgentId = planner_column_exists($pdo, 'orders', 'agent_id');
$hasAgentFirstname = $hasAgentsTable && planner_column_exists($pdo, 'agents', 'firstname');
$hasAgentLastname  = $hasAgentsTable && planner_column_exists($pdo, 'agents', 'lastname');
$hasAgentName      = $hasAgentsTable && planner_column_exists($pdo, 'agents', 'name');
$hasAgentAgent     = $hasAgentsTable && planner_column_exists($pdo, 'agents', 'Agent');

if ($hasAgentsTable && $hasOrderAgentId && $hasAgentFirstname && $hasAgentLastname) {
    $agentNameSql = "TRIM(CONCAT(COALESCE(a.firstname,''), ' ', COALESCE(a.lastname,'')))";
} elseif ($hasAgentsTable && $hasOrderAgentId && $hasAgentName) {
    $agentNameSql = "a.name";
} elseif ($hasAgentsTable && $hasOrderAgentId && $hasAgentAgent) {
    $agentNameSql = "a.Agent";
} else {
    $agentNameSql = "''";
}

$agentJoinSql = ($hasAgentsTable && $hasOrderAgentId)
    ? "LEFT JOIN agents a ON a.id = o.agent_id"
    : "";

$orderAgentIdSql = $hasOrderAgentId ? "o.agent_id" : "NULL";

$agentSelectSql = "'' AS agent_name";
if ($hasAgentsTable) {
    if ($hasAgentFirstname && $hasAgentLastname) {
        $agentSelectSql = "TRIM(CONCAT(COALESCE(firstname,''), ' ', COALESCE(lastname,''))) AS agent_name";
    } elseif ($hasAgentName) {
        $agentSelectSql = "name AS agent_name";
    } elseif ($hasAgentAgent) {
        $agentSelectSql = "Agent AS agent_name";
    }
}

$agents = [];
if ($hasAgentsTable && $hasOrderAgentId) {
    try {
        $agents = $pdo->query("SELECT id, {$agentSelectSql} FROM agents ORDER BY agent_name ASC")->fetchAll(PDO::FETCH_ASSOC);
    } catch (Throwable $e) {
        $agents = [];
    }
}

$folioNumbersSql = "''";
if (
    planner_table_exists($pdo, 'folios')
    && planner_column_exists($pdo, 'folios', 'order_id')
    && planner_column_exists($pdo, 'folios', 'folio_no')
) {
    /*
       Tooltip/search folio fix:
       For group bookings there can be many room and guest folios under the same order.
       The old query pulled every folio for the whole booking on every room bar, so the
       popup repeated many folio numbers.

       Now each room bar only shows:
       - active Master folio(s), and
       - active Room folio(s) for the current order_rooming row.
       Guest folios are intentionally excluded from the hover tooltip because a group room
       can have several guest folios and they clutter/repeat the popup.
    */
    $folioNumbersSql = "(
        SELECT GROUP_CONCAT(DISTINCT f.folio_no ORDER BY
            CASE
                WHEN LOWER(CAST(f.folio_type AS CHAR)) = 'master' THEN 1
                WHEN LOWER(CAST(f.folio_type AS CHAR)) = 'room' THEN 2
                ELSE 9
            END,
            f.id
            SEPARATOR ', '
        )
        FROM folios f
        WHERE f.order_id = o.id
          AND COALESCE(f.status,1) = 1
          AND (
                LOWER(CAST(f.folio_type AS CHAR)) = 'master'
                OR (
                    LOWER(CAST(f.folio_type AS CHAR)) = 'room'
                    AND COALESCE(f.rooming_id,0) = rmg.id
                )
          )
    )";
}

$folioBalanceSql = "0";
if (
    planner_table_exists($pdo, 'folios')
    && planner_column_exists($pdo, 'folios', 'order_id')
    && planner_column_exists($pdo, 'folios', 'balance')
) {
    $folioBalanceSql = "(SELECT COALESCE(SUM(f.balance), 0) FROM folios f WHERE f.order_id = o.id)";
}

$statusCodeSql = "
    CASE
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('1','confirmed','confirm','reserved') THEN 'CONFIRMED'
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('2','pending','provisional','draft') THEN 'PROVISIONAL'
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('3','checked_in','checkin','inhouse','in-house') THEN 'CHECKED_IN'
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('4','checked_out','checkout','completed') THEN 'CHECKED_OUT'
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('0','cancelled','canceled') THEN 'CANCELLED'
        WHEN LOWER(TRIM(CAST(o.status AS CHAR))) IN ('noshow','no_show') THEN 'NO_SHOW'
        ELSE UPPER(TRIM(CAST(o.status AS CHAR)))
    END
";

;

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && isset($_POST['folio_action'])) {
    while (ob_get_level() > 0) { @ob_end_clean(); }
    header('Content-Type: application/json; charset=utf-8');

    try {
        $action = (string)($_POST['folio_action'] ?? '');

        /* Check-in/check-out are intentionally handled by checkin.php and checkout.php.
           The Folio Control Centre only opens those full workflow screens. */

        if ($action === 'move_folio_transaction') {
            $orderId = (int)($_POST['order_id'] ?? 0);
            $txId = (int)($_POST['transaction_id'] ?? 0);
            $targetFolioId = (int)($_POST['target_folio_id'] ?? 0);
            $reason = trim((string)($_POST['move_reason'] ?? ''));

            $pdo->beginTransaction();
            planner_move_folio_transaction($pdo, $orderId, $txId, $targetFolioId, $reason);
            $pdo->commit();

            echo json_encode([
                'ok' => true,
                'message' => 'Transaction moved successfully.',
            ]);
            exit;
        }

        if ($action === 'void_payment') {
            $orderId = (int)($_POST['order_id'] ?? 0);
            $txId = (int)($_POST['transaction_id'] ?? 0);
            $adminPin = trim((string)($_POST['admin_pin'] ?? ''));
            $reason = trim((string)($_POST['void_reason'] ?? ''));

            $pdo->beginTransaction();
            planner_void_payment_transaction($pdo, $orderId, $txId, $adminPin, $reason);
            $pdo->commit();

            echo json_encode([
                'ok' => true,
                'message' => 'Payment voided successfully.',
            ]);
            exit;
        }

        if ($action === 'void_charge') {
            $orderId = (int)($_POST['order_id'] ?? 0);
            $txId = (int)($_POST['transaction_id'] ?? 0);
            $adminPin = trim((string)($_POST['admin_pin'] ?? ''));
            $reason = trim((string)($_POST['void_reason'] ?? ''));

            $pdo->beginTransaction();
            planner_void_charge_transaction($pdo, $orderId, $txId, $adminPin, $reason);
            $pdo->commit();

            echo json_encode([
                'ok' => true,
                'message' => 'Charge voided successfully.',
            ]);
            exit;
        }

        throw new RuntimeException('Unknown folio action.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        http_response_code(500);
        echo json_encode([
            'ok' => false,
            'message' => $e->getMessage(),
        ]);
    }
    exit;
}


if (isset($_GET['preview_restaurant_charge']) && (string)$_GET['preview_restaurant_charge'] === '1') {
    while (ob_get_level() > 0) { @ob_end_clean(); }
    header('Content-Type: application/json; charset=utf-8');

    try {
        $restaurantOrderId = (int)($_GET['source_id'] ?? 0);
        if ($restaurantOrderId <= 0) {
            throw new RuntimeException('Invalid restaurant charge reference.');
        }
        if (!planner_table_exists($pdo, 'restaurant_orders') || !planner_table_exists($pdo, 'restaurant_order_items')) {
            throw new RuntimeException('Restaurant module tables were not found.');
        }

        $stmtRo = $pdo->prepare("SELECT * FROM restaurant_orders WHERE id = :id LIMIT 1");
        $stmtRo->execute([':id' => $restaurantOrderId]);
        $ro = $stmtRo->fetch(PDO::FETCH_ASSOC);
        if (!$ro) {
            throw new RuntimeException('That restaurant order could not be found - it may have been removed.');
        }

        $itemStatusCol = planner_column_exists($pdo, 'restaurant_order_items', 'status') ? 'status' : '';
        $itemSql = "
            SELECT item_name, qty, unit_price, line_total, notes" . ($itemStatusCol !== '' ? ", {$itemStatusCol} AS item_status" : "") . "
            FROM restaurant_order_items
            WHERE restaurant_order_id = :rid
            ORDER BY id ASC
        ";
        $stmtItems = $pdo->prepare($itemSql);
        $stmtItems->execute([':rid' => $restaurantOrderId]);
        $items = [];
        foreach ($stmtItems->fetchAll(PDO::FETCH_ASSOC) as $it) {
            $isVoid = isset($it['item_status']) && strtolower((string)$it['item_status']) === 'void';
            $items[] = [
                'name' => (string)($it['item_name'] ?? 'Item'),
                'qty' => (float)($it['qty'] ?? 1),
                'unit_price' => (float)($it['unit_price'] ?? 0),
                'line_total' => (float)($it['line_total'] ?? 0),
                'notes' => (string)($it['notes'] ?? ''),
                'is_void' => $isVoid,
            ];
        }

        echo json_encode([
            'ok' => true,
            'order' => [
                'order_no' => (string)($ro['order_no'] ?? ('#' . $restaurantOrderId)),
                'guest_name' => (string)($ro['guest_name'] ?? ''),
                'room_no' => (string)($ro['room_no'] ?? ''),
                'table_id' => (string)($ro['table_id'] ?? ''),
                'opened_at' => planner_display_date($ro['opened_at'] ?? '', 'd M Y H:i'),
                'closed_at' => planner_display_date($ro['closed_at'] ?? '', 'd M Y H:i'),
                'status' => (string)($ro['status'] ?? ''),
                'total_amount' => (float)($ro['total_amount'] ?? 0),
            ],
            'items' => $items,
        ]);
    } catch (Throwable $e) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'message' => $e->getMessage()]);
    }
    exit;
}

if (isset($_GET['export_rooming_list']) && (string)$_GET['export_rooming_list'] === '1') {
    while (ob_get_level() > 0) { @ob_end_clean(); }

    try {
        $orderId = (int)($_GET['order_id'] ?? 0);
        if ($orderId <= 0) {
            throw new RuntimeException('Invalid booking selected.');
        }

        $guestNameSql = "''";
        if (planner_table_exists($pdo, 'guests') && planner_column_exists($pdo, 'orders', 'guest_id')) {
            $guestNameSql = "(SELECT TRIM(CONCAT(COALESCE(g.firstname,''), ' ', COALESCE(g.lastname,''))) FROM guests g WHERE g.id = o.guest_id LIMIT 1)";
        }

        $orderSql = "
            SELECT
                o.*,
                p.name AS property_name,
                p.code AS property_code,
                {$guestNameSql} AS guest_name,
                {$agentNameSql} AS agent_name
            FROM orders o
            LEFT JOIN properties p ON p.id = o.property_id
            {$agentJoinSql}
            WHERE o.id = :order_id
            LIMIT 1
        ";
        $stmt = $pdo->prepare($orderSql);
        $stmt->execute([':order_id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) {
            throw new RuntimeException('Booking not found.');
        }

        $roomingResult = planner_build_group_rooming_list($pdo, $order, $orderId);
        $rooming = $roomingResult['rooming'];

        /* Room Numbers in order: natural sort (so "2" comes before "10"), unassigned rooms last. */
        usort($rooming, function ($a, $b) {
            $roomA = trim((string)($a['room_no'] ?? ''));
            $roomB = trim((string)($b['room_no'] ?? ''));
            if ($roomA === '' && $roomB === '') return 0;
            if ($roomA === '') return 1;
            if ($roomB === '') return -1;
            return strnatcasecmp($roomA, $roomB);
        });

        $groupName = trim((string)($order['group_name'] ?? ''));
        $propertyLabel = trim((string)($order['property_name'] ?? '') . (!empty($order['property_code']) ? ' (' . $order['property_code'] . ')' : ''));
        $title = $groupName !== '' ? $groupName : ('Booking ' . (string)($order['order_no'] ?? ('#' . $orderId)));
        $checkInLabel = planner_display_date($order['check_in'] ?? '');
        $checkOutLabel = planner_display_date($order['check_out'] ?? '');

        $filename = 'Rooming_List_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', $title) . '_' . date('Ymd_His') . '.xls';

        header('Content-Type: application/vnd.ms-excel; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Cache-Control: max-age=0');

        echo "\xEF\xBB\xBF"; // UTF-8 BOM so Excel renders accented characters correctly

        $esc = static function ($v) {
            return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
        };

        echo "<html><head><meta charset=\"UTF-8\"></head><body>";
        echo "<table border=\"1\" cellspacing=\"0\" cellpadding=\"4\">";

        echo "<tr><td colspan=\"9\" style=\"font-size:16px;font-weight:bold;\">" . $esc($title) . " - Rooming List</td></tr>";
        echo "<tr><td colspan=\"9\">" . $esc($propertyLabel) . " &nbsp; " . $esc($checkInLabel) . " &rarr; " . $esc($checkOutLabel) . " &nbsp; Exported " . $esc(date('d M Y H:i')) . "</td></tr>";
        echo "<tr><td colspan=\"9\"></td></tr>";

        echo "<tr style=\"background:#1f2937;color:#ffffff;font-weight:bold;\">"
            . "<td>Room No.</td>"
            . "<td>Primary</td>"
            . "<td>Room Configuration</td>"
            . "<td>Guest Name(s)</td>"
            . "<td>Adults</td>"
            . "<td>Children</td>"
            . "<td>Check In</td>"
            . "<td>Check Out</td>"
            . "<td>Booking Ref</td>"
            . "</tr>";

        if (!$rooming) {
            echo "<tr><td colspan=\"9\">No rooms found for this booking.</td></tr>";
        }

        foreach ($rooming as $r) {
            $guests = trim((string)($r['guests'] ?? ''));
            $guestsDisplay = $guests !== '' ? $guests : 'Not yet captured (pre-checkin pending)';
            $roomNoDisplay = trim((string)($r['room_no'] ?? '')) !== '' ? $r['room_no'] : 'Unassigned';

            echo "<tr>"
                . "<td style=\"font-weight:bold;\">" . $esc($roomNoDisplay) . "</td>"
                . "<td>" . (!empty($r['is_primary']) ? 'PRIMARY' : '') . "</td>"
                . "<td>" . $esc($r['room_type'] ?? '') . "</td>"
                . "<td>" . $esc($guestsDisplay) . "</td>"
                . "<td>" . (int)($r['adults'] ?? 0) . "</td>"
                . "<td>" . (int)($r['children'] ?? 0) . "</td>"
                . "<td>" . $esc($r['check_in'] ?? '') . "</td>"
                . "<td>" . $esc($r['check_out'] ?? '') . "</td>"
                . "<td>" . $esc($r['order_no'] ?? '') . "</td>"
                . "</tr>";
        }

        echo "</table></body></html>";
    } catch (Throwable $e) {
        header('Content-Type: text/plain; charset=UTF-8');
        echo 'Could not export rooming list: ' . $e->getMessage();
    }
    exit;
}

if (!function_exists('calendar_center_text')) {
    function calendar_center_text(string $text, int $width = 32): string
    {
        $text = trim($text);
        $padding = max(0, (int)floor(($width - strlen($text)) / 2));
        return str_repeat(' ', $padding) . $text . "\n";
    }
}

if (!function_exists('calendar_money_line')) {
    function calendar_money_line(string $label, float $amount, int $width = 42): string
    {
        $value = number_format($amount, 2);
        $space = max(1, $width - strlen($label) - strlen($value));
        return $label . str_repeat(' ', $space) . $value;
    }
}

if (!function_exists('calendar_build_thermal_ticket')) {
    function calendar_build_thermal_ticket(string $payload): string
    {
        /* Same ESC/POS wrapping convention already used by the restaurant module's
           PrintNode tickets: bold section markers, cut command, feed at the end. */
        $out = "\x1B\x40" . "\x1B\x74\x00" . "\x1C\x2E";
        foreach (explode("\n", $payload) as $line) {
            $line = preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', (string)$line);
            $trimmed = trim($line);
            if (strpos($trimmed, '***') === 0 || strpos($trimmed, '=====') === 0) {
                $out .= "\x1B\x45\x01" . "\x1B\x61\x01" . $trimmed . "\n" . "\x1B\x45\x00" . "\x1B\x61\x00";
            } elseif ($trimmed === '') {
                $out .= "\n";
            } else {
                $out .= $line . "\n";
            }
        }
        $out .= "\n\n\n\n" . "\x1D\x56\x42\x00";
        return $out;
    }
}

if (!function_exists('calendar_printnode_send_raw')) {
    /*
       Reuses the restaurant module's already-configured PrintNode credentials
       whenever reachable, instead of guessing at an API key here. Tries a few
       plausible locations relative to this file since cal.php and
       planner.php may live in different module folders.
    */
    function calendar_printnode_send_raw(int $printerId, string $bytes, string $title): array
    {
        if (!function_exists('printnode_send_raw')) {
            foreach ([
                __DIR__ . '/printnode_print.php',
                __DIR__ . '/../restaurant/printnode_print.php',
                __DIR__ . '/../../modules/restaurant/printnode_print.php',
                __DIR__ . '/../../includes/printnode_print.php',
            ] as $candidate) {
                if (is_file($candidate)) {
                    require_once $candidate;
                    break;
                }
            }
        }

        if (function_exists('printnode_send_raw')) {
            return printnode_send_raw($printerId, $bytes, $title);
        }

        /* Fallback: call the PrintNode API directly if a key is configured
           somewhere reachable (constant, env var, or lodge_settings column). */
        $apiKey = '';
        if (defined('PRINTNODE_API_KEY')) {
            $apiKey = (string)constant('PRINTNODE_API_KEY');
        } elseif (getenv('PRINTNODE_API_KEY')) {
            $apiKey = (string)getenv('PRINTNODE_API_KEY');
        } else {
            global $pdo;
            try {
                if ($pdo instanceof PDO && planner_table_exists($pdo, 'lodge_settings') && planner_column_exists($pdo, 'lodge_settings', 'printnode_api_key')) {
                    $stmt = $pdo->query("SELECT printnode_api_key FROM lodge_settings WHERE id = 1 LIMIT 1");
                    $apiKey = trim((string)($stmt->fetchColumn() ?: ''));
                }
            } catch (Throwable $ignore) {
            }
        }

        if ($apiKey === '') {
            return [
                'ok' => false,
                'error' => 'PrintNode is not configured for this module (printnode_print.php not found and no PrintNode API key available). Ask your developer to either place printnode_print.php next to cal.php, or add a PRINTNODE_API_KEY.',
            ];
        }

        $ch = curl_init('https://api.printnode.com/printjobs');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_USERPWD => $apiKey . ':',
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => json_encode([
                'printerId' => $printerId,
                'title' => $title,
                'contentType' => 'raw_base64',
                'content' => base64_encode($bytes),
                'source' => 'PMS Calendar',
            ]),
            CURLOPT_TIMEOUT => 15,
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError !== '') {
            return ['ok' => false, 'error' => 'PrintNode connection failed: ' . $curlError];
        }
        if ($httpCode < 200 || $httpCode >= 300) {
            return ['ok' => false, 'error' => 'PrintNode rejected the print job (HTTP ' . $httpCode . '): ' . $response];
        }

        return ['ok' => true];
    }
}

if (isset($_GET['print_restaurant_charge']) && (string)$_GET['print_restaurant_charge'] === '1') {
    while (ob_get_level() > 0) { @ob_end_clean(); }
    header('Content-Type: application/json; charset=utf-8');

    try {
        $restaurantOrderId = (int)($_GET['source_id'] ?? 0);
        if ($restaurantOrderId <= 0) {
            throw new RuntimeException('Invalid restaurant charge reference.');
        }
        if (!planner_table_exists($pdo, 'restaurant_orders') || !planner_table_exists($pdo, 'restaurant_order_items')) {
            throw new RuntimeException('Restaurant module tables were not found.');
        }

        $stmtRo = $pdo->prepare("SELECT * FROM restaurant_orders WHERE id = :id LIMIT 1");
        $stmtRo->execute([':id' => $restaurantOrderId]);
        $ro = $stmtRo->fetch(PDO::FETCH_ASSOC);
        if (!$ro) {
            throw new RuntimeException('That restaurant order could not be found - it may have been removed.');
        }

        $itemStatusCol = planner_column_exists($pdo, 'restaurant_order_items', 'status') ? 'status' : '';
        $itemSql = "
            SELECT item_name, qty, unit_price, line_total, notes" . ($itemStatusCol !== '' ? ", {$itemStatusCol} AS item_status" : "") . "
            FROM restaurant_order_items
            WHERE restaurant_order_id = :rid
            ORDER BY id ASC
        ";
        $stmtItems = $pdo->prepare($itemSql);
        $stmtItems->execute([':rid' => $restaurantOrderId]);
        $items = $stmtItems->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $payload = "";
        $payload .= "***** BILL COPY *****\n";
        $payload .= calendar_center_text('Reception Reprint');
        $payload .= "Order: " . (string)($ro['order_no'] ?? ('#' . $restaurantOrderId)) . "\n";
        if (!empty($ro['guest_name'])) $payload .= "Guest: " . $ro['guest_name'] . "\n";
        if (!empty($ro['room_no'])) $payload .= "Room: " . $ro['room_no'] . "\n";
        $payload .= "Printed: " . date('Y-m-d H:i:s') . "\n";
        $payload .= "==========================================\n";

        $total = 0.0;
        foreach ($items as $it) {
            $isVoid = isset($it['item_status']) && strtolower((string)$it['item_status']) === 'void';
            if ($isVoid) continue;

            $qty = rtrim(rtrim(number_format((float)($it['qty'] ?? 1), 2, '.', ''), '0'), '.');
            $lineTotal = (float)($it['line_total'] ?? 0);
            $total += $lineTotal;
            $left = $qty . ' x ' . (string)($it['item_name'] ?? 'Item');
            $payload .= str_pad(substr($left, 0, 30), 31) . str_pad(number_format($lineTotal, 2), 10, ' ', STR_PAD_LEFT) . "\n";
            if (trim((string)($it['notes'] ?? '')) !== '') {
                $payload .= "   Note: " . $it['notes'] . "\n";
            }
        }

        $payload .= "------------------------------------------\n";
        $payload .= calendar_money_line('TOTAL', $total) . "\n";
        $payload .= "==========================================\n";
        $payload .= calendar_center_text('Thank you.');

        $printerId = defined('PRINTNODE_RECEPTION_PRINTER_ID') ? (int)constant('PRINTNODE_RECEPTION_PRINTER_ID') : 75588294;
        $bytes = calendar_build_thermal_ticket($payload);
        $result = calendar_printnode_send_raw($printerId, $bytes, 'Restaurant Bill - ' . (string)($ro['order_no'] ?? $restaurantOrderId));

        if (empty($result['ok'])) {
            throw new RuntimeException($result['error'] ?? 'Print job failed for an unknown reason.');
        }

        echo json_encode(['ok' => true, 'message' => 'Bill sent to reception thermal printer.']);
    } catch (Throwable $e) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'message' => $e->getMessage()]);
    }
    exit;
}

if (isset($_GET['folio_control']) && (string)$_GET['folio_control'] === '1') {
    while (ob_get_level() > 0) { @ob_end_clean(); }
    header('Content-Type: application/json; charset=utf-8');

    try {
        $orderId = (int)($_GET['order_id'] ?? 0);
        if ($orderId <= 0) {
            throw new RuntimeException('Invalid booking selected.');
        }

        $guestNameSql = "''";
        if (planner_table_exists($pdo, 'guests') && planner_column_exists($pdo, 'orders', 'guest_id')) {
            $guestNameSql = "(SELECT TRIM(CONCAT(COALESCE(g.firstname,''), ' ', COALESCE(g.lastname,''))) FROM guests g WHERE g.id = o.guest_id LIMIT 1)";
        }

        $orderSql = "
            SELECT
                o.*,
                p.name AS property_name,
                p.code AS property_code,
                {$guestNameSql} AS guest_name,
                {$agentNameSql} AS agent_name
            FROM orders o
            LEFT JOIN properties p ON p.id = o.property_id
            {$agentJoinSql}
            WHERE o.id = :order_id
            LIMIT 1
        ";
        $stmt = $pdo->prepare($orderSql);
        $stmt->execute([':order_id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) {
            throw new RuntimeException('Booking not found.');
        }

        $notes = [];
        $noteColumns = [
            'notes' => 'Reservation Notes',
            'note' => 'Note',
            'remarks' => 'Remarks',
            'internal_notes' => 'Internal Notes',
            'internal_memo' => 'Internal Memo',
            'special_requests' => 'Special Requests',
            'guest_notes' => 'Guest Notes',
            'housekeeping_notes' => 'Housekeeping Notes',
            'arrival_notes' => 'Arrival Notes',
            'departure_notes' => 'Departure Notes',
            'voucher' => 'Voucher',
            'reference' => 'Reference',
            'booking_ref' => 'Booking Reference',
            'payment_due' => 'Payment Due',
            'payment_status' => 'Payment Status',
        ];
        foreach ($noteColumns as $col => $label) {
            if (array_key_exists($col, $order)) {
                $val = trim((string)$order[$col]);
                if ($val !== '') {
                    $notes[] = ['label' => $label, 'value' => $val];
                }
            }
        }

        $housekeeping = [];

        $extraBedsRequired = 0;
        foreach (['extra_beds_required','extra_bed_required','extra_beds','extra_bed'] as $hkCol) {
            if (array_key_exists($hkCol, $order)) {
                $extraBedsRequired = (int)($order[$hkCol] ?? 0);
                break;
            }
        }
        if ($extraBedsRequired > 0) {
            $housekeeping[] = [
                'label' => 'Extra Bed' . ($extraBedsRequired > 1 ? 's' : ''),
                'value' => $extraBedsRequired . ' required',
                'icon'  => 'bi-person-plus',
            ];
        }

        $babyCotsRequired = 0;
        foreach (['cots_required','baby_cots_required','cot_required','baby_cot_required','cots','baby_cots'] as $hkCol) {
            if (array_key_exists($hkCol, $order)) {
                $babyCotsRequired = (int)($order[$hkCol] ?? 0);
                break;
            }
        }
        if ($babyCotsRequired > 0) {
            $housekeeping[] = [
                'label' => 'Baby Cot' . ($babyCotsRequired > 1 ? 's' : ''),
                'value' => $babyCotsRequired . ' required',
                'icon'  => 'bi-basket',
            ];
        }

        $wheelchairRequired = false;
        foreach (['accessibility_required','wheelchair_required','wheelchair_accessible_required','wheelchair_request','wheelchair'] as $hkCol) {
            if (array_key_exists($hkCol, $order)) {
                $raw = trim((string)($order[$hkCol] ?? ''));
                $wheelchairRequired = ((int)$raw === 1 || in_array(strtolower($raw), ['yes','y','true','required','wheelchair'], true));
                break;
            }
        }
        if ($wheelchairRequired) {
            $housekeeping[] = [
                'label' => 'Wheelchair / Accessibility',
                'value' => 'Required',
                'icon'  => 'bi-universal-access',
            ];
        }

        $interleadingRequired = false;
        foreach (['interleading_required','interleading_room_required','interleading_request','connecting_rooms_required'] as $hkCol) {
            if (array_key_exists($hkCol, $order)) {
                $raw = trim((string)($order[$hkCol] ?? ''));
                $interleadingRequired = ((int)$raw === 1 || in_array(strtolower($raw), ['yes','y','true','required'], true));
                break;
            }
        }
        if ($interleadingRequired) {
            $housekeeping[] = [
                'label' => 'Interleading / Connecting Rooms',
                'value' => 'Required',
                'icon'  => 'bi-door-open',
            ];
        }

        $roomingResult = planner_build_group_rooming_list($pdo, $order, $orderId);
        $rooming = $roomingResult['rooming'];
        $orderKidsNoCharge = $roomingResult['kids_no_charge_total'];

        $folios = [];
        $folioTotals = ['charges' => 0.0, 'payments' => 0.0, 'balance' => 0.0];

        if (planner_table_exists($pdo, 'folios') && planner_column_exists($pdo, 'folios', 'order_id')) {
            $stmt = $pdo->prepare("SELECT * FROM folios WHERE order_id = :order_id ORDER BY id ASC");
            $stmt->execute([':order_id' => $orderId]);
            $folioRows = $stmt->fetchAll(PDO::FETCH_ASSOC);

            $txByFolio = [];
            if ($folioRows && planner_table_exists($pdo, 'folio_transactions') && planner_column_exists($pdo, 'folio_transactions', 'folio_id')) {
                $folioIds = array_map(static fn($f) => (int)$f['id'], $folioRows);
                $placeholders = implode(',', array_fill(0, count($folioIds), '?'));

                $txSelect = ['ft.id', 'ft.folio_id'];
                foreach (['order_id','tx_type','description','debit','credit','created_at','transaction_date','source_table','source_id','is_voided','notes'] as $col) {
                    $txSelect[] = planner_column_exists($pdo, 'folio_transactions', $col) ? "ft.`{$col}`" : "NULL AS `{$col}`";
                }

                $stmtTx = $pdo->prepare("
                    SELECT " . implode(', ', $txSelect) . "
                    FROM folio_transactions ft
                    WHERE ft.folio_id IN ({$placeholders})
                    ORDER BY ft.folio_id ASC, ft.id ASC
                ");
                $stmtTx->execute($folioIds);
                foreach ($stmtTx->fetchAll(PDO::FETCH_ASSOC) as $tx) {
                    $fid = (int)($tx['folio_id'] ?? 0);
                    $txByFolio[$fid][] = [
                        'id' => (int)($tx['id'] ?? 0),
                        'date' => planner_display_date($tx['transaction_date'] ?? $tx['created_at'] ?? '', 'd M Y'),
                        'type' => (string)($tx['tx_type'] ?? ''),
                        'description' => (string)($tx['description'] ?? ''),
                        'debit' => (float)($tx['debit'] ?? 0),
                        'credit' => (float)($tx['credit'] ?? 0),
                        'is_voided' => (int)($tx['is_voided'] ?? 0),
                        'notes' => (string)($tx['notes'] ?? ''),
                        'source_table' => (string)($tx['source_table'] ?? ''),
                        'source_id' => (int)($tx['source_id'] ?? 0),
                    ];
                }
            }

            $roomingById = [];
            $roomNoByRoomingId = [];
            $guestsByRoomingId = [];
            foreach ($rooming as $rr) {
                $rid = (int)($rr['id'] ?? 0);
                if ($rid <= 0) {
                    continue;
                }
                $roomingById[$rid] = $rr;
                $roomNoByRoomingId[$rid] = trim((string)($rr['room_no'] ?? ''));
                $guestsByRoomingId[$rid] = trim((string)($rr['guests'] ?? ''));
            }

            foreach ($folioRows as $f) {
                $folioType = strtolower(trim((string)($f['folio_type'] ?? $f['type'] ?? $f['folio_scope'] ?? $f['account_type'] ?? '')));
                if ($folioType === '') {
                    $folioNo = strtolower((string)($f['folio_no'] ?? $f['folio_name'] ?? ''));
                    if (strpos($folioNo, 'master') !== false || strpos($folioNo, 'main') !== false) $folioType = 'master';
                    elseif (strpos($folioNo, 'guest') !== false) $folioType = 'guest';
                    elseif (strpos($folioNo, 'room') !== false) $folioType = 'room';
                    else $folioType = 'other';
                }

                $folioId = (int)($f['id'] ?? 0);
                $folioNoRaw = trim((string)($f['folio_no'] ?? $f['folio_name'] ?? ('Folio #' . $folioId)));
                $folioHolder = trim((string)($f['folio_holder'] ?? $f['holder_name'] ?? $f['guest_name'] ?? $f['occupant_name'] ?? ''));
                $folioRoomingId = (int)($f['rooming_id'] ?? $f['order_rooming_id'] ?? 0);

                if ($folioHolder === '' && planner_table_exists($pdo, 'order_rooming_guests')) {
                    $folioOccupantId = (int)($f['occupant_id'] ?? $f['guest_id'] ?? 0);
                    if ($folioOccupantId > 0) {
                        try {
                            if (planner_column_exists($pdo, 'order_rooming_guests', 'first_name') && planner_column_exists($pdo, 'order_rooming_guests', 'last_name')) {
                                $occNameExpr = "TRIM(CONCAT(COALESCE(first_name,''), ' ', COALESCE(last_name,'')))";
                            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'firstname') && planner_column_exists($pdo, 'order_rooming_guests', 'lastname')) {
                                $occNameExpr = "TRIM(CONCAT(COALESCE(firstname,''), ' ', COALESCE(lastname,'')))";
                            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'guest_name')) {
                                $occNameExpr = "guest_name";
                            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'name')) {
                                $occNameExpr = "name";
                            } else {
                                $occNameExpr = "''";
                            }
                            $stmtOcc = $pdo->prepare("SELECT {$occNameExpr} FROM order_rooming_guests WHERE id = :id LIMIT 1");
                            $stmtOcc->execute([':id' => $folioOccupantId]);
                            $folioHolder = trim((string)($stmtOcc->fetchColumn() ?: ''));
                        } catch (Throwable $e) {
                            $folioHolder = '';
                        }
                    }
                }
                $folioRoomNo = trim((string)($f['room_no'] ?? ''));
                $folioDisplayName = '';

                if ($folioRoomNo === '' && !empty($f['room_id']) && planner_table_exists($pdo, 'rooms')) {
                    try {
                        $stmtRoomNo = $pdo->prepare("SELECT room_no FROM rooms WHERE id = :room_id LIMIT 1");
                        $stmtRoomNo->execute([':room_id' => (int)$f['room_id']]);
                        $folioRoomNo = trim((string)($stmtRoomNo->fetchColumn() ?: ''));
                    } catch (Throwable $e) {
                        $folioRoomNo = '';
                    }
                }

                if ($folioRoomNo === '' && $folioRoomingId > 0 && !empty($roomNoByRoomingId[$folioRoomingId])) {
                    $folioRoomNo = $roomNoByRoomingId[$folioRoomingId];
                }

                if ($folioType === 'master' || $folioType === 'main') {
                    $folioDisplayName = 'Master Folio';
                } elseif ($folioType === 'room') {
                    $folioDisplayName = $folioRoomNo !== '' ? ('Room ' . $folioRoomNo) : 'Room Folio';

                    if ($folioRoomingId > 0 && !empty($guestsByRoomingId[$folioRoomingId])) {
                        $folioDisplayName .= ' - ' . $guestsByRoomingId[$folioRoomingId];
                    } elseif ($folioHolder !== '') {
                        $folioDisplayName .= ' - ' . $folioHolder;
                    }
                } elseif ($folioType === 'guest') {
                    $folioDisplayName = $folioHolder;

                    if ($folioDisplayName === '' && $folioRoomingId > 0 && !empty($guestsByRoomingId[$folioRoomingId])) {
                        $folioDisplayName = $guestsByRoomingId[$folioRoomingId];
                    }

                    if ($folioDisplayName === '') {
                        $folioDisplayName = 'Guest Folio';
                    }

                    if ($folioRoomNo !== '') {
                        $folioDisplayName .= ' (Room ' . $folioRoomNo . ')';
                    }
                } else {
                    $folioDisplayName = $folioHolder !== '' ? $folioHolder : 'Folio';
                    if ($folioRoomNo !== '') {
                        $folioDisplayName .= ' (Room ' . $folioRoomNo . ')';
                    }
                }

                $folioDisplay = $folioNoRaw;
                if ($folioDisplayName !== '') {
                    $folioDisplay .= ' | ' . $folioDisplayName;
                }

                $charges = (float)($f['total_charges'] ?? $f['charges'] ?? 0);
                $payments = (float)($f['total_payments'] ?? $f['payments'] ?? 0);
                $balance = (float)($f['balance'] ?? ($charges - $payments));

                $folioTotals['charges'] += $charges;
                $folioTotals['payments'] += $payments;
                $folioTotals['balance'] += $balance;

                $folios[] = [
                    'id' => $folioId,
                    'folio_no' => $folioNoRaw,
                    'folio_display' => $folioDisplay,
                    'display_name' => $folioDisplayName,
                    'room_no' => $folioRoomNo,
                    'rooming_id' => $folioRoomingId,
                    'order_rooming_id' => $folioRoomingId,
                    'type' => $folioType,
                    'holder' => $folioHolder,
                    'status' => (string)($f['status'] ?? ''),
                    'charges' => $charges,
                    'payments' => $payments,
                    'balance' => $balance,
                    'transactions' => $txByFolio[$folioId] ?? [],
                ];
            }
        }

        $roomNumbers = [];
        foreach ($rooming as $r) {
            if (trim((string)$r['room_no']) !== '') $roomNumbers[] = trim((string)$r['room_no']);
        }
        $roomNumbers = array_values(array_unique($roomNumbers));

        echo json_encode([
            'ok' => true,
            'booking' => [
                'order_id' => $orderId,
                'order_no' => (string)($order['order_no'] ?? ('#' . $orderId)),
                'guest_name' => (trim((string)($order['group_name'] ?? '')) !== ''
                    ? trim((string)$order['group_name'])
                    : trim((string)($order['guest_name'] ?? '')))
                    ?: ('Booking #' . $orderId),
                'kids_0_3_no_charge' => (int)$orderKidsNoCharge,
                'property' => trim((string)($order['property_name'] ?? '') . (!empty($order['property_code']) ? ' (' . (string)$order['property_code'] . ')' : '')),
                'check_in' => planner_display_date($order['check_in'] ?? ''),
                'check_out' => planner_display_date($order['check_out'] ?? ''),
                'raw_check_in' => (string)($order['check_in'] ?? ''),
                'raw_check_out' => (string)($order['check_out'] ?? ''),
                'agent_name' => trim((string)($order['agent_name'] ?? '')),
                'rooms' => $roomNumbers ? implode(', ', $roomNumbers) : 'Unassigned',
                'status' => planner_humanize_code($order['status'] ?? ''),
                'total_amount' => (float)($order['totalamount'] ?? $order['total_amount'] ?? 0),
                'actions' => planner_order_rooming_check_status($pdo, $orderId),
            ],
            'notes' => $notes,
            'housekeeping' => $housekeeping,
            'rooming' => $rooming,
            'folios' => $folios,
            'totals' => $folioTotals,
        ]);
    } catch (Throwable $e) {
        http_response_code(500);
        echo json_encode([
            'ok' => false,
            'message' => $e->getMessage(),
        ]);
    }
    exit;
}

if (isset($_GET['ajax_search']) && (string)$_GET['ajax_search'] === '1') {
    header('Content-Type: application/json; charset=utf-8');

    try {
        $ajaxPropertyId = (int)($_GET['property_id'] ?? 0);
        $ajaxFrom = trim((string)($_GET['from'] ?? ''));
        $ajaxTo = trim((string)($_GET['to'] ?? ''));
        $ajaxSearch = trim((string)($_GET['q'] ?? ''));
        $ajaxStatus = planner_normalize_booking_status(trim((string)($_GET['booking_status'] ?? '')));
        $ajaxAgentId = (int)($_GET['agent_id'] ?? 0);
        $ajaxRoomId = (int)($_GET['room_id'] ?? 0);

        $sqlAjax = "
            SELECT
                rmg.id AS rooming_id,
                rmg.order_id,
                rmg.property_id,
                rmg.room_no,
                rmg.room_id,
                rmg.check_in,
                rmg.check_out,
                COALESCE(rmg.checked_in_status,0) AS room_checked_in_status,
                COALESCE(rmg.checked_out_status,0) AS room_checked_out_status,
                o.order_no,
                o.status,
                {$statusCodeSql} AS status_code,
                {$orderPaymentDueSql} AS payment_due,
                {$orderPaymentStatusSql} AS payment_status,
                {$orderInvoiceNumberSql} AS invoice_number,
                {$folioBalanceSql} AS balance,
                {$agentNameSql} AS agent_name,
                {$folioNumbersSql} AS folio_numbers,
                g.firstname,
                g.lastname,
                o.group_name,
                rt.title AS room_type,
                p.name AS property_name,
                p.code AS property_code
            FROM order_rooming rmg
            INNER JOIN orders o ON o.id = rmg.order_id
            LEFT JOIN guests g ON g.id = o.guest_id
            {$agentJoinSql}
            LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
            LEFT JOIN properties p ON p.id = o.property_id
            WHERE COALESCE(o.status, 1) <> 0
        ";

        $ajaxParams = [];

        if ($ajaxPropertyId > 0) {
            $sqlAjax .= " AND rmg.property_id = :ajax_property_id ";
            $ajaxParams[':ajax_property_id'] = $ajaxPropertyId;
        }

        if ($ajaxFrom !== '' && $ajaxTo !== '') {
            $sqlAjax .= " AND rmg.check_in < DATE_ADD(:ajax_to_date, INTERVAL 1 DAY) AND rmg.check_out > :ajax_from_date ";
            $ajaxParams[':ajax_from_date'] = $ajaxFrom;
            $ajaxParams[':ajax_to_date'] = $ajaxTo;
        } elseif ($ajaxFrom !== '') {
            $sqlAjax .= " AND rmg.check_out > :ajax_from_date_only ";
            $ajaxParams[':ajax_from_date_only'] = $ajaxFrom;
        } elseif ($ajaxTo !== '') {
            $sqlAjax .= " AND rmg.check_in < DATE_ADD(:ajax_to_date_only, INTERVAL 1 DAY) ";
            $ajaxParams[':ajax_to_date_only'] = $ajaxTo;
        }

        if ($ajaxStatus !== '') {
            $sqlAjax .= " AND ({$statusCodeSql}) = :ajax_status ";
            $ajaxParams[':ajax_status'] = $ajaxStatus;
        }

        if ($ajaxAgentId > 0 && $hasOrderAgentId) {
            $sqlAjax .= " AND o.agent_id = :ajax_agent_id ";
            $ajaxParams[':ajax_agent_id'] = $ajaxAgentId;
        }

        if ($ajaxRoomId > 0) {
            $sqlAjax .= " AND rmg.room_id = :ajax_room_id ";
            $ajaxParams[':ajax_room_id'] = $ajaxRoomId;
        }

        if ($ajaxSearch !== '') {
            $ajaxLike = '%' . $ajaxSearch . '%';
            $sqlAjax .= " AND (
                o.order_no LIKE :ajax_q_order_no
                OR COALESCE(g.firstname, '') LIKE :ajax_q_firstname
                OR COALESCE(g.lastname, '') LIKE :ajax_q_lastname
                OR CONCAT(COALESCE(g.firstname,''), ' ', COALESCE(g.lastname,'')) LIKE :ajax_q_guest_name
                OR COALESCE({$folioNumbersSql}, '') LIKE :ajax_q_folio
                OR COALESCE({$orderPaymentDueSql}, '') LIKE :ajax_q_payment_due
                OR COALESCE({$orderPaymentStatusSql}, '') LIKE :ajax_q_payment_status
                OR COALESCE({$orderInvoiceNumberSql}, '') LIKE :ajax_q_invoice
                OR COALESCE({$agentNameSql}, '') LIKE :ajax_q_agent
                OR COALESCE(rt.title, '') LIKE :ajax_q_room_type
                OR CAST(rmg.room_no AS CHAR) LIKE :ajax_q_room_no
                OR ({$statusCodeSql}) LIKE :ajax_q_status
            ) ";
            $ajaxParams[':ajax_q_order_no'] = $ajaxLike;
            $ajaxParams[':ajax_q_firstname'] = $ajaxLike;
            $ajaxParams[':ajax_q_lastname'] = $ajaxLike;
            $ajaxParams[':ajax_q_guest_name'] = $ajaxLike;
            $ajaxParams[':ajax_q_folio'] = $ajaxLike;
            $ajaxParams[':ajax_q_payment_due'] = $ajaxLike;
            $ajaxParams[':ajax_q_payment_status'] = $ajaxLike;
            $ajaxParams[':ajax_q_invoice'] = $ajaxLike;
            $ajaxParams[':ajax_q_agent'] = $ajaxLike;
            $ajaxParams[':ajax_q_room_type'] = $ajaxLike;
            $ajaxParams[':ajax_q_room_no'] = $ajaxLike;
            $ajaxParams[':ajax_q_status'] = $ajaxLike;
        }

        $sqlAjax .= " ORDER BY rmg.check_in DESC, rmg.order_id DESC, rmg.room_no ASC LIMIT 80 ";

        $stmtAjax = $pdo->prepare($sqlAjax);
        $stmtAjax->execute($ajaxParams);
        $rows = $stmtAjax->fetchAll(PDO::FETCH_ASSOC);

        $items = [];
        foreach ($rows as $row) {
            $groupName = trim((string)($row['group_name'] ?? ''));
            $guestName = $groupName !== '' ? $groupName : trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
            if ($guestName === '') {
                $guestName = 'Booking #' . (int)($row['order_id'] ?? 0);
            }

            [$statusLabel] = planner_status_meta($row['status_code'] ?? $row['status'] ?? '');

            $items[] = [
                'order_id' => (int)($row['order_id'] ?? 0),
                'rooming_id' => (int)($row['rooming_id'] ?? 0),
                'guest_name' => $guestName,
                'checked_in_guest_names' => trim((string)($row['checked_in_guest_names'] ?? '')),
                'order_no' => (string)($row['order_no'] ?? ''),
                'folio_numbers' => planner_clean_folio_numbers($row['folio_numbers'] ?? ''),
                'agent_name' => (string)($row['agent_name'] ?? ''),
                'property_name' => trim((string)($row['property_name'] ?? '') . (!empty($row['property_code']) ? ' (' . (string)$row['property_code'] . ')' : '')),
                'room_label' => 'Room ' . (string)($row['room_no'] ?? '') . (!empty($row['room_type']) ? ' - ' . (string)$row['room_type'] : ''),
                'check_in' => planner_display_date($row['check_in'] ?? ''),
                'check_out' => planner_display_date($row['check_out'] ?? ''),
                'status_label' => $statusLabel,
                'payment_due' => planner_payment_due_label($row['payment_due'] ?? ''),
                'payment_status' => planner_payment_status_label($row['payment_status'] ?? ''),
                'balance' => number_format((float)($row['balance'] ?? 0), 2),
                'open_url' => '/rooms/modules/reservations/reservation_form.php?id=' . (int)($row['order_id'] ?? 0),
            ];
        }

        echo json_encode([
            'ok' => true,
            'count' => count($items),
            'items' => $items,
        ]);
    } catch (Throwable $e) {
        http_response_code(500);
        echo json_encode([
            'ok' => false,
            'message' => $e->getMessage(),
        ]);
    }
    exit;
}

if ($selectedPropertyId > 0) {
    $roomGuestSearchSql = "NULL";
    if (planner_table_exists($pdo, 'order_rooming_guests')) {
        $roomingGuestLinkColumnSearch = '';
        foreach (['order_rooming_id', 'rooming_id'] as $linkCol) {
            if (planner_column_exists($pdo, 'order_rooming_guests', $linkCol)) {
                $roomingGuestLinkColumnSearch = $linkCol;
                break;
            }
        }
        if ($roomingGuestLinkColumnSearch !== '') {
            if (planner_column_exists($pdo, 'order_rooming_guests', 'first_name') && planner_column_exists($pdo, 'order_rooming_guests', 'last_name')) {
                $nameExprSearch = "TRIM(CONCAT(COALESCE(org.first_name,''), ' ', COALESCE(org.last_name,'')))";
            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'firstname') && planner_column_exists($pdo, 'order_rooming_guests', 'lastname')) {
                $nameExprSearch = "TRIM(CONCAT(COALESCE(org.firstname,''), ' ', COALESCE(org.lastname,'')))";
            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'guest_name')) {
                $nameExprSearch = "org.guest_name";
            } elseif (planner_column_exists($pdo, 'order_rooming_guests', 'name')) {
                $nameExprSearch = "org.name";
            } else {
                $nameExprSearch = "''";
            }
            $roomGuestSearchSql = "(SELECT GROUP_CONCAT(NULLIF(TRIM({$nameExprSearch}), '') ORDER BY org.id SEPARATOR ', ') FROM order_rooming_guests org WHERE org.`{$roomingGuestLinkColumnSearch}` = rmg.id)";
        }
    }

    $sqlBookings = "
        SELECT
            rmg.id AS rooming_id,
            rmg.order_id,
            rmg.property_id,
            rmg.room_no,
            rmg.room_id,
            rmg.room_type_id,
            rmg.adults,
            rmg.children,
            rmg.check_in,
            rmg.check_out,
            COALESCE(rmg.checked_in_status,0) AS room_checked_in_status,
            COALESCE(rmg.checked_out_status,0) AS room_checked_out_status,
            o.order_no,
            o.expected_arrival_time,
            o.extra_beds_required,
            o.cots_required,
            o.accessibility_required,
            o.interleading_required,            
            o.status,
            {$statusCodeSql} AS status_code,
            {$orderBookingSourceSql} AS booking_source,
            {$orderPaymentDueSql} AS payment_due,
            {$orderPaymentStatusSql} AS payment_status,
            {$orderInvoiceNumberSql} AS invoice_number,
            {$orderInvoicedSql} AS invoiced,
            {$folioBalanceSql} AS balance,
            {$agentNameSql} AS agent_name,
            {$orderAgentIdSql} AS agent_id,
            {$folioNumbersSql} AS folio_numbers,
            o.totalamount,
            g.firstname,
            g.lastname,
            o.group_name,
            rt.title AS room_type,
            rt.higher_occupancy AS room_higher_occupancy,
            p.name AS property_name,
            p.code AS property_code,
            {$roomGuestSearchSql} AS checked_in_guest_names
        FROM order_rooming rmg
        INNER JOIN orders o ON o.id = rmg.order_id
        LEFT JOIN guests g ON g.id = o.guest_id
        {$agentJoinSql}
        LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
        LEFT JOIN properties p ON p.id = o.property_id
        WHERE rmg.property_id = :property_id
          AND COALESCE(o.status, 1) <> 0
          AND rmg.check_in < DATE_ADD(:toDate, INTERVAL 1 DAY)
          AND rmg.check_out > :fromDate
    ";

    $bookingParams = [
        ':property_id' => $selectedPropertyId,
        ':fromDate'    => $dateFrom,
        ':toDate'      => $dateTo,
    ];

    if ($filterStatus !== '') {
        $sqlBookings .= " AND ({$statusCodeSql}) = :filter_status ";
        $bookingParams[':filter_status'] = $filterStatus;
    }

    if ($filterAgentId > 0 && $hasOrderAgentId) {
        $sqlBookings .= " AND o.agent_id = :filter_agent_id ";
        $bookingParams[':filter_agent_id'] = $filterAgentId;
    }

    if ($filterSearch !== '') {
        $searchLike = '%' . $filterSearch . '%';
        $sqlBookings .= " AND (
            o.order_no LIKE :booking_search_order_no
            OR COALESCE(g.firstname, '') LIKE :booking_search_firstname
            OR COALESCE(g.lastname, '') LIKE :booking_search_lastname
            OR CONCAT(COALESCE(g.firstname,''), ' ', COALESCE(g.lastname,'')) LIKE :booking_search_guest_name
            OR COALESCE({$roomGuestSearchSql}, '') LIKE :booking_search_checkin_guest
            OR COALESCE({$folioNumbersSql}, '') LIKE :booking_search_folio
            OR COALESCE({$orderPaymentDueSql}, '') LIKE :booking_search_payment_due
            OR COALESCE({$orderPaymentStatusSql}, '') LIKE :booking_search_payment_status
            OR ({$statusCodeSql}) LIKE :booking_search_status
        ) ";
        $bookingParams[':booking_search_order_no'] = $searchLike;
        $bookingParams[':booking_search_firstname'] = $searchLike;
        $bookingParams[':booking_search_lastname'] = $searchLike;
        $bookingParams[':booking_search_guest_name'] = $searchLike;
        $bookingParams[':booking_search_checkin_guest'] = $searchLike;
        $bookingParams[':booking_search_folio'] = $searchLike;
        $bookingParams[':booking_search_payment_due'] = $searchLike;
        $bookingParams[':booking_search_payment_status'] = $searchLike;
        $bookingParams[':booking_search_status'] = $searchLike;
    }

    $sqlBookings .= " ORDER BY rmg.check_in, rmg.order_id, rmg.room_no ";

    $stmt = $pdo->prepare($sqlBookings);
    $stmt->execute($bookingParams);
    $bookings = $stmt->fetchAll(PDO::FETCH_ASSOC);
}

$byRoom = [];
$unassigned = [];

foreach ($bookings as $booking) {
    $roomId = (int)($booking['room_id'] ?? 0);
    if ($roomId > 0) {
        $byRoom[$roomId][] = $booking;
    } else {
        $unassigned[] = $booking;
    }
}

if (function_exists('refresh_waitlist_matches') && $selectedPropertyId > 0) {
    refresh_waitlist_matches($pdo, $selectedPropertyId, null);
}

$waitlistSummary = fetch_waitlist_alert_summary($pdo);
$notifiedWaitlists = function_exists('fetch_notified_waitlists') ? fetch_notified_waitlists($pdo, 8) : [];

$totalBookings = count($bookings);
$totalUnassigned = count($unassigned);
$occupiedRooms = 0;
foreach ($rooms as $room) {
    if (!empty($byRoom[(int)$room['id']])) {
        $occupiedRooms++;
    }
}

$currentPropertyLabel = 'No property selected';
foreach ($properties as $prop) {
    if ((int)$prop['id'] === $selectedPropertyId) {
        $currentPropertyLabel = trim($prop['name'] . (!empty($prop['code']) ? ' (' . $prop['code'] . ')' : ''));
        break;
    }
}


$monthBase = strtotime(date('Y-m-01', $fromTs));
$baseCalendarFilters = [
    'property_id' => $selectedPropertyId,
    'room_id' => $focusRoom > 0 ? $focusRoom : null,
    'q' => $filterSearch !== '' ? $filterSearch : null,
    'agent_id' => $filterAgentId > 0 ? $filterAgentId : null,
    'booking_status' => $filterStatus !== '' ? $filterStatus : null,
];
$prevMonthUrl = 'cal.php?' . http_build_query(array_filter(array_merge($baseCalendarFilters, [
    'from' => date('Y-m-01', strtotime('-1 month', $monthBase)),
    'to' => date('Y-m-t', strtotime('-1 month', $monthBase)),
]), static fn($v) => $v !== null && $v !== ''));
$thisMonthUrl = 'cal.php?' . http_build_query(array_filter(array_merge($baseCalendarFilters, [
    'from' => date('Y-m-d', strtotime('-10 days')),
    'to' => date('Y-m-d', strtotime('+20 days')),
]), static fn($v) => $v !== null && $v !== ''));
$nextMonthUrl = 'cal.php?' . http_build_query(array_filter(array_merge($baseCalendarFilters, [
    'from' => date('Y-m-01', strtotime('+1 month', $monthBase)),
    'to' => date('Y-m-t', strtotime('+1 month', $monthBase)),
]), static fn($v) => $v !== null && $v !== ''));

$defaultCalendarFrom = date('Y-m-d', strtotime('-10 days'));
$defaultCalendarTo   = date('Y-m-d', strtotime('+20 days'));

$calendarHasFilters = (
    $filterSearch !== ''
    || $filterStatus !== ''
    || $filterAgentId > 0
    || $focusRoom > 0
    || $dateFrom !== $defaultCalendarFrom
    || $dateTo !== $defaultCalendarTo
);

$calendarResetUrl = 'cal.php?' . http_build_query(array_filter([
    'property_id' => $selectedPropertyId > 0 ? $selectedPropertyId : null,
], static fn($v) => $v !== null && $v !== ''));

$frontOfficeReportDate = date('Y-m-d');
$frontOfficeReportBaseParams = [
    'property_id' => $selectedPropertyId > 0 ? $selectedPropertyId : null,
    'date' => $frontOfficeReportDate,
];
$frontOfficeReportBaseQuery = http_build_query(array_filter($frontOfficeReportBaseParams, static fn($v) => $v !== null && $v !== ''));
$frontOfficeReportsBaseUrl = '/rooms/modules/reservations/front_office_daily_reports.php' . ($frontOfficeReportBaseQuery !== '' ? ('?' . $frontOfficeReportBaseQuery) : '');
$frontOfficeReportLinks = [
    'rooming_list' => '/rooms/modules/reservations/front_office_daily_reports.php?' . http_build_query(array_filter(array_merge($frontOfficeReportBaseParams, ['report' => 'rooming_list']), static fn($v) => $v !== null && $v !== '')),
    'pax_list' => '/rooms/modules/reservations/front_office_daily_reports.php?' . http_build_query(array_filter(array_merge($frontOfficeReportBaseParams, ['report' => 'pax_list']), static fn($v) => $v !== null && $v !== '')),
    'inhouse_activity' => '/rooms/modules/reservations/front_office_daily_reports.php?' . http_build_query(array_filter(array_merge($frontOfficeReportBaseParams, ['report' => 'inhouse_activity']), static fn($v) => $v !== null && $v !== '')),
    'inhouse_by_room' => '/rooms/modules/reservations/front_office_daily_reports.php?' . http_build_query(array_filter(array_merge($frontOfficeReportBaseParams, ['report' => 'inhouse_by_room']), static fn($v) => $v !== null && $v !== '')),
    'bulk_guest_registration' => '/rooms/modules/reservations/bulk_guest_registration_tomorrow.php?' . http_build_query(array_filter([
        'property_id' => $selectedPropertyId > 0 ? $selectedPropertyId : null,
        'date' => date('Y-m-d', strtotime('+1 day')),
    ], static fn($v) => $v !== null && $v !== '')),
    'bulk_guest_registration_pdf' => '/rooms/modules/reservations/bulk_guest_registration_tomorrow.php?' . http_build_query(array_filter([
        'property_id' => $selectedPropertyId > 0 ? $selectedPropertyId : null,
        'date' => date('Y-m-d', strtotime('+1 day')),
        'combined_pdf' => 1,
    ], static fn($v) => $v !== null && $v !== '')),
    'preload_guest_names' => '/rooms/modules/reservations/preload_tomorrow_guest_names.php?' . http_build_query(array_filter([
        'property_id' => $selectedPropertyId > 0 ? $selectedPropertyId : null,
        'date' => date('Y-m-d', strtotime('+1 day')),
    ], static fn($v) => $v !== null && $v !== '')),
];


require_once __DIR__ . '/../../includes/header.php';
?>

<div class="timeline-hero card border-0 shadow-sm mb-3 overflow-hidden compact-top-card">
    <div class="card-body p-3 p-lg-3">
        <div class="row g-3 align-items-center">
            <div class="col-lg-8">
                <div class="page-eyebrow mb-2">
                    <i class="bi bi-calendar-range"></i> Occupancy Timeline
                </div>
                <h4 class="mb-1 fw-bold">Calendar Rooming Dashboard</h4>
                <div class="text-muted small">
                    All rooms shown in one compact monthly calendar with planner colours, drag-and-drop and temporary holding.
                </div>
                <div class="mt-1 small text-muted">
                    Current property: <strong><?php echo planner_e($currentPropertyLabel); ?></strong>
                </div>
            </div>
            <div class="col-lg-4">
                <div class="d-flex flex-wrap justify-content-lg-end gap-2">
                    <a class="btn btn-primary" href="/rooms/modules/reservations/reservation_form.php?property_id=<?php echo (int)$selectedPropertyId; ?>">
                        <i class="bi bi-plus-circle me-2"></i>New Reservation
                    </a>
                    <a class="btn btn-outline-secondary position-relative" href="/rooms/modules/reservations/waitlist.php?property_id=<?php echo (int)$selectedPropertyId; ?>&check_in=<?php echo urlencode($dateFrom); ?>&check_out=<?php echo urlencode($dateTo); ?>&source=availability_planner">
                        <i class="bi bi-clock-history me-2"></i>Waitlist
                        <?php if ((int)$waitlistSummary['notified_count'] > 0): ?>
                            <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger">
                                <?php echo (int)$waitlistSummary['notified_count']; ?>
                            </span>
                        <?php endif; ?>
                    </a>
                    <a class="btn btn-outline-secondary" href="/rooms/modules/reservations/reservation_list.php?property_id=<?php echo (int)$selectedPropertyId; ?>">
                        <i class="bi bi-journal-text me-2"></i>Reservations
                    </a>
                </div>
            </div>
        </div>

        <div class="row g-2 mt-3">
            <div class="col-md-3 col-sm-6">
                <div class="timeline-stat">
                    <div class="timeline-stat-icon bg-primary-subtle text-primary"><i class="bi bi-door-open"></i></div>
                    <div>
                        <div class="timeline-stat-value"><?php echo (int)count($rooms); ?></div>
                        <div class="timeline-stat-label">Rooms</div>
                    </div>
                </div>
            </div>
            <div class="col-md-3 col-sm-6">
                <div class="timeline-stat">
                    <div class="timeline-stat-icon bg-success-subtle text-success"><i class="bi bi-journal-check"></i></div>
                    <div>
                        <div class="timeline-stat-value"><?php echo (int)$totalBookings; ?></div>
                        <div class="timeline-stat-label">Room Slots In Range</div>
                    </div>
                </div>
            </div>
            <div class="col-md-3 col-sm-6">
                <div class="timeline-stat">
                    <div class="timeline-stat-icon bg-warning-subtle text-warning"><i class="bi bi-grid-3x3-gap"></i></div>
                    <div>
                        <div class="timeline-stat-value"><?php echo (int)$occupiedRooms; ?></div>
                        <div class="timeline-stat-label">Occupied Rooms</div>
                    </div>
                </div>
            </div>
            <div class="col-md-3 col-sm-6">
                <div class="timeline-stat">
                    <div class="timeline-stat-icon bg-danger-subtle text-danger"><i class="bi bi-exclamation-circle"></i></div>
                    <div>
                        <div class="timeline-stat-value"><?php echo (int)$totalUnassigned; ?></div>
                        <div class="timeline-stat-label">Unassigned Slots</div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>


<div class="semper-board-title mb-2">
    <div class="semper-title-left">
        <span class="semper-chip-main"><i class="bi bi-grid-3x3-gap me-1"></i>Rooming Chart</span>
        <span class="semper-chip"><?php echo planner_e(date('F Y', $fromTs)); ?></span>
        <span class="semper-chip"><?php echo (int)count($rooms); ?> rooms</span>
        <?php if (!empty($calendarHasFilters)): ?>
            <a class="semper-chip semper-chip-reset" href="<?php echo planner_e($calendarResetUrl); ?>" title="Clear search, dates and filters back to the default calendar view">
                <i class="bi bi-x-circle me-1"></i>Reset Filter
            </a>
            <span class="semper-chip text-primary"><i class="bi bi-funnel me-1"></i>Filtered</span>
        <?php endif; ?>
        <a href="/rooms/modules/frontoffice/front_office_pos.php"
           class="btn btn-success btn-sm">
            <i class="bi bi-bag-check"></i> Front Office POS
        </a>
        <a href="<?php echo planner_e($frontOfficeReportLinks['preload_guest_names']); ?>"
           target="_blank"
           class="btn btn-info btn-sm text-white">
            <i class="bi bi-people-fill"></i> Preload Names
        </a>
        <div class="btn-group front-office-report-menu">
            <button type="button"
                    class="btn btn-sm btn-outline-dark dropdown-toggle fw-semibold"
                    data-bs-toggle="dropdown"
                    aria-expanded="false"
                    title="Daily front office reports">
                <i class="bi bi-clipboard-data me-1"></i>Daily Reports
            </button>
            <ul class="dropdown-menu shadow-sm">
                <li><h6 class="dropdown-header">Front Office Daily Use</h6></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportsBaseUrl); ?>"><i class="bi bi-grid-1x2 me-2"></i>Report Centre</a></li>
                <li><hr class="dropdown-divider"></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['rooming_list']); ?>"><i class="bi bi-door-open me-2"></i>Rooming List</a></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['pax_list']); ?>"><i class="bi bi-people me-2"></i>Pax List</a></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['inhouse_activity']); ?>"><i class="bi bi-calendar-check me-2"></i>In-House Activity Sheet</a></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['inhouse_by_room']); ?>"><i class="bi bi-layout-three-columns me-2"></i>In-House by Room</a></li>
                <li><hr class="dropdown-divider"></li>
                <li><h6 class="dropdown-header">Guest Registration</h6></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['preload_guest_names']); ?>"><i class="bi bi-people-fill me-2"></i>Preload Tomorrow Guest Names</a></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['bulk_guest_registration']); ?>"><i class="bi bi-printer me-2"></i>Bulk Guest Reg Forms</a></li>
                <li><a class="dropdown-item" target="_blank" href="<?php echo planner_e($frontOfficeReportLinks['bulk_guest_registration_pdf']); ?>"><i class="bi bi-file-earmark-pdf me-2"></i>Bulk Guest Reg - One PDF</a></li>
            </ul>
        </div>
    </div>
    <div class="semper-title-actions">
        <a class="btn btn-sm btn-outline-secondary" href="<?php echo planner_e($prevMonthUrl); ?>"><i class="bi bi-arrow-left"></i></a>
        <a class="btn btn-sm btn-outline-primary" href="<?php echo planner_e($thisMonthUrl); ?>">This Month</a>
        <a class="btn btn-sm btn-outline-secondary" href="<?php echo planner_e($nextMonthUrl); ?>"><i class="bi bi-arrow-right"></i></a>
        <button type="button"
                class="btn btn-sm btn-outline-dark planner-search-trigger"
                data-bs-toggle="modal"
                data-bs-target="#plannerSearchModal"
                title="Search and filter rooming chart">
            <i class="bi bi-binoculars"></i>
        </button>
        <a class="btn btn-sm btn-primary" href="/rooms/modules/reservations/reservation_form.php?property_id=<?php echo (int)$selectedPropertyId; ?>"><i class="bi bi-plus-circle me-1"></i>New</a>
    </div>
</div>

<div class="modal fade" id="plannerSearchModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-scrollable">
        <div class="modal-content border-0 shadow planner-search-modal">
            <div class="modal-header">
                <div>
                    <h5 class="modal-title mb-0"><i class="bi bi-binoculars me-2 text-primary"></i>Search Rooming Chart</h5>
                    <div class="small text-muted">Search by anything, or combine property, dates, agent, status and room filters.</div>
                </div>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>

            <form method="get" id="plannerSearchForm">
                <div class="modal-body">
                    <div class="alert alert-light border small mb-3">
                        Dates are optional. Leave them blank to search across all dates, or use one/both dates together with other filters.
                    </div>

                    <div class="row g-3 align-items-end">
                        <div class="col-xl-3 col-md-6">
                            <label class="form-label fw-semibold">Property</label>
                            <select class="form-select" name="property_id" id="modal_property_id">
                                <option value="0">All / Select property</option>
                                <?php foreach ($properties as $property): ?>
                                    <option value="<?php echo (int)$property['id']; ?>" <?php echo $selectedPropertyId === (int)$property['id'] ? 'selected' : ''; ?>>
                                        <?php echo planner_e($property['name']); ?><?php echo !empty($property['code']) ? ' - ' . planner_e($property['code']) : ''; ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <div class="col-xl-2 col-md-3">
                            <label class="form-label fw-semibold">From</label>
                            <input type="date" class="form-control" name="from" value="<?php echo planner_e($dateFrom); ?>">
                        </div>

                        <div class="col-xl-2 col-md-3">
                            <label class="form-label fw-semibold">To</label>
                            <input type="date" class="form-control" name="to" value="<?php echo planner_e($dateTo); ?>">
                        </div>

                        <div class="col-xl-5 col-md-12">
                            <label class="form-label fw-semibold">Search anything</label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-search"></i></span>
                                <input type="text"
                                       class="form-control"
                                       name="q"
                                       id="plannerSearchInput"
                                       value="<?php echo planner_e($filterSearch); ?>"
                                       placeholder="Guest, folio, booking ref, room, agent, status, payment, invoice">
                            </div>
                        </div>

                        <div class="col-xl-3 col-md-6">
                            <label class="form-label fw-semibold">Agent</label>
                            <select class="form-select" name="agent_id">
                                <option value="0" <?php echo $filterAgentId <= 0 ? 'selected' : ''; ?>>All agents</option>
                                <?php foreach ($agents as $agent): ?>
                                    <?php
                                    $agentId = (int)($agent['id'] ?? 0);
                                    $agentLabel = trim((string)($agent['agent_name'] ?? ''));
                                    if ($agentLabel === '') {
                                        $agentLabel = 'Agent #' . $agentId;
                                    }
                                    ?>
                                    <option value="<?php echo $agentId; ?>" <?php echo $filterAgentId === $agentId ? 'selected' : ''; ?>>
                                        <?php echo planner_e($agentLabel); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <div class="col-xl-3 col-md-6">
                            <label class="form-label fw-semibold">Status</label>
                            <select class="form-select" name="booking_status">
                                <option value="" <?php echo $filterStatus === '' ? 'selected' : ''; ?>>All statuses</option>
                                <option value="CONFIRMED" <?php echo $filterStatus === 'CONFIRMED' ? 'selected' : ''; ?>>Confirmed</option>
                                <option value="PROVISIONAL" <?php echo $filterStatus === 'PROVISIONAL' ? 'selected' : ''; ?>>Provisional</option>
                                <option value="CHECKED_IN" <?php echo $filterStatus === 'CHECKED_IN' ? 'selected' : ''; ?>>Checked In / In House</option>
                                <option value="CHECKED_OUT" <?php echo $filterStatus === 'CHECKED_OUT' ? 'selected' : ''; ?>>Checked Out</option>
                                <option value="CANCELLED" <?php echo $filterStatus === 'CANCELLED' ? 'selected' : ''; ?>>Cancelled</option>
                                <option value="NO_SHOW" <?php echo $filterStatus === 'NO_SHOW' ? 'selected' : ''; ?>>No Show</option>
                            </select>
                        </div>

                        <div class="col-xl-3 col-md-6">
                            <label class="form-label fw-semibold">Focus Room</label>
                            <select class="form-select" name="room_id">
                                <option value="0">All rooms</option>
                                <?php foreach ($rooms as $room): ?>
                                    <option value="<?php echo (int)$room['id']; ?>" <?php echo $focusRoom === (int)$room['id'] ? 'selected' : ''; ?>>
                                        Room <?php echo planner_e($room['room_no']); ?><?php echo !empty($room['room_type']) ? ' - ' . planner_e($room['room_type']) : ''; ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <div class="col-xl-3 col-md-6">
                            <label class="form-label fw-semibold">Actions</label>
                            <div class="d-flex gap-2">
                                <button type="button" class="btn btn-outline-primary flex-fill" id="plannerAjaxSearchBtn">
                                    <i class="bi bi-search me-2"></i>Search
                                </button>
                                <button class="btn btn-primary flex-fill">
                                    <i class="bi bi-funnel me-2"></i>Apply
                                </button>
                            </div>
                        </div>
                    </div>

                    <div class="planner-search-results mt-4">
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <div class="fw-bold">Live Search Results</div>
                            <div class="small text-muted" id="plannerAjaxSearchCount">Use Search to preview matches.</div>
                        </div>
                        <div id="plannerAjaxSearchResults" class="planner-result-list">
                            <div class="planner-result-empty">
                                <i class="bi bi-info-circle me-2"></i>Results will appear here without leaving the page.
                            </div>
                        </div>
                    </div>

                    <?php if ($selectedPropertyId > 0): ?>
                        <div class="d-flex flex-wrap gap-2 mt-3 pt-3 border-top">
                            <a class="btn btn-warning" href="/rooms/modules/reservations/waitlist.php?property_id=<?php echo (int)$selectedPropertyId; ?>&check_in=<?php echo urlencode($dateFrom); ?>&check_out=<?php echo urlencode($dateTo); ?>&source=availability_planner_filters">
                                <i class="bi bi-clock-history me-2"></i>Add Selected Stay to Waitlist
                            </a>
                            <a class="btn btn-outline-primary" href="/rooms/modules/reservations/reservation_form.php?property_id=<?php echo (int)$selectedPropertyId; ?>&check_in=<?php echo urlencode($dateFrom); ?>&check_out=<?php echo urlencode($dateTo); ?>">
                                <i class="bi bi-journal-plus me-2"></i>New Reservation For Selected Stay
                            </a>
                        </div>
                    <?php endif; ?>
                </div>

                <div class="modal-footer">
                    <a class="btn btn-outline-secondary" href="cal.php?property_id=<?php echo (int)$selectedPropertyId; ?>">Reset</a>
                    <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
                    <button class="btn btn-primary"><i class="bi bi-check2-circle me-2"></i>Apply Filters to Chart</button>
                </div>
            </form>
        </div>
    </div>
</div>

<?php if ($selectedPropertyId > 0 && $roomTypeSummaries): ?>
<div class="card border-0 shadow-sm mb-3 compact-roomtype-card">
    <div class="card-header bg-white border-0 p-3 pb-0">
        <h5 class="mb-1">Room type availability for selected stay</h5>
        <div class="text-muted">If a room type is full for the selected date range, staff can move directly to the waitlist with the stay prefilled.</div>
    </div>
    <div class="card-body p-3">
        <div class="row g-2">
            <?php foreach ($roomTypeSummaries as $summary): ?>
                <div class="col-xl-3 col-lg-4 col-md-6">
                    <div class="availability-card h-100">
                        <div class="d-flex justify-content-between align-items-start gap-3">
                            <div>
                                <div class="fw-bold"><?php echo planner_e($summary['title']); ?></div>
                                <div class="small text-muted">
                                    <?php echo (int)$summary['occupied_rooms']; ?> occupied / <?php echo (int)$summary['total_rooms']; ?> total
                                </div>
                                <?php if ((int)($summary['higher_occupancy'] ?? 0) >= 3 && stripos((string)$summary['title'], 'family') === false): ?>
                                    <div class="small text-warning fw-semibold mt-1"><i class="bi bi-person-plus me-1"></i>Triple / extra bed possible</div>
                                <?php endif; ?>
                            </div>
                            <?php if ((int)$summary['available_rooms'] > 0): ?>
                                <span class="badge text-bg-success"><?php echo (int)$summary['available_rooms']; ?> available</span>
                            <?php else: ?>
                                <span class="badge text-bg-danger">No availability</span>
                            <?php endif; ?>
                        </div>

                        <div class="mt-3 d-grid gap-2">
                            <?php if ((int)$summary['available_rooms'] > 0): ?>
                                <a href="<?php echo planner_e($summary['book_url']); ?>" class="btn btn-outline-primary btn-sm">
                                    <i class="bi bi-box-arrow-up-right me-2"></i>Book This Type
                                </a>
                            <?php else: ?>
                                <a href="<?php echo planner_e($summary['waitlist_url']); ?>" class="btn btn-warning btn-sm">
                                    <i class="bi bi-clock-history me-2"></i>Add to Waitlist
                                </a>
                            <?php endif; ?>

                            <a href="/rooms/modules/reservations/waitlist.php?property_id=<?php echo (int)$selectedPropertyId; ?>&room_type_id=<?php echo (int)$summary['id']; ?>&check_in=<?php echo urlencode($dateFrom); ?>&check_out=<?php echo urlencode($dateTo); ?>&source=availability_planner_card"
                               class="btn btn-light btn-sm border">
                                Open Waitlist
                            </a>
                        </div>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
    </div>
</div>
<?php endif; ?>

<?php if ($selectedPropertyId <= 0): ?>
    <div class="alert alert-warning">Please select a property to view the planner.</div>
<?php endif; ?>

<?php if ($selectedPropertyId > 0): ?>
<div class="card border-0 shadow-sm mb-3 holding-drop-card compact-holding-card" data-holding-dropzone data-room-id="0">
    <div class="card-header bg-white border-0 p-3 pb-0 d-flex flex-wrap justify-content-between align-items-center gap-2">
        <div>
            <h5 class="mb-1"><i class="bi bi-inboxes me-2 text-warning"></i>Temporary Holding Area</h5>
            <div class="text-muted">Drag a booking here to temporarily move it out of a room while you rearrange nearby rooms for groups.</div>
        </div>
        <span class="badge text-bg-warning px-3 py-2"><?php echo (int)$totalUnassigned; ?> held / unassigned</span>
    </div>
    <div class="card-body p-3">
        <div class="holding-drop-zone">
            <div class="holding-drop-help"><i class="bi bi-arrows-move me-2"></i>Drop a booking here to hold it temporarily. Then drag it back into any suitable available room.</div>
            <?php if ($unassigned): ?>
                <div class="timeline-unassigned mt-3">
                    <?php foreach ($unassigned as $booking): ?>
                        <?php
                        $displayStatusForBar = ((int)($booking['room_checked_out_status'] ?? 0) === 1)
                                        ? '4'
                                        : (((int)($booking['room_checked_in_status'] ?? 0) === 1)
                                            ? '3'
                                            : ($booking['status_code'] ?? $booking['status'] ?? ''));
                                    [$statusLabel, $statusClass, $badgeClass] = planner_status_meta($displayStatusForBar);
                        $groupName = trim((string)($booking['group_name'] ?? ''));
                        $guestName = $groupName !== '' ? $groupName : trim(($booking['firstname'] ?? '') . ' ' . ($booking['lastname'] ?? ''));
                        if ($guestName === '') {
                            $guestName = 'Booking #' . (int)$booking['order_id'];
                        }
                        $slotMeta = planner_room_slot_meta(
                            $booking['room_type'] ?? '',
                            (int)($booking['adults'] ?? 0),
                            (int)($booking['children'] ?? 0),
                            (int)($booking['room_higher_occupancy'] ?? 0)
                        );
                        ?>
                        <div class="timeline-unassigned-card booking-bar-static unassigned-card occupancy-<?php echo planner_e($slotMeta['class']); ?>"
                             style="border-left: 8px solid <?php echo planner_e(planner_booking_color($pdo, $booking)); ?>;"
                             draggable="true"
                             data-held="1"
                             data-room-type="<?php echo planner_e($booking['room_type'] ?? ''); ?>"
                             data-room-type-id="<?php echo (int)($booking['room_type_id'] ?? 0); ?>"
                             data-rooming-id="<?php echo (int)$booking['rooming_id']; ?>"
                             data-order-id="<?php echo (int)$booking['order_id']; ?>"
                             data-check-in="<?php echo planner_e($booking['check_in']); ?>"
                             data-check-out="<?php echo planner_e($booking['check_out']); ?>">
                            <div class="fw-bold"><?php echo planner_e($guestName); ?></div>
                            <div class="small text-muted">
                                <?php echo planner_e($booking['order_no'] ?: ('#' . $booking['order_id'])); ?> •
                                Slot <?php echo (int)$booking['room_no']; ?> •
                                <?php echo planner_e(planner_display_date($booking['check_in'])); ?> →
                                <?php echo planner_e(planner_display_date($booking['check_out'])); ?>
                            </div>
                            <div class="small text-muted">
                                <?php echo (int)$booking['adults']; ?>A<?php echo (int)$booking['children'] > 0 ? ' / ' . (int)$booking['children'] . 'C' : ''; ?>
                                <?php if (!empty($booking['room_type'])): ?> • <?php echo planner_e($booking['room_type']); ?><?php endif; ?>
                            </div>
                            <div class="mt-2 d-flex flex-wrap gap-1">
                                <?php if ($slotMeta['is_triple'] || $slotMeta['is_family']): ?>
                                    <span class="badge text-bg-<?php echo planner_e($slotMeta['badge']); ?>">
                                        <i class="bi <?php echo planner_e($slotMeta['icon']); ?> me-1"></i><?php echo planner_e($slotMeta['label']); ?>
                                    </span>
                                <?php endif; ?>
                                <span class="badge text-bg-<?php echo planner_e($badgeClass); ?>"><?php echo planner_e($statusLabel); ?></span>
                            </div>
                        </div>
                    <?php endforeach; ?>
                </div>
            <?php else: ?>
                <div class="holding-empty mt-3">No bookings are currently in holding.</div>
            <?php endif; ?>
        </div>
    </div>
</div>
<?php endif; ?>

<?php if ($selectedPropertyId > 0): ?>
<div class="card border-0 shadow-sm">
    <div class="card-body p-0">
        <div class="timeline-wrap"
             id="calendarTimelineWrap"
             data-fit-mode="<?php echo planner_e($calendarFitMode); ?>"
             data-total-days="<?php echo (int)$totalDays; ?>"
             data-range-from="<?php echo planner_e($dateFrom); ?>"
             data-range-to="<?php echo planner_e($dateTo); ?>"
             data-room-label-width="<?php echo (int)$roomLabelWidth; ?>"
             data-default-day-width="<?php echo (int)$defaultDayWidth; ?>">
            <div class="timeline-table calendar-fit-mode-<?php echo planner_e($calendarFitMode); ?>" style="min-width: <?php echo (int)$rowMinWidth; ?>px;">
                <div class="timeline-head">
                    <div class="timeline-room-head">Room</div>
                    <div class="timeline-days-head" style="width: <?php echo (int)($totalDays * $dayWidth); ?>px;">
                        <?php foreach ($days as $dayIndex => $day): ?>
                            <div class="timeline-day-head <?php echo $day['is_today'] ? 'is-today' : ''; ?>" data-calendar-day-cell data-day-index="<?php echo (int)$dayIndex; ?>" style="width: <?php echo (int)$dayWidth; ?>px;">
                                <div class="day-name"><?php echo planner_e($day['day']); ?></div>
                                <div class="day-num"><?php echo planner_e($day['num']); ?></div>
                                <div class="day-mon"><?php echo planner_e($day['mon']); ?></div>
                            </div>
                        <?php endforeach; ?>
                    </div>
                </div>

                <?php foreach ($rooms as $room): ?>
                    <?php
                    $roomId = (int)$room['id'];
                    if ($focusRoom > 0 && $focusRoom !== $roomId) {
                        continue;
                    }

                    $roomBookings = $byRoom[$roomId] ?? [];
                    $laneData = planner_assign_lanes($roomBookings, $days, $dayWidth);
                    $roomItems = $laneData['items'];
                    $laneCount = $laneData['lane_count'];

                    $barHeight = 24;
                    $barGap = 2;
                    $topPadding = 3;
                    $bottomPadding = 3;
                    $computedRowHeight = ($laneCount > 0)
                        ? $topPadding + ($laneCount * $barHeight) + (($laneCount - 1) * $barGap) + $bottomPadding
                        : 48;
                    ?>
                    <div class="timeline-row"
                         data-room-dropzone
                         data-room-id="<?php echo $roomId; ?>"
                         data-room-no="<?php echo planner_e($room['room_no']); ?>"
                         data-room-type="<?php echo planner_e($room['room_type'] ?? ''); ?>"
                         data-room-type-id="<?php echo (int)($room['room_type_id'] ?? 0); ?>"
                         style="min-height: <?php echo (int)$computedRowHeight; ?>px;">
                        <div class="timeline-room-meta" style="min-height: <?php echo (int)$computedRowHeight; ?>px;">
                            <div class="room-title">Room <?php echo planner_e($room['room_no']); ?></div>
                            <div class="room-subtitle"><?php echo planner_e($room['room_type'] ?: 'No type'); ?></div>
                            <?php if ((int)($room['room_higher_occupancy'] ?? 0) >= 3 && stripos((string)($room['room_type'] ?? ''), 'family') === false): ?>
                                <div class="room-capacity-badge"><i class="bi bi-person-plus me-1"></i>Triple possible</div>
                            <?php endif; ?>
                            <div class="room-status"><?php echo planner_e((string)$room['status']); ?></div>
                        </div>

                        <div class="timeline-grid-area"
                             style="width: <?php echo (int)($totalDays * $dayWidth); ?>px; min-height: <?php echo (int)$computedRowHeight; ?>px;">
                            <div class="timeline-grid-lines">
                                <?php foreach ($days as $dayIndex => $day): ?>
                                    <div class="timeline-cell <?php echo $day['is_today'] ? 'is-today' : ''; ?>" data-calendar-day-cell data-day-index="<?php echo (int)$dayIndex; ?>" style="width: <?php echo (int)$dayWidth; ?>px;"></div>
                                <?php endforeach; ?>
                            </div>

                            <div class="timeline-bars" style="min-height: <?php echo (int)$computedRowHeight; ?>px;">
                                <?php if (!$roomItems): ?>
                                    <div class="timeline-empty"><strong>Room <?php echo planner_e($room['room_no']); ?></strong><span>Available - Drop room slot here</span></div>
                                <?php endif; ?>

                                <?php foreach ($roomItems as $item): ?>
                                    <?php
                                    $booking = $item['booking'];
                                    $geo = $item['geo'];

                                    [$statusLabel, $statusClass, $badgeClass] = planner_status_meta($booking['status']);
                                    $groupName = trim((string)($booking['group_name'] ?? ''));
                                    $guestName = $groupName !== '' ? $groupName : trim(($booking['firstname'] ?? '') . ' ' . ($booking['lastname'] ?? ''));
                                    if ($guestName === '') {
                                        $guestName = 'Booking #' . (int)$booking['order_id'];
                                    }

                                    $slotMeta = planner_room_slot_meta(
                                        $booking['room_type'] ?? '',
                                        (int)($booking['adults'] ?? 0),
                                        (int)($booking['children'] ?? 0),
                                        (int)($booking['room_higher_occupancy'] ?? 0)
                                    );

                                    $financeHint = planner_booking_finance_hint($booking);
                                    $agentHint = trim((string)($booking['agent_name'] ?? ''));
                                    $folioHint = planner_clean_folio_numbers($booking['folio_numbers'] ?? '');
                                    $tooltip = $guestName
                                        . ' | Ref: ' . ($booking['order_no'] ?: ('#' . $booking['order_id']))
                                        . ' | Slot ' . (int)$booking['room_no']
                                        . ' | ' . planner_display_date($booking['check_in']) . ' → ' . planner_display_date($booking['check_out'])
                                        . ' | ' . (int)$booking['adults'] . 'A' . ((int)$booking['children'] > 0 ? ' / ' . (int)$booking['children'] . 'C' : '')
                                        . ' | ' . ($booking['room_type'] ?? '')
                                        . ' | ' . $slotMeta['label']
                                        . ' | Booking Status: ' . $statusLabel
                                        . ($agentHint !== '' ? ' | Agent: ' . $agentHint : '')
                                        . ($folioHint !== '' ? ' | Folio: ' . $folioHint : '')
                                        . (!empty($booking['expected_arrival_time'])
                                            ? ' | ETA: ' . substr($booking['expected_arrival_time'],0,5)
                                            : '')
                                        . (
                                            (
                                                (int)($booking['extra_beds_required'] ?? 0) +
                                                (int)($booking['cots_required'] ?? 0) +
                                                (int)($booking['accessibility_required'] ?? 0) +
                                                (int)($booking['interleading_required'] ?? 0)
                                            ) > 0
                                            ? ' | Requirements: '
                                                . implode(', ', array_filter([
                                                    !empty($booking['extra_beds_required']) ? 'Extra Bed' : '',
                                                    !empty($booking['cots_required']) ? 'Cot' : '',
                                                    !empty($booking['accessibility_required']) ? 'Wheelchair Access' : '',
                                                    !empty($booking['interleading_required']) ? 'Interleading Rooms' : '',
                                                ]))
                                            : ''
                                        )                                        
                                        . ' | ' . $financeHint;

                                    $top = $topPadding + ($item['lane'] * ($barHeight + $barGap));
                                    ?>
                                    <div class="booking-bar occupancy-<?php echo planner_e($slotMeta['class']); ?>"
                                         style="left: <?php echo (int)$geo['left']; ?>px; width: <?php echo (int)$geo['width']; ?>px; top: <?php echo (int)$top; ?>px; background: <?php echo planner_e(planner_booking_color($pdo,$booking)); ?>;"
                                         draggable="true"
                                         data-rooming-id="<?php echo (int)$booking['rooming_id']; ?>"
                                         data-order-id="<?php echo (int)$booking['order_id']; ?>"
                                         data-check-in="<?php echo planner_e($booking['check_in']); ?>"
                                         data-check-out="<?php echo planner_e($booking['check_out']); ?>"
                                         data-range-offset="<?php echo (int)floor(((int)$geo['left']) / max(1, (int)$dayWidth)); ?>"
                                         data-range-span="<?php echo (int)$geo['span']; ?>"
                                         title="<?php echo planner_e($tooltip); ?>">

                                        <div class="bar-main">
                                            <span class="bar-guest"><?php echo planner_e($guestName); ?></span>
                                            <?php if ($slotMeta['is_triple'] || $slotMeta['is_family']): ?>
                                                <span class="bar-occupancy-badge">
                                                    <i class="bi <?php echo planner_e($slotMeta['icon']); ?>"></i>
                                                    <?php echo $slotMeta['is_triple'] ? 'Triple' : 'Family'; ?>
                                                </span>
                                            <?php endif; ?>
                                        </div>

                                        <a class="bar-open"
                                           href="/rooms/modules/reservations/reservation_form.php?id=<?php echo (int)$booking['order_id']; ?>"
                                           onclick="event.stopPropagation();"
                                           title="Open booking">
                                            <i class="bi bi-box-arrow-up-right"></i>
                                        </a>
                                    </div>
                                <?php endforeach; ?>
                            </div>
                        </div>
                    </div>
                <?php endforeach; ?>

                <?php if (!$rooms): ?>
                    <div class="p-4 text-muted">No rooms found for the selected property.</div>
                <?php endif; ?>
            </div>
        </div>
    </div>
</div>
<?php endif; ?>


<div class="modal fade" id="calendarFolioControlModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
    <div class="modal-dialog modal-xl modal-dialog-scrollable">
        <div class="modal-content calendar-folio-modal border-0 shadow">
            <div class="modal-header">
                <div>
                    <h5 class="modal-title mb-1"><i class="bi bi-wallet2 me-2"></i>Folio Control Centre</h5>
                    <div class="small text-muted" id="calendarFolioSubtitle">Double-click any booking on the chart to view front-office details.</div>
                </div>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div id="calendarFolioLoading" class="alert alert-light border">
                    <i class="bi bi-hourglass-split me-2"></i>Loading booking details...
                </div>

                <div id="calendarFolioBody" style="display:none;">
                    <div class="row g-3 mb-3">
                        <div class="col-md-3"><div class="folio-mini-card"><span>Booking</span><strong id="calendarFolioBooking">-</strong></div></div>
                        <div class="col-md-3"><div class="folio-mini-card"><span>Guest</span><strong id="calendarFolioGuest">-</strong></div></div>
                        <div class="col-md-3"><div class="folio-mini-card"><span>Rooms</span><strong id="calendarFolioRooms">-</strong></div></div>
                        <div class="col-md-3"><div class="folio-mini-card"><span>Total Balance</span><strong id="calendarFolioBalance">0.00</strong></div></div>
                    </div>

                    <div class="folio-action-strip mb-3">
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-danger" id="calendarFolioPrintFull">
                            <i class="bi bi-file-earmark-pdf me-1"></i>Full Proforma
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-primary" id="calendarFolioPrintMaster">
                            <i class="bi bi-collection me-1"></i>Main Folio Proforma
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-success" id="calendarFolioPrintRoom">
                            <i class="bi bi-door-open me-1"></i>Room Proforma
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-warning" id="calendarFolioPrintGuest">
                            <i class="bi bi-person-badge me-1"></i>Guest Proforma
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-dark" id="calendarFolioTransferCharges">
                            <i class="bi bi-arrow-left-right me-1"></i>Transfer Charges
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success" id="calendarFolioExtras">
                            <i class="bi bi-plus-circle me-1"></i>Extras
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary" id="calendarFolioGuestRegPdf">
                            <i class="bi bi-file-earmark-pdf me-1"></i>Guest Reg
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-info" id="calendarFolioPreloadNames">
                            <i class="bi bi-people-fill me-1"></i>Preload Names
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-success" id="calendarFolioCheckinToday" style="display:none;">
                            <i class="bi bi-box-arrow-in-right me-1"></i>Check In Today
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success" id="calendarFolioDelayedCheckin" style="display:none;">
                            <i class="bi bi-clock-history me-1"></i>Delayed Check In
                        </a>
                        <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-info text-white" id="calendarFolioCheckoutToday" style="display:none;">
                            <i class="bi bi-box-arrow-right me-1"></i>Check Out Today
                        </a>
                    </div>

                    <div class="folio-breakdown-grid mb-3" id="calendarFolioBreakdownGrid"></div>

                    <div class="row g-3">
                        <div class="col-lg-5">
                            <div class="folio-section-card h-100">
                                <div class="folio-section-title room d-flex justify-content-between align-items-center">
                                    <span><i class="bi bi-door-open me-2"></i>Rooming Configuration</span>
                                    <a href="#" target="_blank" rel="noopener" class="btn btn-sm btn-outline-dark" id="calendarRoomingExportLink">
                                        <i class="bi bi-file-earmark-excel me-1"></i>Export Rooming List
                                    </a>
                                </div>
                                <div id="calendarRoomingList" class="folio-list"></div>
                            </div>
                        </div>
                        <div class="col-lg-7">
                            <div class="folio-section-card h-100">
                                <div class="folio-section-title guest"><i class="bi bi-card-text me-2"></i>Reservation Notes / Instructions</div>
                                <div id="calendarNotesList" class="folio-list"></div>
                            </div>
                        </div>
                    </div>

                    <div class="row g-3 mt-1">
                        <div class="col-lg-4">
                            <div class="folio-section-card">
                                <div class="folio-section-title master"><i class="bi bi-collection me-2"></i>Main / Master Folio</div>
                                <div id="calendarMasterFolios" class="folio-list"></div>
                            </div>
                        </div>
                        <div class="col-lg-4">
                            <div class="folio-section-card">
                                <div class="folio-section-title room"><i class="bi bi-door-open me-2"></i>Room Folios</div>
                                <div id="calendarRoomFolios" class="folio-list"></div>
                            </div>
                        </div>
                        <div class="col-lg-4">
                            <div class="folio-section-card">
                                <div class="folio-section-title guest"><i class="bi bi-person-badge me-2"></i>Guest / Other Folios</div>
                                <div id="calendarGuestFolios" class="folio-list"></div>
                            </div>
                        </div>
                    </div>
                </div>

                <div id="calendarFolioError" class="alert alert-danger" style="display:none;"></div>
            </div>
            <div class="modal-footer">
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-success" id="calendarFolioExtrasFooter">
                    <i class="bi bi-plus-circle me-1"></i>Extras
                </a>
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-dark" id="calendarFolioTransferChargesFooter">
                    <i class="bi bi-arrow-left-right me-1"></i>Transfer Charges
                </a>
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-info" id="calendarFolioPreloadNamesFooter">
                    <i class="bi bi-people-fill me-1"></i>Preload Names
                </a>
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-primary" id="calendarFolioGuestRegPdfFooter">
                    <i class="bi bi-file-earmark-pdf me-1"></i>Guest Reg
                </a>
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-primary" id="calendarFolioPaymentsLink">
                    <i class="bi bi-cash-stack me-1"></i>Open Payments
                </a>
                <a href="#" target="_blank" rel="noopener" class="btn btn-outline-secondary" id="calendarFolioBookingLink">
                    <i class="bi bi-pencil-square me-1"></i>Open Booking
                </a>
                <button type="button" class="btn btn-dark" data-bs-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>


<?php if ($notifiedWaitlists): ?>
<div class="modal fade" id="waitlistAvailabilityModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-scrollable">
        <div class="modal-content border-0 shadow">
            <div class="modal-header">
                <h5 class="modal-title"><i class="bi bi-bell-fill text-warning me-2"></i>Waitlist availability alerts</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="alert alert-success">
                    One or more waitlisted requests can now be confirmed because rooms have become available.
                </div>

                <div class="list-group">
                    <?php foreach ($notifiedWaitlists as $wl): ?>
                        <div class="list-group-item py-3">
                            <div class="d-flex flex-wrap justify-content-between gap-3">
                                <div>
                                    <div class="fw-bold"><?php echo planner_e($wl['guest_name'] ?: ('Waitlist #' . (int)$wl['id'])); ?></div>
                                    <div class="small text-muted">
                                        <?php echo planner_e($wl['property_name'] ?? ''); ?>
                                        <?php if (!empty($wl['room_type_name'])): ?> • <?php echo planner_e($wl['room_type_name']); ?><?php endif; ?>
                                    </div>
                                    <div class="small text-muted">
                                        <?php echo planner_e(planner_display_date($wl['check_in'])); ?> →
                                        <?php echo planner_e(planner_display_date($wl['check_out'])); ?>
                                        • <?php echo (int)$wl['totalrooms']; ?> room(s)
                                        • <?php echo (int)$wl['adults']; ?> adult(s)
                                        <?php if ((int)$wl['kids'] > 0): ?> • <?php echo (int)$wl['kids']; ?> child(ren)<?php endif; ?>
                                    </div>
                                </div>
                                <div class="d-flex align-items-start gap-2">
                                    <a class="btn btn-primary btn-sm"
                                       href="/rooms/modules/reservations/reservation_form.php?waitlist_id=<?php echo (int)$wl['id']; ?>">
                                        Confirm Now
                                    </a>
                                    <a class="btn btn-outline-secondary btn-sm"
                                       href="/rooms/modules/reservations/waitlist.php">
                                        Open Waitlist
                                    </a>
                                </div>
                            </div>
                        </div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>
    </div>
</div>
<?php endif; ?>


<div class="modal fade" id="calendarMoveFolioTransactionModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
        <form class="modal-content border-0 shadow" id="calendarMoveFolioTransactionForm">
            <div class="modal-header">
                <div>
                    <h5 class="modal-title mb-0"><i class="bi bi-arrow-left-right me-2 text-primary"></i>Move Folio Transaction</h5>
                    <div class="small text-muted">Move a payment or charge to the correct Master, Room, or Guest folio.</div>
                </div>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <input type="hidden" id="calendarMoveOrderId" value="">
                <input type="hidden" id="calendarMoveTransactionId" value="">
                <div class="alert alert-light border small">
                    <div><strong>Transaction:</strong> <span id="calendarMoveTxDescription">—</span></div>
                    <div><strong>Type:</strong> <span id="calendarMoveTxType">—</span></div>
                    <div><strong>Amount:</strong> <span id="calendarMoveTxAmount">0.00</span></div>
                    <div><strong>Current Folio:</strong> <span id="calendarMoveCurrentFolio">—</span></div>
                </div>
                <div class="mb-3">
                    <label class="form-label fw-semibold">Move To Folio</label>
                    <select id="calendarMoveTargetFolioId" class="form-select" required>
                        <option value="">Select destination folio</option>
                    </select>
                    <div class="form-text">Source and destination folio totals will be recalculated immediately.</div>
                </div>
                <div class="mb-0">
                    <label class="form-label fw-semibold">Reason / Audit Note</label>
                    <textarea id="calendarMoveReason" class="form-control" rows="4" required placeholder="Example: Payment was posted to Master but should be moved to Guest Folio."></textarea>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
                <button type="submit" class="btn btn-primary"><i class="bi bi-arrow-left-right me-2"></i>Move Transaction</button>
            </div>
        </form>
    </div>
</div>

<div class="modal fade" id="calendarVoidPaymentModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
    <div class="modal-dialog modal-md modal-dialog-centered">
        <form class="modal-content border-0 shadow" id="calendarVoidPaymentForm">
            <div class="modal-header bg-danger text-white">
                <div>
                    <h5 class="modal-title mb-0" id="calendarVoidModalTitle"><i class="bi bi-shield-lock me-2"></i>Void Payment</h5>
                    <div class="small text-white-50" id="calendarVoidModalSubtitle">Admin PIN authority is required. Only payment/receipt rows can be voided here.</div>
                </div>
                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <input type="hidden" id="calendarVoidOrderId" value="">
                <input type="hidden" id="calendarVoidTransactionId" value="">
                <input type="hidden" id="calendarVoidIsPayment" value="1">
                <div id="calendarVoidChargeNotice" class="alert alert-danger small d-none">
                    <i class="bi bi-exclamation-triangle-fill me-1"></i>
                    <strong>Temporary control.</strong> Voiding a charge changes what the guest owes. Use only to clear a genuinely mis-posted charge (e.g. posted to the wrong account) &mdash; not to cancel a service that was actually provided.
                </div>
                <div class="alert alert-warning small">
                    <div><strong id="calendarVoidItemLabel">Payment:</strong> <span id="calendarVoidPaymentDescription">—</span></div>
                    <div><strong>Amount:</strong> <span id="calendarVoidPaymentAmount">0.00</span></div>
                </div>
                <div class="mb-3">
                    <label class="form-label fw-semibold">Admin PIN</label>
                    <input type="password" class="form-control" id="calendarVoidAdminPin" autocomplete="off" required>
                </div>
                <div>
                    <label class="form-label fw-semibold">Reason</label>
                    <textarea class="form-control" id="calendarVoidReason" rows="3" required placeholder="Example: Payment posted in error / wrong payment method / duplicate receipt."></textarea>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
                <button type="submit" class="btn btn-danger" id="calendarVoidSubmitBtn"><i class="bi bi-x-octagon me-2"></i>Void Payment</button>
            </div>
        </form>
    </div>
</div>

<div class="modal fade" id="calendarRestaurantPreviewModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-md modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <div>
                    <h5 class="modal-title mb-0"><i class="bi bi-receipt me-2"></i>Restaurant Bill Preview</h5>
                    <div class="small text-muted" id="calendarRestaurantPreviewSubtitle">&nbsp;</div>
                </div>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div id="calendarRestaurantPreviewBody">
                    <div class="folio-empty">Loading...</div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" id="calendarRestaurantPreviewPrintBtn" data-source-id="">
                    <i class="bi bi-printer me-1"></i>Print Bill to Reception
                </button>
            </div>
        </div>
    </div>
</div>



<style>
.folio-control-wide{max-width:min(1680px,98vw)}
#calendarFolioControlModal .modal-body{overflow-x:hidden}
.timeline-hero { background: linear-gradient(135deg, #ffffff 0%, #f7faff 100%); border-radius: 24px; }
.timeline-stat { display:flex; align-items:center; gap:10px; padding:10px 12px; border:1px solid #eef2f7; border-radius:14px; background:#fff; height:100%; }
.timeline-stat-icon { width:36px; height:36px; border-radius:12px; display:inline-flex; align-items:center; justify-content:center; font-size:15px; }
.timeline-stat-value { font-size:1.05rem; font-weight:800; line-height:1.1; }
.timeline-stat-label { color:#6c757d; font-size:.76rem; line-height:1.1; }
.availability-card { border:1px solid #e9eef5; border-radius:14px; background:#fff; padding:10px 12px; box-shadow:0 4px 14px rgba(15,23,42,.035); font-size:.86rem; }
.timeline-unassigned { display:flex; flex-wrap:wrap; gap:8px; }
.timeline-unassigned-card { width:280px; max-width:100%; padding:10px 12px; }
.booking-bar-static {
    border-radius: 18px;
    cursor: grab;
    border: 1px solid #e5e7eb;
    background: #ffffff;
    color: #111827;
    box-shadow: 0 8px 24px rgba(15, 23, 42, .08);
}
.unassigned-card {
    background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%);
    border: 1px dashed #f59e0b;
    color: #111827;
}
.unassigned-card .text-muted,
.unassigned-card .small.text-muted {
    color: #6b7280 !important;
}
.unassigned-card .fw-bold {
    color: #111827 !important;
}
.holding-drop-card { border-radius:24px; }
.holding-drop-zone { min-height:70px; border:2px dashed #f59e0b; border-radius:16px; background:linear-gradient(135deg,#fffbeb 0%,#ffffff 100%); padding:10px 12px; transition:.15s ease; }
.holding-drop-card.drag-over .holding-drop-zone, .holding-drop-zone.drag-over { background:#fff7ed; box-shadow:inset 0 0 0 3px rgba(245,158,11,.18); }
.holding-drop-help { font-weight:700; color:#92400e; font-size:.84rem; }
.holding-empty { color:#94a3b8; font-weight:700; background:#fff; border:1px solid #fde68a; border-radius:12px; padding:8px 10px; font-size:.84rem; }
.timeline-wrap { overflow:auto; border-radius:0 0 18px 18px; max-height: calc(100vh - 210px); }
.timeline-table { background:#fff; }
.timeline-head, .timeline-row { display:flex; }
.timeline-head { position:sticky; top:0; z-index:5; background:#fff; }
.timeline-room-head {
    width:160px;
    min-width:160px;
    padding:8px 10px;
    font-weight:800;
    border-right:1px solid #e9eef5;
    border-bottom:1px solid #e9eef5;
    background:#f8fbff;
}
.timeline-days-head { display:flex; border-bottom:1px solid #e9eef5; }
.timeline-day-head { flex:0 0 auto; border-right:1px solid #edf2f7; padding:5px 4px; text-align:center; background:#f8fbff; }
.timeline-day-head.is-today { background:#eef5ff; }
.day-name { font-size:.60rem; color:#6b7280; text-transform:uppercase; letter-spacing:.05em; font-weight:700; }
.day-num { font-size:.80rem; font-weight:800; color:#111827; line-height:1.05; }
.day-mon { font-size:.58rem; color:#6b7280; }
.timeline-row { border-bottom:1px solid #edf2f7; }
.timeline-room-meta {
    width:160px;
    min-width:160px;
    padding:8px 10px;
    border-right:1px solid #edf2f7;
    background:#fff;
}
.room-title { font-weight:800; font-size:.84rem; color:#111827; line-height:1.1; }
.room-subtitle { font-size:.72rem; color:#6b7280; margin-top:2px; line-height:1.1; }
.room-status { margin-top:5px; display:inline-block; padding:3px 7px; background:#f8fafc; border:1px solid #e5e7eb; border-radius:999px; font-size:.62rem; color:#475569; font-weight:700; }
.timeline-grid-area { position:relative; background:#fff; }
.timeline-grid-lines { display:flex; position:absolute; inset:0; }
.timeline-cell { flex:0 0 auto; border-right:1px solid #f1f5f9; }
.timeline-cell.is-today { background:rgba(37,99,235,.05); }
.timeline-bars { position:relative; }
.timeline-empty { position:absolute; top:0; left:0; height:100%; display:flex; flex-direction:column; justify-content:center; padding-left:10px; color:#94a3b8; pointer-events:none; }
.timeline-empty strong { font-size:.78rem; font-weight:800; color:#64748b; line-height:1.3; }
.timeline-empty span { font-size:.68rem; font-weight:600; color:#b4bdc9; line-height:1.2; }

.booking-bar {
    position:absolute;
    height:30px;
    border-radius:7px;
    color:#fff;
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding:0 8px;
    gap:8px;
    cursor:grab;
    box-shadow:0 8px 24px rgba(15,23,42,.18);
    overflow:hidden;
    white-space:nowrap;
    z-index:2;
    box-sizing:border-box;
}
.booking-bar:hover { filter:brightness(1.03); }
.bar-main {
    display:flex;
    align-items:center;
    min-width:0;
    overflow:hidden;
    flex:1 1 auto;
}
.bar-guest {
    font-size:.72rem;
    font-weight:800;
    text-overflow:ellipsis;
    overflow:hidden;
}
.bar-open {
    color:#fff;
    margin-left:8px;
    font-size:.78rem;
    opacity:.95;
    flex:0 0 auto;
}
.bar-open:hover { color:#fff; opacity:1; }


.room-capacity-badge { margin-top:4px; display:inline-flex; align-items:center; padding:2px 6px; border-radius:999px; background:#fff7ed; border:1px solid #fed7aa; color:#9a3412; font-size:.58rem; font-weight:800; }
.booking-bar.occupancy-triple { box-shadow:0 10px 28px rgba(245,158,11,.33), 0 8px 24px rgba(15,23,42,.16); outline:2px solid rgba(253,186,116,.95); outline-offset:0; }
.booking-bar.occupancy-family { box-shadow:0 10px 28px rgba(14,165,233,.28), 0 8px 24px rgba(15,23,42,.16); outline:2px solid rgba(125,211,252,.85); outline-offset:0; }
.bar-occupancy-badge { margin-left:5px; display:inline-flex; align-items:center; gap:3px; padding:2px 5px; border-radius:999px; background:rgba(255,255,255,.9); color:#7c2d12; font-size:.55rem; font-weight:900; text-transform:uppercase; letter-spacing:.03em; flex:0 0 auto; }
.occupancy-family .bar-occupancy-badge { color:#075985; }
.booking-bar-static.occupancy-triple { border-color:#f59e0b; background:linear-gradient(135deg,#fff7ed 0%,#fffbeb 100%); }
.booking-bar-static.occupancy-family { border-color:#38bdf8; background:linear-gradient(135deg,#f0f9ff 0%,#ffffff 100%); }

.timeline-row.drag-over .timeline-grid-area { background: rgba(13, 110, 253, .05); box-shadow: inset 0 0 0 2px rgba(13, 110, 253, .18); }

@media (max-width: 991.98px) {
    .timeline-room-head, .timeline-room-meta {
        width:150px;
        min-width:150px;
    }
}

/* Compact planner UX adjustments */
.compact-top-card .page-eyebrow { font-size:.72rem; margin-bottom:.15rem !important; }
.compact-top-card .btn { padding:.38rem .68rem; border-radius:10px; font-size:.82rem; }
.compact-filter-card label { font-size:.76rem; margin-bottom:.25rem; }
.compact-filter-card .form-control,
.compact-filter-card .form-select { min-height:34px; padding:.32rem .6rem; font-size:.84rem; border-radius:10px; }
.compact-filter-card .btn { min-height:34px; padding:.32rem .55rem; font-size:.82rem; border-radius:10px; }
.compact-roomtype-card .card-header h5,
.compact-holding-card .card-header h5 { font-size:.98rem; }
.compact-roomtype-card .text-muted,
.compact-holding-card .text-muted { font-size:.78rem; }
.compact-roomtype-card .btn-sm,
.compact-holding-card .btn-sm { padding:.22rem .48rem; font-size:.74rem; }
.timeline-head { position:sticky; top:0; z-index:30; }
.timeline-room-head { position:sticky; left:0; z-index:32; }
.timeline-room-meta { position:sticky; left:0; z-index:20; }
.timeline-table { font-size:.86rem; }
.timeline-bars { overflow:hidden; }
@media (max-width: 991.98px) {
    .timeline-wrap { max-height: calc(100vh - 180px); }
}



/* Calendar Rooming Dashboard overrides */
.timeline-hero{display:none!important}
.compact-roomtype-card{display:none!important}
/* Holding area remains visible on cal.php for temporary storage / room reshuffling */
.semper-board-title{
    display:flex;align-items:center;justify-content:space-between;gap:10px;
    padding:6px 8px;border:1px solid #c7d2fe;background:linear-gradient(180deg,#f8fbff,#eef6ff);
    border-radius:10px;box-shadow:0 4px 14px rgba(15,23,42,.06);
}
.semper-title-left,.semper-title-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.semper-chip-main{background:#0f4fbf;color:#fff;border:1px solid #083a93;border-radius:7px;padding:5px 9px;font-size:.78rem;font-weight:900;letter-spacing:.02em}
.semper-chip{background:#fff;color:#1e3a8a;border:1px solid #bfdbfe;border-radius:7px;padding:5px 8px;font-size:.76rem;font-weight:800}
.semper-title-actions .btn{min-height:26px!important;padding:3px 8px!important;font-size:.74rem!important;border-radius:7px!important}
.compact-filter-card{margin-bottom:6px!important;border:1px solid #dbeafe!important;border-radius:10px!important;box-shadow:0 3px 10px rgba(15,23,42,.05)!important}
.compact-filter-card .card-body{padding:7px!important}
.compact-filter-card label{font-size:.64rem!important;margin-bottom:1px!important;color:#1e3a8a!important;text-transform:uppercase;letter-spacing:.04em}
.compact-filter-card .form-control,.compact-filter-card .form-select{min-height:28px!important;height:28px!important;padding:2px 6px!important;font-size:.76rem!important;border-radius:6px!important}
.compact-filter-card .btn{min-height:28px!important;height:28px!important;padding:2px 7px!important;font-size:.72rem!important;border-radius:6px!important}
.compact-filter-card .d-flex.flex-wrap.gap-2.mt-2{display:none!important}
.timeline-wrap{max-height:calc(100vh - 118px)!important;border:2px solid #1347a8;border-radius:3px!important;background:#dbeafe;overflow:auto!important}
.timeline-table{font-size:.72rem!important;background:#fff;}

.calendar-fit-mode-fit{width:100%!important;}
.calendar-fit-mode-fit .timeline-days-head,
.calendar-fit-mode-fit .timeline-grid-area{transition:width .12s ease;}
.calendar-fit-mode-scroll{width:max-content!important;}
.timeline-wrap[data-fit-mode="fit"]{overflow-x:hidden!important;}
.timeline-wrap[data-fit-mode="scroll"]{overflow-x:auto!important;}
.timeline-head{position:sticky!important;top:0!important;z-index:80!important;background:#0b45a3!important}
.timeline-room-head{width:190px!important;min-width:190px!important;position:sticky!important;left:0!important;z-index:95!important;background:linear-gradient(180deg,#2b74d6,#0c3d99)!important;color:#fff!important;border-right:2px solid #062b73!important;border-bottom:2px solid #062b73!important;padding:4px 6px!important;font-weight:900!important;text-transform:uppercase;font-size:.72rem!important}
.timeline-days-head{border-bottom:2px solid #062b73!important;background:#0b45a3!important}
.timeline-day-head{width:var(--calendar-day-width,42px)!important;min-width:var(--calendar-day-width,42px)!important;border-right:1px solid #062b73!important;padding:2px 1px!important;background:linear-gradient(180deg,#2b74d6,#0d47a1)!important;color:#fff!important;height:34px!important}
.timeline-day-head.is-today{background:linear-gradient(180deg,#06b6d4,#0284c7)!important}
.day-name{font-size:.52rem!important;color:#dbeafe!important;line-height:1!important}
.day-num{font-size:.72rem!important;color:#fff!important;line-height:1.05!important;font-weight:900!important}
.day-mon{font-size:.50rem!important;color:#bfdbfe!important;line-height:1!important}
.timeline-row{border-bottom:1px solid #94a3b8!important;background:#fff!important}
.timeline-room-meta{width:190px!important;min-width:190px!important;position:sticky!important;left:0!important;z-index:45!important;background:linear-gradient(90deg,#1d4ed8,#2563eb)!important;color:#fff!important;border-right:2px solid #0f2f78!important;padding:3px 6px!important;display:flex;align-items:center;gap:6px;min-height:30px!important}
.room-title{font-size:.72rem!important;color:#fff!important;line-height:1!important;min-width:64px;font-weight:900!important}
.room-subtitle{font-size:.66rem!important;color:#dbeafe!important;line-height:1!important;margin:0!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:82px}
.room-status,.room-capacity-badge{display:none!important}
.timeline-grid-area{background:#fff!important;}
.timeline-grid-lines{z-index:1!important}
.timeline-cell{width:var(--calendar-day-width,42px)!important;min-width:var(--calendar-day-width,42px)!important;border-right:1px solid #cbd5e1!important;background:#f8fafc!important}
.timeline-cell:nth-child(even){background:#eff6ff!important}
.timeline-cell.is-today{background:#67e8f9!important;opacity:.72}
.timeline-bars{z-index:5!important;overflow:visible!important}
.booking-bar{height:24px!important;border-radius:3px!important;color:#fff!important;border:1px solid rgba(15,23,42,.28)!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 3px rgba(15,23,42,.22)!important;padding:0 4px!important;z-index:10!important;overflow:hidden!important;text-shadow:0 1px 1px rgba(0,0,0,.28)!important}
.booking-bar:hover{filter:brightness(1.05)!important;z-index:60!important}
.bar-guest{font-size:.66rem!important;color:#000!important;font-weight:900!important;letter-spacing:-.01em!important;text-transform:none!important}
.bar-open{display:none!important}
.bar-occupancy-badge{display:none!important}
.booking-bar.occupancy-triple,.booking-bar.occupancy-family{outline:0!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.55),0 1px 2px rgba(15,23,42,.18)!important}
.card.border-0.shadow-sm:has(.timeline-wrap){border-radius:4px!important;border:1px solid #1347a8!important;box-shadow:none!important}

/* Compact temporary holding area on calendar */
.holding-drop-card{display:block!important;margin-bottom:6px!important;border:1px solid #f59e0b!important;border-radius:8px!important;box-shadow:0 3px 10px rgba(15,23,42,.06)!important}
.holding-drop-card .card-header{padding:6px 8px 2px!important}
.holding-drop-card .card-body{padding:6px 8px!important}
.holding-drop-card h5{font-size:.86rem!important;margin:0!important}
.holding-drop-card .text-muted{font-size:.68rem!important}
.holding-drop-zone{min-height:48px!important;padding:6px 8px!important;border-radius:8px!important}
.holding-drop-help{font-size:.74rem!important}
.timeline-unassigned{gap:6px!important}
.timeline-unassigned-card{width:230px!important;padding:7px 8px!important;border-radius:8px!important}
.booking-bar-static{cursor:grab!important}
.timeline-row.drag-over .timeline-grid-area{background:rgba(37,99,235,.10)!important;box-shadow:inset 0 0 0 2px rgba(37,99,235,.35)!important}
.holding-drop-card.drag-over .holding-drop-zone,.holding-drop-zone.drag-over{background:#fff7ed!important;box-shadow:inset 0 0 0 2px rgba(245,158,11,.38)!important}
@media (max-width: 991.98px){
    .timeline-wrap{max-height:calc(100vh - 140px)!important}
    .timeline-room-head,.timeline-room-meta{width:170px!important;min-width:170px!important}
}


/* Right-click reservation action menu */
.calendar-context-menu{
    position:fixed;
    z-index:99999;
    min-width:230px;
    background:#fff;
    border:1px solid rgba(15,23,42,.10);
    border-radius:14px;
    box-shadow:0 22px 55px rgba(15,23,42,.24);
    padding:7px;
    display:none;
}
.calendar-context-menu.is-open{display:block;}
.calendar-context-menu .ctx-title{
    padding:8px 10px 7px;
    border-bottom:1px solid #eef2f7;
    margin-bottom:5px;
    font-size:.75rem;
    font-weight:900;
    color:#0f172a;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
}
.calendar-context-menu a{
    display:flex;
    align-items:center;
    gap:9px;
    padding:8px 10px;
    border-radius:10px;
    color:#1f2937;
    text-decoration:none;
    font-size:.84rem;
    font-weight:700;
}
.calendar-context-menu a:hover{background:#f1f5f9;color:#0f172a;}
.calendar-context-menu .ctx-divider{height:1px;background:#eef2f7;margin:5px 2px;}
.calendar-context-menu a:focus,
.calendar-context-menu .ctx-submenu-toggle:focus{
    outline:2px solid rgba(37,99,235,.35);
    background:#eff6ff;
}
.ctx-icon{
    width:22px;
    height:22px;
    border-radius:8px;
    display:inline-flex;
    align-items:center;
    justify-content:center;
    flex:0 0 auto;
}
.ctx-icon-standard{background:#fee2e2;color:#b91c1c;}
.ctx-icon-master{background:#e0e7ff;color:#3730a3;}
.ctx-icon-room{background:#dcfce7;color:#15803d;}
.ctx-icon-guest{background:#f0fdf4;color:#16a34a;}
.ctx-icon-pay{background:#fef3c7;color:#b45309;}
.ctx-icon-extra{background:#dcfce7;color:#15803d;}
.ctx-icon-folio{background:#f1f5f9;color:#475569;}
.ctx-icon-edit{background:#dbeafe;color:#1d4ed8;}
.ctx-submenu{position:relative;}
.ctx-submenu-toggle{
    display:flex;
    align-items:center;
    gap:9px;
    padding:8px 10px;
    border-radius:10px;
    color:#1f2937;
    text-decoration:none;
    font-size:.84rem;
    font-weight:700;
    cursor:pointer;
}
.ctx-submenu-toggle:hover,
.ctx-submenu.is-active > .ctx-submenu-toggle{background:#f1f5f9;color:#0f172a;}
.ctx-submenu-toggle .ctx-chevron{margin-left:auto;font-size:.75rem;color:#64748b;}
.ctx-submenu-items{
    position:absolute;
    top:-6px;
    left:calc(100% + 7px);
    min-width:245px;
    background:#fff;
    border:1px solid rgba(15,23,42,.10);
    border-radius:14px;
    box-shadow:0 22px 55px rgba(15,23,42,.20);
    padding:7px;
    display:none;
    z-index:100000;
}
.ctx-submenu.is-active .ctx-submenu-items{display:block;}
.ctx-submenu-items::before{
    content:"";
    position:absolute;
    top:0;
    left:-10px;
    width:10px;
    height:100%;
}
.ctx-submenu-items a{font-size:.82rem;}
.ctx-submenu-note{
    padding:6px 10px 7px;
    font-size:.66rem;
    color:#64748b;
    font-weight:800;
    text-transform:uppercase;
    letter-spacing:.05em;
}
@media (max-width: 768px){
    .ctx-submenu-items{left:auto;right:calc(100% + 7px);}
}
.booking-bar,.booking-bar-static{user-select:none;}
.booking-bar.is-context-selected{outline:2px solid rgba(255,255,255,.95)!important;box-shadow:0 0 0 3px rgba(37,99,235,.55),0 10px 26px rgba(15,23,42,.28)!important;}
.booking-bar-static.is-context-selected{outline:2px solid rgba(37,99,235,.55)!important;}


.planner-search-trigger{
    width:34px;
    min-width:34px;
    display:inline-flex;
    align-items:center;
    justify-content:center;
}
.planner-search-modal .modal-header{
    background:linear-gradient(135deg,#f8fbff 0%,#ffffff 100%);
}
.planner-result-list{
    border:1px solid #e5e7eb;
    border-radius:14px;
    background:#fff;
    overflow:hidden;
    max-height:360px;
    overflow-y:auto;
}
.planner-result-empty{
    padding:14px 16px;
    color:#64748b;
    font-weight:700;
    background:#f8fafc;
}
.planner-result-item{
    padding:12px 14px;
    border-bottom:1px solid #eef2f7;
}
.planner-result-item:last-child{border-bottom:0}
.planner-result-title{
    font-weight:900;
    color:#0f172a;
}
.planner-result-meta{
    color:#64748b;
    font-size:.78rem;
    line-height:1.35;
}
.planner-result-badges{
    display:flex;
    flex-wrap:wrap;
    gap:5px;
    margin-top:7px;
}

.semper-chip-reset{color:#dc2626!important;background:#fff1f2!important;border-color:#fecdd3!important;text-decoration:none!important}
.semper-chip-reset:hover{background:#ffe4e6!important;color:#be123c!important}
.calendar-folio-modal .modal-header{background:linear-gradient(135deg,#f8fbff 0%,#ffffff 100%);border-bottom:1px solid #e5e7eb}
.folio-mini-card{border:1px solid #e5e7eb;border-radius:16px;padding:12px 14px;background:#fff;min-height:76px;box-shadow:0 8px 22px rgba(15,23,42,.045)}
.folio-mini-card span{display:block;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:#64748b;font-weight:800;margin-bottom:4px}
.folio-mini-card strong{display:block;font-size:.98rem;color:#0f172a;word-break:break-word}
.folio-action-strip{display:flex;flex-wrap:wrap;gap:8px;border:1px solid #e5e7eb;background:#f8fafc;border-radius:16px;padding:10px}
.folio-breakdown-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.folio-breakdown-pill{border:1px solid #e5e7eb;border-radius:14px;background:#fff;padding:8px 10px}
.folio-breakdown-pill span{display:block;font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:#64748b;font-weight:900}
.folio-breakdown-pill strong{display:block;margin-top:2px;font-size:.90rem;color:#0f172a}
.folio-section-card{border:1px solid #e5e7eb;border-radius:18px;background:#fff;overflow:hidden;min-height:190px;box-shadow:0 10px 26px rgba(15,23,42,.05)}
.folio-section-title{padding:11px 13px;font-size:.82rem;font-weight:900;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e5e7eb}
.folio-section-title.master{background:#eef2ff;color:#3730a3}
.folio-section-title.room{background:#ecfdf5;color:#047857}
.folio-section-title.guest{background:#fff7ed;color:#c2410c}
.folio-list{padding:10px;display:flex;flex-direction:column;gap:8px}
.folio-row{border:1px solid #eef2f7;border-radius:14px;background:#fff;overflow:hidden}
.folio-row summary{list-style:none;cursor:pointer;padding:10px}
.folio-row summary::-webkit-details-marker{display:none}
.folio-row .folio-label{font-weight:800;color:#0f172a;font-size:.86rem;margin-bottom:6px;display:flex;justify-content:space-between;gap:8px}
.folio-row .folio-values{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
.folio-row .folio-values span{display:block;background:#f8fafc;border-radius:10px;padding:6px 7px;font-size:.72rem;color:#64748b}
.folio-row .folio-values strong{display:block;color:#0f172a;font-size:.82rem;margin-top:2px}
.folio-row .folio-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:8px;flex-wrap:wrap}
.folio-row .folio-actions .btn{font-size:.72rem;padding:4px 8px;border-radius:9px;font-weight:700}
.folio-empty{padding:18px 12px;text-align:center;color:#64748b;font-size:.84rem}
.folio-mini-subline{display:block;font-size:.72rem;font-weight:700;color:#0f766e;margin-top:3px;line-height:1.15}
.frontoffice-housekeeping{border:1px solid #bae6fd;background:#f0f9ff;border-radius:13px;padding:10px;margin-bottom:10px}
.frontoffice-housekeeping-title{font-weight:900;color:#075985;font-size:.82rem;margin-bottom:7px;text-transform:uppercase;letter-spacing:.03em}
.frontoffice-housekeeping-item{display:flex;justify-content:space-between;gap:10px;border-top:1px dashed #bae6fd;padding-top:6px;margin-top:6px;font-size:.8rem}
.frontoffice-housekeeping-item:first-of-type{border-top:0;padding-top:0;margin-top:0}
.frontoffice-housekeeping-item strong{color:#0f172a}
.frontoffice-housekeeping-item span{color:#0369a1;font-weight:800}
.frontoffice-note{border:1px solid #e5e7eb;border-radius:12px;background:#fff;padding:9px 10px}
.frontoffice-note strong{display:block;color:#0f172a;font-size:.78rem}
.frontoffice-note span{display:block;color:#475569;font-size:.82rem;white-space:pre-wrap;margin-top:3px}
.rooming-detail-card{border:1px solid #e5e7eb;border-radius:12px;background:#fff;padding:9px 10px}
.rooming-detail-card .rooming-title{font-weight:900;color:#0f172a}
.rooming-detail-card .rooming-meta{font-size:.78rem;color:#64748b;margin-top:2px}
.rooming-detail-card.is-primary-room{border:2px solid #2563eb;background:linear-gradient(135deg,#eff6ff,#ffffff);box-shadow:0 4px 14px rgba(37,99,235,.12)}
.rooming-group-block{margin-bottom:14px}
.rooming-group-block:last-child{margin-bottom:0}
.rooming-group-heading{font-weight:900;color:#0f172a;font-size:.85rem;padding:6px 2px;border-bottom:2px solid #e5e7eb;margin-bottom:8px}
.rooming-group-block .rooming-detail-card{margin-bottom:8px}
.rooming-group-block .rooming-detail-card:last-child{margin-bottom:0}
.tx-tree{border-top:1px solid #eef2f7;background:#fbfdff;padding:10px}
.tx-row{
    display:grid;
    grid-template-columns:22px 1fr auto;
    grid-template-rows:auto auto;
    column-gap:8px;
    row-gap:2px;
    align-items:center;
    padding:7px 8px;
    border:1px solid #e5e7eb;
    background:#fff;
    border-radius:10px;
    margin-bottom:6px;
    font-size:.76rem;
}
.tx-row .tx-select{grid-column:1;grid-row:1 / span 2;display:flex;align-items:center;justify-content:center}
.tx-row .tx-desc{grid-column:2;grid-row:1;min-width:0}
.tx-row .tx-desc strong{display:block;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tx-row .tx-amount{grid-column:3;grid-row:1;text-align:right;font-weight:900;white-space:nowrap}
.tx-row .tx-meta{grid-column:2;grid-row:2;min-width:0;display:flex;flex-wrap:wrap;gap:2px 8px;color:#64748b;font-size:.7rem}
.tx-row .tx-meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.tx-row.is-payment .tx-amount{color:#15803d}
.tx-row.is-charge .tx-amount{color:#b45309}
.tx-row.is-void{opacity:.55;text-decoration:line-through}
@media(max-width:991.98px){.folio-breakdown-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:767.98px){.tx-row{grid-template-columns:18px 1fr auto}.folio-row .folio-values{grid-template-columns:1fr}}

.tx-actions{
    grid-column:3;
    grid-row:2;
    display:flex;
    gap:4px;
    align-items:center;
    justify-self:end;
    flex-wrap:nowrap;
}
.tx-actions .btn{
    white-space:nowrap;
    padding:3px 7px;
    font-size:.72rem;
    line-height:1;
}
.tx-actions .btn[disabled]{opacity:.35;cursor:not-allowed;}

.folio-move-selected-btn{
    font-size:.72rem;
    padding:4px 8px;
    border-radius:9px;
    font-weight:700;
}

/* Planner hover guide: highlight current room row and date column while scrolling */
.timeline-row.is-hover-room .timeline-room-meta{
    background:linear-gradient(90deg,#0f766e,#14b8a6)!important;
    box-shadow:inset -4px 0 0 #facc15,0 0 0 2px rgba(20,184,166,.18)!important;
}
.timeline-row.is-hover-room .timeline-grid-area{
    background:rgba(20,184,166,.045)!important;
}
.timeline-day-head.is-hover-date{
    background:linear-gradient(180deg,#f59e0b,#d97706)!important;
    color:#fff!important;
    box-shadow:inset 0 -3px 0 #fef3c7!important;
}
.timeline-cell.is-hover-date{
    background:rgba(250,204,21,.38)!important;
    box-shadow:inset 0 0 0 2px rgba(245,158,11,.45)!important;
}
.timeline-cell.is-hover-range{
    background:rgba(251,191,36,.25)!important;
}
.booking-bar.is-hover-booking{
    z-index:85!important;
    outline:2px solid rgba(255,255,255,.95)!important;
    box-shadow:0 0 0 3px rgba(245,158,11,.65),0 12px 30px rgba(15,23,42,.32)!important;
}





.planner-availability-strip-inline{
    background:#fff;
    border:1px solid #dbeafe;
    border-radius:16px;
    box-shadow:0 10px 30px rgba(15,23,42,.08);
    padding:10px;
    margin-bottom:18px;
}
.planner-availability-title{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:12px;
    padding:4px 4px 10px;
}
.planner-availability-scroll{
    overflow-x:auto;
    border:1px solid #e5e7eb;
    border-radius:12px;
}
.planner-availability-grid{
    display:grid;
    grid-template-columns:var(--planner-label-width) repeat(<?php echo (int)$totalDays; ?>, var(--planner-day-width));
    min-width:calc(var(--planner-label-width) + (<?php echo (int)$totalDays; ?> * var(--planner-day-width)));
}
.planner-availability-corner,
.planner-availability-date,
.planner-availability-roomtype,
.planner-availability-cell{
    border-right:1px solid #e5e7eb;
    border-bottom:1px solid #e5e7eb;
    min-height:42px;
    box-sizing:border-box;
}
.planner-availability-corner{
    position:sticky;
    left:0;
    z-index:3;
    background:#0f172a;
    color:#fff;
    font-weight:900;
    display:flex;
    align-items:center;
    padding:0 10px;
}
.planner-availability-date{
    background:#f8fafc;
    color:#334155;
    text-align:center;
    font-size:.68rem;
    line-height:1.05;
    padding:5px 2px;
}
.planner-availability-date strong{
    display:block;
    color:#0f172a;
    font-size:.9rem;
}
.planner-availability-date.is-today{
    background:#dbeafe;
    color:#1d4ed8;
}
.planner-availability-roomtype{
    position:sticky;
    left:0;
    z-index:2;
    background:#fff;
    padding:7px 10px;
    display:flex;
    flex-direction:column;
    justify-content:center;
}
.planner-availability-roomtype strong{
    font-size:.78rem;
    color:#0f172a;
    line-height:1.1;
}
.planner-availability-roomtype span{
    font-size:.68rem;
    color:#64748b;
}
.planner-availability-total-label{
    background:#eff6ff;
}
.planner-availability-total-label strong{
    color:#1d4ed8;
}
.planner-availability-cell{
    display:flex;
    align-items:center;
    justify-content:center;
    gap:1px;
    font-size:.72rem;
    background:#ecfdf5;
    color:#065f46;
}
.planner-availability-cell strong{
    font-size:.95rem;
}
.planner-availability-cell.total{
    font-weight:900;
    border-top:2px solid #93c5fd;
    border-bottom:2px solid #93c5fd;
}
.planner-availability-cell.total strong{
    font-size:1.05rem;
}
.planner-availability-cell.medium{
    background:#fef9c3;
    color:#854d0e;
}
.planner-availability-cell.low{
    background:#ffedd5;
    color:#9a3412;
}
.planner-availability-cell.sold-out{
    background:#fee2e2;
    color:#991b1b;
}
@media(max-width:768px){
    .planner-availability-title{align-items:flex-start;flex-direction:column}
}

</style>



<div id="calendarBookingContextMenu" class="calendar-context-menu" aria-hidden="true" role="menu">
    <div class="ctx-title" id="calendarContextTitle">Booking actions</div>
    <a data-context-action="edit" href="#" role="menuitem" tabindex="-1"><span class="ctx-icon ctx-icon-edit"><i class="bi bi-pencil-square"></i></span>Edit Booking</a>
    <a data-context-action="move_to_holding" href="#" role="menuitem" tabindex="-1" style="display:none;"><span class="ctx-icon ctx-icon-extra"><i class="bi bi-inboxes"></i></span>Move to Holding</a>
    <div class="ctx-divider"></div>
    <a data-context-action="folio" href="#" role="menuitem" tabindex="-1"><span class="ctx-icon ctx-icon-folio"><i class="bi bi-journal-richtext"></i></span>Folio</a>
    <a data-context-action="transfer_charges" href="#" role="menuitem" tabindex="-1"><span class="ctx-icon ctx-icon-extra"><i class="bi bi-arrow-left-right"></i></span>Transfer Booking Charges</a>

    <div class="ctx-submenu" data-ctx-submenu>
        <div class="ctx-submenu-toggle" role="menuitem" tabindex="-1" aria-haspopup="true" aria-expanded="false">
            <span class="ctx-icon ctx-icon-standard"><i class="bi bi-file-earmark-pdf"></i></span>
            <span>Proformas</span>
            <i class="bi bi-chevron-right ctx-chevron"></i>
        </div>
        <div class="ctx-submenu-items" role="menu">
            <div class="ctx-submenu-note">Print / Preview</div>
            <a data-context-action="proforma_standard" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-standard"><i class="bi bi-file-earmark-pdf"></i></span>Proforma
            </a>
            <a data-context-action="proforma_master" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-master"><i class="bi bi-collection"></i></span>Main Folio Proforma
            </a>
            <a data-context-action="proforma_room" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-room"><i class="bi bi-door-open"></i></span>Room Proforma
            </a>
            <a data-context-action="proforma_guest" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-guest"><i class="bi bi-person-badge"></i></span>Guest Folio Proforma
            </a>
        </div>
    </div>

    <div class="ctx-submenu" data-ctx-submenu>
        <div class="ctx-submenu-toggle" role="menuitem" tabindex="-1" aria-haspopup="true" aria-expanded="false">
            <span class="ctx-icon ctx-icon-standard"><i class="bi bi-envelope-paper"></i></span>
            <span>Email Proformas</span>
            <i class="bi bi-chevron-right ctx-chevron"></i>
        </div>
        <div class="ctx-submenu-items" role="menu">
            <div class="ctx-submenu-note">Open email client + PDF</div>
            <a data-context-action="email_proforma_standard" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-standard"><i class="bi bi-envelope-paper"></i></span>Email Proforma
            </a>
            <a data-context-action="email_proforma_master" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-master"><i class="bi bi-envelope-paper-heart"></i></span>Email Main Folio Proforma
            </a>
            <a data-context-action="email_proforma_room" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-room"><i class="bi bi-envelope-paper"></i></span>Email Room Proforma
            </a>
            <a data-context-action="email_proforma_guest" href="#" target="_blank" rel="noopener" role="menuitem" tabindex="-1">
                <span class="ctx-icon ctx-icon-guest"><i class="bi bi-envelope-at"></i></span>Email Guest Folio Proforma
            </a>
        </div>
    </div>

    <a data-context-action="payments" href="#" role="menuitem" tabindex="-1"><span class="ctx-icon ctx-icon-pay"><i class="bi bi-credit-card-2-front"></i></span>Payments</a>
    <a data-context-action="extras" href="#" role="menuitem" tabindex="-1"><span class="ctx-icon ctx-icon-extra"><i class="bi bi-plus-circle"></i></span>Extras</a>
</div>

<div class="modal fade" id="calendarReturnToCalendarModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><i class="bi bi-calendar-check me-2"></i>Return Booking to Calendar</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div id="calendarReturnSummary" class="mb-3 small text-muted"></div>
                <div id="calendarReturnMatchingWrap">
                    <div class="fw-semibold small text-uppercase text-muted mb-2">Matching room type</div>
                    <div id="calendarReturnMatchingRooms" class="list-group mb-2"></div>
                    <div id="calendarReturnMatchingEmpty" class="text-muted small d-none">No available rooms of this type for these dates.</div>
                </div>
                <button type="button" id="calendarReturnShowAllBtn" class="btn btn-link btn-sm px-0 mt-2 d-none">
                    <i class="bi bi-grid-3x3-gap me-1"></i>Show all available rooms
                </button>
                <div id="calendarReturnAllWrap" class="d-none mt-2">
                    <div class="fw-semibold small text-uppercase text-muted mb-2">All available rooms</div>
                    <div id="calendarReturnAllRooms" class="list-group"></div>
                    <div id="calendarReturnAllEmpty" class="text-muted small d-none">No available rooms for these dates.</div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    function attachDragSources() {
        document.querySelectorAll('.booking-bar, .booking-bar-static').forEach(function (bar) {
            bar.addEventListener('dragstart', function (e) {
                e.dataTransfer.setData('rooming_id', this.getAttribute('data-rooming-id') || '');
                e.dataTransfer.setData('order_id', this.getAttribute('data-order-id') || '');
                e.dataTransfer.setData('check_in', this.getAttribute('data-check-in') || '');
                e.dataTransfer.setData('check_out', this.getAttribute('data-check-out') || '');
                e.dataTransfer.setData('room_type', this.getAttribute('data-room-type') || '');
                e.dataTransfer.setData('room_type_id', this.getAttribute('data-room-type-id') || '0');
                e.dataTransfer.setData('held', this.getAttribute('data-held') === '1' ? '1' : '0');
            });
        });
    }

    function moveRooming(roomId, roomingId, orderId, checkIn, checkOut, holdingReason) {
        if (!roomingId || !orderId) return;

        const formData = new FormData();
        formData.append('room_id', roomId);
        formData.append('rooming_id', roomingId);
        formData.append('order_id', orderId);
        formData.append('check_in', checkIn || '');
        formData.append('check_out', checkOut || '');
        formData.append('holding_reason', holdingReason || '');

        fetch('move_booking.php', {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        })
        .then(function (r) { return r.json(); })
        .then(function (json) {
            if (json && json.ok) window.location.reload();
            else alert((json && json.message) ? json.message : 'Could not move rooming.');
        })
        .catch(function () { alert('Could not move rooming right now.'); });
    }

    function parseLocalDate(value) {
        if (!value) return null;
        const parts = String(value).substring(0, 10).split('-').map(function (n) { return parseInt(n, 10); });
        if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) return null;
        return new Date(parts[0], parts[1] - 1, parts[2]);
    }

    function daysBetween(a, b) {
        return Math.floor((b.getTime() - a.getTime()) / 86400000);
    }

    function getAvailableRooms(checkIn, checkOut, excludeRoomingId, roomType, roomTypeId) {
        const ci = parseLocalDate(checkIn);
        const co = parseLocalDate(checkOut);
        const rows = document.querySelectorAll('.timeline-row[data-room-dropzone]');
        const results = [];
        const wantedTypeId = parseInt(roomTypeId || '0', 10) || 0;

        rows.forEach(function (row) {
            const rowType = row.getAttribute('data-room-type') || '';
            const rowTypeId = parseInt(row.getAttribute('data-room-type-id') || '0', 10) || 0;

            // Match by the real room_type_id whenever we have one - this is what the
            // server actually validates against. Two room types can share the same
            // display name (e.g. two "Family Room" entries with different IDs), which
            // used to let a room through here that the backend then correctly rejected.
            if (wantedTypeId > 0) {
                if (rowTypeId !== wantedTypeId) return;
            } else if (roomType && rowType !== roomType) {
                return;
            }

            let occupied = false;
            row.querySelectorAll('.booking-bar').forEach(function (bar) {
                if (occupied) return;
                const roomingId = bar.getAttribute('data-rooming-id') || '';
                if (excludeRoomingId && roomingId === excludeRoomingId) return;
                const bCi = parseLocalDate(bar.getAttribute('data-check-in'));
                const bCo = parseLocalDate(bar.getAttribute('data-check-out'));
                if (!bCi || !bCo || !ci || !co) return;
                if (ci < bCo && co > bCi) occupied = true;
            });

            if (!occupied) {
                results.push({
                    roomId: row.getAttribute('data-room-id'),
                    roomNo: row.getAttribute('data-room-no'),
                    roomType: rowType,
                    roomTypeId: rowTypeId
                });
            }
        });

        return results;
    }

    let returnToCalendarModalInstance = null;

    function renderRoomList(container, emptyEl, rooms, bookingData) {
        container.innerHTML = '';
        if (!rooms.length) {
            emptyEl.classList.remove('d-none');
            return;
        }
        emptyEl.classList.add('d-none');
        rooms.forEach(function (r) {
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
            btn.innerHTML = '<span><i class="bi bi-door-open me-2"></i>Room ' + r.roomNo +
                (r.roomType ? ' <span class="text-muted small">(' + r.roomType + ')</span>' : '') + '</span>' +
                '<span class="badge text-bg-success">Available</span>';
            btn.addEventListener('click', function () {
                moveRooming(r.roomId, bookingData.roomingId, bookingData.orderId, bookingData.checkIn, bookingData.checkOut, '');
                if (returnToCalendarModalInstance) returnToCalendarModalInstance.hide();
            });
            container.appendChild(btn);
        });
    }

    function openReturnToCalendarModal(bookingData) {
        const modalEl = document.getElementById('calendarReturnToCalendarModal');
        if (!modalEl || !window.bootstrap) return;

        const summary = document.getElementById('calendarReturnSummary');
        const matchingWrap = document.getElementById('calendarReturnMatchingRooms');
        const matchingEmpty = document.getElementById('calendarReturnMatchingEmpty');
        const showAllBtn = document.getElementById('calendarReturnShowAllBtn');
        const allWrap = document.getElementById('calendarReturnAllWrap');
        const allRoomsList = document.getElementById('calendarReturnAllRooms');
        const allEmpty = document.getElementById('calendarReturnAllEmpty');

        if (summary) {
            summary.textContent = (bookingData.roomType ? bookingData.roomType + ' • ' : '') +
                bookingData.checkIn + ' → ' + bookingData.checkOut +
                ' — choose a room to place this booking back on the calendar.';
        }

        allWrap.classList.add('d-none');
        showAllBtn.classList.remove('d-none');

        const matchingRooms = getAvailableRooms(bookingData.checkIn, bookingData.checkOut, bookingData.roomingId, bookingData.roomType, bookingData.roomTypeId);
        renderRoomList(matchingWrap, matchingEmpty, matchingRooms, bookingData);

        showAllBtn.onclick = function () {
            const allRooms = getAvailableRooms(bookingData.checkIn, bookingData.checkOut, bookingData.roomingId, '', '');
            renderRoomList(allRoomsList, allEmpty, allRooms, bookingData);
            allWrap.classList.remove('d-none');
            showAllBtn.classList.add('d-none');
        };

        returnToCalendarModalInstance = bootstrap.Modal.getOrCreateInstance(modalEl);
        returnToCalendarModalInstance.show();
    }

    function fitCalendarRangeToScreen() {
        const wrap = document.getElementById('calendarTimelineWrap');
        if (!wrap) return;

        const mode = wrap.getAttribute('data-fit-mode') || 'scroll';
        const totalDays = parseInt(wrap.getAttribute('data-total-days') || '0', 10);
        const roomWidth = parseInt(wrap.getAttribute('data-room-label-width') || '190', 10);
        const defaultDayWidth = parseInt(wrap.getAttribute('data-default-day-width') || '42', 10);
        const table = wrap.querySelector('.timeline-table');
        const daysHead = wrap.querySelector('.timeline-days-head');
        const rangeStart = parseLocalDate(wrap.getAttribute('data-range-from'));
        const rangeEnd = parseLocalDate(wrap.getAttribute('data-range-to'));

        if (!table || !daysHead || !totalDays || !rangeStart || !rangeEnd) return;

        let dayWidth = defaultDayWidth;
        if (mode === 'fit') {
            const available = Math.max(0, wrap.clientWidth - roomWidth - 2);
            dayWidth = Math.max(defaultDayWidth, Math.floor(available / totalDays));
        }

        const gridWidth = dayWidth * totalDays;
        const fullWidth = roomWidth + gridWidth;

        wrap.style.setProperty('--calendar-day-width', dayWidth + 'px');
        table.style.minWidth = fullWidth + 'px';
        if (mode === 'fit') table.style.width = '100%';
        else table.style.width = fullWidth + 'px';

        daysHead.style.width = gridWidth + 'px';

        wrap.querySelectorAll('.timeline-grid-area').forEach(function (area) {
            area.style.width = gridWidth + 'px';
        });

        wrap.querySelectorAll('.booking-bar').forEach(function (bar) {
            const checkIn = parseLocalDate(bar.getAttribute('data-check-in'));
            const checkOut = parseLocalDate(bar.getAttribute('data-check-out'));
            if (!checkIn || !checkOut) return;

            const start = checkIn > rangeStart ? checkIn : rangeStart;
            const checkOutMinusOne = new Date(checkOut.getTime());
            checkOutMinusOne.setDate(checkOutMinusOne.getDate() - 1);
            const end = checkOutMinusOne < rangeEnd ? checkOutMinusOne : rangeEnd;

            if (end < rangeStart || start > rangeEnd || end < start) return;

            const offset = Math.max(0, daysBetween(rangeStart, start));
            const span = Math.max(1, daysBetween(start, end) + 1);

            bar.style.left = (offset * dayWidth) + 'px';
            bar.style.width = Math.max(dayWidth, span * dayWidth) + 'px';
        });
    }

    fitCalendarRangeToScreen();
    window.addEventListener('resize', fitCalendarRangeToScreen);


    function setupCalendarHoverGuide() {
        const wrap = document.getElementById('calendarTimelineWrap');
        if (!wrap) return;

        function clearHover() {
            wrap.querySelectorAll('.timeline-row.is-hover-room, .booking-bar.is-hover-booking').forEach(function (el) {
                el.classList.remove('is-hover-room', 'is-hover-booking');
            });
            wrap.querySelectorAll('.timeline-day-head.is-hover-date, .timeline-cell.is-hover-date, .timeline-cell.is-hover-range').forEach(function (el) {
                el.classList.remove('is-hover-date', 'is-hover-range');
            });
        }

        function highlightRow(row) {
            wrap.querySelectorAll('.timeline-row.is-hover-room').forEach(function (el) {
                if (el !== row) el.classList.remove('is-hover-room');
            });
            if (row) row.classList.add('is-hover-room');
        }

        function highlightDay(dayIndex) {
            dayIndex = parseInt(dayIndex, 10);
            wrap.querySelectorAll('.timeline-day-head.is-hover-date, .timeline-cell.is-hover-date, .timeline-cell.is-hover-range').forEach(function (el) {
                el.classList.remove('is-hover-date', 'is-hover-range');
            });
            if (isNaN(dayIndex) || dayIndex < 0) return;
            wrap.querySelectorAll('[data-calendar-day-cell][data-day-index="' + dayIndex + '"]').forEach(function (el) {
                el.classList.add('is-hover-date');
            });
        }

        function highlightRange(start, span) {
            start = parseInt(start, 10);
            span = Math.max(1, parseInt(span || '1', 10));
            wrap.querySelectorAll('.timeline-day-head.is-hover-date, .timeline-cell.is-hover-date, .timeline-cell.is-hover-range').forEach(function (el) {
                el.classList.remove('is-hover-date', 'is-hover-range');
            });
            if (isNaN(start) || start < 0) return;
            for (let i = start; i < start + span; i++) {
                wrap.querySelectorAll('[data-calendar-day-cell][data-day-index="' + i + '"]').forEach(function (el) {
                    el.classList.add(i === start ? 'is-hover-date' : 'is-hover-range');
                });
            }
        }

        function dayIndexFromEvent(area, event) {
            const rect = area.getBoundingClientRect();
            const dayWidth = parseInt(getComputedStyle(wrap).getPropertyValue('--calendar-day-width'), 10) ||
                parseInt(wrap.getAttribute('data-default-day-width') || '42', 10);
            const x = event.clientX - rect.left;
            return Math.floor(x / Math.max(1, dayWidth));
        }

        wrap.querySelectorAll('.timeline-row').forEach(function (row) {
            row.addEventListener('mouseenter', function () { highlightRow(row); });
        });

        wrap.querySelectorAll('.timeline-grid-area').forEach(function (area) {
            area.addEventListener('mousemove', function (e) {
                const row = area.closest('.timeline-row');
                highlightRow(row);
                highlightDay(dayIndexFromEvent(area, e));
            });
        });

        wrap.querySelectorAll('.timeline-day-head').forEach(function (head) {
            head.addEventListener('mouseenter', function () {
                highlightDay(head.getAttribute('data-day-index'));
            });
        });

        wrap.querySelectorAll('.booking-bar').forEach(function (bar) {
            bar.addEventListener('mouseenter', function () {
                const row = bar.closest('.timeline-row');
                highlightRow(row);
                bar.classList.add('is-hover-booking');
                highlightRange(bar.getAttribute('data-range-offset'), bar.getAttribute('data-range-span'));
            });
            bar.addEventListener('mouseleave', function () {
                bar.classList.remove('is-hover-booking');
            });
        });

        wrap.addEventListener('mouseleave', clearHover);
    }

    setupCalendarHoverGuide();

    attachDragSources();

    document.querySelectorAll('[data-room-dropzone]').forEach(function (row) {
        row.addEventListener('dragover', function (e) { e.preventDefault(); this.classList.add('drag-over'); });
        row.addEventListener('dragleave', function () { this.classList.remove('drag-over'); });
        row.addEventListener('drop', function (e) {
            e.preventDefault();
            this.classList.remove('drag-over');

            const roomingId = e.dataTransfer.getData('rooming_id');
            const orderId = e.dataTransfer.getData('order_id');
            const checkIn = e.dataTransfer.getData('check_in');
            const checkOut = e.dataTransfer.getData('check_out');
            const roomType = e.dataTransfer.getData('room_type');
            const roomTypeId = e.dataTransfer.getData('room_type_id');
            const isHeld = e.dataTransfer.getData('held') === '1';

            if (isHeld) {
                openReturnToCalendarModal({
                    roomingId: roomingId,
                    orderId: orderId,
                    checkIn: checkIn,
                    checkOut: checkOut,
                    roomType: roomType,
                    roomTypeId: roomTypeId
                });
                return;
            }

            moveRooming(
                this.getAttribute('data-room-id'),
                roomingId,
                orderId,
                checkIn,
                checkOut,
                ''
            );
        });
    });

    document.querySelectorAll('[data-holding-dropzone]').forEach(function (zone) {
        zone.addEventListener('dragover', function (e) { e.preventDefault(); this.classList.add('drag-over'); });
        zone.addEventListener('dragleave', function () { this.classList.remove('drag-over'); });
        zone.addEventListener('drop', function (e) {
            e.preventDefault();
            this.classList.remove('drag-over');

            const reason = window.prompt('Reason for moving this booking to Holding Area?', 'Room rearrangement / group alignment');
            if (reason === null) return;

            moveRooming(
                0,
                e.dataTransfer.getData('rooming_id'),
                e.dataTransfer.getData('order_id'),
                e.dataTransfer.getData('check_in'),
                e.dataTransfer.getData('check_out'),
                reason
            );
        });
    });



    function setupBookingContextMenu() {
        const menu = document.getElementById('calendarBookingContextMenu');
        const title = document.getElementById('calendarContextTitle');
        if (!menu) return;

        let selectedBooking = null;
        let submenuOpenTimer = null;
        let submenuCloseTimer = null;

        function menuItems() {
            return Array.prototype.slice.call(menu.querySelectorAll('a[data-context-action], .ctx-submenu-toggle'));
        }

        function openSubmenu(submenu) {
            if (!submenu) return;
            clearTimeout(submenuCloseTimer);
            clearTimeout(submenuOpenTimer);
            submenuOpenTimer = setTimeout(function () {
                menu.querySelectorAll('[data-ctx-submenu].is-active').forEach(function (el) {
                    if (el !== submenu) {
                        el.classList.remove('is-active');
                        const t = el.querySelector('.ctx-submenu-toggle');
                        if (t) t.setAttribute('aria-expanded', 'false');
                    }
                });
                submenu.classList.add('is-active');
                const toggle = submenu.querySelector('.ctx-submenu-toggle');
                if (toggle) toggle.setAttribute('aria-expanded', 'true');
            }, 180);
        }

        function closeSubmenus(delay) {
            clearTimeout(submenuOpenTimer);
            clearTimeout(submenuCloseTimer);
            submenuCloseTimer = setTimeout(function () {
                menu.querySelectorAll('[data-ctx-submenu].is-active').forEach(function (el) {
                    el.classList.remove('is-active');
                    const t = el.querySelector('.ctx-submenu-toggle');
                    if (t) t.setAttribute('aria-expanded', 'false');
                });
            }, typeof delay === 'number' ? delay : 220);
        }

        function focusMenuItem(index) {
            const items = menuItems();
            if (!items.length) return;
            const next = ((index % items.length) + items.length) % items.length;
            items[next].focus();
        }

        function currentMenuIndex() {
            return menuItems().indexOf(document.activeElement);
        }

        function closeMenu() {
            closeSubmenus(0);
            menu.classList.remove('is-open');
            menu.setAttribute('aria-hidden', 'true');
            document.querySelectorAll('.booking-bar.is-context-selected, .booking-bar-static.is-context-selected').forEach(function (el) {
                el.classList.remove('is-context-selected');
            });
            selectedBooking = null;
        }

        function setMenuLinks(orderId) {
            const base = '/rooms/modules';
            const encodedOrderId = encodeURIComponent(orderId);
            const links = {
                edit: base + '/reservations/reservation_form.php?id=' + encodedOrderId,
                folio: base + '/payments/folio_view.php?order_id=' + encodedOrderId,
                transfer_charges: base + '/payments/transfer_booking_charges.php?from_order_id=' + encodedOrderId,
                proforma_standard: base + '/payments/proforma_invoice.php?order_id=' + encodedOrderId,
                proforma_master: base + '/payments/proforma_master_folio.php?order_id=' + encodedOrderId,
                proforma_room: base + '/payments/proforma_room_account.php?order_id=' + encodedOrderId,
                proforma_guest: base + '/payments/proforma_guest_folio.php?order_id=' + encodedOrderId,
                email_proforma_standard: base + '/payments/email_proforma.php?type=standard&order_id=' + encodedOrderId,
                email_proforma_master: base + '/payments/email_proforma.php?type=master&order_id=' + encodedOrderId,
                email_proforma_room: base + '/payments/email_proforma.php?type=room&order_id=' + encodedOrderId,
                email_proforma_guest: base + '/payments/email_proforma.php?type=guest&order_id=' + encodedOrderId,
                payments: base + '/payments/payments.php?order_id=' + encodedOrderId,
                extras: base + '/payments/extras.php?order_id=' + encodedOrderId
            };

            Object.keys(links).forEach(function (key) {
                const a = menu.querySelector('[data-context-action="' + key + '"]');
                if (a) a.href = links[key];
            });
        }

        function openMenu(e, bar) {
            const orderId = bar.getAttribute('data-order-id') || '';
            if (!orderId) return;

            e.preventDefault();
            e.stopPropagation();
            closeMenu();

            selectedBooking = bar;
            selectedBooking.classList.add('is-context-selected');
            setMenuLinks(orderId);

            const moveToHoldingItem = menu.querySelector('[data-context-action="move_to_holding"]');
            if (moveToHoldingItem) {
                const isHeld = bar.getAttribute('data-held') === '1';
                moveToHoldingItem.style.display = isHeld ? 'none' : '';
            }

            if (title) {
                const label = (bar.querySelector('.bar-guest') ? bar.querySelector('.bar-guest').textContent : '') ||
                    (bar.querySelector('.fw-bold') ? bar.querySelector('.fw-bold').textContent : '') ||
                    ('Booking #' + orderId);
                title.textContent = label.trim() + ' • Booking #' + orderId;
            }

            menu.classList.add('is-open');
            menu.setAttribute('aria-hidden', 'false');

            const menuRect = menu.getBoundingClientRect();
            const padding = 10;
            let left = e.clientX;
            let top = e.clientY;

            if (left + menuRect.width + padding > window.innerWidth) {
                left = window.innerWidth - menuRect.width - padding;
            }
            if (top + menuRect.height + padding > window.innerHeight) {
                top = window.innerHeight - menuRect.height - padding;
            }

            menu.style.left = Math.max(padding, left) + 'px';
            menu.style.top = Math.max(padding, top) + 'px';

            setTimeout(function () {
                focusMenuItem(0);
            }, 0);
        }

        document.addEventListener('contextmenu', function (e) {
            const bar = e.target.closest('.booking-bar, .booking-bar-static');
            if (!bar) return;
            openMenu(e, bar);
        });

        menu.querySelectorAll('[data-ctx-submenu]').forEach(function (submenu) {
            const toggle = submenu.querySelector('.ctx-submenu-toggle');

            submenu.addEventListener('mouseenter', function () { openSubmenu(submenu); });
            submenu.addEventListener('mouseleave', function () { closeSubmenus(260); });

            if (toggle) {
                toggle.addEventListener('click', function (e) {
                    e.preventDefault();
                    if (submenu.classList.contains('is-active')) {
                        closeSubmenus(0);
                    } else {
                        openSubmenu(submenu);
                    }
                });
                toggle.addEventListener('focus', function () { openSubmenu(submenu); });
            }
        });

        menu.addEventListener('click', function (e) {
            const actionLink = e.target.closest('a[data-context-action]');
            if (!actionLink) return;

            if (actionLink.getAttribute('data-context-action') === 'move_to_holding') {
                e.preventDefault();
                if (selectedBooking) {
                    const reason = window.prompt('Reason for moving this booking to Holding Area?', 'Room rearrangement / group alignment');
                    if (reason !== null) {
                        moveRooming(
                            0,
                            selectedBooking.getAttribute('data-rooming-id'),
                            selectedBooking.getAttribute('data-order-id'),
                            selectedBooking.getAttribute('data-check-in'),
                            selectedBooking.getAttribute('data-check-out'),
                            reason
                        );
                    }
                }
            }

            setTimeout(closeMenu, 80);
        });

        document.addEventListener('click', function (e) {
            if (!menu.contains(e.target)) closeMenu();
        });

        document.addEventListener('keydown', function (e) {
            if (!menu.classList.contains('is-open')) return;

            const items = menuItems();
            const activeIndex = currentMenuIndex();
            const activeSubmenu = document.activeElement ? document.activeElement.closest('[data-ctx-submenu]') : null;

            if (e.key === 'Escape') {
                e.preventDefault();
                closeMenu();
                return;
            }

            if (e.key === 'ArrowDown') {
                e.preventDefault();
                focusMenuItem(activeIndex >= 0 ? activeIndex + 1 : 0);
                return;
            }

            if (e.key === 'ArrowUp') {
                e.preventDefault();
                focusMenuItem(activeIndex >= 0 ? activeIndex - 1 : items.length - 1);
                return;
            }

            if (e.key === 'ArrowRight') {
                const submenu = document.activeElement ? document.activeElement.closest('[data-ctx-submenu]') : null;
                if (submenu) {
                    e.preventDefault();
                    openSubmenu(submenu);
                    const firstSubItem = submenu.querySelector('.ctx-submenu-items a');
                    if (firstSubItem) setTimeout(function () { firstSubItem.focus(); }, 190);
                }
                return;
            }

            if (e.key === 'ArrowLeft') {
                if (activeSubmenu && document.activeElement.closest('.ctx-submenu-items')) {
                    e.preventDefault();
                    const toggle = activeSubmenu.querySelector('.ctx-submenu-toggle');
                    closeSubmenus(0);
                    if (toggle) toggle.focus();
                }
                return;
            }

            if ((e.key === 'Enter' || e.key === ' ') && document.activeElement && document.activeElement.classList.contains('ctx-submenu-toggle')) {
                e.preventDefault();
                const submenu = document.activeElement.closest('[data-ctx-submenu]');
                if (submenu && submenu.classList.contains('is-active')) closeSubmenus(0);
                else openSubmenu(submenu);
            }
        });

        window.addEventListener('scroll', closeMenu, true);
        window.addEventListener('resize', closeMenu);
        const wrap = document.getElementById('calendarTimelineWrap');
        if (wrap) {
            wrap.addEventListener('scroll', closeMenu, { passive: true });
        }
    }

    setupBookingContextMenu();


    function setupPlannerAjaxSearch() {
        var form = document.getElementById('plannerSearchForm');
        var btn = document.getElementById('plannerAjaxSearchBtn');
        var results = document.getElementById('plannerAjaxSearchResults');
        var countEl = document.getElementById('plannerAjaxSearchCount');
        var modalEl = document.getElementById('plannerSearchModal');
        var input = document.getElementById('plannerSearchInput');

        if (!form || !btn || !results || !countEl) {
            return;
        }

        function escapeHtml(value) {
            return String(value || '').replace(/[&<>"']/g, function (m) {
                return ({
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;',
                    "'": '&#039;'
                })[m];
            });
        }

        function renderItems(items) {
            if (!items || !items.length) {
                results.innerHTML = '<div class="planner-result-empty"><i class="bi bi-search me-2"></i>No matching bookings found.</div>';
                return;
            }

            results.innerHTML = items.map(function (item) {
                return ''
                    + '<div class="planner-result-item">'
                    + '  <div class="d-flex flex-wrap justify-content-between gap-2">'
                    + '    <div>'
                    + '      <div class="planner-result-title">' + escapeHtml(item.guest_name) + '</div>'
                    +      (item.checked_in_guest_names ? '      <div class="planner-result-meta"><i class="bi bi-people me-1"></i>Checked-in: ' + escapeHtml(item.checked_in_guest_names) + '</div>' : '')
                    + '      <div class="planner-result-meta">'
                    +          escapeHtml(item.order_no || ('Booking #' + item.order_id))
                    +          (item.folio_numbers ? ' • Folio: ' + escapeHtml(item.folio_numbers) : '')
                    +          (item.agent_name ? ' • Agent: ' + escapeHtml(item.agent_name) : '')
                    + '      </div>'
                    + '      <div class="planner-result-meta">'
                    +          escapeHtml(item.property_name) + ' • ' + escapeHtml(item.room_label)
                    +          ' • ' + escapeHtml(item.check_in) + ' → ' + escapeHtml(item.check_out)
                    + '      </div>'
                    + '      <div class="planner-result-badges">'
                    + '        <span class="badge text-bg-primary">' + escapeHtml(item.status_label) + '</span>'
                    + '        <span class="badge text-bg-light border text-dark">' + escapeHtml(item.payment_due) + '</span>'
                    + '        <span class="badge text-bg-light border text-dark">' + escapeHtml(item.payment_status) + '</span>'
                    + '        <span class="badge text-bg-light border text-dark">Balance ' + escapeHtml(item.balance) + '</span>'
                    + '      </div>'
                    + '    </div>'
                    + '    <div><a class="btn btn-sm btn-outline-primary" href="' + escapeHtml(item.open_url) + '"><i class="bi bi-box-arrow-up-right me-1"></i>Open</a></div>'
                    + '  </div>'
                    + '</div>';
            }).join('');
        }

        function runSearch() {
            var params = new URLSearchParams(new FormData(form));
            params.set('ajax_search', '1');

            countEl.textContent = 'Searching...';
            results.innerHTML = '<div class="planner-result-empty"><i class="bi bi-hourglass-split me-2"></i>Searching bookings...</div>';
            btn.disabled = true;

            fetch('cal.php?' + params.toString(), {
                headers: {'X-Requested-With': 'XMLHttpRequest'}
            })
                .then(function (response) {
                    return response.json();
                })
                .then(function (data) {
                    if (!data || !data.ok) {
                        throw new Error((data && data.message) ? data.message : 'Search failed.');
                    }

                    countEl.textContent = data.count + ' match' + (data.count === 1 ? '' : 'es') + ' found.';
                    renderItems(data.items || []);
                })
                .catch(function (error) {
                    countEl.textContent = 'Search failed.';
                    results.innerHTML = '<div class="planner-result-empty text-danger"><i class="bi bi-exclamation-triangle me-2"></i>' + escapeHtml(error.message || 'Search failed.') + '</div>';
                })
                .finally(function () {
                    btn.disabled = false;
                });
        }

        btn.addEventListener('click', runSearch);

        if (input) {
            var timer = null;
            input.addEventListener('input', function () {
                window.clearTimeout(timer);
                timer = window.setTimeout(function () {
                    if (input.value.trim().length >= 2) {
                        runSearch();
                    }
                }, 450);
            });
        }

        if (modalEl) {
            modalEl.addEventListener('shown.bs.modal', function () {
                if (input) {
                    input.focus();
                    input.select();
                }
            });
        }
    }


    function setupCalendarFolioControl() {
        var modalEl = document.getElementById('calendarFolioControlModal');
        var calendarMoveFolios = [];
        var calendarMoveOrderId = '';
        var modal = modalEl && window.bootstrap ? new bootstrap.Modal(modalEl, {backdrop: 'static', keyboard: false}) : null;
        if (!modalEl || !modal) return;

        function esc(value) {
            return String(value == null ? '' : value).replace(/[&<>"']/g, function (m) {
                return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m] || m;
            });
        }

        function money(value) {
            var n = parseFloat(value || '0') || 0;
            return n.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
        }

        function setText(id, value) {
            var el = document.getElementById(id);
            if (el) el.textContent = value || '-';
        }

        function setHref(id, href) {
            var el = document.getElementById(id);
            if (el) el.href = href || '#';
        }

        function setButtonVisible(id, visible) {
            var el = document.getElementById(id);
            if (el) el.style.display = visible ? '' : 'none';
        }

        function postFolioAction(params) {
            return fetch('cal.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                credentials: 'same-origin',
                body: new URLSearchParams(params).toString()
            }).then(function (response) {
                return response.json().then(function (data) {
                    if (!response.ok || !data || !data.ok) {
                        throw new Error((data && data.message) ? data.message : 'Action failed.');
                    }
                    return data;
                });
            });
        }

        function proformaUrl(type, orderId, folioId, roomingId) {
            var url = '';
            if (type === 'master') {
                url = '/rooms/modules/payments/proforma_master_folio.php?order_id=' + encodeURIComponent(orderId);
            } else if (type === 'room') {
                url = '/rooms/modules/payments/proforma_room_account.php?order_id=' + encodeURIComponent(orderId);
                if (folioId) url += '&folio_id=' + encodeURIComponent(folioId);
                if (roomingId) url += '&rooming_id=' + encodeURIComponent(roomingId);
            } else if (type === 'guest') {
                url = '/rooms/modules/payments/proforma_guest_folio.php?order_id=' + encodeURIComponent(orderId);
                if (folioId) url += '&folio_id=' + encodeURIComponent(folioId);
                if (roomingId) url += '&rooming_id=' + encodeURIComponent(roomingId);
            } else {
                url = '/rooms/modules/payments/proforma_invoice.php?order_id=' + encodeURIComponent(orderId);
            }
            return url;
        }

        function folioProformaType(folio) {
            var t = String((folio && folio.type) || '').toLowerCase();
            if (t.indexOf('master') >= 0 || t.indexOf('main') >= 0) return 'master';
            if (t.indexOf('room') >= 0) return 'room';
            if (t.indexOf('guest') >= 0) return 'guest';
            return 'full';
        }

        function folioHasActivity(folio) {
            if (!folio) return false;

            var charges = Math.abs(parseFloat(folio.charges || '0') || 0);
            var payments = Math.abs(parseFloat(folio.payments || '0') || 0);

            return charges > 0.001 || payments > 0.001;
        }

        function folioProformaButton(folio) {
            if (!folio || !folio.id || !calendarMoveOrderId) return '';

            var type = folioProformaType(folio);

            /*
               Do not show Room / Guest proforma buttons for empty folios.
               A zero-balance folio may still have charges and payments, so we
               check charges/payments activity instead of balance.
            */
            if ((type === 'room' || type === 'guest') && !folioHasActivity(folio)) {
                return '';
            }

            var label = 'Print Proforma';
            var icon = 'bi-printer';

            if (type === 'room') {
                label = 'Print Room Proforma';
                icon = 'bi-door-closed';
            } else if (type === 'guest') {
                label = 'Print Guest Proforma';
                icon = 'bi-person-badge';
            } else if (type === 'master') {
                label = 'Print Master Proforma';
                icon = 'bi-file-earmark-pdf';
            }

            var href = proformaUrl(type, calendarMoveOrderId, folio.id, folio.rooming_id || folio.order_rooming_id || '');
            return ''
                + '<div class="folio-actions">'
                + '  <a class="btn btn-sm btn-outline-dark" target="_blank" rel="noopener" href="' + esc(href) + '">'
                + '    <i class="bi ' + icon + ' me-1"></i>' + esc(label)
                + '  </a>'
                + '</div>';
        }

        function txRows(rows, folios, currentFolioId) {
            rows = Array.isArray(rows) ? rows : [];
            folios = Array.isArray(folios) ? folios : [];
            if (!rows.length) return '<div class="folio-empty">No transactions found on this folio.</div>';
            return rows.map(function (row) {
                var debit = parseFloat(row.debit || '0') || 0;
                var credit = parseFloat(row.credit || '0') || 0;
                var isPayment = credit > 0;
                var isVoid = parseInt(row.is_voided || '0', 10) === 1;
                var amount = isPayment ? credit : debit;
                var desc = row.description || row.type || 'Transaction';
                var currentFolio = (folios || []).find(function (f) { return parseInt(f.id || 0, 10) === parseInt(currentFolioId || 0, 10); }) || {};
                var currentLabel = currentFolio.folio_display || ((currentFolio.folio_no || ('Folio #' + currentFolioId)) + ' - ' + (currentFolio.type || 'folio'));

                var selectCell = isVoid ? '<div></div>' : ''
                    + '<label class="tx-select">'
                    + '  <input type="checkbox" class="tx-select-checkbox" data-tx-id="' + esc(row.id || '') + '" data-folio-id="' + esc(currentFolioId || '') + '">'
                    + '</label>';

                var isRestaurantCharge = /^restaurant/i.test(String(row.source_table || '')) && parseInt(row.source_id || 0, 10) > 0;
                var previewBtn = isRestaurantCharge
                    ? '  <button type="button" class="btn btn-sm btn-outline-secondary tx-preview-btn" title="Preview restaurant bill"'
                    + '      data-source-id="' + esc(row.source_id) + '"'
                    + '      data-description="' + esc(desc) + '">'
                    + '      <i class="bi bi-eye"></i>'
                    + '  </button>'
                    : '';

                var actionsCell = isVoid ? '<div></div>' : ''
                    + '<div class="tx-actions">'
                    + previewBtn
                    + '  <button type="button" class="btn btn-sm btn-outline-primary tx-move-btn" title="Move"'
                    + '      data-tx-id="' + esc(row.id || '') + '"'
                    + '      data-current-folio-id="' + esc(currentFolioId || '') + '"'
                    + '      data-current-folio-label="' + esc(currentLabel) + '"'
                    + '      data-description="' + esc(desc) + '"'
                    + '      data-type="' + esc(row.type || '') + '"'
                    + '      data-amount="' + esc(amount) + '">'
                    + '      <i class="bi bi-arrow-left-right"></i>'
                    + '  </button>'
                    + '  <button type="button" class="btn btn-sm btn-outline-danger tx-void-payment-btn"'
                    + '      title="' + (isPayment ? 'Void payment' : 'Void charge') + '"'
                    + '      data-is-payment="' + (isPayment ? '1' : '0') + '"'
                    + '      data-tx-id="' + esc(row.id || '') + '"'
                    + '      data-description="' + esc(desc) + '"'
                    + '      data-amount="' + esc(amount) + '">'
                    + '      <i class="bi bi-x-octagon"></i>'
                    + '  </button>'
                    + '</div>';

                return ''
                    + '<div class="tx-row ' + (isPayment ? 'is-payment' : 'is-charge') + (isVoid ? ' is-void' : '') + '">'
                    + '  ' + selectCell
                    + '  <div class="tx-desc"><strong>' + esc(desc) + '</strong></div>'
                    + '  <div class="tx-amount">' + (isPayment ? '-' : '') + money(amount) + '</div>'
                    + '  <div class="tx-meta">'
                    + '    <span>' + esc(row.date || '') + '</span>'
                    + (row.type ? '    <span>' + esc(row.type) + '</span>' : '')
                    + (row.entered_by ? '    <span>By: ' + esc(row.entered_by) + '</span>' : '')
                    + (row.notes ? '    <span>' + esc(row.notes) + '</span>' : '')
                    + '  </div>'
                    + '  ' + actionsCell
                    + '</div>';
            }).join('');
        }

        function folioCard(folio, allFolios) {
            var balanceClass = (parseFloat(folio.balance || '0') || 0) > 0 ? 'text-warning' : 'text-success';
            return ''
                + '<details class="folio-row" open>'
                + '  <summary>'
                + '    <div class="folio-label"><span>' + esc(folio.folio_display || folio.folio_no || ('Folio #' + folio.id)) + '</span><span class="' + balanceClass + '">' + money(folio.balance) + '</span></div>'
                + '    <div class="folio-values">'
                + '      <span>Charges<strong>' + money(folio.charges) + '</strong></span>'
                + '      <span>Payments<strong>' + money(folio.payments) + '</strong></span>'
                + '      <span>Status<strong>' + esc(folio.status || 'Open') + '</strong></span>'
                + '    </div>'
                +      folioProformaButton(folio)
                + '    <div class="folio-actions">'
                + '      <button type="button" class="btn btn-sm btn-outline-primary folio-move-selected-btn d-none" data-folio-id="' + esc(folio.id || '') + '">'
                + '        <i class="bi bi-arrow-left-right me-1"></i>Move Selected (<span class="move-selected-count">0</span>)'
                + '      </button>'
                + '    </div>'
                + '  </summary>'
                + '  <div class="tx-tree">' + txRows(folio.transactions || [], allFolios || [], folio.id) + '</div>'
                + '</details>';
        }

        function renderFolios(data) {
            var master = [], room = [], guest = [];
            (data.folios || []).forEach(function (f) {
                var t = String(f.type || '').toLowerCase();
                if (t.indexOf('master') >= 0 || t.indexOf('main') >= 0) master.push(f);
                else if (t.indexOf('room') >= 0) room.push(f);
                else guest.push(f);
            });

            var allFolios = data.folios || [];
            calendarMoveFolios = allFolios;
            calendarMoveOrderId = String((data.booking || {}).order_id || modalEl.getAttribute('data-active-order-id') || '');
            document.getElementById('calendarMasterFolios').innerHTML = master.length ? master.map(function(f){ return folioCard(f, allFolios); }).join('') : '<div class="folio-empty">No main folio found.</div>';
            document.getElementById('calendarRoomFolios').innerHTML = room.length ? room.map(function(f){ return folioCard(f, allFolios); }).join('') : '<div class="folio-empty">No room folios found.</div>';
            document.getElementById('calendarGuestFolios').innerHTML = guest.length ? guest.map(function(f){ return folioCard(f, allFolios); }).join('') : '<div class="folio-empty">No guest / other folios found.</div>';
        }

        function renderRooming(rows) {
            rows = Array.isArray(rows) ? rows : [];
            var wrap = document.getElementById('calendarRoomingList');
            if (!rows.length) {
                wrap.innerHTML = '<div class="folio-empty">No rooming configuration found.</div>';
                return;
            }

            var distinctOrders = {};
            rows.forEach(function (r) { distinctOrders[r.order_id || 0] = r.order_no || ('#' + (r.order_id || 0)); });
            var showGroupHeaders = Object.keys(distinctOrders).length > 1;

            function roomCardHtml(r) {
                var notes = (r.notes || []).map(function (n) {
                    return '<div class="frontoffice-note mt-2"><strong>' + esc(n.label) + '</strong><span>' + esc(n.value) + '</span></div>';
                }).join('');
                var paxText = esc(r.adults) + 'A'
                    + (parseInt(r.children || 0, 10) > 0 ? ' / ' + esc(r.children) + 'C' : '');
                var primaryBadge = r.is_primary ? '<span class="badge text-bg-primary ms-2"><i class="bi bi-star-fill me-1"></i>Primary Room</span>' : '';
                return ''
                    + '<div class="rooming-detail-card' + (r.is_primary ? ' is-primary-room' : '') + '">'
                    + '  <div class="rooming-title">Room ' + esc(r.room_no || 'Unassigned') + (r.room_type ? ' • ' + esc(r.room_type) : '') + primaryBadge + '</div>'
                    + '  <div class="rooming-meta">' + esc(r.check_in) + ' → ' + esc(r.check_out) + ' • ' + paxText + '</div>'
                    + '  <div class="rooming-meta">Guests: ' + esc(r.guests || 'Not captured') + '</div>'
                    + '  <div class="rooming-meta">Checked in: ' + (parseInt(r.checked_in || 0, 10) ? 'Yes' : 'No') + ' • Checked out: ' + (parseInt(r.checked_out || 0, 10) ? 'Yes' : 'No') + '</div>'
                    + notes
                    + '</div>';
            }

            if (!showGroupHeaders) {
                wrap.innerHTML = rows.map(roomCardHtml).join('');
                return;
            }

            // Group booking: multiple orders share this group name - show each booking's rooms under its own heading.
            var byOrder = {};
            var orderSequence = [];
            rows.forEach(function (r) {
                var oid = r.order_id || 0;
                if (!byOrder[oid]) { byOrder[oid] = []; orderSequence.push(oid); }
                byOrder[oid].push(r);
            });

            wrap.innerHTML = orderSequence.map(function (oid) {
                var orderRows = byOrder[oid];
                var isCurrent = orderRows.length && orderRows[0].is_current_order;
                return ''
                    + '<div class="rooming-group-block">'
                    + '  <div class="rooming-group-heading">'
                    + '    <i class="bi bi-people-fill me-1"></i>Booking ' + esc(distinctOrders[oid] || ('#' + oid))
                    + (isCurrent ? ' <span class="badge text-bg-secondary ms-1">Currently Open</span>' : '')
                    + '  </div>'
                    + orderRows.map(roomCardHtml).join('')
                    + '</div>';
            }).join('');
        }

        function renderNotes(notes, housekeeping) {
            notes = Array.isArray(notes) ? notes : [];
            housekeeping = Array.isArray(housekeeping) ? housekeeping : [];

            var wrap = document.getElementById('calendarNotesList');
            var blocks = [];

            if (housekeeping.length) {
                blocks.push(
                    '<div class="frontoffice-housekeeping">'
                    + '<div class="frontoffice-housekeeping-title"><i class="bi bi-stars me-1"></i>Housekeeping Setup</div>'
                    + housekeeping.map(function (h) {
                        return '<div class="frontoffice-housekeeping-item">'
                            + '<strong>' + esc(h.label || '') + '</strong>'
                            + '<span>' + esc(h.value || '') + '</span>'
                            + '</div>';
                    }).join('')
                    + '</div>'
                );
            }

            if (notes.length) {
                blocks.push(notes.map(function (n) {
                    return '<div class="frontoffice-note"><strong>' + esc(n.label) + '</strong><span>' + esc(n.value) + '</span></div>';
                }).join(''));
            }

            if (!blocks.length) {
                wrap.innerHTML = '<div class="folio-empty">No reservation notes or housekeeping instructions captured.</div>';
                return;
            }

            wrap.innerHTML = blocks.join('');
        }

        function renderBreakdown(data) {
            var totals = data.totals || {};
            var booking = data.booking || {};
            document.getElementById('calendarFolioBreakdownGrid').innerHTML = ''
                + '<div class="folio-breakdown-pill"><span>Total Charges</span><strong>' + money(totals.charges) + '</strong></div>'
                + '<div class="folio-breakdown-pill"><span>Total Payments</span><strong>' + money(totals.payments) + '</strong></div>'
                + '<div class="folio-breakdown-pill"><span>Outstanding</span><strong>' + money(totals.balance) + '</strong></div>'
                + '<div class="folio-breakdown-pill"><span>Stay</span><strong>' + esc((booking.check_in || '') + ' → ' + (booking.check_out || '')) + '</strong></div>';
        }

        function loadFolio(orderId) {
            modalEl.setAttribute('data-active-order-id', orderId);
            var loading = document.getElementById('calendarFolioLoading');
            var body = document.getElementById('calendarFolioBody');
            var error = document.getElementById('calendarFolioError');
            loading.style.display = '';
            body.style.display = 'none';
            error.style.display = 'none';
            error.textContent = '';

            modal.show();

            fetch('cal.php?folio_control=1&order_id=' + encodeURIComponent(orderId), {
                headers: {'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json'},
                credentials: 'same-origin'
            })
                .then(function (r) {
                    return r.text().then(function (txt) {
                        var data = null;
                        try {
                            data = JSON.parse(txt);
                        } catch (e) {
                            var plain = String(txt || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
                            throw new Error(plain ? plain.substring(0, 300) : 'Server returned HTML instead of JSON.');
                        }
                        if (!r.ok || !data || !data.ok) {
                            throw new Error((data && data.message) ? data.message : 'Could not load folio control centre.');
                        }
                        return data;
                    });
                })
                .then(function (data) {
                    if (!data || !data.ok) throw new Error((data && data.message) ? data.message : 'Could not load folio control centre.');

                    var booking = data.booking || {};
                    setText('calendarFolioSubtitle', (booking.property || '') + ' • ' + (booking.check_in || '') + ' → ' + (booking.check_out || '') + (booking.agent_name ? ' • Agent: ' + booking.agent_name : ''));
                    var subtitleEl = document.getElementById('calendarFolioSubtitle');
                    if (subtitleEl && booking.agent_name) {
                        subtitleEl.innerHTML = esc(booking.property || '') + ' • ' + esc(booking.check_in || '') + ' → ' + esc(booking.check_out || '') + ' • Agent: <strong>' + esc(booking.agent_name) + '</strong>';
                    }
                    setText('calendarFolioBooking', booking.order_no || ('#' + orderId));
                    var guestEl = document.getElementById('calendarFolioGuest');
                    var kidsNoCharge = parseInt(booking.kids_0_3_no_charge || 0, 10);
                    if (guestEl) {
                        guestEl.innerHTML = esc(booking.guest_name || '-')
                            + (kidsNoCharge > 0
                                ? '<div class="folio-mini-subline">Kids 0-3 yrs (No Charge): ' + esc(kidsNoCharge) + '</div>'
                                : '');
                    }
                    setText('calendarFolioRooms', booking.rooms || '-');
                    setText('calendarFolioBalance', money((data.totals || {}).balance));

                    setHref('calendarFolioPrintFull', proformaUrl('full', orderId));
                    setHref('calendarFolioPrintMaster', proformaUrl('master', orderId));
                    setHref('calendarFolioPrintRoom', proformaUrl('room', orderId));
                    setHref('calendarFolioPrintGuest', proformaUrl('guest', orderId));
                    setHref('calendarFolioTransferCharges', '/rooms/modules/payments/transfer_booking_charges.php?from_order_id=' + encodeURIComponent(orderId));
                    setHref('calendarFolioExtras', '/rooms/modules/payments/extras.php?order_id=' + encodeURIComponent(orderId));
                    setHref('calendarFolioTransferChargesFooter', '/rooms/modules/payments/transfer_booking_charges.php?from_order_id=' + encodeURIComponent(orderId));
                    setHref('calendarFolioExtrasFooter', '/rooms/modules/payments/extras.php?order_id=' + encodeURIComponent(orderId));
                    setHref('calendarFolioPaymentsLink', '/rooms/modules/payments/payments.php?order_id=' + encodeURIComponent(orderId));
                    setHref('calendarFolioBookingLink', '/rooms/modules/reservations/reservation_form.php?id=' + encodeURIComponent(orderId));

                    var firstRoomingId = 0;
                    if (data.rooming && data.rooming.length && data.rooming[0].id) {
                        firstRoomingId = data.rooming[0].id;
                    }

                    var guestRegUrl = '/rooms/modules/reservations/guest_registration_pdf.php?order_id=' + encodeURIComponent(orderId);
                    if (firstRoomingId) {
                        guestRegUrl += '&rooming_id=' + encodeURIComponent(firstRoomingId);
                    }

                    var preloadDate = booking.raw_check_in || '<?php echo date('Y-m-d', strtotime('+1 day')); ?>';
                    var preloadUrl = '/rooms/modules/reservations/preload_tomorrow_guest_names.php?property_id=<?php echo (int)$selectedPropertyId; ?>&date=' + encodeURIComponent(preloadDate);

                    setHref('calendarFolioGuestRegPdf', guestRegUrl);
                    setHref('calendarFolioGuestRegPdfFooter', guestRegUrl);
                    setHref('calendarFolioPreloadNames', preloadUrl);
                    setHref('calendarFolioPreloadNamesFooter', preloadUrl);

                    var actions = booking.actions || {};
                    setButtonVisible('calendarFolioCheckinToday', !!actions.can_checkin_today);
                    setButtonVisible('calendarFolioDelayedCheckin', !!actions.can_delayed_checkin);
                    setButtonVisible('calendarFolioCheckoutToday', !!actions.can_checkout_today);
                    setHref('calendarFolioCheckinToday', '/rooms/modules/payments/checkin.php?order_id=' + encodeURIComponent(orderId) + '&source=folio_control');
                    setHref('calendarFolioDelayedCheckin', '/rooms/modules/payments/checkin.php?order_id=' + encodeURIComponent(orderId) + '&source=delayed_checkin');
                    setHref('calendarFolioCheckoutToday', '/rooms/modules/payments/check_out.php?order_id=' + encodeURIComponent(orderId) + '&source=folio_control');
                    setHref('calendarRoomingExportLink', 'cal.php?export_rooming_list=1&order_id=' + encodeURIComponent(orderId));

                    renderBreakdown(data);
                    renderRooming(data.rooming || []);
                    renderNotes(data.notes || [], data.housekeeping || []);
                    renderFolios(data);

                    loading.style.display = 'none';
                    body.style.display = '';
                })
                .catch(function (err) {
                    loading.style.display = 'none';
                    error.style.display = '';
                    error.textContent = err.message || 'Could not load folio control centre.';
                });
        }

        function openMoveModal(txIds, currentFolioId, currentLabel, description, type, amountText) {
            txIds = Array.isArray(txIds) ? txIds.filter(Boolean) : [txIds].filter(Boolean);
            if (!txIds.length) return;

            var modal = document.getElementById('calendarMoveFolioTransactionModal');
            var targetSelect = document.getElementById('calendarMoveTargetFolioId');

            document.getElementById('calendarMoveOrderId').value = calendarMoveOrderId || modalEl.getAttribute('data-active-order-id') || '';
            document.getElementById('calendarMoveTransactionId').value = txIds.join(',');
            document.getElementById('calendarMoveTxDescription').textContent = description;
            document.getElementById('calendarMoveTxType').textContent = type;
            document.getElementById('calendarMoveTxAmount').textContent = amountText;
            document.getElementById('calendarMoveCurrentFolio').textContent = currentLabel;
            document.getElementById('calendarMoveReason').value = '';

            targetSelect.innerHTML = '<option value="">Select destination folio</option>';
            (calendarMoveFolios || []).forEach(function (f) {
                var id = String(f.id || '');
                if (!id || id === String(currentFolioId)) return;
                var label = (f.folio_display || ((f.folio_no || ('Folio #' + id)) + ' - ' + (f.type || 'folio'))) + ' | Balance ' + money(f.balance || 0);
                var opt = document.createElement('option');
                opt.value = id;
                opt.textContent = label;
                targetSelect.appendChild(opt);
            });

            bootstrap.Modal.getOrCreateInstance(modal).show();
        }

        document.addEventListener('click', function (e) {
            var moveBtn = e.target.closest('.tx-move-btn');
            if (!moveBtn) return;

            var txId = moveBtn.getAttribute('data-tx-id') || '';
            if (!txId) return;

            openMoveModal(
                [txId],
                moveBtn.getAttribute('data-current-folio-id') || '',
                moveBtn.getAttribute('data-current-folio-label') || 'Current folio',
                moveBtn.getAttribute('data-description') || 'Transaction',
                moveBtn.getAttribute('data-type') || '',
                money(moveBtn.getAttribute('data-amount') || 0)
            );
        });

        function updateMoveSelectedButtons() {
            document.querySelectorAll('.folio-move-selected-btn').forEach(function (btn) {
                var folioId = btn.getAttribute('data-folio-id');
                var count = document.querySelectorAll('.tx-select-checkbox[data-folio-id="' + folioId + '"]:checked').length;
                var countEl = btn.querySelector('.move-selected-count');
                if (countEl) countEl.textContent = count;
                btn.classList.toggle('d-none', count === 0);
            });
        }

        document.addEventListener('change', function (e) {
            var cb = e.target.closest('.tx-select-checkbox');
            if (!cb) return;

            // Only one folio's selection is active at a time.
            if (cb.checked) {
                var folioId = cb.getAttribute('data-folio-id');
                document.querySelectorAll('.tx-select-checkbox:checked').forEach(function (other) {
                    if (other !== cb && other.getAttribute('data-folio-id') !== folioId) other.checked = false;
                });
            }

            updateMoveSelectedButtons();
        });

        document.addEventListener('click', function (e) {
            var btn = e.target.closest('.folio-move-selected-btn');
            if (!btn) return;
            e.preventDefault();
            e.stopPropagation();

            var folioId = btn.getAttribute('data-folio-id') || '';
            var checked = Array.prototype.slice.call(
                document.querySelectorAll('.tx-select-checkbox[data-folio-id="' + folioId + '"]:checked')
            );
            if (!checked.length) return;

            var txIds = checked.map(function (cb) { return cb.getAttribute('data-tx-id'); });
            var currentFolio = (calendarMoveFolios || []).find(function (f) { return String(f.id || '') === String(folioId); }) || {};
            var currentLabel = currentFolio.folio_display || ((currentFolio.folio_no || ('Folio #' + folioId)) + ' - ' + (currentFolio.type || 'folio'));

            openMoveModal(
                txIds,
                folioId,
                currentLabel,
                txIds.length + ' transaction(s) selected',
                'Bulk move',
                ''
            );
        });

        document.addEventListener('click', function (e) {
            var previewBtn = e.target.closest('.tx-preview-btn');
            if (!previewBtn) return;

            var sourceId = previewBtn.getAttribute('data-source-id') || '';
            if (!sourceId) return;

            var modalEl2 = document.getElementById('calendarRestaurantPreviewModal');
            var bodyEl = document.getElementById('calendarRestaurantPreviewBody');
            var subtitleEl = document.getElementById('calendarRestaurantPreviewSubtitle');

            subtitleEl.textContent = previewBtn.getAttribute('data-description') || '';
            bodyEl.innerHTML = '<div class="folio-empty">Loading...</div>';
            var printBtn = document.getElementById('calendarRestaurantPreviewPrintBtn');
            if (printBtn) {
                printBtn.setAttribute('data-source-id', sourceId);
                printBtn.disabled = false;
                printBtn.innerHTML = '<i class="bi bi-printer me-1"></i>Print Bill to Reception';
            }
            bootstrap.Modal.getOrCreateInstance(modalEl2).show();

            fetch('cal.php?preview_restaurant_charge=1&source_id=' + encodeURIComponent(sourceId), {credentials: 'same-origin'})
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (!data || !data.ok) {
                        bodyEl.innerHTML = '<div class="alert alert-warning mb-0">' + esc((data && data.message) || 'Could not load this restaurant bill.') + '</div>';
                        return;
                    }

                    var order = data.order || {};
                    var items = data.items || [];

                    subtitleEl.textContent = 'Order ' + (order.order_no || '') + (order.guest_name ? ' • ' + order.guest_name : '') + (order.room_no ? ' • Room ' + order.room_no : '');

                    var rowsHtml = items.map(function (it) {
                        var qty = parseFloat(it.qty || 1);
                        var qtyText = (qty % 1 === 0) ? qty.toFixed(0) : qty;
                        return ''
                            + '<div class="tx-row' + (it.is_void ? ' is-void' : '') + '" style="grid-template-columns:1fr auto;">'
                            + '  <div class="tx-desc"><strong>' + esc(qtyText) + ' x ' + esc(it.name) + '</strong></div>'
                            + '  <div class="tx-amount">' + money(it.line_total) + '</div>'
                            + '  <div class="tx-meta"><span>@ ' + money(it.unit_price) + '</span>' + (it.notes ? '<span>' + esc(it.notes) + '</span>' : '') + '</div>'
                            + '</div>';
                    }).join('');

                    bodyEl.innerHTML = (rowsHtml || '<div class="folio-empty">No items found on this restaurant order.</div>')
                        + '<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">'
                        + '  <strong>Bill Total</strong>'
                        + '  <strong class="fs-5">' + money(order.total_amount) + '</strong>'
                        + '</div>';
                })
                .catch(function () {
                    bodyEl.innerHTML = '<div class="alert alert-danger mb-0">Could not load this restaurant bill.</div>';
                });
        });

        document.addEventListener('click', function (e) {
            var printBillBtn = e.target.closest('#calendarRestaurantPreviewPrintBtn');
            if (!printBillBtn || printBillBtn.disabled) return;

            var sourceId = printBillBtn.getAttribute('data-source-id') || '';
            if (!sourceId) return;

            var originalHtml = printBillBtn.innerHTML;
            printBillBtn.disabled = true;
            printBillBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Printing...';

            fetch('cal.php?print_restaurant_charge=1&source_id=' + encodeURIComponent(sourceId), {credentials: 'same-origin'})
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (!data || !data.ok) {
                        alert((data && data.message) || 'Could not print this bill. Check the reception thermal printer.');
                        printBillBtn.disabled = false;
                        printBillBtn.innerHTML = originalHtml;
                        return;
                    }
                    printBillBtn.innerHTML = '<i class="bi bi-check2 me-1"></i>Sent to Printer';
                    setTimeout(function () {
                        printBillBtn.disabled = false;
                        printBillBtn.innerHTML = originalHtml;
                    }, 2500);
                })
                .catch(function () {
                    alert('Could not reach the print service. Check your connection and try again.');
                    printBillBtn.disabled = false;
                    printBillBtn.innerHTML = originalHtml;
                });
        });

        document.addEventListener('click', function (e) {
            var voidBtn = e.target.closest('.tx-void-payment-btn');
            if (!voidBtn || voidBtn.disabled) return;

            var txId = voidBtn.getAttribute('data-tx-id') || '';
            if (!txId) return;

            var isPayment = voidBtn.getAttribute('data-is-payment') === '1';

            document.getElementById('calendarVoidOrderId').value = calendarMoveOrderId || modalEl.getAttribute('data-active-order-id') || '';
            document.getElementById('calendarVoidTransactionId').value = txId;
            document.getElementById('calendarVoidIsPayment').value = isPayment ? '1' : '0';
            document.getElementById('calendarVoidPaymentDescription').textContent = voidBtn.getAttribute('data-description') || (isPayment ? 'Payment' : 'Charge');
            document.getElementById('calendarVoidPaymentAmount').textContent = money(voidBtn.getAttribute('data-amount') || 0);
            document.getElementById('calendarVoidAdminPin').value = '';
            document.getElementById('calendarVoidReason').value = '';

            document.getElementById('calendarVoidModalTitle').innerHTML = '<i class="bi bi-shield-lock me-2"></i>' + (isPayment ? 'Void Payment' : 'Void Charge');
            document.getElementById('calendarVoidModalSubtitle').textContent = isPayment
                ? 'Admin PIN authority is required. Only payment/receipt rows can be voided here.'
                : 'Admin PIN authority is required. Temporary control for clearing mis-posted charges.';
            document.getElementById('calendarVoidItemLabel').textContent = isPayment ? 'Payment:' : 'Charge:';
            document.getElementById('calendarVoidChargeNotice').classList.toggle('d-none', isPayment);
            document.getElementById('calendarVoidSubmitBtn').innerHTML = '<i class="bi bi-x-octagon me-2"></i>' + (isPayment ? 'Void Payment' : 'Void Charge');

            bootstrap.Modal.getOrCreateInstance(document.getElementById('calendarVoidPaymentModal'), {backdrop: 'static', keyboard: false}).show();
        });

        var voidForm = document.getElementById('calendarVoidPaymentForm');
        if (voidForm) {
            voidForm.addEventListener('submit', function (e) {
                e.preventDefault();
                var submitBtn = voidForm.querySelector('button[type="submit"]');
                var activeOrderId = document.getElementById('calendarVoidOrderId').value || modalEl.getAttribute('data-active-order-id') || '';
                var isPayment = document.getElementById('calendarVoidIsPayment').value === '1';
                var params = {
                    folio_action: isPayment ? 'void_payment' : 'void_charge',
                    order_id: activeOrderId,
                    transaction_id: document.getElementById('calendarVoidTransactionId').value || '',
                    admin_pin: document.getElementById('calendarVoidAdminPin').value || '',
                    void_reason: document.getElementById('calendarVoidReason').value || ''
                };
                if (!params.admin_pin.trim()) { alert('Please enter the admin PIN.'); return; }
                if (!params.void_reason.trim()) { alert('Please enter the void reason.'); return; }

                var confirmMsg = isPayment
                    ? 'Void this payment? This will remove it from folio totals but keep an audit trail.'
                    : 'Void this charge? This will remove it from folio totals but keep an audit trail. Only do this for genuinely mis-posted charges.';
                if (!confirm(confirmMsg)) return;

                if (submitBtn) submitBtn.disabled = true;
                postFolioAction(params)
                    .then(function () {
                        bootstrap.Modal.getOrCreateInstance(document.getElementById('calendarVoidPaymentModal')).hide();
                        if (activeOrderId) loadFolio(activeOrderId);
                    })
                    .catch(function (err) { alert(err.message || (isPayment ? 'Payment void failed.' : 'Charge void failed.')); })
                    .finally(function () { if (submitBtn) submitBtn.disabled = false; });
            });
        }

        var moveForm = document.getElementById('calendarMoveFolioTransactionForm');
        if (moveForm) {
            moveForm.addEventListener('submit', function (e) {
                e.preventDefault();
                var submitBtn = moveForm.querySelector('button[type="submit"]');
                var activeOrderId = document.getElementById('calendarMoveOrderId').value || modalEl.getAttribute('data-active-order-id') || '';
                var txIds = (document.getElementById('calendarMoveTransactionId').value || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
                var targetFolioId = document.getElementById('calendarMoveTargetFolioId').value || '';
                var moveReason = document.getElementById('calendarMoveReason').value || '';

                if (!txIds.length) { alert('No transaction selected.'); return; }
                if (!targetFolioId) { alert('Please select the destination folio.'); return; }
                if (!moveReason.trim()) { alert('Please enter the reason for audit.'); return; }

                if (submitBtn) submitBtn.disabled = true;

                // Move transactions one at a time in sequence so each move is recorded individually.
                txIds.reduce(function (chain, txId) {
                    return chain.then(function () {
                        return postFolioAction({
                            folio_action: 'move_folio_transaction',
                            order_id: activeOrderId,
                            transaction_id: txId,
                            target_folio_id: targetFolioId,
                            move_reason: moveReason
                        });
                    });
                }, Promise.resolve())
                    .then(function () {
                        bootstrap.Modal.getOrCreateInstance(document.getElementById('calendarMoveFolioTransactionModal')).hide();
                        if (activeOrderId) loadFolio(activeOrderId);
                    })
                    .catch(function (err) {
                        alert(err.message || 'Move failed.');
                        if (activeOrderId) loadFolio(activeOrderId);
                    })
                    .finally(function () { if (submitBtn) submitBtn.disabled = false; });
            });
        }

        document.addEventListener('dblclick', function (e) {
            var bar = e.target.closest('.booking-bar, .booking-bar-static');
            if (!bar) return;
            var orderId = bar.getAttribute('data-order-id') || '';
            if (!orderId) return;
            e.preventDefault();
            e.stopPropagation();
            loadFolio(orderId);
        });

        document.addEventListener('click', function (e) {
            var link = e.target.closest('[data-context-action="folio"]');
            if (!link) return;
            var selected = document.querySelector('.booking-bar.is-context-selected, .booking-bar-static.is-context-selected');
            var orderId = selected ? (selected.getAttribute('data-order-id') || '') : '';
            if (!orderId) return;
            e.preventDefault();
            loadFolio(orderId);
        });
    }

    setupCalendarFolioControl();


    setupPlannerAjaxSearch();

    var waitlistModalEl = document.getElementById('waitlistAvailabilityModal');
    if (waitlistModalEl && window.bootstrap && typeof window.bootstrap.Modal === 'function') {
        var waitlistModal = new bootstrap.Modal(waitlistModalEl);
        waitlistModal.show();
    }
});
</script>



<?php
/* Full-range availability strip aligned with planner dates.
   It is inline after the planner, and includes a TOTAL availability row per date. */
$plannerAvailabilityRows = [];
$plannerAvailabilityTotals = [];
if ($selectedPropertyId > 0 && !empty($days)) {
    foreach ($days as $day) {
        $plannerAvailabilityTotals[(string)$day['date']] = [
            'available' => 0,
            'occupied' => 0,
            'total' => 0,
        ];
    }

    try {
        $stmtStripTypes = $pdo->prepare("
            SELECT
                rt.id,
                rt.title,
                COUNT(r.id) AS total_rooms
            FROM room_types rt
            LEFT JOIN rooms r
              ON r.room_type_id = rt.id
             AND r.property_id = rt.property_id
            WHERE rt.property_id = :property_id
            GROUP BY rt.id, rt.title
            HAVING total_rooms > 0
            ORDER BY rt.title ASC
        ");
        $stmtStripTypes->execute([':property_id' => $selectedPropertyId]);
        $stripRoomTypes = $stmtStripTypes->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $stripOccupiedMap = [];
        if ($stripRoomTypes) {
            $stripUnion = [];
            $stripParams = [':property_id' => $selectedPropertyId];

            foreach ($days as $i => $day) {
                $stripUnion[] = "SELECT :strip_day_{$i} AS day_date";
                $stripParams[":strip_day_{$i}"] = $day['date'];
            }

            /*
               Bottom strip availability:
               Count occupied PHYSICAL ROOMS per displayed date.
               Checkout date is available again, so:
                   rmg.check_in <= day_date AND rmg.check_out > day_date
               Do not sum over the displayed range.
            */
            $sqlStripOcc = "
                SELECT
                    r.room_type_id,
                    d.day_date,
                    COUNT(DISTINCT r.id) AS occupied_rooms
                FROM (" . implode(" UNION ALL ", $stripUnion) . ") d
                INNER JOIN rooms r
                    ON r.property_id = :property_id
                INNER JOIN order_rooming rmg
                    ON rmg.room_id = r.id
                   AND rmg.check_in <= d.day_date
                   AND rmg.check_out > d.day_date
                INNER JOIN orders o
                    ON o.id = rmg.order_id
                   AND o.property_id = r.property_id
                WHERE LOWER(TRIM(CAST(COALESCE(o.status, 1) AS CHAR))) NOT IN
                      ('0','cancelled','canceled','void','deleted','no_show','noshow')
                GROUP BY r.room_type_id, d.day_date
            ";

            $stmtStripOcc = $pdo->prepare($sqlStripOcc);
            $stmtStripOcc->execute($stripParams);

            foreach ($stmtStripOcc->fetchAll(PDO::FETCH_ASSOC) ?: [] as $occRow) {
                $stripOccupiedMap[(int)$occRow['room_type_id']][(string)$occRow['day_date']] = (int)$occRow['occupied_rooms'];
            }
        }

        foreach ($stripRoomTypes as $rt) {
            $rtId = (int)$rt['id'];
            $total = (int)$rt['total_rooms'];
            $cells = [];

            foreach ($days as $day) {
                $d = (string)$day['date'];
                $occupied = (int)($stripOccupiedMap[$rtId][$d] ?? 0);
                $available = max(0, $total - $occupied);

                $plannerAvailabilityTotals[$d]['available'] += $available;
                $plannerAvailabilityTotals[$d]['occupied'] += $occupied;
                $plannerAvailabilityTotals[$d]['total'] += $total;

                $cells[] = [
                    'date' => $d,
                    'available' => $available,
                    'occupied' => $occupied,
                    'total' => $total,
                ];
            }

            $plannerAvailabilityRows[] = [
                'id' => $rtId,
                'title' => (string)$rt['title'],
                'total_rooms' => $total,
                'cells' => $cells,
            ];
        }
    } catch (Throwable $e) {
        $plannerAvailabilityError = $e->getMessage();
        $plannerAvailabilityRows = [];
        $plannerAvailabilityTotals = [];
        foreach ($days as $day) {
            $plannerAvailabilityTotals[(string)$day['date']] = [
                'available' => 0,
                'occupied' => 0,
                'total' => 0,
            ];
        }
    }
}
?>

<?php if (!empty($plannerAvailabilityRows) || !empty($plannerAvailabilityError)): ?>
<div class="planner-availability-strip-inline mt-3">
    <div class="planner-availability-title">
        <div>
            <strong><i class="bi bi-grid-3x3-gap me-1"></i> Availability for displayed planner dates</strong> <span class="badge text-bg-success ms-2">Physical-room per-day availability</span></span> <span class="badge text-bg-danger ms-2">ROOMING SLOT COUNT</span>
            <span class="text-muted small ms-2"><?php echo planner_e(planner_display_date($dateFrom) . ' to ' . planner_display_date($dateTo)); ?></span>
        </div>
        <?php if (!empty($plannerAvailabilityError)): ?>
            <span class="badge text-bg-danger">Availability error: <?php echo planner_e($plannerAvailabilityError); ?></span>
        <?php endif; ?>
        <a class="btn btn-sm btn-outline-primary" href="availability_cal.php?<?php echo http_build_query(['property_id'=>$selectedPropertyId,'from'=>$dateFrom,'to'=>$dateTo]); ?>">
            Full Availability
        </a>
    </div>

    <div class="planner-availability-scroll">
        <div class="planner-availability-grid" style="--planner-label-width:<?php echo (int)$roomLabelWidth; ?>px; --planner-day-width:<?php echo (int)$dayWidth; ?>px;">
            <div class="planner-availability-corner">Room Type</div>
            <?php foreach ($days as $day): ?>
                <div class="planner-availability-date <?php echo !empty($day['is_today']) ? 'is-today' : ''; ?>">
                    <div><?php echo planner_e($day['day']); ?></div>
                    <strong><?php echo planner_e($day['num']); ?></strong>
                    <span><?php echo planner_e($day['mon']); ?></span>
                </div>
            <?php endforeach; ?>

            <div class="planner-availability-roomtype planner-availability-total-label">
                <strong>Total Availability</strong>
                <span>All room types</span>
            </div>
            <?php foreach ($days as $day): ?>
                <?php
                    $d = (string)$day['date'];
                    $totalCell = $plannerAvailabilityTotals[$d] ?? ['available'=>0,'occupied'=>0,'total'=>0];
                    $available = (int)$totalCell['available'];
                    $total = max(1, (int)$totalCell['total']);
                    $ratio = $available / $total;
                    $cellClass = $available <= 0 ? 'sold-out' : ($ratio <= 0.25 ? 'low' : ($ratio <= 0.5 ? 'medium' : 'good'));
                ?>
                <div class="planner-availability-cell total <?php echo $cellClass; ?>" title="<?php echo planner_e('Total on '.$d.': '.$available.' available, '.$totalCell['occupied'].' occupied, '.$totalCell['total'].' total'); ?>">
                    <strong><?php echo $available; ?></strong>
                    <span>/<?php echo (int)$totalCell['total']; ?></span>
                </div>
            <?php endforeach; ?>

            <?php foreach ($plannerAvailabilityRows as $row): ?>
                <div class="planner-availability-roomtype">
                    <strong><?php echo planner_e($row['title']); ?></strong>
                    <span><?php echo (int)$row['total_rooms']; ?> rooms</span>
                </div>
                <?php foreach ($row['cells'] as $cell): ?>
                    <?php
                        $available = (int)$cell['available'];
                        $total = max(1, (int)$cell['total']);
                        $ratio = $available / $total;
                        $cellClass = $available <= 0 ? 'sold-out' : ($ratio <= 0.25 ? 'low' : ($ratio <= 0.5 ? 'medium' : 'good'));
                    ?>
                    <div class="planner-availability-cell <?php echo $cellClass; ?>" title="<?php echo planner_e($row['title'].' on '.$cell['date'].': '.$available.' available, '.$cell['occupied'].' occupied, '.$cell['total'].' total'); ?>">
                        <strong><?php echo $available; ?></strong>
                        <span>/<?php echo (int)$cell['total']; ?></span>
                    </div>
                <?php endforeach; ?>
            <?php endforeach; ?>
        </div>
    </div>
</div>
<?php endif; ?>


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