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

/*
 * Force a Unicode connection and upgrade the reservation-note columns that
 * receive free-form text. This prevents MySQL error 3988 when notes contain
 * curly apostrophes, bullets, accented names or other UTF-8 characters while
 * an older column is still latin1_swedish_ci.
 */
try {
    $pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
} catch (Throwable $e) {
    // The save process below will report any real database error.
}

if (!function_exists('rf_upgrade_unicode_text_columns')) {
    function rf_upgrade_unicode_text_columns(PDO $pdo): void
    {
        $targets = [
            'orders' => [
                'reservation_notes',
                'internal_memo',
                'reminder_info',
                'originating_agent',
                'voucher_no',
            ],
            'waitlist' => [
                'reservation_notes',
                'internal_memo',
                'reminder_info',
                'notes',
            ],
            'reservation_waitlist' => [
                'reservation_notes',
                'internal_memo',
                'reminder_info',
                'notes',
            ],
        ];

        foreach ($targets as $table => $columns) {
            foreach ($columns as $column) {
                try {
                    $stmt = $pdo->prepare("\n                        SELECT\n                            COLUMN_TYPE,\n                            IS_NULLABLE,\n                            COLUMN_DEFAULT,\n                            EXTRA,\n                            CHARACTER_SET_NAME,\n                            COLLATION_NAME\n                        FROM information_schema.COLUMNS\n                        WHERE TABLE_SCHEMA = DATABASE()\n                          AND TABLE_NAME = :table_name\n                          AND COLUMN_NAME = :column_name\n                        LIMIT 1\n                    ");
                    $stmt->execute([
                        ':table_name'  => $table,
                        ':column_name' => $column,
                    ]);
                    $meta = $stmt->fetch(PDO::FETCH_ASSOC);

                    if (!$meta || empty($meta['CHARACTER_SET_NAME'])) {
                        continue;
                    }

                    $charset = strtolower((string)$meta['CHARACTER_SET_NAME']);
                    $collation = strtolower((string)$meta['COLLATION_NAME']);
                    if ($charset === 'utf8mb4' && strpos($collation, 'utf8mb4_') === 0) {
                        continue;
                    }

                    $columnType = (string)$meta['COLUMN_TYPE'];
                    $nullable = strtoupper((string)$meta['IS_NULLABLE']) === 'YES' ? ' NULL' : ' NOT NULL';
                    $defaultSql = '';
                    if ($meta['COLUMN_DEFAULT'] !== null) {
                        $defaultSql = ' DEFAULT ' . $pdo->quote((string)$meta['COLUMN_DEFAULT']);
                    } elseif (strtoupper((string)$meta['IS_NULLABLE']) === 'YES') {
                        $defaultSql = ' DEFAULT NULL';
                    }
                    $extra = trim((string)$meta['EXTRA']);
                    $extraSql = $extra !== '' ? ' ' . $extra : '';
                    $safeTable = str_replace('`', '``', $table);
                    $safeColumn = str_replace('`', '``', $column);

                    $pdo->exec(
                        "ALTER TABLE `{$safeTable}` MODIFY `{$safeColumn}` {$columnType} "
                        . "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
                        . $nullable . $defaultSql . $extraSql
                    );
                } catch (Throwable $e) {
                    // Continue; a manual migration can be run if ALTER privilege is unavailable.
                }
            }
        }
    }
}

rf_upgrade_unicode_text_columns($pdo);


/**
 * Meal plans, rate resolution and occupancy pricing live in
 * includes/rate_support.php - ONE definition, shared by every page that
 * prices a night. It arrives via bootstrap.php -> functions.php, so there is
 * no include needed here.
 *
 * They used to be duplicated in this file and in functions.php as well, each
 * guarded by function_exists(), so whichever loaded first silently won. The
 * copies had drifted: fetch_rate_plan was 5-arg in one and 6-arg in the
 * other (so $totalRooms - and therefore group rates - was being dropped),
 * and an invalid meal plan defaulted to '' in one and 'HB' in the other.
 */

$pageTitle = 'Reservation Form';

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

if (!function_exists('display_money_local')) {
    function display_money_local($amount): string
    {
        return number_format((float)$amount, 2);
    }
}

if (!function_exists('season_badge_class')) {
    function season_badge_class(?string $color): string
    {
        $allowed = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark'];
        $color = strtolower(trim((string)$color));
        return in_array($color, $allowed, true) ? $color : 'secondary';
    }
}



/*
   The single source of truth for meal plans across RoomZ.
   FB (Full Board) was added alongside HB/BB - Semper contracts such as
   "STO FB", "Rack FB" and "VOL Rate Full Board" already exist, and before
   this an FB booking was silently normalised to HB and quoted at HB rates.
*/




if (!function_exists('rf_zero_rate_plan')) {
    /**
     * Fallback "rate plan" used when no active rate plan exists for a given
     * room type / night. Instead of blocking the booking, we hold the rooms
     * at a zero price so reservations staff can capture the stay and fill in
     * pricing later. Shaped to match the columns fetch_rate_plan() returns.
     */
    function rf_zero_rate_plan(string $mealPlan): array
    {
        return [
            'id'                 => 0,
            'title'              => 'No rate plan (held at 0)',
            'currency'           => '',
            'season_label'       => '',
            'season_color'       => 'secondary',
            'meal_plan'          => normalize_meal_plan_for_rates($mealPlan),
            'sharing_rate'       => 0.0,
            'single_rate'        => 0.0,
            'single_supplement'  => 0.0,
            'child_rate_percent' => 0.0,
            'rate_source'        => 'zero_hold',
        ];
    }
}

if (!function_exists('room_type_allows_triple_label')) {
    function room_type_allows_triple_label(string $title, int $higherOccupancy): bool
    {
        return $higherOccupancy >= 3 && stripos($title, 'family') === false;
    }
}

if (!function_exists('room_slot_display_label')) {
    function room_slot_display_label(string $title, int $adults, int $children, int $higherOccupancy): string
    {
        $totalPax = max(0, $adults) + max(0, $children);

        if ($totalPax === 3 && room_type_allows_triple_label($title, $higherOccupancy)) {
            return 'Triple / Extra Bed';
        }

        if (stripos($title, 'family') !== false) {
            return 'Family Room';
        }

        return '';
    }
}


/*
 * Price one room night from the occupancy actually assigned to that room.
 *
 * Children may legitimately occupy a separate interleading/adjacent room while
 * the parents occupy another room. The shared rate engine historically treated
 * 0-adult rooms as empty, so a 0A + 2C room was either priced at zero or was
 * redistributed back into the parents' room when the booking was reopened.
 *
 * Children-only rule:
 *   child total = sharing rate x child percentage x number of children
 *   adult total = 0
 *   no single supplement
 *
 * Every normal occupancy combination still uses the shared canonical rate
 * function, keeping the Reservation Form aligned with the rest of RoomZ.
 */
if (!function_exists('rf_calculate_room_night_total')) {
    function rf_calculate_room_night_total(
        int $adults,
        int $children,
        float $sharingRate,
        float $singleRate,
        float $singleSupplement,
        float $childRatePercent
    ): array {
        $adults = max(0, $adults);
        $children = max(0, $children);

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

            return [
                'adult_total'          => 0.00,
                'child_total'          => $childTotal,
                'room_total'           => $childTotal,
                'applied_rate_type'    => 'children_only',
                'rate_label'           => 'Children Only',
            ];
        }

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

if (!function_exists('fetch_room_type_meta')) {
    function fetch_room_type_meta(PDO $pdo, int $roomTypeId): array
    {
        static $cache = [];

        if ($roomTypeId <= 0) {
            return [
                'title'            => '',
                'higher_occupancy' => 0,
            ];
        }

        if (isset($cache[$roomTypeId])) {
            return $cache[$roomTypeId];
        }

        $stmt = $pdo->prepare("
            SELECT title, COALESCE(higher_occupancy, 0) AS higher_occupancy
            FROM room_types
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $roomTypeId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $cache[$roomTypeId] = [
            'title'            => (string)($row['title'] ?? ''),
            'higher_occupancy' => (int)($row['higher_occupancy'] ?? 0),
        ];

        return $cache[$roomTypeId];
    }
}









if (!function_exists('split_guest_name')) {
    function split_guest_name(string $fullName): array
    {
        $fullName = trim(preg_replace('/\s+/', ' ', $fullName));
        if ($fullName === '') {
            return ['', ''];
        }

        $parts = explode(' ', $fullName, 2);
        return [
            trim($parts[0] ?? ''),
            trim($parts[1] ?? ''),
        ];
    }
}

function find_or_create_guest_by_name(
    PDO $pdo,
    string $guestName,
    int $existingGuestId = 0
): int {
    [$first, $last] = split_guest_name($guestName);

    if ($first === '' && $last === '') {
        throw new RuntimeException('Guest name is required.');
    }

    /*
     * Editing an existing booking:
     * retain its current independent guest row when the name did not change.
     */
    if ($existingGuestId > 0) {
        $stmt = $pdo->prepare("
            SELECT firstname, lastname
            FROM guests
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $existingGuestId]);
        $existing = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($existing) {
            $oldFirst = trim((string)($existing['firstname'] ?? ''));
            $oldLast  = trim((string)($existing['lastname'] ?? ''));

            if (
                strcasecmp($oldFirst, $first) === 0
                && strcasecmp($oldLast, $last) === 0
            ) {
                return $existingGuestId;
            }
        }
    }

    /*
     * New booking, or a deliberately changed name:
     * always create a fresh guest row, even when another guest has the same name.
     */
    $stmt = $pdo->prepare("
        INSERT INTO guests (firstname, lastname)
        VALUES (:firstname, :lastname)
    ");
    $stmt->execute([
        ':firstname' => $first,
        ':lastname'  => $last,
    ]);

    return (int)$pdo->lastInsertId();
}

if (!function_exists('allocate_guests_to_rooms')) {
    function allocate_guests_to_rooms(int $adults, int $children, int $totalRooms): array
    {
        $totalRooms = max(1, $totalRooms);
        $adults = max(0, $adults);
        $children = max(0, $children);

        $rooms = [];
        for ($i = 0; $i < $totalRooms; $i++) {
            $rooms[] = [
                'adults'   => 0,
                'children' => 0,
            ];
        }

        for ($i = 0; $i < $totalRooms && $adults > 0; $i++) {
            $rooms[$i]['adults']++;
            $adults--;
        }

        for ($i = 0; $i < $totalRooms && $adults > 0; $i++) {
            while ($rooms[$i]['adults'] < 2 && $adults > 0) {
                $rooms[$i]['adults']++;
                $adults--;
            }
        }

        $i = 0;
        while ($adults > 0) {
            $rooms[$i % $totalRooms]['adults']++;
            $adults--;
            $i++;
        }

        /*
           Children.

           This used to only place a child in a room that ALREADY had an adult:

               while ($children > 0) {
                   if ($rooms[$i % $totalRooms]['adults'] > 0) { ...place... }
                   $i++;             // ran even when nothing was placed
               }

           Two faults. A room with no adults never received a child, so a
           children-only room (an interleading room next to the parents')
           allocated 0A + 0C and priced at 0.00. And if NO room had an adult,
           $children never decremented while $i incremented forever - an
           infinite loop that hung the request until PHP timed it out.

           Children are now dealt round-robin across every room. Rooms holding
           an adult are filled first, so a family stays together where possible,
           and any remainder goes to the adult-less rooms rather than nowhere.
        */
        for ($i = 0; $i < $totalRooms && $children > 0; $i++) {
            if ($rooms[$i]['adults'] > 0) {
                $rooms[$i]['children']++;
                $children--;
            }
        }

        $i = 0;
        while ($children > 0) {
            $rooms[$i % $totalRooms]['children']++;
            $children--;
            $i++;
        }

        return $rooms;
    }
}



if (!function_exists('rf_active_order_status_sql')) {
    function rf_active_order_status_sql(string $alias = 'o'): string
    {
        $alias = preg_replace('/[^A-Za-z0-9_]/', '', $alias);
        if ($alias === '') {
            $alias = 'o';
        }

        return "LOWER(TRIM(CAST(COALESCE({$alias}.status, 1) AS CHAR))) NOT IN ('0','cancelled','canceled','void','deleted','no_show','noshow')";
    }
}

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,
                'assigned_rooms'  => 0,
                'holding_rooms'   => 0,
                'legacy_rooms'    => 0,
            ];
        }

        /*
           Same availability approach as calendar.php:
           - Count physical rooms for the selected property + room type.
           - Count occupied PHYSICAL rooms per room night.
           - Checkout date is available again:
                 rmg.check_in <= night_date AND rmg.check_out > night_date
           - For a multi-night stay, use the busiest night.
           - This prevents the form from blocking a booking when the calendar
             correctly shows physical rooms free.
        */

        $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,
                'assigned_rooms'  => 0,
                'holding_rooms'   => 0,
                'legacy_rooms'    => 0,
            ];
        }

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

        if (!$days) {
            return [
                'total_rooms'     => $totalRooms,
                'occupied_rooms'  => 0,
                'available_rooms' => $totalRooms,
                'assigned_rooms'  => 0,
                'holding_rooms'   => 0,
                'legacy_rooms'    => 0,
            ];
        }

        $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 = rf_active_order_status_sql('o');

        $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.room_id > 0
                   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),
            'assigned_rooms'  => $occupiedRooms,
            'holding_rooms'   => 0,
            'legacy_rooms'    => 0,
        ];
    }
}

if (!function_exists('count_available_rooms_property_wide')) {
    /*
       Whole-lodge availability, IGNORING room types.
       Counts every physical room at the property and the busiest occupied
       night across the stay, so the front desk can see how many rooms exist
       in total regardless of type. Used to offer a "juggle the calendar"
       option when a specific room type is full but the lodge as a whole
       still has capacity - we never want to lose a booking to type rules
       that a manual room move could satisfy.
    */
    function count_available_rooms_property_wide(PDO $pdo, int $propertyId, string $checkIn, string $checkOut, int $excludeOrderId = 0): array
    {
        if ($propertyId <= 0 || $checkIn === '' || $checkOut === '') {
            return ['total_rooms' => 0, 'occupied_rooms' => 0, 'available_rooms' => 0];
        }

        $stmtTotal = $pdo->prepare("
            SELECT COUNT(*)
            FROM rooms
            WHERE property_id = :property_id
        ");
        $stmtTotal->execute([':property_id' => $propertyId]);
        $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);
        }
        if (!$days) {
            return ['total_rooms' => $totalRooms, 'occupied_rooms' => 0, 'available_rooms' => $totalRooms];
        }

        $dayUnion = [];
        $params = [':property_id' => $propertyId];
        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 = rf_active_order_status_sql('o');

        $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
                LEFT JOIN order_rooming rmg
                    ON rmg.room_id = r.id
                   AND rmg.room_id > 0
                   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_preview_rows')) {
    function build_preview_rows(PDO $pdo, int $roomTypeId, ?int $agentId, string $checkIn, string $checkOut, int $totalRooms, int $adults, int $children, string $mealPlan = 'HB'): array
    {
        $rows = [];
        $inTs  = strtotime($checkIn);
        $outTs = strtotime($checkOut);

        if (!$inTs || !$outTs || $outTs <= $inTs || $roomTypeId <= 0 || $totalRooms <= 0) {
            return $rows;
        }

        $mealPlan = normalize_meal_plan_for_rates($mealPlan);

        $allocation = allocate_guests_to_rooms($adults, $children, $totalRooms);

        $cursor = $inTs;
        while ($cursor < $outTs) {
            $stayDate = date('Y-m-d', $cursor);
            $plan = fetch_rate_plan($pdo, $roomTypeId, $agentId, $stayDate, $mealPlan, $totalRooms);

            if ($plan) {
                $nightAdultTotal = 0.0;
                $nightChildTotal = 0.0;
                $nightTotal = 0.0;
                $appliedTypes = [];
                $roomBreakdown = [];

                foreach ($allocation as $index => $roomAlloc) {
                    $calc = rf_calculate_room_night_total(
                        (int)$roomAlloc['adults'],
                        (int)$roomAlloc['children'],
                        (float)$plan['sharing_rate'],
                        (float)$plan['single_rate'],
                        (float)$plan['single_supplement'],
                        (float)$plan['child_rate_percent']
                    );

                    $nightAdultTotal += $calc['adult_total'];
                    $nightChildTotal += $calc['child_total'];
                    $nightTotal += $calc['room_total'];

                    if ($calc['applied_rate_type'] !== 'empty_room') {
                        $appliedTypes[] = $calc['applied_rate_type'];
                    }

                    $roomBreakdown[] = [
                        'room_no'             => $index + 1,
                        'adults'              => (int)$roomAlloc['adults'],
                        'children'            => (int)$roomAlloc['children'],
                        'rate_type'           => $calc['rate_label'],
                        'room_total'          => round($calc['room_total'], 2),
                        'rate_plan_title'     => (string)$plan['title'],
                        'rate_plan_currency'  => (string)($plan['currency'] ?? ''),
                        'rate_plan_id'        => (int)$plan['id'],
                        'rate_plan_season'    => (string)($plan['season_label'] ?? ''),
                        'rate_plan_color'     => (string)($plan['season_color'] ?? 'secondary'),
                        'meal_plan'           => normalize_meal_plan_for_rates($plan['meal_plan'] ?? $mealPlan ?? 'HB'),
                        'sharing_rate'        => (float)$plan['sharing_rate'],
                        'single_rate'         => (float)$plan['single_rate'],
                        'single_supplement'   => (float)$plan['single_supplement'],
                        'child_rate_percent'  => (float)$plan['child_rate_percent'],
                    ];
                }

                $rows[] = [
                    'date'                  => $stayDate,
                    'rate_plan'             => $plan['title'],
                    'rate_plan_id'          => (int)$plan['id'],
                    'rate_plan_currency'    => (string)($plan['currency'] ?? ''),
                    'rate_plan_season'      => (string)($plan['season_label'] ?? ''),
                    'rate_plan_color'       => (string)($plan['season_color'] ?? 'secondary'),
                    'meal_plan'             => normalize_meal_plan_for_rates($plan['meal_plan'] ?? $mealPlan ?? 'HB'),
                    'sharing_rate'          => (float)$plan['sharing_rate'],
                    'single_rate'           => (float)$plan['single_rate'],
                    'single_supplement'     => (float)$plan['single_supplement'],
                    'child_rate_percent'    => (float)$plan['child_rate_percent'],
                    'adult_total'           => round($nightAdultTotal, 2),
                    'child_total'           => round($nightChildTotal, 2),
                    'night_total'           => round($nightTotal, 2),
                    'applied_rate_type'     => implode(', ', array_unique($appliedTypes)),
                    // 'public_fallback' means an agent was set but has no contract
                    // for this night, so rack rate was applied silently.
                    'rate_source'           => (string)($plan['rate_source'] ?? 'public'),
                    'room_breakdown'        => $roomBreakdown,
                ];
            } else {
                $rows[] = [
                    'date'                  => $stayDate,
                    'rate_plan'             => 'No rate plan',
                    'rate_plan_id'          => 0,
                    'rate_plan_currency'    => '',
                    'rate_plan_season'      => '',
                    'rate_plan_color'       => 'secondary',
                    'sharing_rate'          => 0,
                    'single_rate'           => 0,
                    'single_supplement'     => 0,
                    'child_rate_percent'    => 50,
                    'adult_total'           => 0,
                    'child_total'           => 0,
                    'night_total'           => 0,
                    'applied_rate_type'     => 'missing_rate',
                    'room_breakdown'        => [],
                ];
            }

            $cursor = strtotime('+1 day', $cursor);
        }

        return $rows;
    }
}

if (!function_exists('build_saved_preview_rows_from_order')) {
    function build_saved_preview_rows_from_order(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("
            SELECT id, adults, kids, totalrooms
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) {
            return [];
        }

        $stmt = $pdo->prepare("
            SELECT *
            FROM rel_orders_prices
            WHERE order_id = :order_id
            ORDER BY date ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $nightRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (!$nightRows) {
            return [];
        }

        $stmt = $pdo->prepare("
            SELECT room_no, room_type_id, adults, children
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_no ASC, id ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $rooming = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (!$rooming) {
            $rooming = allocate_guests_to_rooms(
                (int)$order['adults'],
                (int)$order['kids'],
                max(1, (int)$order['totalrooms'])
            );
            $tmp = [];
            foreach ($rooming as $idx => $r) {
                $tmp[] = [
                    'room_no'  => $idx + 1,
                    'adults'   => (int)$r['adults'],
                    'children' => (int)$r['children'],
                ];
            }
            $rooming = $tmp;
        }

        $rows = [];
        foreach ($nightRows as $night) {
            $nightAdultTotal = 0.0;
            $nightChildTotal = 0.0;
            $nightTotal = 0.0;
            $appliedTypes = [];
            $roomBreakdown = [];

            foreach ($rooming as $slot) {
                $roomTypeId = (int)($slot['room_type_id'] ?? 0);
                $roomTypeMeta = fetch_room_type_meta($pdo, $roomTypeId);
                $roomDisplayLabel = room_slot_display_label(
                    (string)$roomTypeMeta['title'],
                    (int)$slot['adults'],
                    (int)$slot['children'],
                    (int)$roomTypeMeta['higher_occupancy']
                );

                $calc = rf_calculate_room_night_total(
                    (int)$slot['adults'],
                    (int)$slot['children'],
                    (float)$night['sharing_rate'],
                    (float)$night['single_rate'],
                    (float)$night['single_supplement'],
                    (float)$night['child_rate_percent']
                );

                $nightAdultTotal += $calc['adult_total'];
                $nightChildTotal += $calc['child_total'];
                $nightTotal += $calc['room_total'];

                if ($calc['applied_rate_type'] !== 'empty_room') {
                    $appliedTypes[] = $calc['applied_rate_type'];
                }

                $roomBreakdown[] = [
                    'room_no'             => (int)$slot['room_no'],
                    'room_type_id'        => $roomTypeId,
                    'room_type_title'     => (string)$roomTypeMeta['title'],
                    'higher_occupancy'    => (int)$roomTypeMeta['higher_occupancy'],
                    'display_label'       => $roomDisplayLabel,
                    'adults'              => (int)$slot['adults'],
                    'children'            => (int)$slot['children'],
                    'rate_type'           => $roomDisplayLabel !== '' ? $roomDisplayLabel : $calc['rate_label'],
                    'room_total'          => round($calc['room_total'], 2),
                    'rate_plan_title'     => (string)($night['rate_plan_title'] ?? ''),
                    'rate_plan_currency'  => (string)($night['rate_plan_currency'] ?? ''),
                    'rate_plan_id'        => (int)($night['rate_plan_id'] ?? 0),
                    'rate_plan_season'    => (string)($night['rate_plan_season_label'] ?? ''),
                    'rate_plan_color'     => (string)($night['rate_plan_season_color'] ?? 'secondary'),
                    'sharing_rate'        => (float)$night['sharing_rate'],
                    'single_rate'         => (float)$night['single_rate'],
                    'single_supplement'   => (float)$night['single_supplement'],
                    'child_rate_percent'  => (float)$night['child_rate_percent'],
                ];
            }

            /*
               Honour a saved nightly override. This function recomputes the
               night from the raw sharing/single/child columns, but if the night
               was overridden those recomputed figures are NOT the saved price -
               the authoritative amount lives in rel_orders_prices.override_total
               (falling back to total). Ignoring it here is what made an edited
               nightly price on a pricing-locked booking snap back to the
               calculated rate on reopen. Pin the row to the saved override.
            */
            $isOverridden = (int)($night['rate_overridden'] ?? 0) === 1;
            $savedOverrideTotal = null;
            if ($isOverridden) {
                if (array_key_exists('override_total', $night) && $night['override_total'] !== null && $night['override_total'] !== '') {
                    $savedOverrideTotal = round((float)$night['override_total'], 2);
                } elseif (array_key_exists('total', $night) && $night['total'] !== null && $night['total'] !== '') {
                    $savedOverrideTotal = round((float)$night['total'], 2);
                }
            }

            $displayNightTotal = $isOverridden && $savedOverrideTotal !== null
                ? $savedOverrideTotal
                : round($nightTotal, 2);

            $originalTotal = ($night['original_total'] ?? null) !== null
                ? round((float)$night['original_total'], 2)
                : round($nightTotal, 2);

            $overrideComponents = null;
            if (isset($night['override_components']) && trim((string)$night['override_components']) !== '') {
                $decodedComp = json_decode((string)$night['override_components'], true);
                if (is_array($decodedComp)) {
                    $overrideComponents = $decodedComp;
                }
            }
            $perRoomMapSaved = (is_array($overrideComponents) && isset($overrideComponents['per_room']) && is_array($overrideComponents['per_room']))
                ? $overrideComponents['per_room']
                : null;

            $rows[] = [
                'date'                  => (string)$night['date'],
                'rate_plan'             => (string)($night['rate_plan_title'] ?? 'Saved Rate'),
                'rate_plan_id'          => (int)($night['rate_plan_id'] ?? 0),
                'rate_plan_currency'    => (string)($night['rate_plan_currency'] ?? ''),
                'rate_plan_season'      => (string)($night['rate_plan_season_label'] ?? ''),
                'rate_plan_color'       => (string)($night['rate_plan_season_color'] ?? 'secondary'),
                'sharing_rate'          => (float)$night['sharing_rate'],
                'single_rate'           => (float)$night['single_rate'],
                'single_supplement'     => (float)$night['single_supplement'],
                'child_rate_percent'    => (float)$night['child_rate_percent'],
                'adult_total'           => round($nightAdultTotal, 2),
                'child_total'           => round($nightChildTotal, 2),
                'night_total'           => $displayNightTotal,
                'original_total'        => $originalTotal,
                'rate_overridden'       => $isOverridden ? 1 : 0,
                'override_type'         => $isOverridden ? (string)($night['override_type'] ?? 'total') : '',
                'override_value'        => ($night['override_value'] ?? null) !== null ? round((float)$night['override_value'], 2) : null,
                'override_total'        => $savedOverrideTotal,
                'override_reason'       => (string)($night['override_reason'] ?? ''),
                'override_components'   => $overrideComponents,
                'room_overrides'        => $perRoomMapSaved ?: [],
                'applied_rate_type'     => implode(', ', array_unique($appliedTypes)),
                'room_breakdown'        => $roomBreakdown,
            ];
        }

        return $rows;
    }
}

if (!function_exists('rebuild_order_nightly_prices')) {
    function rebuild_order_nightly_prices(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("
            SELECT id, property_id, check_in, check_out, adults, kids, room_type_id, totalrooms, agent_id, meal_plan
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$order) {
            throw new RuntimeException('Order not found');
        }

        $mealPlan = normalize_meal_plan_for_rates($order['meal_plan'] ?? 'HB');

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

        if (!$checkIn || !$checkOut || $checkOut <= $checkIn) {
            throw new RuntimeException('Invalid stay dates');
        }

        $totalRooms = max(1, (int)($order['totalrooms'] ?? 1));
        $allocation = allocate_guests_to_rooms(
            (int)$order['adults'],
            (int)$order['kids'],
            $totalRooms
        );

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

        /*
           Legacy single-room path: one row per night, no per-room breakdown.
           The multi-room rebuild below is what writes one row per room per
           night; this is left exactly as it was.
        */
        $insert = $pdo->prepare("
            INSERT INTO rel_orders_prices
            (
                order_id, property_id, date, price, additional_person, additional_person_price, total, room_id,
                rate_plan_id, rate_plan_title, rate_plan_currency, rate_plan_season_label, rate_plan_season_color,
                adults_count, kids_count, sharing_rate, single_rate,
                single_supplement, child_rate_percent, adult_total, child_total, applied_rate_type
            )
            VALUES
            (
                :order_id, :property_id, :date, :price, :additional_person, :additional_person_price, :total, :room_id,
                :rate_plan_id, :rate_plan_title, :rate_plan_currency, :rate_plan_season_label, :rate_plan_season_color,
                :adults_count, :kids_count, :sharing_rate, :single_rate,
                :single_supplement, :child_rate_percent, :adult_total, :child_total, :applied_rate_type
            )
        ");

        $cursor = $checkIn;
        $grandTotal = 0.0;
        $nightCount = 0;

        while ($cursor < $checkOut) {
            $stayDate = date('Y-m-d', $cursor);
            $plan = fetch_rate_plan(
                $pdo,
                (int)$order['room_type_id'],
                !empty($order['agent_id']) ? (int)$order['agent_id'] : null,
                $stayDate,
                $mealPlan,
                $totalRooms
            );

            if (!$plan) {
                // No active rate plan for this night: hold the room(s) at zero
                // instead of blocking the booking. Staff can price it later.
                $plan = rf_zero_rate_plan($mealPlan);
            }

            $nightAdultTotal = 0.0;
            $nightChildTotal = 0.0;
            $nightTotal = 0.0;
            $appliedTypes = [];

            foreach ($allocation as $roomAlloc) {
                $calc = rf_calculate_room_night_total(
                    (int)$roomAlloc['adults'],
                    (int)$roomAlloc['children'],
                    (float)$plan['sharing_rate'],
                    (float)$plan['single_rate'],
                    (float)$plan['single_supplement'],
                    (float)$plan['child_rate_percent']
                );

                $nightAdultTotal += $calc['adult_total'];
                $nightChildTotal += $calc['child_total'];
                $nightTotal += $calc['room_total'];

                if ($calc['applied_rate_type'] !== 'empty_room') {
                    $appliedTypes[] = $calc['applied_rate_type'];
                }
            }

            $insert->execute([
                ':order_id'                 => $orderId,
                ':property_id'              => (int)$order['property_id'],
                ':date'                     => $stayDate,
                ':price'                    => (float)$plan['sharing_rate'],
                ':additional_person'        => max(0, (int)$order['adults'] - $totalRooms),
                ':additional_person_price'  => round($nightChildTotal, 2),
                ':total'                    => round($nightTotal, 2),
                ':room_id'                  => 0,
                ':rate_plan_id'             => (int)$plan['id'],
                ':rate_plan_title'          => (string)$plan['title'],
                ':rate_plan_currency'       => (string)($plan['currency'] ?? ''),
                ':rate_plan_season_label'   => (string)($plan['season_label'] ?? ''),
                ':rate_plan_season_color'   => (string)($plan['season_color'] ?? 'secondary'),
                ':adults_count'             => (int)$order['adults'],
                ':kids_count'               => (int)$order['kids'],
                ':sharing_rate'             => (float)$plan['sharing_rate'],
                ':single_rate'              => (float)$plan['single_rate'],
                ':single_supplement'        => (float)$plan['single_supplement'],
                ':child_rate_percent'       => (float)$plan['child_rate_percent'],
                ':adult_total'              => round($nightAdultTotal, 2),
                ':child_total'              => round($nightChildTotal, 2),
                ':applied_rate_type'        => implode(', ', array_unique($appliedTypes)),
            ]);

            $grandTotal += $nightTotal;
            $nightCount++;
            $cursor = strtotime('+1 day', $cursor);
        }

        $upd = $pdo->prepare("
            UPDATE orders
            SET nights = :nights,
                base_price = :base_price,
                amount = :amount,
                totalamount = :totalamount
            WHERE id = :id
        ");
        $upd->execute([
            ':nights'      => $nightCount,
            ':base_price'  => round($grandTotal, 2),
            ':amount'      => round($grandTotal, 2),
            ':totalamount' => round($grandTotal, 2),
            ':id'          => $orderId,
        ]);

        return [
            'nights' => $nightCount,
            'total'  => round($grandTotal, 2),
        ];
    }
}

if (!function_exists('rebuild_order_rooming')) {
    function rebuild_order_rooming(PDO $pdo, int $orderId): void
    {
        $stmt = $pdo->prepare("
            SELECT id, property_id, check_in, check_out, adults, kids, totalrooms
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$order) {
            throw new RuntimeException('Order not found for rooming rebuild.');
        }

        $oldRowsStmt = $pdo->prepare("
            SELECT room_no, room_id
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_no ASC, id ASC
        ");
        $oldRowsStmt->execute([':order_id' => $orderId]);
        $oldRows = $oldRowsStmt->fetchAll(PDO::FETCH_ASSOC);

        $oldRoomMap = [];
        foreach ($oldRows as $r) {
            $oldRoomMap[(int)$r['room_no']] = (int)$r['room_id'];
        }

        $allocation = allocate_guests_to_rooms(
            (int)$order['adults'],
            (int)$order['kids'],
            max(1, (int)$order['totalrooms'])
        );

        $pdo->prepare("DELETE FROM order_rooming WHERE order_id = :order_id")
            ->execute([':order_id' => $orderId]);

        $insert = $pdo->prepare("
            INSERT INTO order_rooming
            (
                order_id,
                property_id,
                room_no,
                room_id,
                adults,
                children,
                extra_beds_required,
                cots_required,
                wheelchair_room_required,
                interleading_required,
                room_setup_notes,
                housekeeping_notes,
                special_occasion,
                check_in,
                check_out
            )
            VALUES
            (
                :order_id,
                :property_id,
                :room_no,
                :room_id,
                :adults,
                :children,
                :extra_beds_required,
                :cots_required,
                :wheelchair_room_required,
                :interleading_required,
                :room_setup_notes,
                :housekeeping_notes,
                :special_occasion,
                :check_in,
                :check_out
            )
        ");

        foreach ($allocation as $idx => $roomAlloc) {
            $roomNo = $idx + 1;
            $roomId = $oldRoomMap[$roomNo] ?? 0;

            $insert->execute([
                ':order_id'    => $orderId,
                ':property_id' => (int)$order['property_id'],
                ':room_no'     => $roomNo,
                ':room_id'     => $roomId,
                ':adults'      => (int)$roomAlloc['adults'],
                ':children'    => (int)$roomAlloc['children'],
                ':extra_beds_required' => 0,
                ':cots_required' => 0,
                ':wheelchair_room_required' => 0,
                ':interleading_required' => 0,
                ':room_setup_notes' => null,
                ':housekeeping_notes' => null,
                ':special_occasion' => null,
                ':check_in'    => $order['check_in'],
                ':check_out'   => $order['check_out'],
            ]);
        }

        $firstAssignedStmt = $pdo->prepare("
            SELECT room_id
            FROM order_rooming
            WHERE order_id = :order_id
              AND room_id > 0
            ORDER BY room_no ASC, id ASC
            LIMIT 1
        ");
        $firstAssignedStmt->execute([':order_id' => $orderId]);
        $headerRoomId = (int)($firstAssignedStmt->fetchColumn() ?: 0);

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


if (!function_exists('room_matches_requested_type')) {
    function room_matches_requested_type(PDO $pdo, int $roomId, int $propertyId, int $roomTypeId): bool
    {
        if ($roomId <= 0 || $propertyId <= 0 || $roomTypeId <= 0) {
            return false;
        }

        $stmt = $pdo->prepare("
            SELECT COUNT(*)
            FROM rooms
            WHERE id = :room_id
              AND property_id = :property_id
              AND room_type_id = :room_type_id
        ");
        $stmt->execute([
            ':room_id'      => $roomId,
            ':property_id'  => $propertyId,
            ':room_type_id' => $roomTypeId,
        ]);

        return ((int)$stmt->fetchColumn() > 0);
    }
}

if (!function_exists('rooming_room_type_label')) {
    function rooming_room_type_label(PDO $pdo, int $roomTypeId): string
    {
        $meta = fetch_room_type_meta($pdo, $roomTypeId);
        $title = trim((string)($meta['title'] ?? ''));
        return $title !== '' ? $title : ('Room Type #' . $roomTypeId);
    }
}


if (!function_exists('room_is_free_for_full_stay')) {
    function room_is_free_for_full_stay(PDO $pdo, int $roomId, string $checkIn, string $checkOut, int $excludeRoomingId = 0, int $excludeOrderId = 0): bool
    {
        $sql = "
            SELECT COUNT(*)
            FROM order_rooming rmg
            INNER JOIN orders o ON o.id = rmg.order_id
            WHERE rmg.room_id = :room_id
              AND rmg.room_id > 0
              AND LOWER(TRIM(CAST(COALESCE(o.status, 1) AS CHAR))) NOT IN ('0','cancelled','canceled','void','deleted','no_show','noshow')
              AND rmg.check_in < :check_out
              AND rmg.check_out > :check_in
        ";

        $params = [
            ':room_id'   => $roomId,
            ':check_in'  => $checkIn,
            ':check_out' => $checkOut,
        ];

        if ($excludeRoomingId > 0) {
            $sql .= " AND rmg.id <> :exclude_rooming_id ";
            $params[':exclude_rooming_id'] = $excludeRoomingId;
        }

        /*
           Exclude the whole booking, not just one rooming row.

           save_order_rooming_rows() deletes and re-inserts the rooming rows, so
           after an edit the surviving rows carry BRAND NEW ids. Excluding only a
           single rmg.id therefore let a booking collide with its OWN other rows:
           a room the booking already held looked "occupied by someone else", the
           re-assign was refused, room_id stayed 0 and the booking fell into the
           unassigned tray. Excluding by order_id makes the check ask the real
           question: is anyone ELSE in this room?
        */
        if ($excludeOrderId > 0) {
            $sql .= " AND rmg.order_id <> :exclude_order_id ";
            $params[':exclude_order_id'] = $excludeOrderId;
        }

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);

        return ((int)$stmt->fetchColumn() === 0);
    }
}

if (!function_exists('sync_order_header_room')) {
    function sync_order_header_room(PDO $pdo, int $orderId): void
    {
        $stmt = $pdo->prepare("
            SELECT room_id
            FROM order_rooming
            WHERE order_id = :order_id
              AND room_id > 0
            ORDER BY room_no ASC, id ASC
            LIMIT 1
        ");
        $stmt->execute([':order_id' => $orderId]);
        $headerRoomId = (int)($stmt->fetchColumn() ?: 0);

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

if (!function_exists('auto_assign_rooming_best_fit')) {
    function auto_assign_rooming_best_fit(PDO $pdo, int $orderId): void
    {
        $stmt = $pdo->prepare("
            SELECT
                rmg.id AS rooming_id,
                rmg.room_no,
                rmg.room_id,
                rmg.room_type_id,
                rmg.adults,
                rmg.children,
                rmg.check_in,
                rmg.check_out,
                rmg.property_id,
                COALESCE(rmg.extra_beds_required,0) AS extra_beds_required,
                COALESCE(rmg.cots_required,0) AS cots_required,
                COALESCE(rmg.wheelchair_room_required,0) AS wheelchair_room_required,
                COALESCE(rmg.interleading_required,0) AS interleading_required
            FROM order_rooming rmg
            WHERE rmg.order_id = :order_id
            ORDER BY rmg.room_no ASC, rmg.id ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $slots = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (!$slots) {
            sync_order_header_room($pdo, $orderId);
            return;
        }

        $roomsStmt = $pdo->prepare("
            SELECT id, room_no
            FROM rooms
            WHERE property_id = :property_id
              AND room_type_id = :room_type_id
            ORDER BY room_no ASC
        ");

        /*
           Rooms already taken by an EARLIER slot of this same booking during
           this run. We exclude the whole order from the availability check (see
           room_is_free_for_full_stay) so the booking stops blocking itself after
           its rooming rows are re-created with new ids - but that means we must
           track intra-booking claims here, otherwise two slots could both grab
           the same physical room.
        */
        $claimedThisRun = [];

        foreach ($slots as $slot) {
            $roomingId     = (int)$slot['rooming_id'];
            $roomTypeId    = (int)$slot['room_type_id'];
            $propertyId    = (int)$slot['property_id'];
            $checkIn       = (string)$slot['check_in'];
            $checkOut      = (string)$slot['check_out'];
            $currentRoomId = (int)$slot['room_id'];

            $assignedRoomId = 0;
            $fallbackFreeRoomId = 0;

            /*
               Keep the current room only if it still belongs to the requested room type.
               Example: when a booking is edited from Double to Family, an old Double
               room assignment must be released and replaced by a Family room.
            */
            if (
                $currentRoomId > 0
                && !isset($claimedThisRun[$currentRoomId])
                && room_matches_requested_type($pdo, $currentRoomId, $propertyId, $roomTypeId)
                && room_is_free_for_full_stay($pdo, $currentRoomId, $checkIn, $checkOut, $roomingId, $orderId)
            ) {
                $fallbackFreeRoomId = $currentRoomId;
                if (!empty(room_satisfies_rooming_requirements($pdo, $currentRoomId, $slot)['ok'])) {
                    $assignedRoomId = $currentRoomId;
                }
            }

            if ($assignedRoomId <= 0) {
                $roomsStmt->execute([
                    ':property_id'  => $propertyId,
                    ':room_type_id' => $roomTypeId,
                ]);
                $rooms = $roomsStmt->fetchAll(PDO::FETCH_ASSOC);

                foreach ($rooms as $room) {
                    $roomId = (int)$room['id'];

                    // Never hand the same room to two slots of this booking.
                    if (isset($claimedThisRun[$roomId])) {
                        continue;
                    }

                    if (!room_is_free_for_full_stay($pdo, $roomId, $checkIn, $checkOut, $roomingId, $orderId)) {
                        continue;
                    }

                    if ($fallbackFreeRoomId <= 0) {
                        $fallbackFreeRoomId = $roomId;
                    }

                    if (!empty(room_satisfies_rooming_requirements($pdo, $roomId, $slot)['ok'])) {
                        $assignedRoomId = $roomId;
                        break;
                    }
                }
            }

            /*
               Do not leave a booking in temporary/unassigned holding when a room of the
               selected type is genuinely available. If no room satisfies optional setup
               requirements such as cot/extra bed/wheelchair/interleading, assign the
               first free room of that type and show a setup warning for Front Desk.
            */
            if ($assignedRoomId <= 0 && $fallbackFreeRoomId > 0 && !isset($claimedThisRun[$fallbackFreeRoomId])) {
                $assignedRoomId = $fallbackFreeRoomId;
                $_SESSION['reservation_assignment_warning'] = trim(
                    (string)($_SESSION['reservation_assignment_warning'] ?? '') . ' Room slot ' . (int)$slot['room_no']
                    . ' was assigned to an available room of the requested type, but one or more setup preferences may need manual review.'
                );
            }

            if ($assignedRoomId > 0) {
                $claimedThisRun[$assignedRoomId] = true;
            }

            $pdo->prepare("
                UPDATE order_rooming
                SET room_id = :room_id
                WHERE id = :rooming_id
            ")->execute([
                ':room_id'    => $assignedRoomId,
                ':rooming_id' => $roomingId,
            ]);
        }

        sync_order_header_room($pdo, $orderId);
    }
}

if (!function_exists('next_order_no')) {
    function next_order_no(PDO $pdo): string
    {
        do {
            $orderNo = 'BK-' . date('YmdHis') . '-' . random_int(100, 999);
            $stmt = $pdo->prepare("
                SELECT COUNT(*)
                FROM orders
                WHERE order_no = :order_no
            ");
            $stmt->execute([':order_no' => $orderNo]);
            $exists = (int)$stmt->fetchColumn() > 0;
        } while ($exists);

        return $orderNo;
    }
}

if (!function_exists('fetch_room_types_by_property')) {
    function fetch_room_types_by_property(PDO $pdo, int $propertyId): array
    {
        if ($propertyId <= 0) {
            return [];
        }

        $stmt = $pdo->prepare("
            SELECT id, title, higher_occupancy
            FROM room_types
            WHERE property_id = :property_id
            ORDER BY title ASC
        ");
        $stmt->execute([':property_id' => $propertyId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}


if (!function_exists('normalize_room_rows')) {
    function normalize_room_rows(array $rows): array
    {
        $clean = [];

        foreach ($rows as $row) {
            $roomTypeId = (int)($row['room_type_id'] ?? 0);
            $qtyRooms   = max(1, (int)($row['qty_rooms'] ?? 1));
            $adults     = max(0, (int)($row['adults'] ?? 0));
            $children   = max(0, (int)($row['children'] ?? 0));
            $extraBeds = max(0, (int)($row['extra_beds_required'] ?? 0));
            $cots = max(0, (int)($row['cots_required'] ?? 0));
            $wheelchairRequired = !empty($row['wheelchair_room_required']) ? 1 : 0;
            $interleadingRequired = !empty($row['interleading_required']) ? 1 : 0;
            $roomSetupNotes = trim((string)($row['room_setup_notes'] ?? ''));
            $housekeepingNotes = trim((string)($row['housekeeping_notes'] ?? ''));
            $specialOccasion = trim((string)($row['special_occasion'] ?? ''));

            if ($roomTypeId <= 0 && $adults <= 0 && $children <= 0) {
                continue;
            }

            $clean[] = [
                'row_no'       => count($clean) + 1,
                'room_type_id' => $roomTypeId,
                'qty_rooms'    => $qtyRooms,
                'adults'       => $adults,
                'children'     => $children,
                'extra_beds_required' => $extraBeds,
                'cots_required' => $cots,
                'wheelchair_room_required' => $wheelchairRequired,
                'interleading_required' => $interleadingRequired,
                'room_setup_notes' => $roomSetupNotes,
                'housekeeping_notes' => $housekeepingNotes,
                'special_occasion' => $specialOccasion,
            ];
        }

        if (!$clean) {
            $clean[] = [
                'row_no'       => 1,
                'room_type_id' => 0,
                'qty_rooms'    => 1,
                'adults'       => 1,
                'children'     => 0,
                'extra_beds_required' => 0,
                'cots_required' => 0,
                'wheelchair_room_required' => 0,
                'interleading_required' => 0,
                'room_setup_notes' => '',
                'housekeeping_notes' => '',
                'special_occasion' => '',
            ];
        }

        return $clean;
    }
}

if (!function_exists('summarize_room_rows')) {
    function summarize_room_rows(array $roomRows): array
    {
        $totalRooms = 0;
        $adults = 0;
        $kids = 0;

        foreach ($roomRows as $row) {
            $totalRooms += max(1, (int)($row['qty_rooms'] ?? 1));
            $adults += (int)$row['adults'];
            $kids   += (int)$row['children'];
        }

        return [
            'totalrooms' => $totalRooms,
            'adults'     => $adults,
            'kids'       => $kids,
            'total_pax'  => $adults + $kids,
        ];
    }
}

if (!function_exists('group_room_rows_by_type')) {
    function group_room_rows_by_type(array $roomRows): array
    {
        $grouped = [];

        foreach ($roomRows as $row) {
            $roomTypeId = (int)($row['room_type_id'] ?? 0);
            if ($roomTypeId <= 0) {
                continue;
            }

            if (!isset($grouped[$roomTypeId])) {
                $grouped[$roomTypeId] = [
                    'room_type_id' => $roomTypeId,
                    'rooms_needed' => 0,
                    'adults'       => 0,
                    'children'     => 0,
                    'extra_beds_required' => 0,
                    'cots_required' => 0,
                    'wheelchair_room_required' => 0,
                    'interleading_required' => 0,
                    'room_setup_notes' => '',
                    'housekeeping_notes' => '',
                    'special_occasion' => '',
                ];
            }

            $grouped[$roomTypeId]['rooms_needed'] += max(1, (int)($row['qty_rooms'] ?? 1));
            $grouped[$roomTypeId]['adults'] += (int)($row['adults'] ?? 0);
            $grouped[$roomTypeId]['children'] += (int)($row['children'] ?? 0);
        }

        return array_values($grouped);
    }
}


if (!function_exists('validate_room_type_availability_for_booking')) {
    function validate_room_type_availability_for_booking(
        PDO $pdo,
        int $propertyId,
        string $checkIn,
        string $checkOut,
        array $roomRows,
        int $excludeOrderId = 0,
        bool $allowWaitlist = false
    ): array {
        $messages = [];

        if ($propertyId <= 0 || $checkIn === '' || $checkOut === '') {
            return $messages;
        }

        $groupedNeeds = group_room_rows_by_type($roomRows);

        foreach ($groupedNeeds as $need) {
            $roomTypeId  = (int)($need['room_type_id'] ?? 0);
            $roomsNeeded = (int)($need['rooms_needed'] ?? 0);

            if ($roomTypeId <= 0 || $roomsNeeded <= 0) {
                continue;
            }

            $availability = count_available_rooms_by_type(
                $pdo,
                $propertyId,
                $roomTypeId,
                $checkIn,
                $checkOut,
                $excludeOrderId
            );

            $roomTypeName = rooming_room_type_label($pdo, $roomTypeId);
            $totalRooms   = (int)($availability['total_rooms'] ?? 0);
            $available    = (int)($availability['available_rooms'] ?? 0);

            if ($totalRooms <= 0) {
                $messages[] = 'No physical rooms exist for ' . $roomTypeName . ' in this property.';
                continue;
            }

            if ($roomsNeeded > $available) {
                $messages[] = 'Only ' . $available . ' room(s) are available for ' . $roomTypeName
                    . ' for ' . $checkIn . ' to ' . $checkOut
                    . '. Requested ' . $roomsNeeded . '. Availability uses physical-room per-night count.';
            }
        }

        if ($messages && !$allowWaitlist) {
            throw new RuntimeException(implode(' ', $messages));
        }

        return $messages;
    }
}

if (!function_exists('lock_requested_room_type_inventory')) {
    function lock_requested_room_type_inventory(PDO $pdo, int $propertyId, array $roomRows): void
    {
        if ($propertyId <= 0) {
            return;
        }

        $typeIds = [];
        foreach (group_room_rows_by_type($roomRows) as $need) {
            $roomTypeId = (int)($need['room_type_id'] ?? 0);
            if ($roomTypeId > 0) {
                $typeIds[$roomTypeId] = $roomTypeId;
            }
        }

        if (!$typeIds) {
            return;
        }

        $placeholders = implode(',', array_fill(0, count($typeIds), '?'));
        $params = array_merge([$propertyId], array_values($typeIds));

        /*
           Locks candidate room rows for this transaction so that the final
           availability recheck and room assignment are not done against stale
           inventory while another user is saving a competing booking.
        */
        try {
            $stmt = $pdo->prepare("SELECT id FROM rooms WHERE property_id = ? AND room_type_id IN ($placeholders) ORDER BY id ASC FOR UPDATE");
            $stmt->execute($params);
            $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable $e) {
            /* Some MySQL hosts/storage engines may not allow FOR UPDATE here. Validation still runs below. */
        }
    }
}

if (!function_exists('room_satisfies_rooming_requirements')) {
    function room_satisfies_rooming_requirements(PDO $pdo, int $roomId, array $slot): array
    {
        if ($roomId <= 0) {
            return ['ok' => false, 'message' => 'No room selected.'];
        }

        $stmt = $pdo->prepare("SELECT * FROM rooms WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $roomId]);
        $room = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$room) {
            return ['ok' => false, 'message' => 'Room not found.'];
        }

        $extraBedsRequired = (int)($slot['extra_beds_required'] ?? 0);
        $cotsRequired = (int)($slot['cots_required'] ?? 0);
        $wheelchairRequired = !empty($slot['wheelchair_room_required']);
        $interleadingRequired = !empty($slot['interleading_required']);

        if ($extraBedsRequired > 0) {
            if (rf_column_exists($pdo, 'rooms', 'allows_extra_bed') && (int)($room['allows_extra_bed'] ?? 0) !== 1) {
                return ['ok' => false, 'message' => 'Selected room does not allow extra beds.'];
            }
            if (rf_column_exists($pdo, 'rooms', 'max_extra_beds') && (int)($room['max_extra_beds'] ?? 0) < $extraBedsRequired) {
                return ['ok' => false, 'message' => 'Selected room cannot fit the requested number of extra beds.'];
            }
        }

        if ($cotsRequired > 0) {
            if (rf_column_exists($pdo, 'rooms', 'allows_baby_cot') && (int)($room['allows_baby_cot'] ?? 0) !== 1) {
                return ['ok' => false, 'message' => 'Selected room does not allow baby cots.'];
            }
            if (rf_column_exists($pdo, 'rooms', 'max_cots') && (int)($room['max_cots'] ?? 0) < $cotsRequired) {
                return ['ok' => false, 'message' => 'Selected room cannot fit the requested number of baby cots.'];
            }
        }

        if ($wheelchairRequired && rf_column_exists($pdo, 'rooms', 'wheelchair_accessible') && (int)($room['wheelchair_accessible'] ?? 0) !== 1) {
            return ['ok' => false, 'message' => 'Selected room is not wheelchair accessible.'];
        }

        if ($interleadingRequired && rf_column_exists($pdo, 'rooms', 'interleading_group') && trim((string)($room['interleading_group'] ?? '')) === '') {
            return ['ok' => false, 'message' => 'Selected room is not configured as interleading.'];
        }

        return ['ok' => true, 'message' => 'Room satisfies requested setup.', 'room' => $room];
    }
}


if (!function_exists('validate_room_assignment_matches_rooming_type')) {
    function validate_room_assignment_matches_rooming_type(PDO $pdo, int $roomingId, int $targetRoomId): array
    {
        if ($roomingId <= 0 || $targetRoomId <= 0) {
            return [
                'ok' => false,
                'message' => 'Missing rooming line or target room.',
            ];
        }

        $stmt = $pdo->prepare("\n            SELECT\n                rmg.id,\n                rmg.order_id,\n                rmg.property_id,\n                rmg.room_type_id AS requested_room_type_id,\n                rmg.check_in,\n                rmg.check_out,\n                rt.title AS requested_room_type\n            FROM order_rooming rmg\n            LEFT JOIN room_types rt ON rt.id = rmg.room_type_id\n            WHERE rmg.id = :rooming_id\n            LIMIT 1\n        ");
        $stmt->execute([':rooming_id' => $roomingId]);
        $rooming = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$rooming) {
            return [
                'ok' => false,
                'message' => 'Rooming line not found.',
            ];
        }

        $stmt = $pdo->prepare("\n            SELECT\n                r.id,\n                r.room_no,\n                r.property_id,\n                r.room_type_id AS target_room_type_id,\n                rt.title AS target_room_type\n            FROM rooms r\n            LEFT JOIN room_types rt ON rt.id = r.room_type_id\n            WHERE r.id = :room_id\n            LIMIT 1\n        ");
        $stmt->execute([':room_id' => $targetRoomId]);
        $room = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$room) {
            return [
                'ok' => false,
                'message' => 'Target room not found.',
            ];
        }

        if ((int)$room['property_id'] !== (int)$rooming['property_id']) {
            return [
                'ok' => false,
                'message' => 'Target room belongs to a different property.',
            ];
        }

        if ((int)$room['target_room_type_id'] !== (int)$rooming['requested_room_type_id']) {
            return [
                'ok' => false,
                'message' => 'Room type mismatch. ' . trim((string)($rooming['requested_room_type'] ?? 'This booking'))
                    . ' bookings can only be assigned to ' . trim((string)($rooming['requested_room_type'] ?? 'the same room type'))
                    . ' rooms. Target room ' . trim((string)($room['room_no'] ?? '#' . $targetRoomId))
                    . ' is ' . trim((string)($room['target_room_type'] ?? 'a different room type')) . '.',
            ];
        }

        if (!room_is_free_for_full_stay(
            $pdo,
            $targetRoomId,
            (string)$rooming['check_in'],
            (string)$rooming['check_out'],
            $roomingId
        )) {
            return [
                'ok' => false,
                'message' => 'Target room is not available for the full stay.',
            ];
        }

        $requirementCheck = room_satisfies_rooming_requirements($pdo, $targetRoomId, $rooming);
        if (empty($requirementCheck['ok'])) {
            return [
                'ok' => false,
                'message' => (string)($requirementCheck['message'] ?? 'Target room does not satisfy the requested setup.'),
            ];
        }

        return [
            'ok' => true,
            'message' => 'Room assignment is valid.',
            'rooming' => $rooming,
            'room' => $room,
        ];
    }
}

if (!function_exists('fetch_order_room_rows')) {
    function fetch_order_room_rows(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("
            SELECT room_type_id, adults, children,
                   COALESCE(extra_beds_required,0) AS extra_beds_required,
                   COALESCE(cots_required,0) AS cots_required,
                   COALESCE(wheelchair_room_required,0) AS wheelchair_room_required,
                   COALESCE(interleading_required,0) AS interleading_required,
                   COALESCE(room_setup_notes,'') AS room_setup_notes,
                   COALESCE(housekeeping_notes,'') AS housekeeping_notes,
                   COALESCE(special_occasion,'') AS special_occasion
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_type_id ASC, room_no ASC, id ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $slots = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        if (!$slots) {
            return [];
        }

        $grouped = [];
        foreach ($slots as $slot) {
            $roomTypeId = (int)($slot['room_type_id'] ?? 0);
            $extraBeds = (int)($slot['extra_beds_required'] ?? 0);
            $cots = (int)($slot['cots_required'] ?? 0);
            $wheelchairRequired = (int)($slot['wheelchair_room_required'] ?? 0);
            $interleadingRequired = (int)($slot['interleading_required'] ?? 0);
            $roomSetupNotes = trim((string)($slot['room_setup_notes'] ?? ''));
            $housekeepingNotes = trim((string)($slot['housekeeping_notes'] ?? ''));
            $specialOccasion = trim((string)($slot['special_occasion'] ?? ''));
            /*
             * Occupancy is part of the room-row identity.
             *
             * Without adults/children in this key, two Pool View rooms such as
             * 2A+0C and 0A+2C were merged into one row (2 rooms, 2 adults,
             * 2 children). Saving that merged row ran the automatic allocator
             * again and changed the real rooming to 1A+1C in each room.
             */
            $slotAdults = max(0, (int)($slot['adults'] ?? 0));
            $slotChildren = max(0, (int)($slot['children'] ?? 0));
            $key = implode('|', [
                $roomTypeId,
                $slotAdults,
                $slotChildren,
                $extraBeds,
                $cots,
                $wheelchairRequired,
                $interleadingRequired,
                $roomSetupNotes,
                $housekeepingNotes,
                $specialOccasion,
            ]);

            if (!isset($grouped[$key])) {
                $grouped[$key] = [
                    'row_no'       => count($grouped) + 1,
                    'room_type_id' => $roomTypeId,
                    'qty_rooms'    => 0,
                    'adults'       => 0,
                    'children'     => 0,
                    'extra_beds_required' => $extraBeds,
                    'cots_required' => $cots,
                    'wheelchair_room_required' => $wheelchairRequired,
                    'interleading_required' => $interleadingRequired,
                    'room_setup_notes' => $roomSetupNotes,
                    'housekeeping_notes' => $housekeepingNotes,
                    'special_occasion' => $specialOccasion,
                ];
            }

            $grouped[$key]['qty_rooms']++;
            $grouped[$key]['adults'] += (int)($slot['adults'] ?? 0);
            $grouped[$key]['children'] += (int)($slot['children'] ?? 0);
        }

        return array_values($grouped);
    }
}

if (!function_exists('rf_price_slots_from_rooming')) {
    /**
     * One pricing slot per order_rooming row, carrying its physical room_id.
     *
     * order_rooming IS the rooming list: one row per room slot, with the room
     * assigned to it and the pax actually in it. Pricing from it means the
     * priced rows line up 1:1 with the rooming list, keep their room_id, and
     * use each room's real occupancy.
     *
     * Returns [] when the booking has no rooming rows yet (a fresh quote), so
     * the caller can fall back to expanding the requested mix.
     */
    function rf_price_slots_from_rooming(PDO $pdo, int $orderId): array
    {
        if ($orderId <= 0) return [];
        try {
            $stmt = $pdo->prepare("
                SELECT
                    id,
                    room_no,
                    room_type_id,
                    COALESCE(room_id, 0) AS room_id,
                    COALESCE(adults, 0) AS adults,
                    COALESCE(children, 0) AS children,
                    COALESCE(extra_beds_required, 0) AS extra_beds_required,
                    COALESCE(cots_required, 0) AS cots_required,
                    COALESCE(wheelchair_room_required, 0) AS wheelchair_room_required,
                    COALESCE(interleading_required, 0) AS interleading_required,
                    COALESCE(room_setup_notes, '') AS room_setup_notes,
                    COALESCE(housekeeping_notes, '') AS housekeeping_notes,
                    COALESCE(special_occasion, '') AS special_occasion
                FROM order_rooming
                WHERE order_id = :order_id
                ORDER BY room_no ASC, id ASC
            ");
            $stmt->execute([':order_id' => $orderId]);
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) {
            return [];
        }

        $slots = [];
        foreach ($rows as $r) {
            if ((int)$r['room_type_id'] <= 0) continue;
            $slots[] = [
                'room_type_id'             => (int)$r['room_type_id'],
                'room_id'                  => (int)$r['room_id'],
                'rooming_id'               => (int)$r['id'],
                'room_no'                  => (int)$r['room_no'],
                'adults'                   => (int)$r['adults'],
                'children'                 => (int)$r['children'],
                'extra_beds_required'      => (int)$r['extra_beds_required'],
                'cots_required'            => (int)$r['cots_required'],
                'wheelchair_room_required' => (int)$r['wheelchair_room_required'],
                'interleading_required'    => (int)$r['interleading_required'],
                'room_setup_notes'         => (string)$r['room_setup_notes'],
                'housekeeping_notes'       => (string)$r['housekeeping_notes'],
                'special_occasion'         => (string)$r['special_occasion'],
            ];
        }
        return $slots;
    }
}

if (!function_exists('expand_room_rows_to_slots')) {
    function expand_room_rows_to_slots(array $roomRows): array
    {
        $slots = [];

        foreach ($roomRows as $row) {
            $roomTypeId = (int)($row['room_type_id'] ?? 0);
            $qtyRooms   = max(1, (int)($row['qty_rooms'] ?? 1));
            $adults     = max(0, (int)($row['adults'] ?? 0));
            $children   = max(0, (int)($row['children'] ?? 0));
            $extraBeds = max(0, (int)($row['extra_beds_required'] ?? 0));
            $cots = max(0, (int)($row['cots_required'] ?? 0));
            $wheelchairRequired = !empty($row['wheelchair_room_required']) ? 1 : 0;
            $interleadingRequired = !empty($row['interleading_required']) ? 1 : 0;
            $roomSetupNotes = trim((string)($row['room_setup_notes'] ?? ''));
            $housekeepingNotes = trim((string)($row['housekeeping_notes'] ?? ''));
            $specialOccasion = trim((string)($row['special_occasion'] ?? ''));

            $allocation = allocate_guests_to_rooms($adults, $children, $qtyRooms);

            foreach ($allocation as $alloc) {
                $slots[] = [
                    'room_type_id' => $roomTypeId,
                    'adults'       => (int)($alloc['adults'] ?? 0),
                    'children'     => (int)($alloc['children'] ?? 0),
                    'extra_beds_required' => $extraBeds,
                    'cots_required' => $cots,
                    'wheelchair_room_required' => $wheelchairRequired,
                    'interleading_required' => $interleadingRequired,
                    'room_setup_notes' => $roomSetupNotes,
                    'housekeeping_notes' => $housekeepingNotes,
                    'special_occasion' => $specialOccasion,
                ];
            }
        }

        return $slots;
    }
}

if (!function_exists('build_preview_rows_multi')) {
    function build_preview_rows_multi(PDO $pdo, array $roomRows, ?int $agentId, string $checkIn, string $checkOut, string $mealPlan = 'HB'): array
    {
        $rows = [];
        $inTs  = strtotime($checkIn);
        $outTs = strtotime($checkOut);

        if (!$inTs || !$outTs || $outTs <= $inTs || !$roomRows) {
            return $rows;
        }

        $mealPlan = normalize_meal_plan_for_rates($mealPlan);

        $expandedSlots = expand_room_rows_to_slots($roomRows);
        if (!$expandedSlots) {
            return $rows;
        }

        $cursor = $inTs;
        while ($cursor < $outTs) {
            $stayDate = date('Y-m-d', $cursor);

            $nightAdultTotal = 0.0;
            $nightChildTotal = 0.0;
            $nightTotal = 0.0;
            $roomBreakdown = [];
            $firstPlan = null;
            $missingRate = false;

            foreach ($expandedSlots as $idx => $slot) {
                $roomTypeId = (int)$slot['room_type_id'];
                $roomTypeMeta = fetch_room_type_meta($pdo, $roomTypeId);
                $roomDisplayLabel = room_slot_display_label(
                    (string)$roomTypeMeta['title'],
                    (int)$slot['adults'],
                    (int)$slot['children'],
                    (int)$roomTypeMeta['higher_occupancy']
                );
                $plan = fetch_rate_plan($pdo, $roomTypeId, $agentId, $stayDate, $mealPlan, count($expandedSlots));

                if (!$plan) {
                    $missingRate = true;
                    $roomBreakdown[] = [
                        'room_no'            => $idx + 1,
                        'room_type_id'       => $roomTypeId,
                        'room_type_title'    => (string)$roomTypeMeta['title'],
                        'higher_occupancy'   => (int)$roomTypeMeta['higher_occupancy'],
                        'display_label'      => $roomDisplayLabel,
                        'adults'             => (int)$slot['adults'],
                        'children'           => (int)$slot['children'],
                        'rate_type'          => $roomDisplayLabel !== '' ? $roomDisplayLabel : 'No rate plan',
                        'room_total'         => 0,
                        'rate_plan_title'    => 'No rate plan',
                        'rate_plan_currency' => '',
                        'rate_plan_id'       => 0,
                        'rate_plan_season'   => '',
                        'rate_plan_color'    => 'secondary',
                        'meal_plan'          => normalize_meal_plan_for_rates($mealPlan ?? 'HB'),
                        'sharing_rate'       => 0,
                        'single_rate'        => 0,
                        'single_supplement'  => 0,
                        'child_rate_percent' => 50,
                    ];
                    continue;
                }

                if ($firstPlan === null) {
                    $firstPlan = $plan;
                }

                $calc = rf_calculate_room_night_total(
                    (int)$slot['adults'],
                    (int)$slot['children'],
                    (float)$plan['sharing_rate'],
                    (float)$plan['single_rate'],
                    (float)$plan['single_supplement'],
                    (float)$plan['child_rate_percent']
                );

                $nightAdultTotal += $calc['adult_total'];
                $nightChildTotal += $calc['child_total'];
                $nightTotal += $calc['room_total'];

                $roomBreakdown[] = [
                    'room_no'             => $idx + 1,
                    'room_type_id'        => $roomTypeId,
                    'room_type_title'     => (string)$roomTypeMeta['title'],
                    'higher_occupancy'    => (int)$roomTypeMeta['higher_occupancy'],
                    'display_label'       => $roomDisplayLabel,
                    'adults'              => (int)$slot['adults'],
                    'children'            => (int)$slot['children'],
                    'rate_type'           => $roomDisplayLabel !== '' ? $roomDisplayLabel : $calc['rate_label'],
                    'room_total'          => round($calc['room_total'], 2),
                    'rate_plan_title'     => (string)$plan['title'],
                    'rate_plan_currency'  => (string)($plan['currency'] ?? ''),
                    'rate_plan_id'        => (int)$plan['id'],
                    'rate_plan_season'    => (string)($plan['season_label'] ?? ''),
                    'rate_plan_color'     => (string)($plan['season_color'] ?? 'secondary'),
                    'meal_plan'           => normalize_meal_plan_for_rates($plan['meal_plan'] ?? $mealPlan ?? 'HB'),
                    'sharing_rate'        => (float)$plan['sharing_rate'],
                    'single_rate'         => (float)$plan['single_rate'],
                    'single_supplement'   => (float)$plan['single_supplement'],
                    'child_rate_percent'  => (float)$plan['child_rate_percent'],
                    'rate_source'         => (string)($plan['rate_source'] ?? 'public'),
                ];
            }

            $rows[] = [
                'date'               => $stayDate,
                'rate_plan'          => $missingRate ? 'Mixed room booking (missing rate)' : 'Mixed room booking',
                'rate_plan_id'       => (int)($firstPlan['id'] ?? 0),
                'rate_plan_currency' => (string)($firstPlan['currency'] ?? ''),
                'rate_plan_season'   => (string)($firstPlan['season_label'] ?? ''),
                'rate_plan_color'    => (string)($firstPlan['season_color'] ?? 'secondary'),
                'meal_plan'          => normalize_meal_plan_for_rates($firstPlan['meal_plan'] ?? $mealPlan ?? 'HB'),
                'sharing_rate'       => (float)($firstPlan['sharing_rate'] ?? 0),
                'single_rate'        => (float)($firstPlan['single_rate'] ?? 0),
                'single_supplement'  => (float)($firstPlan['single_supplement'] ?? 0),
                'child_rate_percent' => (float)($firstPlan['child_rate_percent'] ?? 50),
                'adult_total'        => round($nightAdultTotal, 2),
                'child_total'        => round($nightChildTotal, 2),
                'night_total'        => round($nightTotal, 2),
                'applied_rate_type'  => $missingRate ? 'missing_rate' : 'multi_room_mix',
                'room_breakdown'     => $roomBreakdown,
            ];

            $cursor = strtotime('+1 day', $cursor);
        }

        return $rows;
    }
}

if (!function_exists('save_order_rooming_rows')) {
    function save_order_rooming_rows(PDO $pdo, int $orderId, int $propertyId, string $checkIn, string $checkOut, array $roomRows): void
    {
        /*
           Preserve existing physical room assignments across an edit.

           BUG THIS FIXES:
           Assignments used to be carried over by POSITION (old room_no -> new
           slot index). Adding or removing a room in the middle of a group
           booking shifts every later slot up or down, so slot 2 would inherit
           the room that belonged to slot 3, etc. When the shifted room's type
           no longer matched the slot, the id was zeroed - and because the rows
           had just been deleted and re-inserted with brand new ids, the
           availability check then saw the booking's own rooms as "taken by
           someone else" and refused to re-assign them. Net effect: room_id = 0
           and the booking dropped into the unassigned tray.

           Now assignments are preserved by IDENTITY: we pool the previously
           assigned rooms per room type and hand them back to slots of that same
           type. Rooms are therefore kept wherever they are still valid, no
           matter how the slot order shifts.
        */
        $oldRowsStmt = $pdo->prepare("
            SELECT room_no, room_id, room_type_id
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_no ASC, id ASC
        ");
        $oldRowsStmt->execute([':order_id' => $orderId]);
        $oldRows = $oldRowsStmt->fetchAll(PDO::FETCH_ASSOC);

        // Pool previously-assigned rooms by the type they were booked under.
        $roomsByType = [];   // [room_type_id => [room_id, room_id, ...]]
        $oldRoomBySlot = []; // [room_no => room_id]  (used only as a tie-breaker)
        foreach ($oldRows as $r) {
            $rid = (int)$r['room_id'];
            $rtid = (int)$r['room_type_id'];
            $rno = (int)$r['room_no'];
            if ($rid > 0) {
                if (!isset($roomsByType[$rtid])) {
                    $roomsByType[$rtid] = [];
                }
                $roomsByType[$rtid][] = $rid;
                $oldRoomBySlot[$rno] = $rid;
            }
        }

        $expandedSlots = expand_room_rows_to_slots($roomRows);

        /*
           Pass 1 - stable matching.
           For each new slot, claim a previously-assigned room of the SAME type.
           Prefer the room that already sat in this slot position (so an
           unchanged booking keeps its exact layout), otherwise take the next
           available room from that type's pool.
        */
        $slotRoomIds = [];
        foreach ($expandedSlots as $idx => $slot) {
            $roomNo = $idx + 1;
            $slotType = (int)$slot['room_type_id'];
            $slotRoomIds[$idx] = 0;

            if (empty($roomsByType[$slotType])) {
                continue;
            }

            // Preferred: the room that previously occupied this same slot number,
            // but only if it is of the right type and still unclaimed.
            $preferred = (int)($oldRoomBySlot[$roomNo] ?? 0);
            if ($preferred > 0) {
                $pos = array_search($preferred, $roomsByType[$slotType], true);
                if ($pos !== false) {
                    $slotRoomIds[$idx] = $preferred;
                    array_splice($roomsByType[$slotType], $pos, 1);
                    continue;
                }
            }

            // Otherwise take the next still-unclaimed room of this type.
            $slotRoomIds[$idx] = (int)array_shift($roomsByType[$slotType]);
        }

        $pdo->prepare("DELETE FROM order_rooming WHERE order_id = :order_id")
            ->execute([':order_id' => $orderId]);

        $insert = $pdo->prepare("
            INSERT INTO order_rooming
            (
                order_id,
                property_id,
                room_no,
                room_type_id,
                room_id,
                adults,
                children,
                extra_beds_required,
                cots_required,
                wheelchair_room_required,
                interleading_required,
                room_setup_notes,
                housekeeping_notes,
                special_occasion,
                check_in,
                check_out
            )
            VALUES
            (
                :order_id,
                :property_id,
                :room_no,
                :room_type_id,
                :room_id,
                :adults,
                :children,
                :extra_beds_required,
                :cots_required,
                :wheelchair_room_required,
                :interleading_required,
                :room_setup_notes,
                :housekeeping_notes,
                :special_occasion,
                :check_in,
                :check_out
            )
        ");

        foreach ($expandedSlots as $idx => $slot) {
            $roomNo = $idx + 1;

            $slotRoomTypeId = (int)$slot['room_type_id'];
            $keepRoomId = (int)($slotRoomIds[$idx] ?? 0);

            /*
               Final safety net: never carry a physical room whose type no longer
               matches the slot. Pass 1 already matches on type, so this should
               not normally fire - it only catches a room whose type was changed
               on the rooms master since the booking was made.
            */
            if ($keepRoomId > 0 && !room_matches_requested_type($pdo, $keepRoomId, $propertyId, $slotRoomTypeId)) {
                $keepRoomId = 0;
            }

            $insert->execute([
                ':order_id'     => $orderId,
                ':property_id'  => $propertyId,
                ':room_no'      => $roomNo,
                ':room_type_id' => $slotRoomTypeId,
                ':room_id'      => $keepRoomId,
                ':adults'       => (int)$slot['adults'],
                ':children'     => (int)$slot['children'],
                ':extra_beds_required' => (int)($slot['extra_beds_required'] ?? 0),
                ':cots_required' => (int)($slot['cots_required'] ?? 0),
                ':wheelchair_room_required' => !empty($slot['wheelchair_room_required']) ? 1 : 0,
                ':interleading_required' => !empty($slot['interleading_required']) ? 1 : 0,
                ':room_setup_notes' => trim((string)($slot['room_setup_notes'] ?? '')) !== '' ? trim((string)$slot['room_setup_notes']) : null,
                ':housekeeping_notes' => trim((string)($slot['housekeeping_notes'] ?? '')) !== '' ? trim((string)$slot['housekeeping_notes']) : null,
                ':special_occasion' => trim((string)($slot['special_occasion'] ?? '')) !== '' ? trim((string)$slot['special_occasion']) : null,
                ':check_in'     => $checkIn,
                ':check_out'    => $checkOut,
            ]);
        }

        sync_order_header_room($pdo, $orderId);
    }
}

if (!function_exists('rebuild_order_nightly_prices_multi')) {
    function rebuild_order_nightly_prices_multi(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("
            SELECT id, property_id, check_in, check_out, agent_id, meal_plan
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$order) {
            throw new RuntimeException('Order not found');
        }

        $mealPlan = normalize_meal_plan_for_rates($order['meal_plan'] ?? 'HB');

        $roomRows = fetch_order_room_rows($pdo, $orderId);
        if (!$roomRows) {
            throw new RuntimeException('No room rows found for order');
        }

        /*
           Price from order_rooming DIRECTLY where it has rows.

           expand_room_rows_to_slots() rebuilds slots from the requested room
           MIX (room type + qty + total pax) and re-allocates guests across
           them. That is right when a booking is being quoted and no rooming
           rows exist yet - but for a saved booking it is a round trip that
           throws away what order_rooming already knows: which PHYSICAL room
           each slot has, and exactly how many adults and children are in it.

           Pricing from the mix left room_id = 0 on every priced row, so the
           proforma could not name a room and fell back to "Room 1 of 6". It
           could also re-allocate pax differently from the rooming list, which
           would price a room the guest is not actually in.
        */
        $expandedSlots = rf_price_slots_from_rooming($pdo, $orderId);
        if (!$expandedSlots) {
            $expandedSlots = expand_room_rows_to_slots($roomRows);
        }
        if (!$expandedSlots) {
            throw new RuntimeException('No room slots found for order');
        }

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

        if (!$checkIn || !$checkOut || $checkOut <= $checkIn) {
            throw new RuntimeException('Invalid stay dates');
        }

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

        /*
           room_type_id is written when the column exists. proforma_invoice.php
           reads rop.room_type_id when present, and on an unassigned booking
           (room_id = 0) it is the only thing saying WHICH room type a priced
           row belongs to. Guarded rather than assumed: this file has to run on
           installs that predate the column.
        */
        $ropHasRoomTypeId = rf_column_exists($pdo, 'rel_orders_prices', 'room_type_id');

        $ropCols = ['order_id', 'property_id', 'date', 'price', 'additional_person',
            'additional_person_price', 'total', 'room_id', 'rate_plan_id', 'rate_plan_title',
            'rate_plan_currency', 'rate_plan_season_label', 'rate_plan_season_color',
            'adults_count', 'kids_count', 'sharing_rate', 'single_rate', 'single_supplement',
            'child_rate_percent', 'adult_total', 'child_total', 'applied_rate_type'];
        if ($ropHasRoomTypeId) {
            $ropCols[] = 'room_type_id';
        }
        $ropPlaceholders = array_map(function ($c) { return ':' . $c; }, $ropCols);

        $insert = $pdo->prepare(
            'INSERT INTO rel_orders_prices (' . implode(', ', $ropCols) . ')'
            . ' VALUES (' . implode(', ', $ropPlaceholders) . ')'
        );

        $totalAdults = 0;
        $totalKids = 0;
        foreach ($roomRows as $roomRow) {
            $totalAdults += (int)$roomRow['adults'];
            $totalKids   += (int)$roomRow['children'];
        }

        $cursor = $checkIn;
        $grandTotal = 0.0;
        $nightCount = 0;

        while ($cursor < $checkOut) {
            $stayDate = date('Y-m-d', $cursor);

            $nightAdultTotal = 0.0;
            $nightChildTotal = 0.0;
            $nightTotal = 0.0;
            $firstPlan = null;

            /*
               ONE ROW PER ROOM PER NIGHT.

               BUG THIS FIXES:
               The INSERT used to sit OUTSIDE this loop. Every room's rate was
               resolved and priced correctly, then summed into $nightTotal and
               written away as a SINGLE row per night: room_id = 0,
               rate_plan_title = 'Mixed room booking', applied_rate_type =
               'multi_room_mix', and every per-room column ($sharing_rate,
               $single_rate, $adults_count...) taken from $firstPlan - i.e. the
               FIRST room's plan, regardless of what the other rooms resolved to.

               The per-room detail the Nightly Pricing Preview shows on screen
               was therefore never persisted. Downstream, proforma_invoice.php
               found one row where there were six rooms, hit its "Case 2 - the
               night is collapsed into fewer rows than there are rooms" branch,
               and DIVIDED the night evenly to itemise it. A booking of five
               rooms at 292.00 sharing plus one single at 205.00 invoiced as six
               identical lines of 277.50. The total was right; every line was
               wrong, and the guest paying for the single was billed as though
               they were sharing.

               rel_orders_prices already had room_id, adults_count, kids_count,
               sharing_rate, single_rate, applied_rate_type and adult_total -
               the table was built for per-room rows all along. Writing one row
               per slot fills them, and the proforma's "Case 1 - the accurate
               path" takes over on its own with no change needed there.
            */
            foreach ($expandedSlots as $slotIdx => $slot) {
                $plan = fetch_rate_plan(
                    $pdo,
                    (int)$slot['room_type_id'],
                    !empty($order['agent_id']) ? (int)$order['agent_id'] : null,
                    $stayDate,
                    $mealPlan,
                    count($expandedSlots)
                );

                if (!$plan) {
                    // No active rate plan for this room type/night: hold at zero
                    // instead of blocking the booking. Staff can price it later.
                    $plan = rf_zero_rate_plan($mealPlan);
                }

                if ($firstPlan === null) {
                    $firstPlan = $plan;
                }

                $calc = rf_calculate_room_night_total(
                    (int)$slot['adults'],
                    (int)$slot['children'],
                    (float)$plan['sharing_rate'],
                    (float)$plan['single_rate'],
                    (float)$plan['single_supplement'],
                    (float)$plan['child_rate_percent']
                );

                $nightAdultTotal += $calc['adult_total'];
                $nightChildTotal += $calc['child_total'];
                $nightTotal += $calc['room_total'];

                /*
                   The physical room for this slot, where one is assigned. It is
                   what lets the proforma print "Room 19 Family Room" instead of
                   "Room 4 of 6". Zero is fine - the booking may still be in the
                   unassigned tray.
                */
                $slotRoomId = (int)($slot['room_id'] ?? 0);

                /*
                   'price' is the per-adult rate this room was actually priced
                   on - the SINGLE rate where single occupancy applied, the
                   sharing rate otherwise. It used to always be sharing_rate,
                   so a single room reported 146.00 in `price` while `total`
                   correctly said 205.00. total and applied_rate_type were
                   right, so nothing downstream broke, but any report reading
                   `price` directly misstated that room.
                */
                $appliedType = (string)($calc['applied_rate_type'] ?? '');
                $rowPrice = (float)($plan['sharing_rate'] ?? 0);
                if ($appliedType === 'single_rate' || $appliedType === 'child_single_rate') {
                    $rowPrice = (float)($plan['single_rate'] ?? 0);
                }

                $insParams = [
                    ':order_id'                 => $orderId,
                    ':property_id'              => (int)$order['property_id'],
                    ':date'                     => $stayDate,
                    ':price'                    => $rowPrice,
                    ':additional_person'        => (int)$slot['children'],
                    ':additional_person_price'  => round($calc['child_total'], 2),
                    ':total'                    => round($calc['room_total'], 2),
                    ':room_id'                  => $slotRoomId,
                    ':rate_plan_id'             => (int)($plan['id'] ?? 0),
                    // The room's OWN plan, not a 'Mixed room booking' placeholder.
                    ':rate_plan_title'          => (string)($plan['title'] ?? ''),
                    ':rate_plan_currency'       => (string)($plan['currency'] ?? ''),
                    ':rate_plan_season_label'   => (string)($plan['season_label'] ?? ''),
                    ':rate_plan_season_color'   => (string)($plan['season_color'] ?? 'secondary'),
                    ':adults_count'             => (int)$slot['adults'],
                    ':kids_count'               => (int)$slot['children'],
                    ':sharing_rate'             => (float)($plan['sharing_rate'] ?? 0),
                    ':single_rate'              => (float)($plan['single_rate'] ?? 0),
                    ':single_supplement'        => (float)($plan['single_supplement'] ?? 0),
                    ':child_rate_percent'       => (float)($plan['child_rate_percent'] ?? 50),
                    ':adult_total'              => round($calc['adult_total'], 2),
                    ':child_total'              => round($calc['child_total'], 2),
                    // 'sharing' / 'single_rate' / 'child_single_rate' etc - the
                    // real occupancy rule this room was priced on.
                    ':applied_rate_type'        => $appliedType,
                ];
                if ($ropHasRoomTypeId) {
                    $insParams[':room_type_id'] = (int)$slot['room_type_id'];
                }
                $insert->execute($insParams);
            }

            $grandTotal += $nightTotal;
            $nightCount++;
            $cursor = strtotime('+1 day', $cursor);
        }

        $upd = $pdo->prepare("
            UPDATE orders
            SET nights = :nights,
                base_price = :base_price,
                amount = :amount,
                totalamount = :totalamount
            WHERE id = :id
        ");
        $upd->execute([
            ':nights'      => $nightCount,
            ':base_price'  => round($grandTotal, 2),
            ':amount'      => round($grandTotal, 2),
            ':totalamount' => round($grandTotal, 2),
            ':id'          => $orderId,
        ]);

        return [
            'nights' => $nightCount,
            'total'  => round($grandTotal, 2),
        ];
    }
}

if (!function_exists('rf_column_exists')) {
    function rf_column_exists(PDO $pdo, string $table, string $column): bool
    {
        static $cache = [];
        $key = $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('rf_pick_first_existing_column')) {
    function rf_pick_first_existing_column(PDO $pdo, string $table, array $candidates): ?string
    {
        foreach ($candidates as $candidate) {
            if (rf_column_exists($pdo, $table, $candidate)) {
                return $candidate;
            }
        }
        return null;
    }
}

if (!function_exists('rf_fetch_agent_contacts')) {
    function rf_fetch_agent_contacts(PDO $pdo, int $agentId): array
    {
        if ($agentId <= 0) {
            return [];
        }

        /*
           Exact schema confirmed:
           agents.id -> consultants.agent_id
           No fallback to all consultants. If this returns empty, the selected agent genuinely has no linked contact.
        */
        $stmt = $pdo->prepare("
            SELECT
                id,
                firstname,
                lastname,
                email,
                mobile,
                phone
            FROM consultants
            WHERE agent_id = :agent_id
            ORDER BY firstname ASC, lastname ASC, id ASC
        ");
        $stmt->execute([':agent_id' => $agentId]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }
}

if (!function_exists('rf_current_user_id')) {
    function rf_current_user_id(): int
    {
        foreach (['user_id', 'consultant_id', 'login_user_id', 'id'] as $key) {
            if (isset($_SESSION[$key]) && (int)$_SESSION[$key] > 0) {
                return (int)$_SESSION[$key];
            }
        }
        return 0;
    }
}

if (!function_exists('rf_ensure_order_audit_schema')) {
    function rf_ensure_order_audit_schema(PDO $pdo): void
    {
        $orderColumns = [
            'created_by' => "ALTER TABLE orders ADD COLUMN created_by INT NULL AFTER ordered_on",
            'updated_by' => "ALTER TABLE orders ADD COLUMN updated_by INT NULL AFTER updated_on",
            'updated_at' => "ALTER TABLE orders ADD COLUMN updated_at DATETIME NULL AFTER updated_by",
        ];

        foreach ($orderColumns as $column => $ddl) {
            if (!rf_column_exists($pdo, 'orders', $column)) {
                try { $pdo->exec($ddl); } catch (Throwable $e) { /* ignore if host disallows DDL */ }
            }
        }

        try {
            $pdo->exec("
                CREATE TABLE IF NOT EXISTS order_update_logs (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    order_id INT NOT NULL,
                    user_id INT NULL,
                    action VARCHAR(50) NOT NULL,
                    notes TEXT NULL,
                    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    INDEX idx_order_update_logs_order_id (order_id),
                    INDEX idx_order_update_logs_user_id (user_id),
                    INDEX idx_order_update_logs_created_at (created_at)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            ");
        } catch (Throwable $e) { /* ignore if host disallows DDL */ }
    }
}


if (!function_exists('rf_ensure_booking_colour_schema')) {
    function rf_ensure_booking_colour_schema(PDO $pdo): void
    {
        $orderColumns = [
            'invoiced' => "ALTER TABLE orders ADD COLUMN invoiced TINYINT(1) NOT NULL DEFAULT 0 AFTER payment_status",
            'booking_source' => "ALTER TABLE orders ADD COLUMN booking_source VARCHAR(80) NULL AFTER invoiced",
            'payment_due' => "ALTER TABLE orders ADD COLUMN payment_due VARCHAR(80) NULL AFTER booking_source",
            'invoice_number' => "ALTER TABLE orders ADD COLUMN invoice_number VARCHAR(120) NULL AFTER payment_due",
            'qrCodeUrl' => "ALTER TABLE orders ADD COLUMN qrCodeUrl TEXT NULL AFTER invoice_number",
            'verificationCode' => "ALTER TABLE orders ADD COLUMN verificationCode VARCHAR(120) NULL AFTER qrCodeUrl",
            'fiscalDeviceId' => "ALTER TABLE orders ADD COLUMN fiscalDeviceId VARCHAR(120) NULL AFTER verificationCode",
            'globalNumber' => "ALTER TABLE orders ADD COLUMN globalNumber VARCHAR(120) NULL AFTER fiscalDeviceId",
            'fiscalDayNumber' => "ALTER TABLE orders ADD COLUMN fiscalDayNumber VARCHAR(120) NULL AFTER globalNumber",
            'booking_color_code_id' => "ALTER TABLE orders ADD COLUMN booking_color_code_id INT NULL AFTER fiscalDayNumber",
            'expected_arrival_time' => "ALTER TABLE orders ADD COLUMN expected_arrival_time TIME NULL DEFAULT '14:00:00' AFTER check_in",
            'group_name' => "ALTER TABLE orders ADD COLUMN group_name VARCHAR(180) NULL AFTER originating_agent",
            'group_leader' => "ALTER TABLE orders ADD COLUMN group_leader VARCHAR(180) NULL AFTER group_name",
            'meal_plan' => "ALTER TABLE orders ADD COLUMN meal_plan VARCHAR(20) NULL AFTER group_leader",
            'block_room_posting' => "ALTER TABLE orders ADD COLUMN block_room_posting TINYINT(1) NOT NULL DEFAULT 0 AFTER meal_plan",
        ];

        foreach ($orderColumns as $column => $ddl) {
            if (!rf_column_exists($pdo, 'orders', $column)) {
                try { $pdo->exec($ddl); } catch (Throwable $e) { /* keep page usable if DDL is restricted */ }
            }
        }

        /*
           payment_status already exists in many Pure Africa PMS installs as an INT:
           0 unpaid, 1 part-paid, 2 paid. We keep that compatible and use
           payment_due / booking_source / invoiced for operational colour logic.
        */
        if (!rf_column_exists($pdo, 'orders', 'payment_status')) {
            try { $pdo->exec("ALTER TABLE orders ADD COLUMN payment_status INT NOT NULL DEFAULT 0"); } catch (Throwable $e) {}
        }

        try {
            $pdo->exec("
                CREATE TABLE IF NOT EXISTS booking_color_codes (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    status_key VARCHAR(80) NOT NULL UNIQUE,
                    status_label VARCHAR(120) NOT NULL,
                    hex_color VARCHAR(20) NOT NULL,
                    text_color VARCHAR(20) NOT NULL DEFAULT '#111827',
                    css_class VARCHAR(120) NULL,
                    priority INT NOT NULL DEFAULT 100,
                    applies_when VARCHAR(255) NULL,
                    is_active TINYINT(1) NOT NULL DEFAULT 1,
                    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
                    INDEX idx_booking_color_active_priority (is_active, priority)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            ");

            $seed = [
                ['active_out_balance', 'Active Out - Balance', '#DC2626', '#FFFFFF', 'status-active-out', 10, 'Checked out or active out with balance outstanding'],
                ['chase_payment', 'Chase Payment', '#EC4899', '#FFFFFF', 'status-chase-payment', 20, 'Payment needs chasing'],
                ['pay_on_arrival', 'Pay On Arrival', '#8B5CF6', '#FFFFFF', 'status-pay-arrival', 30, 'Payment due on arrival'],
                ['checked_in', 'Checked In', '#22C55E', '#FFFFFF', 'status-checkedin', 40, 'Guest checked in'],
                ['online_payment', 'Online Payment', '#F97316', '#FFFFFF', 'status-online-payment', 50, 'Booking/payment originated online'],
                ['confirmed', 'Confirmed', '#FACC15', '#111827', 'status-confirmed', 60, 'Confirmed booking'],
                ['invoiced', 'Invoiced', '#FDBA74', '#111827', 'status-invoiced', 70, 'Invoice issued'],
                ['provisional', 'Provisional', '#9CA3AF', '#FFFFFF', 'status-provisional', 80, 'Provisional booking'],
                ['checked_out_paid', 'Checked Out - Paid', '#3B82F6', '#FFFFFF', 'status-checkedout', 90, 'Checked out and fully paid'],
                ['cancelled', 'Cancelled', '#6B7280', '#FFFFFF', 'status-cancelled', 100, 'Cancelled booking'],
                ['no_show', 'No Show', '#92400E', '#FFFFFF', 'status-no-show', 110, 'No-show booking'],
            ];

            $stmt = $pdo->prepare("
                INSERT INTO booking_color_codes
                    (status_key, status_label, hex_color, text_color, css_class, priority, applies_when, is_active)
                VALUES
                    (:status_key, :status_label, :hex_color, :text_color, :css_class, :priority, :applies_when, 1)
                ON DUPLICATE KEY UPDATE
                    status_label = VALUES(status_label),
                    hex_color = VALUES(hex_color),
                    text_color = VALUES(text_color),
                    css_class = VALUES(css_class),
                    priority = VALUES(priority),
                    applies_when = VALUES(applies_when),
                    is_active = 1
            ");

            foreach ($seed as $row) {
                $stmt->execute([
                    ':status_key' => $row[0],
                    ':status_label' => $row[1],
                    ':hex_color' => $row[2],
                    ':text_color' => $row[3],
                    ':css_class' => $row[4],
                    ':priority' => $row[5],
                    ':applies_when' => $row[6],
                ]);
            }
        } catch (Throwable $e) {
            /* Colour table should never block reservation saving. */
        }
    }
}

if (!function_exists('rf_determine_booking_colour_key')) {
    function rf_determine_booking_colour_key(array $form): string
    {
        $paymentDue = strtolower(trim((string)($form['payment_due'] ?? '')));
        $bookingSource = strtolower(trim((string)($form['booking_source'] ?? '')));
        $paymentStatus = (string)($form['payment_status'] ?? '0');
        $invoiced = !empty($form['invoiced']);

        if ($paymentDue === 'active_out_balance') return 'active_out_balance';
        if ($paymentDue === 'chase_payment') return 'chase_payment';
        if ($paymentDue === 'pay_on_arrival') return 'pay_on_arrival';
        if ($bookingSource === 'online' || $bookingSource === 'ota' || $paymentDue === 'online_payment') return 'online_payment';
        if ($invoiced) return 'invoiced';
        if ($paymentStatus === '2' || $paymentStatus === 'paid') return 'checked_out_paid';
        if ($paymentDue === 'account') return 'confirmed';

        return 'provisional';
    }
}

if (!function_exists('rf_booking_colour_id')) {
    function rf_booking_colour_id(PDO $pdo, string $statusKey): ?int
    {
        try {
            $stmt = $pdo->prepare("SELECT id FROM booking_color_codes WHERE status_key = :status_key AND is_active = 1 LIMIT 1");
            $stmt->execute([':status_key' => $statusKey]);
            $id = (int)($stmt->fetchColumn() ?: 0);
            return $id > 0 ? $id : null;
        } catch (Throwable $e) {
            return null;
        }
    }
}

if (!function_exists('rf_save_booking_tracking_fields')) {
    function rf_save_booking_tracking_fields(PDO $pdo, int $orderId, array $form, int $userId = 0): void
    {
        if ($orderId <= 0) {
            return;
        }

        $statusKey = rf_determine_booking_colour_key($form);
        $colourId = rf_booking_colour_id($pdo, $statusKey);

        $fields = [
            'invoiced' => !empty($form['invoiced']) ? 1 : 0,
            'booking_source' => trim((string)($form['booking_source'] ?? '')) ?: null,
            'group_name' => trim((string)($form['group_name'] ?? '')) ?: null,
            'group_leader' => trim((string)($form['group_leader'] ?? '')) ?: null,
            'meal_plan' => in_array((string)($form['meal_plan'] ?? ''), ['BB', 'HB', 'FB'], true) ? (string)$form['meal_plan'] : null,
            'block_room_posting' => !empty($form['block_room_posting']) ? 1 : 0,
            'payment_due' => trim((string)($form['payment_due'] ?? '')) ?: null,
            'invoice_number' => trim((string)($form['invoice_number'] ?? '')) ?: null,
            'qrCodeUrl' => trim((string)($form['qrCodeUrl'] ?? '')) ?: null,
            'verificationCode' => trim((string)($form['verificationCode'] ?? '')) ?: null,
            'fiscalDeviceId' => trim((string)($form['fiscalDeviceId'] ?? '')) ?: null,
            'globalNumber' => trim((string)($form['globalNumber'] ?? '')) ?: null,
            'fiscalDayNumber' => trim((string)($form['fiscalDayNumber'] ?? '')) ?: null,
            'booking_color_code_id' => $colourId,
        ];

        /*
           Keep existing numeric PMS payment_status compatible:
           0 = unpaid, 1 = part paid, 2 = paid.
        */
        if (array_key_exists('payment_status', $form)) {
            $ps = (string)$form['payment_status'];
            if (in_array($ps, ['0', '1', '2'], true)) {
                $fields['payment_status'] = (int)$ps;
            }
        }

        $sets = [];
        $params = [':id' => $orderId];
        foreach ($fields as $column => $value) {
            if (rf_column_exists($pdo, 'orders', $column)) {
                $sets[] = $column . ' = :' . $column;
                $params[':' . $column] = $value;
            }
        }

        if (!$sets) {
            return;
        }

        if (rf_column_exists($pdo, 'orders', 'updated_by')) {
            $sets[] = 'updated_by = :tracking_updated_by';
            $params[':tracking_updated_by'] = $userId > 0 ? $userId : null;
        }
        if (rf_column_exists($pdo, 'orders', 'updated_at')) {
            $sets[] = 'updated_at = NOW()';
        }

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

if (!function_exists('rf_log_order_action')) {
    function rf_log_order_action(PDO $pdo, int $orderId, int $userId, string $action, string $notes = ''): void
    {
        if ($orderId <= 0) {
            return;
        }

        try {
            $stmt = $pdo->prepare("
                INSERT INTO order_update_logs (order_id, user_id, action, notes, created_at)
                VALUES (:order_id, :user_id, :action, :notes, NOW())
            ");
            $stmt->execute([
                ':order_id' => $orderId,
                ':user_id'  => $userId > 0 ? $userId : null,
                ':action'   => $action,
                ':notes'    => $notes !== '' ? $notes : null,
            ]);
        } catch (Throwable $e) {
            /* Audit log should never block reservation saving. */
        }
    }
}

if (!function_exists('rf_get_or_create_master_folio')) {
    function rf_get_or_create_master_folio(PDO $pdo, int $orderId): int
    {
        $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_type = 'master' LIMIT 1");
        $stmt->execute([':order_id' => $orderId]);
        $id = (int)($stmt->fetchColumn() ?: 0);
        if ($id > 0) return $id;

        $stmt = $pdo->prepare("SELECT property_id, order_no FROM orders WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $o = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$o) throw new RuntimeException('Booking not found while creating master folio.');

        $stmt = $pdo->prepare("INSERT INTO folios (property_id, order_id, folio_no, folio_name, folio_type, status, total_charges, total_payments, balance) VALUES (:property_id, :order_id, :folio_no, :folio_name, 'master', 1, 0, 0, 0)");
        $stmt->execute([
            ':property_id' => (int)($o['property_id'] ?? 0),
            ':order_id' => $orderId,
            ':folio_no' => 'FOL-M-' . $orderId,
            ':folio_name' => 'Master Folio - ' . ($o['order_no'] ?: ('Booking #' . $orderId)),
        ]);
        return (int)$pdo->lastInsertId();
    }
}

if (!function_exists('rf_fetch_booking_room_folio_targets')) {
    /*
       Return one folio target per allocated room for a booking.

       IMPORTANT: rooming_id is the primary identity. Older versions used only
       room_id / room_no and created generic folios such as FOL-R-{order}-{slot}.
       When check-in later created a rooming-linked folio, those legacy rows were
       not recognised and duplicates were inserted. This routine now reuses and
       upgrades an existing folio in this order:
         1. rooming_id (or legacy order_rooming_id)
         2. existing folio_no
         3. order_id + physical room_id
         4. order_id + legacy room_no/slot
       Only when none exists is a new room folio inserted.
    */
    function rf_fetch_booking_room_folio_targets(PDO $pdo, int $orderId, int $propertyId): array
    {
        if ($orderId <= 0) {
            return [];
        }

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

        $hasRoomingId      = rf_column_exists($pdo, 'folios', 'rooming_id');
        $hasOldRoomingId   = rf_column_exists($pdo, 'folios', 'order_rooming_id');
        $hasRoomId         = rf_column_exists($pdo, 'folios', 'room_id');
        $hasRoomNo         = rf_column_exists($pdo, 'folios', 'room_no');
        $hasFolioType      = rf_column_exists($pdo, 'folios', 'folio_type');
        $hasUpdatedAt      = rf_column_exists($pdo, 'folios', 'updated_at');

        $typeSql = $hasFolioType ? " AND folio_type = 'room' " : '';
        $targets = [];

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

            // Keep reservation-time numbering stable. Check-in will rename it
            // to the physical-room format once an actual room is assigned.
            $folioNo = 'FOL-R-' . $orderId . '-' . $roomNo;
            $folioId = 0;

            // 1) Preferred identity: current rooming row.
            if ($hasRoomingId && $roomingId > 0) {
                $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND rooming_id = :rooming_id {$typeSql} ORDER BY id ASC LIMIT 1");
                $sel->execute([':order_id' => $orderId, ':rooming_id' => $roomingId]);
                $folioId = (int)($sel->fetchColumn() ?: 0);
            }
            if ($folioId <= 0 && $hasOldRoomingId && $roomingId > 0) {
                $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND order_rooming_id = :rooming_id {$typeSql} ORDER BY id ASC LIMIT 1");
                $sel->execute([':order_id' => $orderId, ':rooming_id' => $roomingId]);
                $folioId = (int)($sel->fetchColumn() ?: 0);
            }

            // 2) Same reservation-slot folio number.
            if ($folioId <= 0) {
                $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_no = :folio_no {$typeSql} ORDER BY id ASC LIMIT 1");
                $sel->execute([':order_id' => $orderId, ':folio_no' => $folioNo]);
                $folioId = (int)($sel->fetchColumn() ?: 0);
            }

            // 3) Legacy generic folio tied only to the physical room.
            if ($folioId <= 0 && $hasRoomId && $roomId > 0) {
                $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND room_id = :room_id {$typeSql} ORDER BY CASE WHEN rooming_id IS NULL THEN 0 ELSE 1 END, id ASC LIMIT 1");
                $sel->execute([':order_id' => $orderId, ':room_id' => $roomId]);
                $folioId = (int)($sel->fetchColumn() ?: 0);
            }

            // 4) Very old schemas linked by room_no / reservation slot.
            if ($folioId <= 0 && $hasRoomNo && $roomNo > 0) {
                $sel = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND room_no = :room_no {$typeSql} ORDER BY id ASC LIMIT 1");
                $sel->execute([':order_id' => $orderId, ':room_no' => $roomNo]);
                $folioId = (int)($sel->fetchColumn() ?: 0);
            }

            if ($folioId > 0) {
                $sets = [
                    'property_id = :property_id',
                    'order_id = :order_id',
                    'folio_name = :folio_name',
                    'status = 1',
                ];
                $params = [
                    ':property_id' => $propertyId,
                    ':order_id'    => $orderId,
                    ':folio_name'  => $roomLabel . ' Folio',
                    ':id'          => $folioId,
                ];

                if ($hasRoomingId) {
                    $sets[] = 'rooming_id = :rooming_id';
                    $params[':rooming_id'] = $roomingId > 0 ? $roomingId : null;
                }
                if ($hasOldRoomingId) {
                    $sets[] = 'order_rooming_id = :old_rooming_id';
                    $params[':old_rooming_id'] = $roomingId > 0 ? $roomingId : null;
                }
                if ($hasRoomId) {
                    $sets[] = 'room_id = :room_id';
                    $params[':room_id'] = $roomId > 0 ? $roomId : null;
                }
                if ($hasRoomNo) {
                    $sets[] = 'room_no = :room_no';
                    $params[':room_no'] = $roomNo > 0 ? $roomNo : null;
                }
                if ($hasFolioType) {
                    $sets[] = "folio_type = 'room'";
                }
                if ($hasUpdatedAt) {
                    $sets[] = 'updated_at = NOW()';
                }

                $pdo->prepare('UPDATE folios SET ' . implode(', ', $sets) . ' WHERE id = :id')->execute($params);
            } else {
                $cols = ['property_id', 'order_id', 'folio_no', 'folio_name', 'status', 'total_charges', 'total_payments', 'balance'];
                $vals = [':property_id', ':order_id', ':folio_no', ':folio_name', '1', '0', '0', '0'];
                $params = [
                    ':property_id' => $propertyId,
                    ':order_id'    => $orderId,
                    ':folio_no'    => $folioNo,
                    ':folio_name'  => $roomLabel . ' Folio',
                ];

                if ($hasFolioType) {
                    $cols[] = 'folio_type';
                    $vals[] = "'room'";
                }
                if ($hasRoomingId) {
                    $cols[] = 'rooming_id';
                    $vals[] = ':rooming_id';
                    $params[':rooming_id'] = $roomingId > 0 ? $roomingId : null;
                } elseif ($hasOldRoomingId) {
                    $cols[] = 'order_rooming_id';
                    $vals[] = ':rooming_id';
                    $params[':rooming_id'] = $roomingId > 0 ? $roomingId : null;
                }
                if ($hasRoomId) {
                    $cols[] = 'room_id';
                    $vals[] = ':room_id';
                    $params[':room_id'] = $roomId > 0 ? $roomId : null;
                }
                if ($hasRoomNo) {
                    $cols[] = 'room_no';
                    $vals[] = ':room_no';
                    $params[':room_no'] = $roomNo > 0 ? $roomNo : null;
                }

                $insSql = 'INSERT INTO folios (' . implode(', ', $cols) . ') VALUES (' . implode(', ', $vals) . ')';
                try {
                    $pdo->prepare($insSql)->execute($params);
                    $folioId = (int)$pdo->lastInsertId();
                } catch (PDOException $e) {
                    if ((string)$e->getCode() !== '23000') {
                        throw $e;
                    }
                    $again = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_no = :folio_no LIMIT 1");
                    $again->execute([':order_id' => $orderId, ':folio_no' => $folioNo]);
                    $folioId = (int)($again->fetchColumn() ?: 0);
                    if ($folioId <= 0) {
                        throw $e;
                    }
                }
            }

            $targets[] = [
                'folio_id'   => $folioId,
                'room_no'    => $roomNo,
                'room_id'    => $roomId,
                'rooming_id' => $roomingId,
                'label'      => $roomLabel,
            ];
        }

        return $targets;
    }
}

if (!function_exists('rf_recalculate_folio_totals')) {
    function rf_recalculate_folio_totals(PDO $pdo, int $folioId): void
    {
        /*
           Recalculate the folio exactly from folio_transactions.
           Payment/void/reversal rows must not inflate charges.
        */
        $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'
                             )
                             AND LOWER(COALESCE(source_table, '')) NOT IN (
                                'payment', 'payments', 'receipt', 'receipts'
                             )
                        THEN COALESCE(debit, 0)
                        ELSE 0
                    END
                ), 0) AS total_charges,

                GREATEST(
                    0,
                    COALESCE(SUM(
                        CASE
                            WHEN COALESCE(is_voided, 0) = 0
                                 AND (
                                    LOWER(COALESCE(tx_type, '')) IN (
                                        'payment', 'receipt', 'deposit', 'payment_received',
                                        'folio_payment', 'guest_payment'
                                    )
                                    OR LOWER(COALESCE(source_table, '')) IN (
                                        'payment', 'payments', 'receipt', 'receipts'
                                    )
                                 )
                                 AND COALESCE(credit, 0) > 0
                            THEN COALESCE(credit, 0)

                            WHEN COALESCE(is_voided, 0) = 0
                                 AND (
                                    LOWER(COALESCE(tx_type, '')) IN (
                                        'payment_void', 'void_payment', 'payment_reversal',
                                        'reversal', 'refund', 'payment_refund'
                                    )
                                    OR LOWER(COALESCE(description, '')) LIKE '%void%'
                                    OR LOWER(COALESCE(description, '')) LIKE '%reversal%'
                                    OR LOWER(COALESCE(description, '')) LIKE '%refund%'
                                 )
                                 AND COALESCE(debit, 0) > 0
                            THEN -1 * COALESCE(debit, 0)

                            WHEN COALESCE(is_voided, 0) = 0
                                 AND (
                                    LOWER(COALESCE(tx_type, '')) IN (
                                        'payment_void', 'void_payment', 'payment_reversal',
                                        'reversal', 'refund', 'payment_refund'
                                    )
                                    OR LOWER(COALESCE(description, '')) LIKE '%void%'
                                    OR LOWER(COALESCE(description, '')) LIKE '%reversal%'
                                    OR LOWER(COALESCE(description, '')) LIKE '%refund%'
                                 )
                                 AND COALESCE(credit, 0) < 0
                            THEN COALESCE(credit, 0)

                            ELSE 0
                        END
                    ), 0)
                ) AS total_payments
            FROM folio_transactions
            WHERE folio_id = :folio_id
        ");
        $stmt->execute([':folio_id' => $folioId]);
        $t = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $charges  = round((float)($t['total_charges'] ?? 0), 2);
        $payments = round((float)($t['total_payments'] ?? 0), 2);
        $balance  = round($charges - $payments, 2);

        $stmt = $pdo->prepare("
            UPDATE folios
            SET total_charges = :charges,
                total_payments = :payments,
                balance = :balance,
                updated_at = NOW()
            WHERE id = :id
        ");
        $stmt->execute([
            ':charges'  => $charges,
            ':payments' => $payments,
            ':balance'  => $balance,
            ':id'       => $folioId,
        ]);
    }
}

if (!function_exists('rf_sync_order_payment_status')) {
    function rf_sync_order_payment_status(PDO $pdo, int $orderId): void
    {
        if (!rf_column_exists($pdo, 'orders', 'payment_status')) return;
        $stmt = $pdo->prepare("SELECT COALESCE(SUM(total_payments),0) AS payments, COALESCE(SUM(balance),0) AS balance FROM folios WHERE order_id = :order_id");
        $stmt->execute([':order_id'=>$orderId]);
        $r = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
        $payments=(float)($r['payments']??0); $balance=(float)($r['balance']??0);
        $status = $payments <= 0.00001 ? 0 : ($balance <= 0.00001 ? 2 : 1);
        $pdo->prepare("UPDATE orders SET payment_status = :s WHERE id = :id")->execute([':s'=>$status, ':id'=>$orderId]);
    }
}


if (!function_exists('rf_rebuild_master_folio_room_charges_from_prices')) {
    function rf_rebuild_master_folio_room_charges_from_prices(PDO $pdo, int $orderId): void
    {
        if ($orderId <= 0) {
            return;
        }

        $folioId = rf_get_or_create_master_folio($pdo, $orderId);

        $stmt = $pdo->prepare("
            SELECT property_id, order_no, check_in, check_out
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) {
            throw new RuntimeException('Booking not found while rebuilding folio room charges.');
        }

        /*
           Remove only system-generated room charges. Extras and payments remain untouched.
           This is what prevents the reservation list from depending on payments.php to fix totals.
        */
        $pdo->prepare("
            DELETE FROM folio_transactions
            WHERE order_id = :order_id
              AND folio_id = :folio_id
              AND LOWER(COALESCE(tx_type, '')) = 'room_charge'
              AND LOWER(COALESCE(source_table, '')) = 'rel_orders_prices'
        ")->execute([
            ':order_id' => $orderId,
            ':folio_id' => $folioId,
        ]);

        /*
           id, room_id and the pax counts are selected so each folio line can
           name its room and trace back to the exact priced row it came from.
           rel_orders_prices now holds one row per room per night.

           Ordered by date then room so the folio reads night-by-night,
           room-by-room, the same way the Nightly Pricing Preview does.
        */
        $stmt = $pdo->prepare("
            SELECT
                id,
                property_id,
                date,
                total,
                room_id,
                adults_count,
                kids_count,
                rate_plan_id,
                rate_plan_title,
                rate_plan_currency,
                rate_plan_season_label,
                applied_rate_type
            FROM rel_orders_prices
            WHERE order_id = :order_id
            ORDER BY date ASC, room_id ASC, id ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $nightRows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        if (!$nightRows) {
            rf_recalculate_folio_totals($pdo, $folioId);
            rf_sync_order_payment_status($pdo, $orderId);
            return;
        }

        $insert = $pdo->prepare("
            INSERT INTO folio_transactions
            (
                property_id,
                folio_id,
                order_id,
                tx_date,
                tx_type,
                description,
                debit,
                credit,
                source_table,
                source_id
            )
            VALUES
            (
                :property_id,
                :folio_id,
                :order_id,
                :tx_date,
                'room_charge',
                :description,
                :debit,
                0,
                'rel_orders_prices',
                :source_id
            )
        ");

        foreach ($nightRows as $night) {
            $amount = round((float)($night['total'] ?? 0), 2);
            if ($amount <= 0) {
                continue;
            }

            $stayDate = (string)($night['date'] ?? '');
            if ($stayDate === '' || strtotime($stayDate) === false) {
                $stayDate = date('Y-m-d');
            }

            /*
               Name the ROOM on the folio line.

               rel_orders_prices now holds one row per room per night, so each
               folio line is a specific room - and a line reading only
               "Accommodation charge for 2026-07-25" against six near-identical
               rows is unreadable. Falls back silently where no room is
               assigned yet.
            */
            $descParts = ['Accommodation charge for ' . $stayDate];

            $nightRoomId = (int)($night['room_id'] ?? 0);
            if ($nightRoomId > 0) {
                $roomLabel = rf_room_label_for_folio($pdo, $nightRoomId);
                if ($roomLabel !== '') {
                    $descParts[] = $roomLabel;
                }
            }

            $paxBits = [];
            $nA = (int)($night['adults_count'] ?? 0);
            $nK = (int)($night['kids_count'] ?? 0);
            if ($nA > 0) { $paxBits[] = $nA . 'A'; }
            if ($nK > 0) { $paxBits[] = $nK . 'C'; }
            if ($paxBits) { $descParts[] = implode(' ', $paxBits); }

            $planTitle = trim((string)($night['rate_plan_title'] ?? ''));
            $currency  = trim((string)($night['rate_plan_currency'] ?? ''));
            $season    = trim((string)($night['rate_plan_season_label'] ?? ''));
            $rateType  = trim((string)($night['applied_rate_type'] ?? ''));

            if ($planTitle !== '') {
                $planText = $planTitle;
                if ($currency !== '') {
                    $planText .= ' (' . $currency . ')';
                }
                if ($season !== '') {
                    $planText .= ' - ' . $season;
                }
                $descParts[] = $planText;
            }
            if ($rateType !== '') {
                $descParts[] = ucwords(str_replace('_', ' ', $rateType));
            }

            $insert->execute([
                ':property_id' => (int)($night['property_id'] ?? $order['property_id'] ?? 0),
                ':folio_id'    => $folioId,
                ':order_id'    => $orderId,
                ':tx_date'     => $stayDate . ' 00:00:00',
                ':description' => implode(' | ', $descParts),
                ':debit'       => $amount,
                /*
                   The rel_orders_prices ROW id - not rate_plan_id.

                   BUG THIS FIXES:
                   source_id used to be the rate_plan_id, so every night of a
                   booking on the same plan wrote the SAME source_id. With
                   source_table = 'rel_orders_prices', that claims all those
                   lines came from one row. Anything that dedupes or rebuilds on
                   (source_table, source_id) - as post_payment_to_folio() does
                   for payments - would collapse them. It also made the folio
                   impossible to trace back to the priced night it came from.
                */
                ':source_id'   => (int)($night['id'] ?? 0),
            ]);
        }

        rf_recalculate_folio_totals($pdo, $folioId);
        rf_sync_order_payment_status($pdo, $orderId);
    }
}

if (!function_exists('rf_room_label_for_folio')) {
    /**
     * "Room 19 Family Room" for a folio line, from a physical room id.
     *
     * Cached per request: a six-room, ten-night booking would otherwise run 60
     * identical lookups while posting one folio.
     */
    function rf_room_label_for_folio(PDO $pdo, int $roomId): string
    {
        static $cache = [];
        if ($roomId <= 0) return '';
        if (array_key_exists($roomId, $cache)) return $cache[$roomId];

        try {
            $stmt = $pdo->prepare("
                SELECT r.room_no, rt.title AS room_type_title
                FROM rooms r
                LEFT JOIN room_types rt ON rt.id = r.room_type_id
                WHERE r.id = :id
                LIMIT 1
            ");
            $stmt->execute([':id' => $roomId]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) {
                $cache[$roomId] = '';
                return $cache[$roomId];
            }
            $label = '';
            $no = trim((string)($row['room_no'] ?? ''));
            if ($no !== '' && $no !== '0') {
                $label = 'Room ' . $no;
            }
            $type = trim((string)($row['room_type_title'] ?? ''));
            if ($type !== '') {
                $label = $label !== '' ? ($label . ' ' . $type) : $type;
            }
            $cache[$roomId] = $label;
        } catch (Throwable $e) {
            $cache[$roomId] = '';
        }
        return $cache[$roomId];
    }
}


if (!function_exists('rf_ensure_booking_extra_agent_payers_schema')) {
    function rf_ensure_booking_extra_agent_payers_schema(PDO $pdo): void
    {
        try {
            $pdo->exec("CREATE TABLE IF NOT EXISTS booking_extra_agent_payers (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                folio_transaction_id BIGINT UNSIGNED NOT NULL,
                order_id BIGINT UNSIGNED NOT NULL,
                service_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                payer_agent_id BIGINT UNSIGNED NOT NULL,
                folio_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (id),
                UNIQUE KEY uq_booking_extra_payer_tx (folio_transaction_id),
                KEY idx_booking_extra_payer_order (order_id),
                KEY idx_booking_extra_payer_agent (payer_agent_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");

            /*
             * Older Extras / Control Centre builds created this table with
             * transaction_id + agent_id instead of folio_transaction_id +
             * payer_agent_id. CREATE TABLE IF NOT EXISTS does not upgrade an
             * existing table, which caused edit pages to fail with an unknown
             * column error. Add and backfill the newer aliases safely.
             */
            $adds = [
                'folio_transaction_id' => "ALTER TABLE booking_extra_agent_payers ADD COLUMN folio_transaction_id BIGINT UNSIGNED NULL",
                'service_id'           => "ALTER TABLE booking_extra_agent_payers ADD COLUMN service_id BIGINT UNSIGNED NOT NULL DEFAULT 0",
                'payer_agent_id'       => "ALTER TABLE booking_extra_agent_payers ADD COLUMN payer_agent_id BIGINT UNSIGNED NULL",
                'folio_id'             => "ALTER TABLE booking_extra_agent_payers ADD COLUMN folio_id BIGINT UNSIGNED NOT NULL DEFAULT 0",
            ];
            foreach ($adds as $column => $ddl) {
                if (!rf_column_exists($pdo, 'booking_extra_agent_payers', $column)) {
                    try { $pdo->exec($ddl); } catch (Throwable $ignore) {}
                }
            }

            if (rf_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id')) {
                try { $pdo->exec("UPDATE booking_extra_agent_payers SET folio_transaction_id = transaction_id WHERE COALESCE(folio_transaction_id,0)=0"); } catch (Throwable $ignore) {}
            }
            if (rf_column_exists($pdo, 'booking_extra_agent_payers', 'agent_id')) {
                try { $pdo->exec("UPDATE booking_extra_agent_payers SET payer_agent_id = agent_id WHERE COALESCE(payer_agent_id,0)=0"); } catch (Throwable $ignore) {}
            }
            try { $pdo->exec("ALTER TABLE booking_extra_agent_payers ADD UNIQUE KEY uq_booking_extra_payer_tx (folio_transaction_id)"); } catch (Throwable $ignore) {}
        } catch (Throwable $e) {
            throw new RuntimeException('Unable to initialise the Other Agent payer table: ' . $e->getMessage());
        }
    }
}

if (!function_exists('rf_agent_display_name')) {
    function rf_agent_display_name(PDO $pdo, int $agentId): string
    {
        if ($agentId <= 0) return '';
        $nameParts = [];
        foreach (['company', 'Agent', 'RegisteredName', 'registered_name', 'agent_name', 'agency_name', 'name', 'firstname'] as $column) {
            if (rf_column_exists($pdo, 'agents', $column)) {
                $safeColumn = str_replace('`', '``', $column);
                $nameParts[] = "NULLIF(TRIM(`{$safeColumn}`), '')";
            }
        }
        if (rf_column_exists($pdo, 'agents', 'firstname') && rf_column_exists($pdo, 'agents', 'lastname')) {
            array_unshift($nameParts, "NULLIF(TRIM(CONCAT(COALESCE(firstname,''), ' ', COALESCE(lastname,''))), '')");
        }
        if (rf_column_exists($pdo, 'agents', 'email')) {
            $nameParts[] = "NULLIF(TRIM(email), '')";
        }
        $displaySql = $nameParts ? "COALESCE(" . implode(', ', array_unique($nameParts)) . ")" : "NULL";
        $stmt = $pdo->prepare("SELECT {$displaySql} FROM agents WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $agentId]);
        return trim((string)$stmt->fetchColumn());
    }
}

if (!function_exists('rf_get_or_create_other_agent_folio')) {
    function rf_get_or_create_other_agent_folio(PDO $pdo, int $orderId, int $propertyId, int $agentId): int
    {
        if ($agentId <= 0) throw new RuntimeException('Select the agent responsible for paying the extra.');
        $agentName = rf_agent_display_name($pdo, $agentId);
        if ($agentName === '') throw new RuntimeException('The selected paying agent was not found.');

        $folioNo = 'FOL-O-' . $orderId . '-' . $agentId;
        $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :oid AND folio_no = :fno ORDER BY id ASC LIMIT 1");
        $stmt->execute([':oid' => $orderId, ':fno' => $folioNo]);
        $id = (int)($stmt->fetchColumn() ?: 0);
        if ($id > 0) {
            $pdo->prepare("UPDATE folios SET folio_name = :name, status = 1 WHERE id = :id")
                ->execute([':name' => 'Other Payer - ' . $agentName, ':id' => $id]);
            return $id;
        }

        $sql = "INSERT INTO folios (property_id, order_id, folio_no, folio_name, folio_type, status, total_charges, total_payments, balance)
                VALUES (:pid, :oid, :fno, :fname, 'other', 1, 0, 0, 0)";
        try {
            $pdo->prepare($sql)->execute([
                ':pid' => $propertyId, ':oid' => $orderId, ':fno' => $folioNo,
                ':fname' => 'Other Payer - ' . $agentName,
            ]);
        } catch (Throwable $e) {
            // Older databases may have an ENUM that does not yet contain "other".
            // Keep it non-master and identify it reliably by folio_no + payer map.
            $fallback = "INSERT INTO folios (property_id, order_id, folio_no, folio_name, folio_type, status, total_charges, total_payments, balance)
                         VALUES (:pid, :oid, :fno, :fname, 'room', 1, 0, 0, 0)";
            $pdo->prepare($fallback)->execute([
                ':pid' => $propertyId, ':oid' => $orderId, ':fno' => $folioNo,
                ':fname' => 'Other Payer - ' . $agentName,
            ]);
        }
        return (int)$pdo->lastInsertId();
    }
}

if (!function_exists('normalize_booking_extra_rows')) {
    function normalize_booking_extra_rows(array $rows): array
    {
        $clean=[];
        foreach ($rows as $row) {
            $categoryId=(int)($row['category_id']??0); $serviceId=(int)($row['service_id']??0); $qty=(float)($row['quantity']??0);
            $txDate=trim((string)($row['tx_date']??date('Y-m-d'))); $note=trim((string)($row['note']??''));
            $overridePrice=trim((string)($row['override_price']??''));
            $chargeTo=strtolower(trim((string)($row['charge_to']??'master')));
            if (!in_array($chargeTo, ['master','room','other'], true)) $chargeTo='master';
            $payerAgentId=(int)($row['payer_agent_id']??0);
            if ($chargeTo !== 'other') $payerAgentId = 0;
            if ($categoryId<=0 && $serviceId<=0 && $qty<=0 && $note==='') continue;
            $clean[]=['category_id'=>$categoryId,'service_id'=>$serviceId,'quantity'=>$qty>0?$qty:1,'tx_date'=>$txDate!==''?$txDate:date('Y-m-d'),'note'=>$note,'override_price'=>$overridePrice,'charge_to'=>$chargeTo,'payer_agent_id'=>$payerAgentId];
        }
        return $clean;
    }
}


if (!function_exists('fetch_booking_stage_extras_for_edit')) {
    function fetch_booking_stage_extras_for_edit(PDO $pdo, int $orderId): array
    {
        if ($orderId <= 0) {
            return [];
        }

        /*
           Booking-stage extras are posted into folio_transactions with:
           source_table = tblservices
           source_id    = tblservices.id

           They may sit on the master folio (charge_to = master) or, when the
           guest pays, be split across the booking's room folios (charge_to =
           room). We join the folio so we can tell which, and we collapse the
           per-room split rows back into a single logical extra on edit.
        */
        $hasFolioType = rf_column_exists($pdo, 'folios', 'folio_type');
        $folioTypeSelect = $hasFolioType ? "f.folio_type AS folio_type" : "'master' AS folio_type";

        $stmt = $pdo->prepare("
            SELECT
                ft.id AS transaction_id,
                ft.folio_id,
                {$folioTypeSelect},
                ft.tx_date,
                ft.description,
                ft.debit,
                ft.source_id AS service_id,
                s.category AS category_id,
                s.price_per_person,
                COALESCE(bep.payer_agent_id, 0) AS payer_agent_id,
                TRIM(CONCAT(COALESCE(pa.firstname,''), ' ', COALESCE(pa.lastname,''))) AS payer_agent_name
            FROM folio_transactions ft
            LEFT JOIN tblservices s ON s.id = ft.source_id
            LEFT JOIN folios f ON f.id = ft.folio_id
            LEFT JOIN booking_extra_agent_payers bep ON bep.folio_transaction_id = ft.id
            LEFT JOIN agents pa ON pa.id = bep.payer_agent_id
            WHERE ft.order_id = :order_id
              AND COALESCE(ft.is_voided, 0) = 0
              AND LOWER(COALESCE(ft.source_table, '')) IN ('tblservices','booking_extras')
              AND ft.source_id IS NOT NULL
              AND ft.source_id > 0
            ORDER BY ft.tx_date ASC, ft.id ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $extras = [];
        $roomGroups = []; // key => aggregated room-charged extra

        foreach ($rows as $row) {
            $serviceId = (int)($row['service_id'] ?? 0);
            $categoryId = (int)($row['category_id'] ?? 0);
            $unitPrice = (float)($row['price_per_person'] ?? 0);
            $amount = (float)($row['debit'] ?? 0);

            if ($serviceId <= 0 || $categoryId <= 0) {
                continue;
            }

            $desc = (string)($row['description'] ?? '');
            $folioType = strtolower(trim((string)($row['folio_type'] ?? 'master')));
            $payerAgentId = (int)($row['payer_agent_id'] ?? 0);
            $isOtherRow = $payerAgentId > 0;
            // A room-split row is tagged with a [Room ...] suffix by post_booking_stage_extras.
            $isRoomRow = !$isOtherRow && (($folioType === 'room') || (bool)preg_match('/\[[^\]]*\]\s*$/', $desc));
            $chargeTo = $isOtherRow ? 'other' : ($isRoomRow ? 'room' : 'master');

            /*
               Reconstruct qty and the actual charged unit rate.
               Parse "x N.NN @ R.RR" first - it preserves the true full quantity
               and unit rate even when this row only carries a split fraction of
               the amount (room charges) or an overridden rate.
            */
            $qty = 0.0;
            $chargedUnit = 0.0;
            if (preg_match('/x\s*([0-9]+(?:\.[0-9]+)?)\s*@\s*([0-9]+(?:\.[0-9]+)?)/i', $desc, $m)) {
                $qty = (float)$m[1];
                $chargedUnit = (float)$m[2];
            }

            $txDate = trim((string)($row['tx_date'] ?? ''));
            if ($txDate !== '') {
                $txDate = substr($txDate, 0, 10);
            }

            if ($isRoomRow) {
                /*
                   Group split rows back into one extra. Key on service + date +
                   parsed qty/rate so distinct extras stay distinct. Sum the
                   debit fragments to recover the full charged amount.
                */
                $key = $serviceId . '|' . $txDate . '|' . number_format($qty, 4, '.', '') . '|' . number_format($chargedUnit, 4, '.', '');
                if (!isset($roomGroups[$key])) {
                    $roomGroups[$key] = [
                        'transaction_id' => (int)($row['transaction_id'] ?? 0),
                        'category_id'    => $categoryId,
                        'service_id'     => $serviceId,
                        'qty'            => $qty,
                        'charged_unit'   => $chargedUnit,
                        'unit_price'     => $unitPrice,
                        'amount_sum'     => 0.0,
                        'tx_date'        => $txDate !== '' ? $txDate : date('Y-m-d'),
                    ];
                }
                $roomGroups[$key]['amount_sum'] += $amount;
                continue;
            }

            // MASTER row - one transaction is one extra (existing behaviour).
            if ($qty <= 0) {
                $qty = ($unitPrice > 0 && $amount > 0) ? round($amount / $unitPrice, 4) : 1;
            }
            if ($chargedUnit <= 0) {
                $chargedUnit = ($qty > 0 && $amount > 0) ? round($amount / $qty, 2) : $unitPrice;
            }
            $overridePrice = (abs($chargedUnit - $unitPrice) > 0.001 && $chargedUnit > 0)
                ? number_format($chargedUnit, 2, '.', '')
                : '';

            $extras[] = [
                'transaction_id' => (int)($row['transaction_id'] ?? 0),
                'category_id'    => $categoryId,
                'service_id'     => $serviceId,
                'quantity'       => $qty > 0 ? $qty : 1,
                'tx_date'        => $txDate !== '' ? $txDate : date('Y-m-d'),
                'note'           => '',
                'override_price' => $overridePrice,
                'charge_to'      => $chargeTo,
                'payer_agent_id' => $payerAgentId,
                'payer_agent_name' => (string)($row['payer_agent_name'] ?? ''),
            ];
        }

        // Emit the collapsed room-charged extras.
        foreach ($roomGroups as $g) {
            $qty = (float)$g['qty'];
            $chargedUnit = (float)$g['charged_unit'];
            $unitPrice = (float)$g['unit_price'];
            $amountSum = (float)$g['amount_sum'];

            if ($qty <= 0) {
                $qty = ($unitPrice > 0 && $amountSum > 0) ? round($amountSum / $unitPrice, 4) : 1;
            }
            if ($chargedUnit <= 0) {
                $chargedUnit = ($qty > 0 && $amountSum > 0) ? round($amountSum / $qty, 2) : $unitPrice;
            }
            $overridePrice = (abs($chargedUnit - $unitPrice) > 0.001 && $chargedUnit > 0)
                ? number_format($chargedUnit, 2, '.', '')
                : '';

            $extras[] = [
                'transaction_id' => (int)$g['transaction_id'],
                'category_id'    => (int)$g['category_id'],
                'service_id'     => (int)$g['service_id'],
                'quantity'       => $qty > 0 ? $qty : 1,
                'tx_date'        => $g['tx_date'],
                'note'           => '',
                'override_price' => $overridePrice,
                'charge_to'      => 'room',
                'payer_agent_id' => 0,
                'payer_agent_name' => '',
            ];
        }

        return $extras;
    }
}

if (!function_exists('post_booking_stage_extras')) {
    function post_booking_stage_extras(PDO $pdo, int $orderId, int $propertyId, array $extraRows): void
    {
        rf_ensure_booking_extra_agent_payers_schema($pdo);
        $folioId = rf_get_or_create_master_folio($pdo, $orderId);

        /*
           Room targets (one folio per allocated room) for extras that guests
           pay directly. Built once and reused. If the booking has no allocated
           rooms yet, room-charged extras gracefully fall back to the master
           folio so nothing is lost.
        */
        $roomTargets = rf_fetch_booking_room_folio_targets($pdo, $orderId, $propertyId);
        $roomFolioIds = [];
        foreach ($roomTargets as $rt) {
            if ((int)($rt['folio_id'] ?? 0) > 0) {
                $roomFolioIds[] = (int)$rt['folio_id'];
            }
        }

        /*
           Replace booking-stage extras everywhere they were previously posted,
           including Other Agent folios. Remove payer links first, then ledger
           rows, so changing Charge To moves rather than duplicates an extra.
        */
        $oldTxStmt = $pdo->prepare("SELECT id, folio_id FROM folio_transactions
            WHERE order_id = :oid
              AND LOWER(COALESCE(source_table, '')) IN ('tblservices','booking_extras')");
        $oldTxStmt->execute([':oid' => $orderId]);
        $oldTxRows = $oldTxStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $oldTxIds = [];
        $oldFolioIds = [];
        foreach ($oldTxRows as $oldTx) {
            $oldTxIds[] = (int)$oldTx['id'];
            $oldFolioIds[] = (int)$oldTx['folio_id'];
        }
        if ($oldTxIds) {
            $marks = implode(',', array_fill(0, count($oldTxIds), '?'));
            $pdo->prepare("DELETE FROM booking_extra_agent_payers WHERE folio_transaction_id IN ($marks)")->execute($oldTxIds);
        }
        $pdo->prepare("DELETE FROM folio_transactions
            WHERE order_id = :oid
              AND LOWER(COALESCE(source_table, '')) IN ('tblservices','booking_extras')")
            ->execute([':oid' => $orderId]);

        if (!$extraRows) {
            rf_recalculate_folio_totals($pdo, $folioId);
            foreach (array_values(array_unique(array_merge($roomFolioIds, $oldFolioIds))) as $rfid) {
                if ($rfid > 0) rf_recalculate_folio_totals($pdo, $rfid);
            }
            rf_sync_order_payment_status($pdo, $orderId);
            return;
        }
        $svcStmt = $pdo->prepare("SELECT s.id, s.service, s.price_per_person, s.category, c.category AS category_name FROM tblservices s LEFT JOIN tblservicecategory c ON c.id=s.category WHERE s.id=:id LIMIT 1");
        $ins = $pdo->prepare("INSERT INTO folio_transactions (property_id, folio_id, order_id, tx_date, tx_type, description, debit, credit, source_table, source_id) VALUES (:property_id, :folio_id, :order_id, :tx_date, :tx_type, :description, :debit, 0, 'tblservices', :source_id)");
        $payerColumns = ['folio_transaction_id','order_id','service_id','payer_agent_id','folio_id'];
        $payerValues  = [':txid',':oid',':sid',':aid',':pfid'];
        $payerHasLegacyTransactionId = rf_column_exists($pdo, 'booking_extra_agent_payers', 'transaction_id');
        $payerHasLegacyAgentId = rf_column_exists($pdo, 'booking_extra_agent_payers', 'agent_id');
        /*
         * PDO native prepared statements cannot reuse the same named placeholder
         * twice in one SQL statement. The compatibility insert previously used
         * :txid for both folio_transaction_id and transaction_id, and :aid for
         * both payer_agent_id and agent_id, causing SQLSTATE[HY093]. Give the
         * legacy columns their own placeholders instead.
         */
        if ($payerHasLegacyTransactionId) {
            $payerColumns[] = 'transaction_id';
            $payerValues[] = ':legacy_txid';
        }
        if ($payerHasLegacyAgentId) {
            $payerColumns[] = 'agent_id';
            $payerValues[] = ':legacy_aid';
        }
        $payerMapIns = $pdo->prepare("INSERT INTO booking_extra_agent_payers (`" . implode('`,`',$payerColumns) . "`) VALUES (" . implode(',',$payerValues) . ")");
        $touchedFolioIds = array_values(array_unique(array_merge([$folioId], $oldFolioIds)));
        foreach ($extraRows as $i=>$extra) {
            $svcId=(int)$extra['service_id']; $catId=(int)$extra['category_id']; $qty=(float)$extra['quantity'];
            if ($svcId<=0 || $catId<=0 || $qty<=0) throw new RuntimeException('Extra row '.($i+1).' is incomplete.');
            $svcStmt->execute([':id'=>$svcId]); $svc=$svcStmt->fetch(PDO::FETCH_ASSOC);
            if (!$svc) throw new RuntimeException('Extra row '.($i+1).': selected service was not found.');
            if ((int)$svc['category'] !== $catId) throw new RuntimeException('Extra row '.($i+1).': service does not belong to selected category.');
            $cat=trim((string)($svc['category_name']??'Extra')); if ($cat==='') $cat='Extra';
            $txType=trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/','_',$cat)),'_') ?: 'extra';
            /*
               Rate resolution: default to the service master price, but if a
               manual override_price was supplied (extra was charged incorrectly
               and the user corrected it inline), honour that instead. A blank or
               non-positive override falls back to the master price so existing
               behaviour is unchanged.
            */
            $unit=(float)$svc['price_per_person'];
            $overrideRaw = array_key_exists('override_price', $extra) ? trim((string)$extra['override_price']) : '';
            $isOverridden = false;
            if ($overrideRaw !== '') {
                $overrideVal = (float)str_replace(',', '', $overrideRaw);
                if ($overrideVal > 0 && abs($overrideVal - $unit) > 0.001) {
                    $unit = round($overrideVal, 2);
                    $isOverridden = true;
                }
            }
            $amount=round($unit*$qty,2); if ($amount<=0) throw new RuntimeException('Extra row '.($i+1).': amount must be greater than zero.');
            $descBase=trim((string)$svc['service']).' x '.number_format($qty,2).' @ '.number_format($unit,2);
            if ($isOverridden) $descBase.=' (adj)';
            if (!empty($extra['note'])) $descBase.=' - '.$extra['note'];
            $txDate=trim((string)$extra['tx_date']); if ($txDate==='' || strtotime($txDate)===false) $txDate=date('Y-m-d');

            $chargeTo = strtolower(trim((string)($extra['charge_to'] ?? 'master')));
            if (!in_array($chargeTo, ['master','room','other'], true)) $chargeTo = 'master';
            $payerAgentId = (int)($extra['payer_agent_id'] ?? 0);

            if ($chargeTo === 'other') {
                if ($payerAgentId <= 0) throw new RuntimeException('Extra row '.($i+1).': select the Other Agent responsible for payment.');
                $otherFolioId = rf_get_or_create_other_agent_folio($pdo, $orderId, $propertyId, $payerAgentId);
                $agentName = rf_agent_display_name($pdo, $payerAgentId);
                $otherDesc = $descBase . ' [Paid by ' . $agentName . ']';
                $ins->execute([':property_id'=>$propertyId, ':folio_id'=>$otherFolioId, ':order_id'=>$orderId, ':tx_date'=>$txDate.' 00:00:00', ':tx_type'=>$txType, ':description'=>$otherDesc, ':debit'=>$amount, ':source_id'=>$svcId]);
                $txId = (int)$pdo->lastInsertId();
                $payerParams = [
                    ':txid' => $txId,
                    ':oid'  => $orderId,
                    ':sid'  => $svcId,
                    ':aid'  => $payerAgentId,
                    ':pfid' => $otherFolioId,
                ];
                if ($payerHasLegacyTransactionId) $payerParams[':legacy_txid'] = $txId;
                if ($payerHasLegacyAgentId) $payerParams[':legacy_aid'] = $payerAgentId;
                $payerMapIns->execute($payerParams);
                $touchedFolioIds[] = $otherFolioId;
                continue;
            }

            /* MASTER charge, or room charge with no room folios available. */
            if ($chargeTo === 'master' || empty($roomTargets)) {
                $ins->execute([':property_id'=>$propertyId, ':folio_id'=>$folioId, ':order_id'=>$orderId, ':tx_date'=>$txDate.' 00:00:00', ':tx_type'=>$txType, ':description'=>$descBase, ':debit'=>$amount, ':source_id'=>$svcId]);
                continue;
            }

            /*
               ROOM (guest) charge: split the extra across the booking's room
               folios so each guest carries their share. Even split, with any
               rounding remainder added to the first room so the totals reconcile
               exactly to $amount.
            */
            $usable = array_values(array_filter($roomTargets, function ($t) { return (int)($t['folio_id'] ?? 0) > 0; }));
            if (empty($usable)) {
                // No usable room folio - fall back to master rather than drop it.
                $ins->execute([':property_id'=>$propertyId, ':folio_id'=>$folioId, ':order_id'=>$orderId, ':tx_date'=>$txDate.' 00:00:00', ':tx_type'=>$txType, ':description'=>$descBase, ':debit'=>$amount, ':source_id'=>$svcId]);
                continue;
            }
            $n = count($usable);
            $per = floor(($amount / $n) * 100) / 100; // 2dp share per room
            $allocated = round($per * $n, 2);
            $remainder = round($amount - $allocated, 2);
            foreach ($usable as $ri => $target) {
                $share = $per + ($ri === 0 ? $remainder : 0);
                $share = round($share, 2);
                if ($share <= 0) continue;
                $roomDesc = $descBase . ' [' . trim((string)$target['label']) . ($n > 1 ? ' - split ' . ($ri + 1) . '/' . $n : '') . ']';
                $ins->execute([':property_id'=>$propertyId, ':folio_id'=>(int)$target['folio_id'], ':order_id'=>$orderId, ':tx_date'=>$txDate.' 00:00:00', ':tx_type'=>$txType, ':description'=>$roomDesc, ':debit'=>$share, ':source_id'=>$svcId]);
                $touchedFolioIds[] = (int)$target['folio_id'];
            }
        }
        foreach (array_values(array_unique($touchedFolioIds)) as $fid) {
            rf_recalculate_folio_totals($pdo, $fid);
        }
        rf_sync_order_payment_status($pdo, $orderId);
    }
}


if (!function_exists('rf_ensure_room_configuration_schema')) {
    function rf_ensure_room_configuration_schema(PDO $pdo): void
    {
        $roomColumns = [
            'max_adults' => "ALTER TABLE rooms ADD COLUMN max_adults INT NOT NULL DEFAULT 2 AFTER room_type_id",
            'max_children' => "ALTER TABLE rooms ADD COLUMN max_children INT NOT NULL DEFAULT 0 AFTER max_adults",
            'max_occupancy' => "ALTER TABLE rooms ADD COLUMN max_occupancy INT NOT NULL DEFAULT 2 AFTER max_children",
            'allows_extra_bed' => "ALTER TABLE rooms ADD COLUMN allows_extra_bed TINYINT(1) NOT NULL DEFAULT 0 AFTER max_occupancy",
            'max_extra_beds' => "ALTER TABLE rooms ADD COLUMN max_extra_beds INT NOT NULL DEFAULT 0 AFTER allows_extra_bed",
            'allows_baby_cot' => "ALTER TABLE rooms ADD COLUMN allows_baby_cot TINYINT(1) NOT NULL DEFAULT 0 AFTER max_extra_beds",
            'max_cots' => "ALTER TABLE rooms ADD COLUMN max_cots INT NOT NULL DEFAULT 0 AFTER allows_baby_cot",
            'wheelchair_accessible' => "ALTER TABLE rooms ADD COLUMN wheelchair_accessible TINYINT(1) NOT NULL DEFAULT 0 AFTER max_cots",
            'interleading_group' => "ALTER TABLE rooms ADD COLUMN interleading_group VARCHAR(50) NULL AFTER wheelchair_accessible",
            'room_configuration_notes' => "ALTER TABLE rooms ADD COLUMN room_configuration_notes TEXT NULL AFTER interleading_group",
        ];

        foreach ($roomColumns as $column => $ddl) {
            if (!rf_column_exists($pdo, 'rooms', $column)) {
                try { $pdo->exec($ddl); } catch (Throwable $e) {}
            }
        }

        $roomingColumns = [
            'extra_beds_required' => "ALTER TABLE order_rooming ADD COLUMN extra_beds_required INT NOT NULL DEFAULT 0 AFTER children",
            'cots_required' => "ALTER TABLE order_rooming ADD COLUMN cots_required INT NOT NULL DEFAULT 0 AFTER extra_beds_required",
            'wheelchair_room_required' => "ALTER TABLE order_rooming ADD COLUMN wheelchair_room_required TINYINT(1) NOT NULL DEFAULT 0 AFTER cots_required",
            'interleading_required' => "ALTER TABLE order_rooming ADD COLUMN interleading_required TINYINT(1) NOT NULL DEFAULT 0 AFTER wheelchair_room_required",
            'room_setup_notes' => "ALTER TABLE order_rooming ADD COLUMN room_setup_notes VARCHAR(255) NULL AFTER interleading_required",
            'housekeeping_notes' => "ALTER TABLE order_rooming ADD COLUMN housekeeping_notes VARCHAR(255) NULL AFTER room_setup_notes",
            'special_occasion' => "ALTER TABLE order_rooming ADD COLUMN special_occasion VARCHAR(80) NULL AFTER housekeeping_notes",
        ];

        foreach ($roomingColumns as $column => $ddl) {
            if (!rf_column_exists($pdo, 'order_rooming', $column)) {
                try { $pdo->exec($ddl); } catch (Throwable $e) {}
            }
        }

        try { $pdo->exec("ALTER TABLE order_rooming MODIFY room_id INT UNSIGNED NULL DEFAULT NULL"); } catch (Throwable $e) {}
    }
}


if (!function_exists('rf_ensure_nightly_override_columns')) {
    function rf_ensure_nightly_override_columns(PDO $pdo): void
    {
        $columns = [
            'original_total'     => "ALTER TABLE rel_orders_prices ADD COLUMN original_total DECIMAL(12,2) NULL",
            'override_total'     => "ALTER TABLE rel_orders_prices ADD COLUMN override_total DECIMAL(12,2) NULL",
            'override_reason'    => "ALTER TABLE rel_orders_prices ADD COLUMN override_reason VARCHAR(255) NULL",
            'rate_overridden'    => "ALTER TABLE rel_orders_prices ADD COLUMN rate_overridden TINYINT(1) NOT NULL DEFAULT 0",
            'override_type'      => "ALTER TABLE rel_orders_prices ADD COLUMN override_type VARCHAR(30) NULL",
            'override_value'     => "ALTER TABLE rel_orders_prices ADD COLUMN override_value DECIMAL(12,2) NULL",
            'extra_bed_override' => "ALTER TABLE rel_orders_prices ADD COLUMN extra_bed_override DECIMAL(12,2) NULL",
            'override_by'        => "ALTER TABLE rel_orders_prices ADD COLUMN override_by INT NULL",
            'override_at'        => "ALTER TABLE rel_orders_prices ADD COLUMN override_at DATETIME NULL",
            'override_components' => "ALTER TABLE rel_orders_prices ADD COLUMN override_components TEXT NULL"
        ];

        foreach ($columns as $column => $ddl) {
            if (!rf_column_exists($pdo, 'rel_orders_prices', $column)) {
                try { $pdo->exec($ddl); } catch (Throwable $e) { /* ignore if host disallows DDL */ }
            }
        }
    }
}

if (!function_exists('rf_apply_component_override_to_row')) {
    function rf_apply_component_override_to_row(array $row, array $override): array
    {
        $baseTotal = round((float)($row['original_total'] ?? $row['night_total'] ?? 0), 2);
        $adultTotal = round((float)($row['adult_total'] ?? 0), 2);
        $childTotal = round((float)($row['child_total'] ?? 0), 2);
        $type = trim((string)($override['type'] ?? $override['override_type'] ?? 'total'));

        // -------- Per-room override (new model) --------
        if ($type === 'per_room') {
            $rooms = (isset($override['rooms']) && is_array($override['rooms'])) ? $override['rooms'] : [];
            $breakdown = is_array($row['room_breakdown'] ?? null) ? $row['room_breakdown'] : [];
            $total = 0.0;
            $bits = [];
            foreach ($breakdown as &$rb) {
                $roomNo = (string)($rb['room_no'] ?? '');
                $a = max(0, (int)($rb['adults'] ?? 0));
                $roomTotal = (float)($rb['room_total'] ?? 0);
                if (isset($rooms[$roomNo]) && is_array($rooms[$roomNo])) {
                    $mode = (string)($rooms[$roomNo]['mode'] ?? 'keep');
                    $val = (float)($rooms[$roomNo]['value'] ?? 0);
                    if ($mode === 'flat') {
                        $roomTotal = round($val, 2);
                        $bits[] = 'Room ' . $roomNo . ($val == 0.0 ? ' waived' : ' ' . number_format($val, 2));
                    } elseif ($mode === 'perPerson') {
                        $roomTotal = round($val * max(1, $a), 2);
                        $bits[] = 'Room ' . $roomNo . ' ' . number_format($val, 2) . '/pp';
                    }
                }
                $rb['room_total'] = round($roomTotal, 2);
                $total += $roomTotal;
            }
            unset($rb);

            $finalTotal = round($total, 2);
            // Warn (but keep) if the saved room map no longer lines up with the
            // current breakdown - staff asked to be warned rather than auto-drop.
            $breakdownKeys = [];
            foreach ($breakdown as $rb2) { $breakdownKeys[(string)($rb2['room_no'] ?? '')] = true; }
            $mismatch = false;
            foreach ($rooms as $rk => $rv2) {
                if (!isset($breakdownKeys[(string)$rk])) { $mismatch = true; break; }
            }
            $row['original_total'] = $baseTotal;
            $row['rate_overridden'] = 1;
            $row['override_type'] = 'per_room';
            $row['override_total'] = $finalTotal;
            $row['final_total'] = $finalTotal;
            $row['night_total'] = $finalTotal;
            $row['override_reason'] = (string)($override['reason'] ?? $override['override_reason'] ?? '');
            $row['override_detail'] = $bits ? implode(' | ', $bits) : (string)($override['override_detail'] ?? '');
            $row['room_overrides'] = $rooms;
            $row['room_breakdown'] = $breakdown;
            $row['room_override_mismatch'] = $mismatch ? 1 : 0;
            return $row;
        }
        // -------- Legacy total/component override --------
        $allowed = ['total', 'extra_bed', 'sharing', 'single', 'child'];
        if (!in_array($type, $allowed, true)) $type = 'total';

        $value = null;
        if (array_key_exists('value', $override)) $value = (float)$override['value'];
        elseif (array_key_exists('override_value', $override)) $value = (float)$override['override_value'];
        elseif ($type === 'total' && array_key_exists('override_total', $override)) $value = (float)$override['override_total'];
        elseif ($type === 'extra_bed' && array_key_exists('extra_bed', $override)) $value = (float)$override['extra_bed'];
        $value = max(0, round((float)$value, 2));

        $roomBreakdown = is_array($row['room_breakdown'] ?? null) ? $row['room_breakdown'] : [];
        $sharingAdults = 0;
        $singleRooms = 0;
        $children = 0;
        foreach ($roomBreakdown as $rb) {
            $a = max(0, (int)($rb['adults'] ?? 0));
            $c = max(0, (int)($rb['children'] ?? 0));
            if ($a >= 2) $sharingAdults += $a;
            if ($a === 1) $singleRooms++;
            $children += $c;
        }
        if ($sharingAdults <= 0) $sharingAdults = max(0, (int)($row['adults_count'] ?? 0));
        if ($children <= 0) $children = max(0, (int)($row['kids_count'] ?? 0));

        $finalTotal = $baseTotal;
        $detail = '';
        switch ($type) {
            case 'total':
                $finalTotal = $value;
                $detail = 'Full night total override';
                break;
            case 'extra_bed':
                $finalTotal = $baseTotal + $value;
                $detail = 'Extra bed / triple supplement +' . number_format($value, 2);
                break;
            case 'sharing':
                $oldRate = (float)($row['sharing_rate'] ?? 0);
                $delta = ($value - $oldRate) * $sharingAdults;
                $finalTotal = $baseTotal + $delta;
                $adultTotal = max(0, $adultTotal + $delta);
                $detail = 'Sharing rate override: ' . number_format($value, 2) . ' x ' . $sharingAdults;
                break;
            case 'single':
                $oldRate = (float)($row['single_rate'] ?? 0);
                $delta = ($value - $oldRate) * max(1, $singleRooms);
                $finalTotal = $baseTotal + $delta;
                $adultTotal = max(0, $adultTotal + $delta);
                $detail = 'Single rate override: ' . number_format($value, 2) . ' x ' . max(1, $singleRooms);
                break;
            case 'child':
                $newChildTotal = $value * $children;
                $delta = $newChildTotal - $childTotal;
                $finalTotal = $baseTotal + $delta;
                $childTotal = max(0, $newChildTotal);
                $detail = 'Child rate override: ' . number_format($value, 2) . ' x ' . $children;
                break;
        }

        $row['original_total'] = $baseTotal;
        $row['rate_overridden'] = 1;
        $row['override_type'] = $type;
        $row['override_value'] = $value;
        $row['override_total'] = round($finalTotal, 2);
        $row['final_total'] = round($finalTotal, 2);
        $row['override_reason'] = (string)($override['reason'] ?? $override['override_reason'] ?? '');
        $row['override_detail'] = $detail;
        $row['extra_bed_override'] = $type === 'extra_bed' ? $value : 0;
        $row['adult_total'] = round($adultTotal, 2);
        $row['child_total'] = round($childTotal, 2);
        $row['night_total'] = round($finalTotal, 2);

        // If this override carried per-component rates, repaint each room's
        // room_total so the reloaded breakdown cards show the overridden
        // figure (e.g. 200 not 232), matching the client-side behaviour.
        $components = (isset($override['components']) && is_array($override['components'])) ? $override['components'] : null;
        if ($components !== null && is_array($row['room_breakdown'] ?? null)) {
            $shRate = ($components['sharing'] ?? null);
            $siRate = ($components['single'] ?? null);
            $chRate = ($components['child'] ?? null);
            $oldSharing = (float)($row['sharing_rate'] ?? 0);
            $oldSingle  = (float)($row['single_rate'] ?? 0);

            foreach ($row['room_breakdown'] as &$rb) {
                $a = (int)($rb['adults'] ?? 0);
                $c = (int)($rb['children'] ?? 0);
                $adultPart = (float)($rb['room_total'] ?? 0);
                if ($a >= 2) {
                    $rate = ($shRate !== null && $shRate !== '') ? (float)$shRate : $oldSharing;
                    $adultPart = $rate * $a;
                } elseif ($a === 1) {
                    $rate = ($siRate !== null && $siRate !== '') ? (float)$siRate : $oldSingle;
                    $adultPart = $rate;
                }
                $childPart = ($chRate !== null && $chRate !== '') ? ((float)$chRate * $c) : 0.0;
                $rb['room_total'] = round($adultPart + $childPart, 2);
            }
            unset($rb);
        }

        return $row;
    }
}

if (!function_exists('normalize_nightly_overrides_json')) {
    function normalize_nightly_overrides_json($json): array
    {
        if (is_array($json)) {
            $data = $json;
        } else {
            $json = trim((string)$json);
            if ($json === '') return [];
            $data = json_decode($json, true);
        }

        if (!is_array($data)) return [];
        $clean = [];

        foreach ($data as $date => $row) {
            if (!is_array($row)) continue;
            $date = trim((string)$date);
            if ($date === '' || strtotime($date) === false) continue;

            $type = trim((string)($row['type'] ?? $row['override_type'] ?? 'total'));

            // -------- Per-room override (new model) --------
            if ($type === 'per_room') {
                $roomsIn = (isset($row['rooms']) && is_array($row['rooms'])) ? $row['rooms'] : [];
                $rooms = [];
                foreach ($roomsIn as $roomNo => $roomOv) {
                    if (!is_array($roomOv)) continue;
                    $mode = trim((string)($roomOv['mode'] ?? 'keep'));
                    if (!in_array($mode, ['perPerson', 'flat'], true)) continue;
                    if (!array_key_exists('value', $roomOv) || $roomOv['value'] === '' || $roomOv['value'] === null) continue;
                    $rv = (float)$roomOv['value'];
                    if ($rv < 0) $rv = 0;
                    $rooms[(string)$roomNo] = ['mode' => $mode, 'value' => round($rv, 2)];
                }
                if (!$rooms) continue; // nothing overridden

                $finalTotal = null;
                if (array_key_exists('final_total', $row)) $finalTotal = (float)$row['final_total'];
                elseif (array_key_exists('override_total', $row)) $finalTotal = (float)$row['override_total'];
                if ($finalTotal !== null && $finalTotal < 0) $finalTotal = 0;

                $clean[$date] = [
                    'enabled'         => true,
                    'type'            => 'per_room',
                    'rooms'           => $rooms,
                    'value'           => $finalTotal !== null ? round($finalTotal, 2) : null,
                    'override_value'  => $finalTotal !== null ? round($finalTotal, 2) : null,
                    'override_total'  => $finalTotal !== null ? round($finalTotal, 2) : null,
                    'final_total'     => $finalTotal !== null ? round($finalTotal, 2) : null,
                    'reason'          => trim((string)($row['reason'] ?? $row['override_reason'] ?? '')),
                    'override_detail' => trim((string)($row['override_detail'] ?? '')),
                    'components'      => null,
                ];
                continue;
            }
            // -------- Legacy total/component override --------
            $allowed = ['total', 'extra_bed', 'sharing', 'single', 'child'];
            if (!in_array($type, $allowed, true)) $type = 'total';

            $enabled = !empty($row['enabled']) || !empty($row['rate_overridden']);
            $value = null;
            if (array_key_exists('value', $row)) $value = (float)$row['value'];
            elseif (array_key_exists('override_value', $row)) $value = (float)$row['override_value'];
            elseif ($type === 'total' && array_key_exists('override_total', $row)) $value = (float)$row['override_total'];
            elseif ($type === 'extra_bed' && array_key_exists('extra_bed', $row)) $value = (float)$row['extra_bed'];

            $finalTotal = null;
            if (array_key_exists('final_total', $row)) $finalTotal = (float)$row['final_total'];
            elseif (array_key_exists('override_total', $row) && $type !== 'total') $finalTotal = (float)$row['override_total'];

            $reason = trim((string)($row['reason'] ?? $row['override_reason'] ?? ''));

            if (!$enabled && ($value === null || $value <= 0) && $reason === '') continue;
            if ($value !== null && $value < 0) $value = 0;
            if ($finalTotal !== null && $finalTotal < 0) $finalTotal = 0;

            // Preserve the per-component values and detail so the modal can
            // be reopened showing exactly what was entered (sharing/single/etc).
            $components = null;
            if (isset($row['components']) && is_array($row['components'])) {
                $components = [];
                foreach (['sharing', 'single', 'child', 'extraBed'] as $ck) {
                    $cv = $row['components'][$ck] ?? null;
                    $components[$ck] = ($cv === null || $cv === '') ? null : round((float)$cv, 2);
                }
            }
            $detail = trim((string)($row['override_detail'] ?? ''));

            $clean[$date] = [
                'enabled'        => $enabled && $value !== null,
                'type'           => $type,
                'value'          => $value !== null ? round((float)$value, 2) : null,
                'override_value' => $value !== null ? round((float)$value, 2) : null,
                'override_total' => $type === 'total' && $value !== null ? round((float)$value, 2) : ($finalTotal !== null ? round((float)$finalTotal, 2) : null),
                'final_total'    => $finalTotal !== null ? round((float)$finalTotal, 2) : null,
                'reason'         => $reason,
                'components'     => $components,
                'override_detail'=> $detail,
            ];
        }

        return $clean;
    }
}

if (!function_exists('normalize_nightly_overrides_post_lines')) {
    function normalize_nightly_overrides_post_lines($lines): array
    {
        if (!is_array($lines)) {
            return [];
        }

        $raw = [];
        foreach ($lines as $line) {
            if (!is_array($line)) {
                continue;
            }
            $date = trim((string)($line['date'] ?? ''));
            if ($date === '' || strtotime($date) === false) {
                continue;
            }
            $type = trim((string)($line['type'] ?? 'total'));
            $value = $line['value'] ?? $line['override_value'] ?? $line['override_total'] ?? null;
            if ($value === null || $value === '') {
                continue;
            }
            $raw[$date] = [
                'enabled' => true,
                'type' => $type,
                'value' => $value,
                'override_value' => $value,
                'override_total' => $type === 'total' ? $value : ($line['override_total'] ?? null),
                'final_total' => $type === 'total' ? $value : ($line['final_total'] ?? $line['override_total'] ?? null),
                'original_total' => $line['original_total'] ?? null,
                'reason' => $line['reason'] ?? '',
            ];
        }

        return normalize_nightly_overrides_json($raw);
    }
}

if (!function_exists('apply_nightly_overrides_to_preview_rows')) {
    function apply_nightly_overrides_to_preview_rows(array $rows, array $overrides): array
    {
        // Normalise override keys to Y-m-d so lookups are format-agnostic.
        $ovByDay = [];
        foreach ($overrides as $ovKey => $ovVal) {
            $k = substr(trim((string)$ovKey), 0, 10);
            if ($k !== '') $ovByDay[$k] = $ovVal;
        }
        foreach ($rows as &$row) {
            $date = substr((string)($row['date'] ?? ''), 0, 10);
            $originalTotal = (float)($row['night_total'] ?? 0);
            $row['original_total'] = round($originalTotal, 2);
            $row['rate_overridden'] = 0;
            $row['override_total'] = null;
            $row['override_value'] = null;
            $row['override_type'] = '';
            $row['override_reason'] = '';
            $row['override_detail'] = '';
            $row['extra_bed_override'] = 0;

            if ($date !== '' && isset($ovByDay[$date]) && !empty($ovByDay[$date]['enabled'])) {
                $row = rf_apply_component_override_to_row($row, $ovByDay[$date]);
            }
        }
        unset($row);
        return $rows;
    }
}

if (!function_exists('apply_saved_nightly_overrides_to_preview_rows')) {
    function apply_saved_nightly_overrides_to_preview_rows(PDO $pdo, int $orderId, array $rows): array
    {
        if ($orderId <= 0) return $rows;
        if (!rf_column_exists($pdo, 'rel_orders_prices', 'rate_overridden')) return $rows;

        $select = "date, total, original_total, override_total, override_reason, rate_overridden";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'override_type') ? ", override_type" : ", NULL AS override_type";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'override_value') ? ", override_value" : ", NULL AS override_value";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'extra_bed_override') ? ", extra_bed_override" : ", NULL AS extra_bed_override";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'override_components') ? ", override_components" : ", NULL AS override_components";

        $stmt = $pdo->prepare("SELECT {$select} FROM rel_orders_prices WHERE order_id = :order_id ORDER BY date ASC");
        $stmt->execute([':order_id' => $orderId]);
        $saved = [];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
            // Key by Y-m-d so DATE and DATETIME columns both match the preview rows.
            $saved[substr((string)$r['date'], 0, 10)] = $r;
        }

        foreach ($rows as &$row) {
            $date = substr((string)($row['date'] ?? ''), 0, 10);
            if ($date === '' || !isset($saved[$date])) continue;
            $r = $saved[$date];
            $original = $r['original_total'] !== null ? (float)$r['original_total'] : (float)($row['night_total'] ?? $r['total'] ?? 0);
            $row['original_total'] = round($original, 2);
            $row['rate_overridden'] = (int)($r['rate_overridden'] ?? 0);
            $row['override_type'] = (string)($r['override_type'] ?: 'total');
            $row['override_value'] = $r['override_value'] !== null ? round((float)$r['override_value'], 2) : null;
            $row['extra_bed_override'] = $r['extra_bed_override'] !== null ? round((float)$r['extra_bed_override'], 2) : 0;
            $row['override_total'] = $r['override_total'] !== null ? round((float)$r['override_total'], 2) : null;
            $row['override_reason'] = (string)($r['override_reason'] ?? '');

            // Restore the per-component values (JSON) so the modal reopens
            // showing the exact sharing/single/child/extra-bed rates entered.
            $row['override_components'] = null;
            if (isset($r['override_components']) && trim((string)$r['override_components']) !== '') {
                $decoded = json_decode((string)$r['override_components'], true);
                if (is_array($decoded)) {
                    $row['override_components'] = $decoded;
                }
            }

            if ((int)$row['rate_overridden'] === 1) {
                // Detect the per-room map shape stored in override_components:
                // {"per_room": { "1": {mode,value}, ... }}
                $perRoomMap = null;
                if (isset($row['override_components']['per_room']) && is_array($row['override_components']['per_room'])) {
                    $perRoomMap = $row['override_components']['per_room'];
                }

                if ($perRoomMap !== null || $row['override_type'] === 'per_room') {
                    $override = [
                        'enabled' => true,
                        'type' => 'per_room',
                        'rooms' => $perRoomMap ?: [],
                        'override_total' => $row['override_total'],
                        'final_total' => $row['override_total'],
                        'reason' => $row['override_reason'],
                    ];
                    $row['room_overrides'] = $perRoomMap ?: [];
                } else {
                    $override = [
                        'enabled' => true,
                        'type' => $row['override_type'],
                        'value' => $row['override_value'] !== null ? $row['override_value'] : $row['override_total'],
                        'override_total' => $row['override_total'],
                        'final_total' => $row['override_total'],
                        'reason' => $row['override_reason'],
                        'components' => (isset($row['override_components']) && is_array($row['override_components'])) ? $row['override_components'] : null,
                    ];
                }
                $row = rf_apply_component_override_to_row($row, $override);

                /*
                   Authoritative total: the price the user actually saved lives in
                   rel_orders_prices.override_total (and total). The component
                   recompute above is only for the per-line detail display; if any
                   recompute input drifted on reopen (room mix, adults, rounding)
                   it must NOT change the saved nightly price. Pin the displayed
                   night total to the stored override so it can never silently
                   revert to a recalculated figure.
                */
                $savedOverrideTotal = $r['override_total'] !== null ? round((float)$r['override_total'], 2) : null;
                if ($savedOverrideTotal === null && $r['total'] !== null) {
                    $savedOverrideTotal = round((float)$r['total'], 2);
                }
                if ($savedOverrideTotal !== null) {
                    $row['night_total']    = $savedOverrideTotal;
                    $row['override_total'] = $savedOverrideTotal;
                    $row['final_total']    = $savedOverrideTotal;
                }
            }
        }
        unset($row);
        return $rows;
    }
}

if (!function_exists('preview_rows_to_nightly_overrides')) {
    function preview_rows_to_nightly_overrides(array $rows): array
    {
        $out = [];
        foreach ($rows as $row) {
            if (!empty($row['rate_overridden'])) {
                $date = (string)($row['date'] ?? '');
                if ($date !== '') {
                    $isPerRoom = (($row['override_type'] ?? '') === 'per_room')
                        || (isset($row['room_overrides']) && is_array($row['room_overrides']) && $row['room_overrides']);
                    if ($isPerRoom) {
                        $out[$date] = [
                            'enabled' => true,
                            'type' => 'per_room',
                            'rooms' => (isset($row['room_overrides']) && is_array($row['room_overrides'])) ? $row['room_overrides'] : [],
                            'override_total' => round((float)($row['override_total'] ?? $row['night_total'] ?? 0), 2),
                            'final_total' => round((float)($row['night_total'] ?? $row['override_total'] ?? 0), 2),
                            'reason' => (string)($row['override_reason'] ?? ''),
                            'override_detail' => (string)($row['override_detail'] ?? ''),
                        ];
                    } else {
                        $out[$date] = [
                            'enabled' => true,
                            'type' => (string)($row['override_type'] ?? 'total'),
                            'value' => isset($row['override_value']) ? round((float)$row['override_value'], 2) : round((float)($row['override_total'] ?? $row['night_total'] ?? 0), 2),
                            'override_value' => isset($row['override_value']) ? round((float)$row['override_value'], 2) : null,
                            'override_total' => round((float)($row['override_total'] ?? $row['night_total'] ?? 0), 2),
                            'final_total' => round((float)($row['night_total'] ?? $row['override_total'] ?? 0), 2),
                            'reason' => (string)($row['override_reason'] ?? ''),
                            'components' => (isset($row['override_components']) && is_array($row['override_components'])) ? $row['override_components'] : null,
                        ];
                    }
                }
            }
        }
        return $out;
    }
}

if (!function_exists('rf_night_room_context')) {
    /*
       Return per-room context for one night of a booking, keyed by room_no:
         [ room_no => ['adults'=>int, 'children'=>int, 'room_total'=>float] ]
       adults/children come from order_rooming (the real occupancy), and
       room_total is the calculated per-room amount for that night using the
       night's stored rates. Used to compute per-room overrides authoritatively
       on the server so the saved night total never depends on client state.
    */
    function rf_night_room_context(PDO $pdo, int $orderId, string $dayKey): array
    {
        if ($orderId <= 0 || $dayKey === '') {
            return [];
        }

        // Night rates for this date (match on the date portion for DATE/DATETIME).
        $rateStmt = $pdo->prepare("
            SELECT sharing_rate, single_rate, single_supplement, child_rate_percent
            FROM rel_orders_prices
            WHERE order_id = :order_id AND DATE(date) = :day
            ORDER BY date ASC
            LIMIT 1
        ");
        try {
            $rateStmt->execute([':order_id' => $orderId, ':day' => $dayKey]);
            $rate = $rateStmt->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable $e) {
            $rate = null;
        }
        if (!$rate) {
            return [];
        }

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

        // Real per-room occupancy.
        $roomStmt = $pdo->prepare("
            SELECT room_no, adults, children
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_no ASC, id ASC
        ");
        $roomStmt->execute([':order_id' => $orderId]);
        $roomRows = $roomStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        if (!$roomRows) {
            return [];
        }

        $ctx = [];
        foreach ($roomRows as $rr) {
            $roomNo   = (int)($rr['room_no'] ?? 0);
            $adults   = max(0, (int)($rr['adults'] ?? 0));
            $children = max(0, (int)($rr['children'] ?? 0));
            $calc = rf_calculate_room_night_total($adults, $children, $sharing, $single, $suppl, $childPct);
            $ctx[(string)$roomNo] = [
                'adults'     => $adults,
                'children'   => $children,
                'room_total' => round((float)($calc['room_total'] ?? 0), 2),
            ];
        }

        return $ctx;
    }
}

if (!function_exists('apply_nightly_overrides_to_order')) {
    function apply_nightly_overrides_to_order(PDO $pdo, int $orderId, array $overrides, int $userId = 0): void
    {
        if ($orderId <= 0) {
            return;
        }

        rf_ensure_nightly_override_columns($pdo);

        /*
           Important:
           Some installations of rel_orders_prices do NOT have an id column.
           Therefore we update rows by order_id + date, not by id.
        */
        /*
           Read original_total too. If a row was already overridden, total contains
           the previous override amount. Using that as the base is what made edited
           nightly overrides snap back or compound from the old amount after Update
           Reservation. The safe base is original_total when the row is overridden,
           otherwise the current calculated total.
        */
        $select = "date, total";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'original_total') ? ", original_total" : ", NULL AS original_total";
        $select .= rf_column_exists($pdo, 'rel_orders_prices', 'rate_overridden') ? ", rate_overridden" : ", 0 AS rate_overridden";

        $stmt = $pdo->prepare("
            SELECT
                id, {$select}
            FROM rel_orders_prices
            WHERE order_id = :order_id
            ORDER BY date ASC
        ");
        $stmt->execute([':order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (!$rows) {
            return;
        }

        $hasComponentsCol = rf_column_exists($pdo, 'rel_orders_prices', 'override_components');
        $componentsSet = $hasComponentsCol ? ", override_components = :override_components" : "";

        /*
           Keyed on the ROW id, not the date.

           BUG THIS FIXES:
           This used to be `WHERE order_id = :order_id AND date = :date`. That
           matched exactly one row back when rel_orders_prices held one row per
           NIGHT. It now holds one row per ROOM per night, so a six-room booking
           has six rows sharing a date - and every one of them was overwritten
           with the same `total` on each pass of the loop. adult_total and
           child_total survived only because they are not in the SET clause,
           which is why a booking could show correct per-room adult_total
           alongside a total of 1665.00 on all six rows.

           This runs on EVERY save, override or not: the "clear" branch below
           writes total = :total to reset a night that has no override.
        */
        $upd = $pdo->prepare("
            UPDATE rel_orders_prices
            SET original_total = :original_total,
                override_total = :override_total,
                override_reason = :override_reason,
                rate_overridden = :rate_overridden,
                override_type = :override_type,
                override_value = :override_value,
                extra_bed_override = :extra_bed_override,
                override_by = :override_by,
                override_at = :override_at,
                total = :total
                {$componentsSet}
            WHERE id = :row_id
        ");

        $grandTotal = 0.0;

        /*
           Build a date-normalised lookup of the overrides. rel_orders_prices.date
           may be a DATE ('2026-07-31') or DATETIME ('2026-07-31 00:00:00')
           column, but the override keys always arrive as 'Y-m-d'. Matching the
           raw DB value against the 'Y-m-d' key silently failed on DATETIME
           installs, so the row fell through to the clear branch and the edited
           price snapped back to the calculated rate. Normalise both sides to
           'Y-m-d' before matching.
        */
        $overridesByDay = [];
        foreach ($overrides as $ovKey => $ovVal) {
            $k = substr(trim((string)$ovKey), 0, 10);
            if ($k !== '') {
                $overridesByDay[$k] = $ovVal;
            }
        }

        foreach ($rows as $row) {
            $date = (string)($row['date'] ?? '');
            $dayKey = substr(trim($date), 0, 10);
            $savedOriginalTotal = $row['original_total'] !== null ? (float)$row['original_total'] : null;
            $wasOverridden = (int)($row['rate_overridden'] ?? 0) === 1;
            $currentTotal = round(($wasOverridden && $savedOriginalTotal !== null) ? $savedOriginalTotal : (float)($row['total'] ?? 0), 2);
            $override = $overridesByDay[$dayKey] ?? ($overrides[$date] ?? null);

            if (is_array($override) && !empty($override['enabled'])) {
                $type = trim((string)($override['type'] ?? $override['override_type'] ?? 'total'));
                $isPerRoom = ($type === 'per_room');
                if (!$isPerRoom) {
                    $allowed = ['total', 'extra_bed', 'sharing', 'single', 'child'];
                    if (!in_array($type, $allowed, true)) {
                        $type = 'total';
                    }
                }

                $value = 0.0;
                if (array_key_exists('value', $override)) {
                    $value = (float)$override['value'];
                } elseif (array_key_exists('override_value', $override)) {
                    $value = (float)$override['override_value'];
                } elseif ($type === 'total' && array_key_exists('override_total', $override)) {
                    $value = (float)$override['override_total'];
                } elseif ($type === 'extra_bed' && array_key_exists('extra_bed', $override)) {
                    $value = (float)$override['extra_bed'];
                }
                $value = max(0, round($value, 2));

                $originalTotal = isset($override['original_total']) && $override['original_total'] !== ''
                    ? round((float)$override['original_total'], 2)
                    : $currentTotal;

                /*
                   If preview has already calculated the component final total, use that.
                   This prevents backend from incorrectly recalculating sharing/single/child
                   without full room breakdown context.
                */
                if ($isPerRoom) {
                    /*
                       AUTHORITATIVE server-side math. Do not trust the client's
                       final_total blindly - recompute the night from the rooms
                       map and the booking's real per-room adult counts:
                         perPerson -> rate x adults in that room
                         flat      -> the exact amount (0 = waived)
                       Rooms not in the map keep their calculated room_total.
                       Only fall back to the client total if we genuinely can't
                       resolve any room context.
                    */
                    $rooms = (isset($override['rooms']) && is_array($override['rooms'])) ? $override['rooms'] : [];

                    // Real per-room adult counts + calculated room totals for this night.
                    $roomCtx = rf_night_room_context($pdo, $orderId, $dayKey);

                    $computed = null;
                    if (!empty($roomCtx)) {
                        $sum = 0.0;
                        foreach ($roomCtx as $roomNo => $ctx) {
                            $key = (string)$roomNo;
                            if (isset($rooms[$key]) && is_array($rooms[$key])) {
                                $mode = (string)($rooms[$key]['mode'] ?? 'keep');
                                $val  = (float)($rooms[$key]['value'] ?? 0);
                                if ($mode === 'flat') {
                                    $sum += round($val, 2);
                                } elseif ($mode === 'perPerson') {
                                    $sum += round($val * max(1, (int)$ctx['adults']), 2);
                                } else {
                                    $sum += round((float)$ctx['room_total'], 2);
                                }
                            } else {
                                $sum += round((float)$ctx['room_total'], 2);
                            }
                        }
                        $computed = round($sum, 2);
                    }

                    if ($computed !== null) {
                        $finalTotal = $computed;
                    } else {
                        // No room context available - trust client sum as a fallback.
                        $finalTotal = isset($override['final_total']) && $override['final_total'] !== ''
                            ? round((float)$override['final_total'], 2)
                            : (isset($override['override_total']) ? round((float)$override['override_total'], 2) : $currentTotal);
                    }
                } elseif ($type === 'total') {
                    /*
                       For Full Night Total the typed value IS the new saved nightly price.
                       Do not prefer final_total/override_total from stale client state here.
                    */
                    $finalTotal = round($value, 2);
                } elseif (isset($override['final_total']) && $override['final_total'] !== '') {
                    $finalTotal = round((float)$override['final_total'], 2);
                } elseif (isset($override['override_total']) && $override['override_total'] !== '') {
                    $finalTotal = round((float)$override['override_total'], 2);
                } elseif ($type === 'extra_bed') {
                    $finalTotal = round($originalTotal + $value, 2);
                } else {
                    $finalTotal = $currentTotal;
                }

                $execParams = [
                    ':original_total'     => $originalTotal,
                    ':override_total'     => $finalTotal,
                    ':override_reason'    => trim((string)($override['reason'] ?? $override['override_reason'] ?? '')),
                    ':rate_overridden'    => 1,
                    ':override_type'      => $type,
                    ':override_value'     => $isPerRoom ? $finalTotal : $value,
                    ':extra_bed_override' => $type === 'extra_bed' ? $value : null,
                    ':override_by'        => $userId > 0 ? $userId : null,
                    ':override_at'        => date('Y-m-d H:i:s'),
                    ':total'              => $finalTotal,
                    ':row_id'             => (int)$row['id'],
                ];
                if ($hasComponentsCol) {
                    // Store either the per-room map (new) or the component values
                    // (legacy) as JSON in the same column.
                    if ($isPerRoom) {
                        $rooms = (isset($override['rooms']) && is_array($override['rooms'])) ? $override['rooms'] : null;
                        $execParams[':override_components'] = $rooms !== null
                            ? json_encode(['per_room' => $rooms], JSON_UNESCAPED_SLASHES)
                            : null;
                    } else {
                        $comp = (isset($override['components']) && is_array($override['components'])) ? $override['components'] : null;
                        $execParams[':override_components'] = $comp !== null
                            ? json_encode($comp, JSON_UNESCAPED_SLASHES)
                            : null;
                    }
                }
                $upd->execute($execParams);

                $grandTotal += $finalTotal;
            } else {
                $clearParams = [
                    ':original_total'     => null,
                    ':override_total'     => null,
                    ':override_reason'    => null,
                    ':rate_overridden'    => 0,
                    ':override_type'      => null,
                    ':override_value'     => null,
                    ':extra_bed_override' => null,
                    ':override_by'        => null,
                    ':override_at'        => null,
                    ':total'              => $currentTotal,
                    ':row_id'             => (int)$row['id'],
                ];
                if ($hasComponentsCol) {
                    $clearParams[':override_components'] = null;
                }
                $upd->execute($clearParams);

                $grandTotal += $currentTotal;
            }
        }

        $nightCount = count($rows);

        $updOrder = $pdo->prepare("
            UPDATE orders
            SET nights = :nights,
                base_price = :base_price,
                amount = :amount,
                totalamount = :totalamount
            WHERE id = :id
        ");
        $updOrder->execute([
            ':nights'      => $nightCount,
            ':base_price'  => round($grandTotal, 2),
            ':amount'      => round($grandTotal, 2),
            ':totalamount' => round($grandTotal, 2),
            ':id'          => $orderId,
        ]);
    }
}

if (!function_exists('persist_preview_rows_to_order')) {
    /*
       Persist the EXACT nightly totals the reservation preview shows.

       This is the source-of-truth save. Instead of rebuilding from rate plans
       and hoping a separate override re-application matches by date, we take the
       preview rows (which already contain night_total with any per-room / total
       overrides applied - the same figure shown on screen) and write each
       night's total straight into rel_orders_prices, plus the override metadata
       so it reopens correctly. If a night carries no override we still write its
       calculated total, so nothing is lost.

       $rows are the output of build_preview_rows_multi() + the override overlay,
       i.e. identical to what the user is looking at when they click Update.
    */
    function persist_preview_rows_to_order(PDO $pdo, int $orderId, array $rows): void
    {
        if ($orderId <= 0 || !$rows) {
            return;
        }

        rf_ensure_nightly_override_columns($pdo);

        $hasComponentsCol = rf_column_exists($pdo, 'rel_orders_prices', 'override_components');
        $componentsSet = $hasComponentsCol ? ", override_components = :override_components" : "";

        /*
           This function persists the CLIENT's nightly preview, which is
           per-night data ($row['night_total']) with no per-room breakdown - so
           unlike apply_nightly_overrides_to_order() it cannot be keyed on a
           row id. It stays keyed on the date.

           What it must NOT do any more is write `total`. rel_orders_prices now
           holds one row per ROOM per night, so writing a NIGHT total to
           `WHERE DATE(date) = :day` set every room on that night to the whole
           night's amount - a six-room night at 1,665.00 became six rows of
           1,665.00 each, and the booking totalled 9,990.00.

           The override columns below ARE per-night and are still written by
           date. `total` is left to rebuild_order_nightly_prices_multi(), which
           computes it per room, EXCEPT where a night is genuinely overridden -
           see the split below.
        */
        $upd = $pdo->prepare("
            UPDATE rel_orders_prices
            SET original_total = :original_total,
                override_total = :override_total,
                override_reason = :override_reason,
                rate_overridden = :rate_overridden,
                override_type = :override_type,
                override_value = :override_value
                {$componentsSet}
            WHERE order_id = :order_id AND DATE(date) = :day
        ");

        /*
           An overridden night IS a night-level amount, so it has to be spread
           back across that night's rooms rather than stamped on each of them.
           Distributed in proportion to each room's calculated total, with the
           rounding remainder going to the first room so the parts sum exactly
           to the override.
        */
        $updTotal = $pdo->prepare("UPDATE rel_orders_prices SET total = :total WHERE id = :row_id");
        $selNight = $pdo->prepare("
            SELECT
                rop.id,
                rop.total,
                rop.room_id,
                COALESCE(rmg.room_no, 0) AS room_slot_no,
                COALESCE(rop.adults_count, rmg.adults, 0) AS adults_count
            FROM rel_orders_prices rop
            LEFT JOIN order_rooming rmg
              ON rmg.order_id = rop.order_id
             AND rmg.room_id = rop.room_id
            WHERE rop.order_id = :order_id
              AND DATE(rop.date) = :day
            ORDER BY COALESCE(rmg.room_no, 999999) ASC, rop.room_id ASC, rop.id ASC
        ");

        $grandTotal = 0.0;

        foreach ($rows as $row) {
            $day = substr((string)($row['date'] ?? ''), 0, 10);
            if ($day === '') {
                continue;
            }

            $isOverridden = (int)($row['rate_overridden'] ?? 0) === 1;
            $nightTotal   = round((float)($row['night_total'] ?? 0), 2);
            $originalTotal = ($row['original_total'] ?? null) !== null
                ? round((float)$row['original_total'], 2)
                : $nightTotal;

            $overrideType = $isOverridden ? (string)($row['override_type'] ?? 'total') : null;
            $overrideValue = $isOverridden && ($row['override_value'] ?? null) !== null
                ? round((float)$row['override_value'], 2)
                : null;

            $params = [
                ':original_total' => $isOverridden ? $originalTotal : null,
                ':override_total' => $isOverridden ? $nightTotal : null,
                ':override_reason'=> $isOverridden ? (string)($row['override_reason'] ?? '') : null,
                ':rate_overridden'=> $isOverridden ? 1 : 0,
                ':override_type'  => $overrideType,
                ':override_value' => $overrideValue,
                ':order_id'       => $orderId,
                ':day'            => $day,
            ];

            if ($hasComponentsCol) {
                $comp = null;
                if ($isOverridden) {
                    if (($row['override_type'] ?? '') === 'per_room') {
                        $roomsMap = (isset($row['room_overrides']) && is_array($row['room_overrides']))
                            ? $row['room_overrides']
                            : ((isset($row['override_components']['per_room']) && is_array($row['override_components']['per_room'])) ? $row['override_components']['per_room'] : null);
                        if ($roomsMap !== null) {
                            $comp = json_encode(['per_room' => $roomsMap], JSON_UNESCAPED_SLASHES);
                        }
                    } elseif (isset($row['override_components']) && is_array($row['override_components'])) {
                        $comp = json_encode($row['override_components'], JSON_UNESCAPED_SLASHES);
                    }
                }
                $params[':override_components'] = $comp;
            }

            $upd->execute($params);

            /*
               Only an OVERRIDDEN night rewrites `total`, and it is distributed
               across that night's rooms in proportion to each room's calculated
               amount - never stamped on all of them. A night with no override
               keeps whatever rebuild_order_nightly_prices_multi() computed per
               room.
            */
            if ($isOverridden) {
                $selNight->execute([':order_id' => $orderId, ':day' => $day]);
                $nightRoomRows = $selNight->fetchAll(PDO::FETCH_ASSOC) ?: [];

                if (count($nightRoomRows) === 1) {
                    $updTotal->execute([
                        ':total'  => $nightTotal,
                        ':row_id' => (int)$nightRoomRows[0]['id'],
                    ]);
                } elseif (count($nightRoomRows) > 1) {
                    /*
                       A per-room override must be written to the room that was
                       edited. The previous proportional split only made the NIGHT
                       total correct; it redistributed that total across every room,
                       so Room 7 at 75/pp became roughly 223.29 like all the others.
                       The preview still showed 75.00 because it read the JSON map,
                       while folio posting read the wrongly distributed `total` rows.
                    */
                    if (($row['override_type'] ?? '') === 'per_room') {
                        $roomsMap = (isset($row['room_overrides']) && is_array($row['room_overrides']))
                            ? $row['room_overrides']
                            : ((isset($row['override_components']['per_room']) && is_array($row['override_components']['per_room']))
                                ? $row['override_components']['per_room']
                                : []);

                        $breakdownByRoom = [];
                        foreach ((array)($row['room_breakdown'] ?? []) as $rb) {
                            $slotKey = (string)($rb['room_no'] ?? '');
                            if ($slotKey !== '') {
                                $breakdownByRoom[$slotKey] = round((float)($rb['room_total'] ?? 0), 2);
                            }
                        }

                        $assigned = 0.0;
                        $lastIdx = count($nightRoomRows) - 1;
                        foreach ($nightRoomRows as $idx => $nr) {
                            $slotKey = (string)((int)($nr['room_slot_no'] ?? 0));
                            $share = array_key_exists($slotKey, $breakdownByRoom)
                                ? (float)$breakdownByRoom[$slotKey]
                                : round((float)($nr['total'] ?? 0), 2);

                            // Final row absorbs only any rounding difference.
                            if ($idx === $lastIdx) {
                                $share = round($nightTotal - $assigned, 2);
                            }

                            $assigned = round($assigned + $share, 2);
                            $updTotal->execute([':total' => $share, ':row_id' => (int)$nr['id']]);
                        }
                    } else {
                        $calcSum = 0.0;
                        foreach ($nightRoomRows as $nr) {
                            $calcSum += (float)$nr['total'];
                        }

                        $assigned = 0.0;
                        $lastIdx = count($nightRoomRows) - 1;
                        foreach ($nightRoomRows as $idx => $nr) {
                            if ($idx === $lastIdx) {
                                $share = round($nightTotal - $assigned, 2);
                            } elseif ($calcSum > 0) {
                                $share = round($nightTotal * ((float)$nr['total'] / $calcSum), 2);
                            } else {
                                $share = round($nightTotal / count($nightRoomRows), 2);
                            }
                            $assigned = round($assigned + $share, 2);
                            $updTotal->execute([':total' => $share, ':row_id' => (int)$nr['id']]);
                        }
                    }
                }
            }

            $grandTotal += $nightTotal;
        }

        $updOrder = $pdo->prepare("
            UPDATE orders
            SET base_price = :base_price,
                amount = :amount,
                totalamount = :totalamount
            WHERE id = :id
        ");
        $updOrder->execute([
            ':base_price'  => round($grandTotal, 2),
            ':amount'      => round($grandTotal, 2),
            ':totalamount' => round($grandTotal, 2),
            ':id'          => $orderId,
        ]);
    }
}
$errors = [];
$loggedInUserId = rf_current_user_id();
rf_ensure_order_audit_schema($pdo);
rf_ensure_booking_colour_schema($pdo);
rf_ensure_room_configuration_schema($pdo);


$bookingSourceOptions = [
    '' => 'Select source',
    'direct' => 'Direct',
    'agent' => 'Agent',
    'ota' => 'OTA',
    'online' => 'Online',
    'walkin' => 'Walk-in',
    'group' => 'Group',
    'corporate' => 'Corporate',
];

$paymentDueOptions = [
    '' => 'Standard / Not set',
    'account' => 'Account',
    'pay_on_arrival' => 'Pay on arrival',
    'chase_payment' => 'Chase payment',
    'online_payment' => 'Online payment',
    'active_out_balance' => 'Active Out - still with balance',
];

$paymentStatusOptions = [
    '0' => 'Unpaid',
    '1' => 'Part paid',
    '2' => 'Paid',
];

$bookingStatusOptions = [
        '2' => 'Provisional',
    '1' => 'Confirmed',

];

$operationalBookingStatusLabels = [
    '0' => 'Cancelled',
    '1' => 'Confirmed',
    '2' => 'Provisional',
    '3' => 'Checked In / In House',
    '4' => 'Checked Out',
    'no_show' => 'No Show',
    'noshow' => 'No Show',
];

/*
   Edit mode fix:
   The form later uses $isEdit and $id, but they must be initialised before
   the prefill/edit block. Without this, reservation_form.php?id=34 opens as
   a new blank reservation instead of loading booking #34.
*/
$id = (int)($_GET['id'] ?? $_GET['order_id'] ?? $_POST['id'] ?? 0);
$isEdit = $id > 0;

/*
   TEMP: on-screen viewer for the override debug log so the save payload can be
   inspected without SSH/file access. Open:
     reservation_form.php?view_override_log=1
   Remove this block (and the debug writes near the save) once resolved.
*/
if (isset($_GET['view_override_log'])) {
    header('Content-Type: text/plain; charset=utf-8');
    $logPath = __DIR__ . '/override_debug.log';
    if (is_file($logPath)) {
        $lines = @file($logPath, FILE_IGNORE_NEW_LINES) ?: [];
        $tail = array_slice($lines, -60);
        echo "=== Last " . count($tail) . " override_debug.log lines ===\n\n";
        echo implode("\n", $tail) . "\n";
    } else {
        echo "No override_debug.log yet. Save a booking with a nightly override first, then reload this URL.\n";
    }
    exit;
}

$nightlyOverrides = normalize_nightly_overrides_json($_POST['nightly_overrides_json'] ?? '');
$success = isset($_GET['saved']) ? 'Reservation saved successfully.' : '';
if (isset($_GET['waitlisted'])) {
    $success = 'Guest added to waitlist successfully.';
}
if (isset($_GET['cancelled'])) {
    $success = 'Reservation cancelled and availability released.';
}

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

/* Load every agent; no record limit. Use company-style names when present. */
$agentNameColumns = [];
foreach (['company', 'Agent', 'RegisteredName', 'registered_name', 'agent_name', 'agency_name', 'name', 'firstname'] as $agentNameColumn) {
    if (rf_column_exists($pdo, 'agents', $agentNameColumn)) {
        $safeAgentNameColumn = str_replace('`', '``', $agentNameColumn);
        $agentNameColumns[] = "NULLIF(TRIM(`{$safeAgentNameColumn}`), '')";
    }
}
if (rf_column_exists($pdo, 'agents', 'firstname') && rf_column_exists($pdo, 'agents', 'lastname')) {
    array_unshift($agentNameColumns, "NULLIF(TRIM(CONCAT(COALESCE(firstname,''), ' ', COALESCE(lastname,''))), '')");
}
if (rf_column_exists($pdo, 'agents', 'email')) {
    $agentNameColumns[] = "NULLIF(TRIM(email), '')";
}
$agentNameSql = $agentNameColumns
    ? "COALESCE(" . implode(', ', array_unique($agentNameColumns)) . ", CONCAT('Agent #', id))"
    : "CONCAT('Agent #', id)";

$agents = $pdo->query("
    SELECT id,
           " . (rf_column_exists($pdo, 'agents', 'firstname') ? "firstname" : "'' AS firstname") . ",
           " . (rf_column_exists($pdo, 'agents', 'lastname') ? "lastname" : "'' AS lastname") . ",
           {$agentNameSql} AS display_name
    FROM agents
    ORDER BY display_name ASC, id ASC
")->fetchAll(PDO::FETCH_ASSOC);

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

    foreach ($consultantRows as $consultantRow) {
        $agentKey = (string)(int)($consultantRow['agent_id'] ?? 0);
        if ($agentKey === '0') {
            continue;
        }
        if (!isset($consultantsByAgent[$agentKey])) {
            $consultantsByAgent[$agentKey] = [];
        }
        $consultantsByAgent[$agentKey][] = [
            'id'        => (int)($consultantRow['id'] ?? 0),
            'firstname' => (string)($consultantRow['firstname'] ?? ''),
            'lastname'  => (string)($consultantRow['lastname'] ?? ''),
            'email'     => (string)($consultantRow['email'] ?? ''),
            'mobile'    => (string)($consultantRow['mobile'] ?? ''),
            'phone'     => (string)($consultantRow['phone'] ?? ''),
        ];
    }
} catch (Throwable $e) {
    $consultantsByAgent = [];
}

$nationalities = $pdo->query("
    SELECT id, nationality
    FROM nationality
    ORDER BY nationality ASC
")->fetchAll(PDO::FETCH_ASSOC);

$occassions = $pdo->query("
    SELECT id, occassion_name
    FROM occassions
    ORDER BY occassion_name ASC
")->fetchAll(PDO::FETCH_ASSOC);
$extraCategories = [];
$extraServices = [];
try {
    $extraCategories = $pdo->query("SELECT id, category FROM tblservicecategory ORDER BY category ASC")->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) { $extraCategories = []; }
try {
    $extraServices = $pdo->query("SELECT s.id, s.service, s.price_per_person, s.category, c.category AS category_name FROM tblservices s LEFT JOIN tblservicecategory c ON c.id = s.category ORDER BY c.category ASC, s.service ASC")->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) { $extraServices = []; }

rf_ensure_booking_extra_agent_payers_schema($pdo);

/*
   Instant-save endpoint for extras while editing an existing reservation.
   The browser sends the complete visible extras grid. Replacing the booking's
   booking-stage extra rows as one transaction keeps master/room folio splits,
   totals and payment status fully reconciled.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['ajax'] ?? '') === 'save_booking_extras') {
    header('Content-Type: application/json; charset=utf-8');
    try {
        $orderId = (int)($_POST['order_id'] ?? 0);
        if ($orderId <= 0) {
            throw new RuntimeException('Save the reservation first before using instant extra updates.');
        }

        $stmt = $pdo->prepare("SELECT id, property_id FROM orders WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $orderRow = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$orderRow) {
            throw new RuntimeException('Reservation not found.');
        }

        $rawJson = (string)($_POST['extras_json'] ?? '[]');
        $decoded = json_decode($rawJson, true);
        if (!is_array($decoded)) {
            throw new RuntimeException('Invalid extras data received.');
        }
        $extras = normalize_booking_extra_rows($decoded);

        $pdo->beginTransaction();
        post_booking_stage_extras($pdo, $orderId, (int)$orderRow['property_id'], $extras);
        rf_log_order_action($pdo, $orderId, (int)($_SESSION['user_id'] ?? $_SESSION['id'] ?? 0), 'extras_inline_saved', count($extras) . ' booking extra(s) saved inline.');
        $pdo->commit();

        echo json_encode([
            'ok' => true,
            'message' => count($extras) ? 'Extras saved.' : 'All booking extras removed.',
            'rows' => fetch_booking_stage_extras_for_edit($pdo, $orderId),
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) { $pdo->rollBack(); }
        http_response_code(422);
        echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    exit;
}

if (isset($_GET['ajax']) && $_GET['ajax'] === 'room_types') {
    header('Content-Type: application/json; charset=utf-8');
    $propertyId = (int)($_GET['property_id'] ?? 0);
    echo json_encode([
        'ok'   => true,
        'rows' => fetch_room_types_by_property($pdo, $propertyId),
    ]);
    exit;
}
if (isset($_GET['ajax']) && $_GET['ajax'] === 'agent_contacts') {
    header('Content-Type: application/json; charset=utf-8');
    $agentId = (int)($_GET['agent_id'] ?? 0);

    try {
        $rows = rf_fetch_agent_contacts($pdo, $agentId);
        echo json_encode([
            'ok' => true,
            'rows' => $rows,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    } catch (Throwable $e) {
        echo json_encode([
            'ok' => false,
            'message' => $e->getMessage(),
            'rows' => [],
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    exit;
}

/*
   Inline "add new agent" - used when the desired agent is missing from the
   dropdown while creating/editing a booking. Keeps the user in-flow instead of
   forcing them to leave and set the agent up elsewhere first.
   Schema: agents(id, firstname[, lastname]). lastname is written only if the
   column exists, for cross-installation safety.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['ajax'] ?? '') === 'create_agent') {
    header('Content-Type: application/json; charset=utf-8');
    try {
        $firstname = trim((string)($_POST['firstname'] ?? ''));
        $lastname  = trim((string)($_POST['lastname'] ?? ''));
        if ($firstname === '') {
            throw new RuntimeException('Agent name is required.');
        }

        // Guard against obvious duplicates on name.
        $dupStmt = $pdo->prepare("SELECT id FROM agents WHERE LOWER(TRIM(firstname)) = LOWER(:fn) LIMIT 1");
        $dupStmt->execute([':fn' => $firstname]);
        $existingId = (int)($dupStmt->fetchColumn() ?: 0);
        if ($existingId > 0) {
            echo json_encode(['ok' => true, 'id' => $existingId, 'firstname' => $firstname, 'duplicate' => true], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            exit;
        }

        $hasLastname = rf_column_exists($pdo, 'agents', 'lastname');
        if ($hasLastname) {
            $ins = $pdo->prepare("INSERT INTO agents (firstname, lastname) VALUES (:fn, :ln)");
            $ins->execute([':fn' => $firstname, ':ln' => $lastname]);
        } else {
            $ins = $pdo->prepare("INSERT INTO agents (firstname) VALUES (:fn)");
            $ins->execute([':fn' => $firstname]);
        }
        $newId = (int)$pdo->lastInsertId();

        echo json_encode([
            'ok'        => true,
            'id'        => $newId,
            'firstname' => $firstname,
            'lastname'  => $lastname,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    } catch (Throwable $e) {
        echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    exit;
}

/*
   Inline "add new consultant/contact" for the selected agent - used when the
   agent has no matching contact in the list. Requires an agent_id so the new
   contact is correctly linked (consultants.agent_id = agents.id).
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['ajax'] ?? '') === 'create_consultant') {
    header('Content-Type: application/json; charset=utf-8');
    try {
        $agentId   = (int)($_POST['agent_id'] ?? 0);
        $firstname = trim((string)($_POST['firstname'] ?? ''));
        $lastname  = trim((string)($_POST['lastname'] ?? ''));
        $email     = trim((string)($_POST['email'] ?? ''));
        $mobile    = trim((string)($_POST['mobile'] ?? ''));

        if ($agentId <= 0) {
            throw new RuntimeException('Select an agent before adding a contact.');
        }
        if ($firstname === '' && $lastname === '') {
            throw new RuntimeException('Contact name is required.');
        }

        // Build column/value list adaptively so we only write columns that exist.
        $cols = ['agent_id', 'firstname', 'lastname'];
        $vals = [':agent_id' => $agentId, ':firstname' => $firstname, ':lastname' => $lastname];
        if (rf_column_exists($pdo, 'consultants', 'email'))  { $cols[] = 'email';  $vals[':email']  = $email; }
        if (rf_column_exists($pdo, 'consultants', 'mobile')) { $cols[] = 'mobile'; $vals[':mobile'] = $mobile; }

        $placeholders = array_keys($vals);
        $sql = "INSERT INTO consultants (" . implode(', ', $cols) . ") VALUES (" . implode(', ', $placeholders) . ")";
        $pdo->prepare($sql)->execute($vals);
        $newId = (int)$pdo->lastInsertId();

        echo json_encode([
            'ok'        => true,
            'id'        => $newId,
            'agent_id'  => $agentId,
            'firstname' => $firstname,
            'lastname'  => $lastname,
            'email'     => $email,
            'mobile'    => $mobile,
            'phone'     => '',
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    } catch (Throwable $e) {
        echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    exit;
}

$form = [
    'id'                 => 0,
    'property_id'        => '',
    'guest_name'         => '',
    'agent_id'           => '',
    'contact_id'         => '',
    'originating_agent'  => '',
    'voucher_no'         => '',
    'occassion_id'       => '1',
    'nationality_id'     => '1',
    'reservation_notes'  => '',
    'internal_memo'      => '',
    'reminder_info'      => '',
    'check_in'           => date('Y-m-d'),
    'expected_arrival_time' => '14:00',
    'check_out'          => date('Y-m-d', strtotime('+1 day')),
    'room_type_id'       => '',
    'totalrooms'         => 1,
    'adults'             => 2,
    'kids'               => 0,
    'kids_0_3'           => 0,
    'waitlist_id'        => 0,
    'booking_extras'     => [],
    'priority'           => 0,
    'booking_status'     => '2',
    'invoiced'           => 0,
    'booking_source'     => '',
    'group_name'         => '',
    'group_leader'       => '',
    'meal_plan'          => 'BB',
    'block_room_posting' => 0,
    'payment_due'        => '',
    'invoice_number'     => '',
    'qrCodeUrl'          => '',
    'verificationCode'   => '',
    'fiscalDeviceId'     => '',
    'globalNumber'       => '',
    'fiscalDayNumber'    => '',
    'payment_status'     => '0',
    'booking_color_code_id' => '',
    'room_rows'          => [
        [
            'row_no'       => 1,
            'room_type_id' => '',
            'qty_rooms'    => 1,
            'adults'       => 2,
            'children'     => 0,
        ]
    ],
];

$currentGuestId = 0;
$roomingRows = [];
$pricingLocked = 0;
$pricingLockedAt = '';
$pricingLockedBy = 0;

$prefillWaitlistId = (int)($_GET['waitlist_id'] ?? $_POST['waitlist_id'] ?? 0);
if (!$isEdit && $prefillWaitlistId > 0) {
    $waitlistRow = fetch_waitlist_by_id($pdo, $prefillWaitlistId);
    if ($waitlistRow && in_array((string)$waitlistRow['status'], ['waiting', 'notified'], true)) {
        $form = array_merge($form, [
            'guest_name'         => (string)($waitlistRow['guest_name'] ?? ''),
            'property_id'        => (string)($waitlistRow['property_id'] ?? ''),
            'agent_id'           => (string)($waitlistRow['agent_id'] ?? ''),
            'contact_id'         => (string)($waitlistRow['contact_id'] ?? ''),
            'originating_agent'  => (string)($waitlistRow['originating_agent'] ?? ''),
            'group_name'         => '',
            'group_leader'       => '',
            'meal_plan'          => 'HB',
            'block_room_posting' => 0,
            'voucher_no'         => (string)($waitlistRow['voucher_no'] ?? ''),
            'occassion_id'       => (string)((int)($waitlistRow['occassion_id'] ?? 0) > 0 ? (int)$waitlistRow['occassion_id'] : 1),
            'nationality_id'     => (string)((int)($waitlistRow['nationality_id'] ?? 0) > 0 ? (int)$waitlistRow['nationality_id'] : 1),
            'reservation_notes'  => (string)($waitlistRow['reservation_notes'] ?? ''),
            'internal_memo'      => (string)($waitlistRow['internal_memo'] ?? ''),
            'reminder_info'      => (string)($waitlistRow['reminder_info'] ?? ''),
            'check_in'           => (string)($waitlistRow['check_in'] ?? ''),
            'expected_arrival_time' => '14:00',
            'check_out'          => (string)($waitlistRow['check_out'] ?? ''),
            'room_type_id'       => (string)($waitlistRow['room_type_id'] ?? ''),
            'totalrooms'         => (int)($waitlistRow['totalrooms'] ?? 1),
            'adults'             => (int)($waitlistRow['adults'] ?? 1),
            'kids'               => (int)($waitlistRow['kids'] ?? 0),
            'kids_0_3'           => (int)($waitlistRow['kids_0_3'] ?? 0),
            'waitlist_id'        => (int)$waitlistRow['id'],
            'priority'           => (int)($waitlistRow['priority'] ?? 0),
            'room_rows'          => [
                [
                    'row_no'       => 1,
                    'room_type_id' => (string)($waitlistRow['room_type_id'] ?? ''),
                    'qty_rooms'    => (int)($waitlistRow['totalrooms'] ?? 1),
                    'adults'       => (int)($waitlistRow['adults'] ?? 1),
                    'children'     => (int)($waitlistRow['kids'] ?? 0),
                ]
            ],
        ]);
        $currentGuestId = (int)($waitlistRow['guest_id'] ?? 0);
    }
}

if ($isEdit && $_SERVER['REQUEST_METHOD'] !== 'POST') {
    $stmt = $pdo->prepare("
        SELECT
            o.*,
            g.firstname,
            g.lastname
        FROM orders o
        LEFT JOIN guests g ON g.id = o.guest_id
        WHERE o.id = :id
        LIMIT 1
    ");
    $stmt->execute([':id' => $id]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($row) {
        $guestName = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));

        /*
           Some older bookings may not have a linked guest row. Keep the page
           usable by falling back to common order-level guest/name fields if present.
        */
        if ($guestName === '') {
            foreach (['guest_name', 'client_name', 'pax_name', 'customer_name'] as $guestField) {
                if (!empty($row[$guestField])) {
                    $guestName = trim((string)$row[$guestField]);
                    break;
                }
            }
        }

        $form = array_merge($form, [
            'id'                 => (int)$row['id'],
            'property_id'        => (string)($row['property_id'] ?? ''),
            'guest_name'         => $guestName,
            'agent_id'           => (string)($row['agent_id'] ?? ''),
            'contact_id'         => (string)($row['contact_id'] ?? ''),
            'originating_agent'  => (string)($row['originating_agent'] ?? ''),
            'voucher_no'         => (string)($row['voucher_no'] ?? ''),
            'occassion_id'       => (string)((int)($row['occassion_id'] ?? 0) > 0 ? (int)$row['occassion_id'] : 1),
            'nationality_id'     => (string)((int)($row['nationality_id'] ?? 0) > 0 ? (int)$row['nationality_id'] : 1),
            'reservation_notes'  => (string)($row['reservation_notes'] ?? ''),
            'internal_memo'      => (string)($row['internal_memo'] ?? ''),
            'reminder_info'      => (string)($row['reminder_info'] ?? ''),
            'check_in'           => (string)($row['check_in'] ?? ''),
            'expected_arrival_time' => !empty($row['expected_arrival_time']) ? substr((string)$row['expected_arrival_time'], 0, 5) : '14:00',
            'check_out'          => (string)($row['check_out'] ?? ''),
            'room_type_id'       => (string)($row['room_type_id'] ?? ''),
            'totalrooms'         => (int)($row['totalrooms'] ?? 1),
            'adults'             => (int)($row['adults'] ?? 2),
            'kids'               => (int)($row['kids'] ?? 0),
            'kids_0_3'           => (int)($row['kids_0_3'] ?? 0),
            'booking_status'     => (string)($row['status'] ?? '1'),
            'invoiced'           => (int)($row['invoiced'] ?? 0),
            'booking_source'     => (string)($row['booking_source'] ?? ''),
            'group_name'         => (string)($row['group_name'] ?? ''),
            'group_leader'       => (string)($row['group_leader'] ?? ''),
            'meal_plan'          => (string)($row['meal_plan'] ?? ''),
            'block_room_posting' => (int)($row['block_room_posting'] ?? 0),
            'payment_due'        => (string)($row['payment_due'] ?? ''),
            'invoice_number'     => (string)($row['invoice_number'] ?? ''),
            'qrCodeUrl'          => (string)($row['qrCodeUrl'] ?? ''),
            'verificationCode'   => (string)($row['verificationCode'] ?? ''),
            'fiscalDeviceId'     => (string)($row['fiscalDeviceId'] ?? ''),
            'globalNumber'       => (string)($row['globalNumber'] ?? ''),
            'fiscalDayNumber'    => (string)($row['fiscalDayNumber'] ?? ''),
            'payment_status'     => (string)($row['payment_status'] ?? '0'),
            'booking_color_code_id' => (string)($row['booking_color_code_id'] ?? ''),
        ]);

        $savedRoomRows = fetch_order_room_rows($pdo, $id);
        if ($savedRoomRows) {
            $form['room_rows'] = [];
            foreach ($savedRoomRows as $rr) {
                $form['room_rows'][] = [
                    'row_no'       => (int)($rr['row_no'] ?? (count($form['room_rows']) + 1)),
                    'room_type_id' => (string)($rr['room_type_id'] ?? ''),
                    'qty_rooms'    => (int)($rr['qty_rooms'] ?? 1),
                    'adults'       => (int)$rr['adults'],
                    'children'     => (int)$rr['children'],
                    'extra_beds_required' => (int)($rr['extra_beds_required'] ?? 0),
                    'cots_required' => (int)($rr['cots_required'] ?? 0),
                    'wheelchair_room_required' => (int)($rr['wheelchair_room_required'] ?? 0),
                    'interleading_required' => (int)($rr['interleading_required'] ?? 0),
                    'room_setup_notes' => (string)($rr['room_setup_notes'] ?? ''),
                    'housekeeping_notes' => (string)($rr['housekeeping_notes'] ?? ''),
                    'special_occasion' => (string)($rr['special_occasion'] ?? ''),
                ];
            }

            $summary = summarize_room_rows($form['room_rows']);
            $form['totalrooms'] = $summary['totalrooms'];
            $form['adults']     = $summary['adults'];
            $form['kids']       = $summary['kids'];
        } else {
            $form['room_rows'] = [
                [
                    'row_no'       => 1,
                    'room_type_id' => (string)($row['room_type_id'] ?? ''),
                    'qty_rooms'    => (int)($row['totalrooms'] ?? 1),
                    'adults'       => (int)($row['adults'] ?? 2),
                    'children'     => (int)($row['kids'] ?? 0),
                    'extra_beds_required' => 0,
                    'cots_required' => 0,
                    'wheelchair_room_required' => 0,
                    'interleading_required' => 0,
                    'room_setup_notes' => '',
                    'housekeeping_notes' => '',
                    'special_occasion' => '',
                ]
            ];
        }

        /*
           Prefill extras already captured at booking stage.
           Without this, edit mode shows accommodation/rooms but extras section appears empty.
        */
        $savedBookingExtras = fetch_booking_stage_extras_for_edit($pdo, $id);
        if ($savedBookingExtras) {
            $form['booking_extras'] = $savedBookingExtras;
        }

        $currentGuestId = (int)($row['guest_id'] ?? 0);
        // Locking disabled by request: always treat pricing as editable so nightly
        // rate edits always rebuild, save, and display exactly as entered.
        $pricingLocked = 0;
        $pricingLockedAt = '';
        $pricingLockedBy = 0;
        // Proactively clear any stale lock left on this booking so it can never
        // route through the old locked-reopen path again.
        if ((int)($row['pricing_locked'] ?? 0) === 1) {
            try {
                $pdo->prepare("UPDATE orders SET pricing_locked = 0, pricing_locked_at = NULL, pricing_locked_by = NULL WHERE id = :id")
                    ->execute([':id' => (int)$row['id']]);
            } catch (Throwable $e) { /* non-fatal */ }
        }
    } else {
        $errors[] = 'Booking not found.';
        $isEdit = false;
        $id = 0;
    }
}

/* Lock / Unlock pricing.
   Locking has been disabled by request. The old "Lock" button now simply
   clears any existing lock instead of setting one, so pricing always stays
   editable and nightly rate edits always save and display as entered. */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['lock_pricing']) && (int)($_POST['id'] ?? 0) > 0) {
    $lockId = (int)$_POST['id'];

    $stmt = $pdo->prepare("
        UPDATE orders
        SET pricing_locked = 0,
            pricing_locked_at = NULL,
            pricing_locked_by = NULL,
            updated_on = NOW(),
            updated_by = :user_id,
            updated_at = NOW()
        WHERE id = :id
    ");
    $stmt->execute([
        ':user_id' => $loggedInUserId,
        ':id'      => $lockId,
    ]);

    header('Location: reservation_form.php?id=' . $lockId . '&saved=1');
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['unlock_pricing']) && (int)($_POST['id'] ?? 0) > 0) {
    $unlockId = (int)$_POST['id'];

    $stmt = $pdo->prepare("
        UPDATE orders
        SET pricing_locked = 0,
            pricing_locked_at = NULL,
            pricing_locked_by = NULL,
            updated_on = NOW(),
            updated_by = :user_id,
            updated_at = NOW()
        WHERE id = :id
    ");
    $stmt->execute([
        ':user_id' => $loggedInUserId,
        ':id'      => $unlockId,
    ]);
    rf_log_order_action($pdo, $unlockId, $loggedInUserId, 'pricing_unlocked', 'Pricing unlocked from reservation form.');

    header('Location: reservation_form.php?id=' . $unlockId . '&saved=1');
    exit;
}

$isSaveAction = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_reservation']));
$isAjaxPreview = ($_SERVER['REQUEST_METHOD'] === 'POST' && (($_GET['ajax'] ?? '') === 'rate_preview'));
$isPreviewOnly = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['preview_only']) && !isset($_POST['save_reservation']));
$isWaitlistAction = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_waitlist']));

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $instantCalendarBooking = !empty($_POST['instant_calendar_booking']);
    $form['id']                = (int)($_POST['id'] ?? 0);
    $form['property_id']       = trim((string)($_POST['property_id'] ?? ''));
    $form['guest_name']        = trim((string)($_POST['guest_name'] ?? ''));
    $form['agent_id']          = trim((string)($_POST['agent_id'] ?? ''));
    $form['check_in']          = trim((string)($_POST['check_in'] ?? ''));
    $form['expected_arrival_time'] = trim((string)($_POST['expected_arrival_time'] ?? '14:00'));
    if ($form['expected_arrival_time'] === '' || !preg_match('/^\d{2}:\d{2}$/', $form['expected_arrival_time'])) {
        $form['expected_arrival_time'] = '14:00';
    }
    $form['check_out']         = trim((string)($_POST['check_out'] ?? ''));
    $form['kids_0_3']          = max(0, (int)($_POST['kids_0_3'] ?? 0));
    $currentGuestId            = (int)($_POST['current_guest_id'] ?? 0);
    $form['contact_id']        = trim((string)($_POST['contact_id'] ?? ''));
    $form['originating_agent'] = trim((string)($_POST['originating_agent'] ?? ''));
    $form['voucher_no']        = trim((string)($_POST['voucher_no'] ?? ''));
    $form['occassion_id']      = trim((string)($_POST['occassion_id'] ?? '1'));
    $form['nationality_id']    = trim((string)($_POST['nationality_id'] ?? '1'));
    if ((int)$form['occassion_id'] <= 0) {
        $form['occassion_id'] = '1';
    }
    if ((int)$form['nationality_id'] <= 0) {
        $form['nationality_id'] = '1';
    }
    $form['reservation_notes'] = trim((string)($_POST['reservation_notes'] ?? ''));
    $form['internal_memo']     = trim((string)($_POST['internal_memo'] ?? ''));
    $form['reminder_info']     = trim((string)($_POST['reminder_info'] ?? ''));
    $form['waitlist_id']       = (int)($_POST['waitlist_id'] ?? $form['waitlist_id'] ?? 0);
    $form['priority']          = max(0, (int)($_POST['priority'] ?? $form['priority'] ?? 0));

    /*
       Booking Status is now selected explicitly on creation.
       Pre-arrival bookings may be Confirmed (1) or Provisional (2).
       If an existing booking has already moved to an operational state
       such as Checked In/Checked Out/Cancelled, preserve that status.
    */
    $postedBookingStatus = trim((string)($_POST['booking_status'] ?? ''));
    $currentOrderStatus  = trim((string)($_POST['current_order_status'] ?? ($form['booking_status'] ?? '2')));
    if (in_array($postedBookingStatus, ['2', '1'], true)) {
        $form['booking_status'] = $postedBookingStatus;
    } elseif (in_array($currentOrderStatus, ['0', '1', '2', '3', '4', 'no_show', 'noshow'], true)) {
        $form['booking_status'] = $currentOrderStatus;
    } else {
        $form['booking_status'] = '2';
    }

    $form['invoiced']          = isset($_POST['invoiced']) ? 1 : 0;
    $form['booking_source']    = trim((string)($_POST['booking_source'] ?? ''));
    $form['group_name']        = trim((string)($_POST['group_name'] ?? ''));
    $form['group_leader']      = trim((string)($_POST['group_leader'] ?? ''));
    $postedMealPlan            = strtoupper(trim((string)($_POST['meal_plan'] ?? '')));
    $form['meal_plan']         = in_array($postedMealPlan, ['BB', 'HB', 'FB'], true) ? $postedMealPlan : '';
    $form['block_room_posting'] = isset($_POST['block_room_posting']) ? 1 : 0;
    $form['payment_due']       = trim((string)($_POST['payment_due'] ?? ''));
    $form['invoice_number']    = trim((string)($_POST['invoice_number'] ?? ''));
    $form['qrCodeUrl']         = trim((string)($_POST['qrCodeUrl'] ?? ''));
    $form['verificationCode']  = trim((string)($_POST['verificationCode'] ?? ''));
    $form['fiscalDeviceId']    = trim((string)($_POST['fiscalDeviceId'] ?? ''));
    $form['globalNumber']      = trim((string)($_POST['globalNumber'] ?? ''));
    $form['fiscalDayNumber']   = trim((string)($_POST['fiscalDayNumber'] ?? ''));
    $form['payment_status']    = trim((string)($_POST['payment_status'] ?? '0'));
    $form['booking_extras']    = normalize_booking_extra_rows(is_array($_POST['booking_extras'] ?? null) ? $_POST['booking_extras'] : []);
    $nightlyOverrides          = normalize_nightly_overrides_json($_POST['nightly_overrides_json'] ?? '');

    /*
       PRIMARY source: nightly_override_full[] - one hidden input per overridden
       night, each a full JSON object (including the per_room rooms map). These
       are written directly into the form on Apply/submit and survive regardless
       of what happens to the single JSON field, which was arriving empty ([]).
       Decode them and merge over whatever the JSON field gave us.
    */
    if (!empty($_POST['nightly_override_full']) && is_array($_POST['nightly_override_full'])) {
        $fullMap = [];
        foreach ($_POST['nightly_override_full'] as $rawEntry) {
            $decoded = json_decode((string)$rawEntry, true);
            if (is_array($decoded) && !empty($decoded['date'])) {
                $fullMap[(string)$decoded['date']] = $decoded;
            }
        }
        if (!empty($fullMap)) {
            $fullNormalized = normalize_nightly_overrides_json($fullMap);
            foreach ($fullNormalized as $d => $ov) {
                $nightlyOverrides[$d] = $ov; // direct inputs win
            }
        }
    }

    $postedNightlyOverrideLines = normalize_nightly_overrides_post_lines($_POST['nightly_override_lines'] ?? []);
    if (!empty($postedNightlyOverrideLines)) {
        /*
           Merge the flat fallback lines over the JSON overrides, but NEVER let a
           fallback line replace a per_room entry that the JSON already carries -
           the fallback line has no rooms map, so replacing would drop the
           per-room detail and make the override snap back after save.
        */
        foreach ($postedNightlyOverrideLines as $lineDate => $lineOverride) {
            $existing = $nightlyOverrides[$lineDate] ?? null;
            if (is_array($existing) && (($existing['type'] ?? '') === 'per_room')) {
                continue; // keep the richer JSON per_room entry
            }
            $nightlyOverrides[$lineDate] = $lineOverride;
        }
    }

    $postedRoomRows = $_POST['room_rows'] ?? [];
    $form['room_rows'] = normalize_room_rows(is_array($postedRoomRows) ? $postedRoomRows : []);

    $seenRoomTypes = [];
    foreach ($form['room_rows'] as $idx => $grp) {
        $rt = (int)($grp['room_type_id'] ?? 0);
        if ($rt > 0) {
            if (isset($seenRoomTypes[$rt])) {
                $errors[] = 'Duplicate room type detected on row ' . ($idx + 1) . '. Combine rows with the same room type.';
                break;
            }
            $seenRoomTypes[$rt] = true;
        }
    }

    $roomSummary = summarize_room_rows($form['room_rows']);
    $form['totalrooms'] = $roomSummary['totalrooms'];
    $form['adults']     = $roomSummary['adults'];
    $form['kids']       = $roomSummary['kids'];

    $form['room_type_id'] = !empty($form['room_rows'][0]['room_type_id'])
        ? (string)$form['room_rows'][0]['room_type_id']
        : '';

    if ((int)$form['id'] <= 0 && !in_array((string)$form['booking_status'], ['1', '2'], true)) {
        $errors[] = 'Please select Confirmed or Provisional booking status.';
    }

    if ((int)$form['id'] > 0) {
        $stmt = $pdo->prepare("
            SELECT pricing_locked, pricing_locked_at, pricing_locked_by,
                   property_id, agent_id, check_in, check_out, room_type_id, totalrooms, adults, kids
            FROM orders
            WHERE id = :id
            LIMIT 1
        ");
        $stmt->execute([':id' => (int)$form['id']]);
        $existingPricing = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($existingPricing) {
            // Locking disabled by request: never treat pricing as locked, so the
            // save is never blocked and nightly edits always persist.
            $pricingLocked   = 0;
            $pricingLockedAt = '';
            $pricingLockedBy = 0;

            $existingRoomRows = fetch_order_room_rows($pdo, (int)$form['id']);
            $normalizedExisting = [];
            foreach ($existingRoomRows as $rr) {
                $normalizedExisting[] = [
                    'room_type_id' => (int)($rr['room_type_id'] ?? 0),
                    'qty_rooms'    => (int)($rr['qty_rooms'] ?? 1),
                    'adults'       => (int)($rr['adults'] ?? 0),
                    'children'     => (int)($rr['children'] ?? 0),
                ];
            }

            $normalizedPosted = [];
            foreach ($form['room_rows'] as $rr) {
                $normalizedPosted[] = [
                    'room_type_id' => (int)($rr['room_type_id'] ?? 0),
                    'qty_rooms'    => (int)($rr['qty_rooms'] ?? 1),
                    'adults'       => (int)($rr['adults'] ?? 0),
                    'children'     => (int)($rr['children'] ?? 0),
                ];
            }

            $pricingDriversChanged =
                (int)$existingPricing['property_id'] !== (int)$form['property_id'] ||
                (int)$existingPricing['agent_id'] !== ((int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : 0) ||
                (string)$existingPricing['check_in'] !== $form['check_in'] ||
                (string)$existingPricing['check_out'] !== $form['check_out'] ||
                json_encode($normalizedExisting) !== json_encode($normalizedPosted);

            if ($pricingLocked && ($isSaveAction || $isPreviewOnly || $isWaitlistAction) && $pricingDriversChanged) {
                $errors[] = 'Pricing is locked. Unlock pricing before changing property, agent, dates, or requested room mix.';
            }
        }
    }

    if ($isAjaxPreview) {
        header('Content-Type: application/json; charset=utf-8');

        $ajaxErrors = $errors;
        $ajaxPreviewRows = [];
        $ajaxPreviewTotal = 0.0;
        $ajaxAvailabilityMessages = [];
        $ajaxTotalInventory = 0;
        $ajaxAvailableRooms = 0;
        $ajaxAvailabilityByType = [];
        $ajaxCanSave = true;
        $ajaxLodgeTotal = 0;
        $ajaxLodgeAvailable = 0;

        try {
            if ((int)$form['property_id'] > 0 && $form['check_in'] !== '' && $form['check_out'] !== '') {
                $ajaxGroupedNeeds = group_room_rows_by_type($form['room_rows']);
                foreach ($ajaxGroupedNeeds as $need) {
                    $roomTypeId = (int)($need['room_type_id'] ?? 0);
                    if ($roomTypeId <= 0) {
                        continue;
                    }

                    $info = count_available_rooms_by_type(
                        $pdo,
                        (int)$form['property_id'],
                        $roomTypeId,
                        $form['check_in'],
                        $form['check_out'],
                        (int)$form['id']
                    );

                    $meta = fetch_room_type_meta($pdo, $roomTypeId);
                    $typeTitle = trim((string)($meta['title'] ?? ''));
                    if ($typeTitle === '') {
                        $typeTitle = 'Type #' . $roomTypeId;
                    }

                    $ajaxTotalInventory += (int)($info['total_rooms'] ?? 0);
                    $ajaxAvailableRooms += (int)($info['available_rooms'] ?? 0);
                    $ajaxAvailabilityByType[$roomTypeId] = [
                        'total_rooms' => (int)($info['total_rooms'] ?? 0),
                        'occupied_rooms' => (int)($info['occupied_rooms'] ?? 0),
                        'available_rooms' => (int)($info['available_rooms'] ?? 0),
                        'rooms_needed' => (int)($need['rooms_needed'] ?? 0),
                        'title' => $typeTitle,
                    ];

                    $ajaxAvailabilityMessages[] = $typeTitle
                        . ': need ' . (int)$need['rooms_needed']
                        . ', available ' . (int)($info['available_rooms'] ?? 0);

                    if ((int)$need['rooms_needed'] > (int)($info['available_rooms'] ?? 0)) {
                        $ajaxCanSave = false;
                    }
                }
            }

            /*
               Whole-lodge availability (ignores room types). When a specific
               type falls short we still surface how many rooms the lodge has
               overall, so the front desk can juggle the calendar rather than
               lose the booking.
            */
            if ((int)$form['property_id'] > 0 && $form['check_in'] !== '' && $form['check_out'] !== '') {
                $lodgeInfo = count_available_rooms_property_wide(
                    $pdo,
                    (int)$form['property_id'],
                    $form['check_in'],
                    $form['check_out'],
                    (int)$form['id']
                );
                $ajaxLodgeTotal = (int)($lodgeInfo['total_rooms'] ?? 0);
                $ajaxLodgeAvailable = (int)($lodgeInfo['available_rooms'] ?? 0);

                if (!$ajaxCanSave && $ajaxLodgeAvailable >= (int)$form['totalrooms'] && (int)$form['totalrooms'] > 0) {
                    $ajaxAvailabilityMessages[] = 'If you ignore room types you can have '
                        . $ajaxLodgeAvailable . ' room(s) across the lodge. Juggle the calendar to create availability.';
                }
            }

            if (!$pricingLocked) {
                $ajaxPreviewRows = build_preview_rows_multi(
                    $pdo,
                    $form['room_rows'],
                    (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
                    $form['check_in'],
                    $form['check_out'],
                    (string)($form['meal_plan'] ?? 'HB')
                );
            } elseif ((int)$form['id'] > 0) {
                $ajaxPreviewRows = build_saved_preview_rows_from_order($pdo, (int)$form['id']);
            }

            $ajaxPreviewRows = apply_nightly_overrides_to_preview_rows($ajaxPreviewRows, $nightlyOverrides);
            foreach ($ajaxPreviewRows as $row) {
                $ajaxPreviewTotal += (float)($row['night_total'] ?? 0);
            }
        } catch (Throwable $e) {
            $ajaxErrors[] = $e->getMessage();
        }

        $ajaxExtrasTotal = 0.0;
        if (!empty($form['booking_extras'])) {
            $servicePriceMap = [];
            foreach ($extraServices as $svc) {
                $servicePriceMap[(int)$svc['id']] = (float)$svc['price_per_person'];
            }
            foreach ($form['booking_extras'] as $extraRow) {
                $svcId = (int)($extraRow['service_id'] ?? 0);
                $qty = (float)($extraRow['quantity'] ?? 0);
                if ($svcId > 0 && $qty > 0 && isset($servicePriceMap[$svcId])) {
                    $unit = $servicePriceMap[$svcId];
                    $ovr = trim((string)($extraRow['override_price'] ?? ''));
                    if ($ovr !== '') {
                        $ovrVal = (float)str_replace(',', '', $ovr);
                        if ($ovrVal > 0) $unit = $ovrVal;
                    }
                    $ajaxExtrasTotal += ($unit * $qty);
                }
            }
        }

        /*
           Summarise which contract priced this booking, for the banner. Worst
           case wins: one rack night on an agent booking matters more than ten
           contracted ones, so it must be what the banner reports.
        */
        $rcSummary = ['plan' => '', 'source' => '', 'meal_plan' => '', 'note' => ''];
        $rcRank = ['agent' => 1, 'agent_derived' => 2, 'public' => 3, 'public_fallback' => 4, 'zero_hold' => 5];
        foreach ($ajaxPreviewRows as $rcRow) {
            $rcSrc = (string)($rcRow['rate_source'] ?? '');
            if ($rcSrc === '') continue;
            if ($rcSummary['plan'] === '') {
                $rcSummary['plan'] = (string)($rcRow['rate_plan'] ?? ($rcRow['rate_plan_title'] ?? ''));
                $rcSummary['meal_plan'] = (string)($rcRow['meal_plan'] ?? ($form['meal_plan'] ?? ''));
            }
            $cur = isset($rcRank[$rcSummary['source']]) ? $rcRank[$rcSummary['source']] : 0;
            $new = isset($rcRank[$rcSrc]) ? $rcRank[$rcSrc] : 0;
            if ($new >= $cur) {
                $rcSummary['source'] = $rcSrc;
                if ($new >= 2) {
                    // Report the plan that triggered the worst case, not the first night.
                    $rcSummary['plan'] = (string)($rcRow['rate_plan'] ?? ($rcRow['rate_plan_title'] ?? $rcSummary['plan']));
                }
            }
            if (!empty($rcRow['derived_from_meal_plan'])) {
                $rcSummary['derived_from'] = (string)$rcRow['derived_from_meal_plan'];
            }
            if (!empty($rcRow['unresolved_meal_plan'])) {
                $rcSummary['unresolved'] = (string)$rcRow['unresolved_meal_plan'];
            }
        }

        echo json_encode([
            'ok' => empty($ajaxErrors),
            'errors' => $ajaxErrors,
            'rate_contract' => $rcSummary,
            'DEBUG_BUILD' => 'ratefix-v4',
            'DEBUG_agent_id' => (int)$form['agent_id'],
            'DEBUG_meal_plan' => (string)($form['meal_plan'] ?? ''),
            'DEBUG_rate_trace' => $GLOBALS['RATE_TRACE'] ?? [],
            'DEBUG_rate_source' => $ajaxPreviewRows[0]['rate_source'] ?? ($ajaxPreviewRows[0]['room_breakdown'][0]['rate_source'] ?? null),
            'DEBUG_rate_plan' => $ajaxPreviewRows[0]['rate_plan'] ?? null,
            'DEBUG_first_night_total' => $ajaxPreviewRows[0]['night_total'] ?? null,
            'rows' => $ajaxPreviewRows,
            'nights' => count($ajaxPreviewRows),
            'total_rooms' => (int)$form['totalrooms'],
            'adults' => (int)$form['adults'],
            'children' => (int)$form['kids'],
            'kids_0_3' => (int)($form['kids_0_3'] ?? 0),
            'total_inventory' => $ajaxTotalInventory,
            'available_rooms' => $ajaxAvailableRooms,
            'lodge_total' => $ajaxLodgeTotal,
            'lodge_available' => $ajaxLodgeAvailable,
            'availability_summary' => implode(' | ', $ajaxAvailabilityMessages),
            'availability_by_type' => $ajaxAvailabilityByType,
            'can_save_direct' => $ajaxCanSave,
            'accommodation_total' => round($ajaxPreviewTotal, 2),
            'extras_total' => round($ajaxExtrasTotal, 2),
            'grand_total' => round($ajaxPreviewTotal + $ajaxExtrasTotal, 2),
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
    if ($isSaveAction || $isWaitlistAction) {
        if ((int)$form['property_id'] <= 0) {
            $errors[] = 'Please select a property.';
        }
        if ($form['guest_name'] === '') {
            $errors[] = 'Please enter guest name.';
        }
        if ((int)$form['agent_id'] <= 0) {
            $errors[] = 'Please select an agent.';
        }
        if (!$instantCalendarBooking && (int)$form['contact_id'] <= 0) {
            $errors[] = 'Please select an agent contact.';
        }
        if ((int)$form['nationality_id'] <= 0) {
            $form['nationality_id'] = '1';
        }
        if (!in_array((string)$form['meal_plan'], ['BB', 'HB', 'FB'], true)) {
            $errors[] = 'Please select meal plan: BB or HB.';
        }
        if ((int)$form['occassion_id'] <= 0) {
            $form['occassion_id'] = '1';
        }
        if ($form['check_in'] === '' || $form['check_out'] === '') {
            $errors[] = 'Please enter check-in and check-out dates.';
        } else {
            $inTs  = strtotime($form['check_in']);
            $outTs = strtotime($form['check_out']);
            if (!$inTs || !$outTs || $outTs <= $inTs) {
                $errors[] = 'Check-out must be at least 1 day after check-in.';
            }
        }

        if (empty($form['room_rows'])) {
            $errors[] = 'Please add at least one room.';
        }

        foreach ($form['room_rows'] as $idx => $roomRow) {
            $rowNo      = $idx + 1;
            $roomTypeId = (int)($roomRow['room_type_id'] ?? 0);
            $qtyRooms   = max(1, (int)($roomRow['qty_rooms'] ?? 1));
            $adults     = max(0, (int)($roomRow['adults'] ?? 0));
            $children   = max(0, (int)($roomRow['children'] ?? 0));
            $totalPax   = $adults + $children;

            if ($roomTypeId <= 0) {
                $errors[] = 'Please select a room type for row ' . $rowNo . '.';
                continue;
            }

            if ($totalPax <= 0) {
                $errors[] = 'Row ' . $rowNo . ' must have at least 1 guest.';
                continue;
            }

            if ($qtyRooms > $totalPax) {
                $errors[] = 'Row ' . $rowNo . ': number of rooms cannot be more than total adults + children.';
            }

            $stmtOcc = $pdo->prepare("
                SELECT title, COALESCE(higher_occupancy, 0) AS higher_occupancy
                FROM room_types
                WHERE id = :id
                LIMIT 1
            ");
            $stmtOcc->execute([':id' => $roomTypeId]);
            $roomTypeInfo = $stmtOcc->fetch(PDO::FETCH_ASSOC) ?: [];
            $roomTypeTitle = (string)($roomTypeInfo['title'] ?? 'Selected room type');
            $higherOccupancy = (int)($roomTypeInfo['higher_occupancy'] ?? 0);

            if ($higherOccupancy > 0) {
                $maxPaxAllowed = $higherOccupancy * $qtyRooms;
                if ($totalPax > $maxPaxAllowed) {
                    $errors[] = 'Row ' . $rowNo . ': ' . $roomTypeTitle . ' allows maximum '
                        . $higherOccupancy . ' guest(s) per room x ' . $qtyRooms
                        . ' room(s) = ' . $maxPaxAllowed . ' guest(s).';
                }
            }
        }

        $insufficientAvailabilityForBooking = false;
        try {
            $availabilityErrors = validate_room_type_availability_for_booking(
                $pdo,
                (int)$form['property_id'],
                $form['check_in'],
                $form['check_out'],
                $form['room_rows'],
                (int)$form['id'],
                $isWaitlistAction
            );

            if ($availabilityErrors) {
                $insufficientAvailabilityForBooking = true;
                if (!$isWaitlistAction) {
                    foreach ($availabilityErrors as $availabilityError) {
                        $errors[] = $availabilityError;
                    }
                }
            }
        } catch (Throwable $e) {
            $insufficientAvailabilityForBooking = true;
            if (!$isWaitlistAction) {
                $errors[] = $e->getMessage();
            }
        }

        $previewRowsForValidation = build_preview_rows_multi(
            $pdo,
            $form['room_rows'],
            (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
            $form['check_in'],
            $form['check_out'],
            (string)($form['meal_plan'] ?? 'HB')
        );

        // Missing rate plans no longer block the booking. Any night without an
        // active rate plan is held at zero price (see rf_zero_rate_plan) so the
        // rooms can be secured and priced later. We surface a non-blocking note.
        $hasMissingRateNight = false;
        foreach ($previewRowsForValidation as $r) {
            if ($r['applied_rate_type'] === 'missing_rate') {
                $hasMissingRateNight = true;
                break;
            }
        }

        if ($isSaveAction && !empty($form['booking_extras'])) {
            foreach ($form['booking_extras'] as $extraIdx => $extraRow) {
                if ((int)$extraRow['category_id'] <= 0) $errors[] = 'Extra row ' . ($extraIdx + 1) . ': please select a category.';
                if ((int)$extraRow['service_id'] <= 0) $errors[] = 'Extra row ' . ($extraIdx + 1) . ': please select a service.';
                if ((float)$extraRow['quantity'] <= 0) $errors[] = 'Extra row ' . ($extraIdx + 1) . ': quantity must be greater than zero.';
            }
        }
        if (!$errors) {
            try {
                $pdo->beginTransaction();

                lock_requested_room_type_inventory($pdo, (int)$form['property_id'], $form['room_rows']);
                validate_room_type_availability_for_booking(
                    $pdo,
                    (int)$form['property_id'],
                    $form['check_in'],
                    $form['check_out'],
                    $form['room_rows'],
                    (int)$form['id'],
                    false
                );

                $guestId = find_or_create_guest_by_name($pdo, $form['guest_name'], $currentGuestId);
                $primaryRoomTypeId = !empty($form['room_rows'][0]['room_type_id'])
                    ? (int)$form['room_rows'][0]['room_type_id']
                    : null;

                if ($isWaitlistAction) {
                    $waitlistId = save_waitlist_entry($pdo, [
                        'id'                 => (int)$form['waitlist_id'],
                        'property_id'        => (int)$form['property_id'],
                        'guest_id'           => $guestId,
                        'guest_name'         => $form['guest_name'],
                        'agent_id'           => (int)$form['agent_id'],
                        'contact_id'         => (int)$form['contact_id'],
                        'originating_agent'  => (int)$form['originating_agent'],
                        'voucher_no'         => $form['voucher_no'],
                        'occassion_id'       => (int)$form['occassion_id'] > 0 ? (int)$form['occassion_id'] : 1,
                        'nationality_id'     => (int)$form['nationality_id'] > 0 ? (int)$form['nationality_id'] : 1,
                        'reservation_notes'  => $form['reservation_notes'],
                        'internal_memo'      => $form['internal_memo'],
                        'reminder_info'      => $form['reminder_info'],
                        'check_in'           => $form['check_in'],
                        'check_out'          => $form['check_out'],
                        'room_type_id'       => (int)$primaryRoomTypeId,
                        'totalrooms'         => (int)$form['totalrooms'],
                        'adults'             => (int)$form['adults'],
                        'kids'               => (int)$form['kids'],
                        'priority'           => (int)$form['priority'],
                        'status'             => 'waiting',
                        'source'             => $prefillWaitlistId > 0 ? 'waitlist_page' : 'reservation_form',
                        'available_rooms_last' => 0,
                    ], (int)($_SESSION['user_id'] ?? 0));

                    refresh_waitlist_matches($pdo, (int)$form['property_id'], (int)$primaryRoomTypeId);
                    $pdo->commit();
                    header('Location: waitlist.php?id=' . $waitlistId . '&saved=1');
                    exit;
                }

                $orderId = (int)$form['id'];
                // Remember whether this POST is creating a brand-new booking.
                // Once the INSERT runs, $orderId contains the new id, so this
                // flag is required to distinguish create from edit at redirect time.
                $isNewBooking = ($orderId <= 0);

                if ($orderId > 0) {
                    $stmt = $pdo->prepare("
                        UPDATE orders
                        SET property_id = :property_id,
                            guest_id = :guest_id,
                            agent_id = :agent_id,
                            contact_id = :contact_id,
                            originating_agent = :originating_agent,
                            voucher_no = :voucher_no,
                            occassion_id = :occassion_id,
                            nationality_id = :nationality_id,
                            reservation_notes = :reservation_notes,
                            internal_memo = :internal_memo,
                            reminder_info = :reminder_info,
                            check_in = :check_in,
                            expected_arrival_time = :expected_arrival_time,
                            check_out = :check_out,
                            adults = :adults,
                            kids = :kids,
                            room_type_id = :room_type_id,
                            totalrooms = :totalrooms,
                            status = :booking_status,
                            room_id = 0,
                            updated_on = NOW(),
                            updated_by = :updated_by,
                            updated_at = NOW()
                        WHERE id = :id
                    ");
                    $stmt->execute([
                        ':property_id'       => (int)$form['property_id'],
                        ':guest_id'          => $guestId,
                        ':agent_id'          => (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
                        ':contact_id'        => (int)$form['contact_id'] > 0 ? (int)$form['contact_id'] : null,
                        ':originating_agent' => $form['originating_agent'] !== '' ? $form['originating_agent'] : null,
                        ':voucher_no'        => $form['voucher_no'] !== '' ? $form['voucher_no'] : null,
                        ':occassion_id'      => (int)$form['occassion_id'] > 0 ? (int)$form['occassion_id'] : 1,
                        ':nationality_id'    => (int)$form['nationality_id'] > 0 ? (int)$form['nationality_id'] : 1,
                        ':reservation_notes' => $form['reservation_notes'] !== '' ? $form['reservation_notes'] : null,
                        ':internal_memo'     => $form['internal_memo'] !== '' ? $form['internal_memo'] : null,
                        ':reminder_info'     => $form['reminder_info'] !== '' ? $form['reminder_info'] : null,
                        ':check_in'          => $form['check_in'],
                        ':expected_arrival_time' => $form['expected_arrival_time'],
                        ':check_out'         => $form['check_out'],
                        ':adults'            => (int)$form['adults'],
                        ':kids'              => (int)$form['kids'],
                        ':room_type_id'      => $primaryRoomTypeId,
                        ':totalrooms'        => (int)$form['totalrooms'],
                        ':booking_status'    => in_array((string)$form['booking_status'], ['0','1','2','3','4'], true) ? (int)$form['booking_status'] : 1,
                        ':updated_by'        => $loggedInUserId > 0 ? $loggedInUserId : null,
                        ':id'                => $orderId,
                    ]);
                    rf_log_order_action($pdo, $orderId, $loggedInUserId, 'updated', 'Reservation updated from reservation form.');
                } else {
                    $orderNo = next_order_no($pdo);

                    $stmt = $pdo->prepare("
                        INSERT INTO orders
                        (
                            property_id,
                            order_no,
                            guest_id,
                            agent_id,
                            contact_id,
                            originating_agent,
                            voucher_no,
                            occassion_id,
                            nationality_id,
                            reservation_notes,
                            internal_memo,
                            reminder_info,
                            check_in,
                            expected_arrival_time,
                            check_out,
                            adults,
                            kids,
                            room_type_id,
                            totalrooms,
                            room_id,
                            ordered_on,
                            created_by,
                            updated_on,
                            updated_by,
                            updated_at,
                            status,
                            payment_status,
                            currency,
                            currency_unit,
                            is_new
                        )
                        VALUES
                        (
                            :property_id,
                            :order_no,
                            :guest_id,
                            :agent_id,
                            :contact_id,
                            :originating_agent,
                            :voucher_no,
                            :occassion_id,
                            :nationality_id,
                            :reservation_notes,
                            :internal_memo,
                            :reminder_info,
                            :check_in,
                            :expected_arrival_time,
                            :check_out,
                            :adults,
                            :kids,
                            :room_type_id,
                            :totalrooms,
                            0,
                            NOW(),
                            :created_by,
                            NOW(),
                            :updated_by,
                            NOW(),
                            :booking_status,
                            0,
                            'USD',
                            1.0000,
                            1
                        )
                    ");
                    $stmt->execute([
                        ':property_id'       => (int)$form['property_id'],
                        ':order_no'          => $orderNo,
                        ':guest_id'          => $guestId,
                        ':agent_id'          => (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
                        ':contact_id'        => (int)$form['contact_id'] > 0 ? (int)$form['contact_id'] : null,
                        ':originating_agent' => $form['originating_agent'] !== '' ? $form['originating_agent'] : null,
                        ':voucher_no'        => $form['voucher_no'] !== '' ? $form['voucher_no'] : null,
                        ':occassion_id'      => (int)$form['occassion_id'] > 0 ? (int)$form['occassion_id'] : 1,
                        ':nationality_id'    => (int)$form['nationality_id'] > 0 ? (int)$form['nationality_id'] : 1,
                        ':reservation_notes' => $form['reservation_notes'] !== '' ? $form['reservation_notes'] : null,
                        ':internal_memo'     => $form['internal_memo'] !== '' ? $form['internal_memo'] : null,
                        ':reminder_info'     => $form['reminder_info'] !== '' ? $form['reminder_info'] : null,
                        ':check_in'          => $form['check_in'],
                        ':expected_arrival_time' => $form['expected_arrival_time'],
                        ':check_out'         => $form['check_out'],
                        ':adults'            => (int)$form['adults'],
                        ':kids'              => (int)$form['kids'],
                        ':room_type_id'      => $primaryRoomTypeId,
                        ':totalrooms'        => (int)$form['totalrooms'],
                        ':booking_status'    => in_array((string)$form['booking_status'], ['1','2'], true) ? (int)$form['booking_status'] : 1,
                        ':created_by'        => $loggedInUserId > 0 ? $loggedInUserId : null,
                        ':updated_by'        => $loggedInUserId > 0 ? $loggedInUserId : null,
                    ]);

                    $orderId = (int)$pdo->lastInsertId();
                    rf_log_order_action($pdo, $orderId, $loggedInUserId, 'created', 'Reservation created from reservation form.');
                }

                rf_save_booking_tracking_fields($pdo, $orderId, $form, $loggedInUserId);

                save_order_rooming_rows(
                    $pdo,
                    $orderId,
                    (int)$form['property_id'],
                    $form['check_in'],
                    $form['check_out'],
                    $form['room_rows']
                );

                /*
                   Assign physical rooms.

                   BUG THIS FIXES:
                   auto_assign_rooming_best_fit() was defined but NEVER CALLED.
                   save_order_rooming_rows() only PRESERVES assignments across an
                   edit - it has no way to pick a room for a slot that does not
                   already have one. A new booking therefore has no old rows to
                   preserve from, every slot was inserted with room_id = 0, and
                   the booking dropped into the unassigned/holding tray even when
                   the lodge was almost empty.

                   The function itself is sound: it keeps a valid existing
                   assignment, honours cot / extra bed / wheelchair / interleading
                   requirements, falls back to any free room of the right type
                   (warning Front Desk that setup needs a look), and tracks
                   intra-booking claims so two slots cannot grab the same room.
                   It just was not wired up.

                   Non-fatal by design: if assignment fails the reservation is
                   still saved, and lands in holding exactly as it does today.
                   Losing the booking would be far worse than not assigning it.
                */
                try {
                    auto_assign_rooming_best_fit($pdo, $orderId);
                } catch (Throwable $e) {
                    $_SESSION['reservation_assignment_warning'] = trim(
                        (string)($_SESSION['reservation_assignment_warning'] ?? '')
                        . ' Rooms could not be auto-assigned (' . $e->getMessage()
                        . '). The booking was saved and is awaiting manual assignment.'
                    );
                }

                /*
                   TEMP DIAGNOSTIC (safe, non-fatal): record what nightly-override
                   payload the server actually received at save time and the DB
                   totals before/after applying it. Check the file
                   /rooms/modules/reservations/override_debug.log after saving.
                   Remove this block once the revert issue is confirmed fixed.
                */
                try {
                    $dbgBefore = $pdo->prepare("SELECT date, total, rate_overridden, override_total FROM rel_orders_prices WHERE order_id = :id ORDER BY date");
                    $dbgBefore->execute([':id' => $orderId]);
                    $dbgRowsBefore = $dbgBefore->fetchAll(PDO::FETCH_ASSOC);
                    $dbgLine = "[" . date('Y-m-d H:i:s') . "] BUILD=directinput order={$orderId} pricingLocked=" . (int)$pricingLocked . "\n"
                        . "    POST_json=" . substr((string)($_POST['nightly_overrides_json'] ?? '(none)'), 0, 1000) . "\n"
                        . "    POST_lines=" . substr(json_encode($_POST['nightly_override_lines'] ?? '(none)'), 0, 600) . "\n"
                        . "    POST_full=" . substr(json_encode($_POST['nightly_override_full'] ?? '(none)'), 0, 800) . "\n"
                        . "    parsed_overrides=" . json_encode($nightlyOverrides) . "\n"
                        . "    prices_before=" . json_encode($dbgRowsBefore) . "\n";
                    @file_put_contents(__DIR__ . '/override_debug.log', $dbgLine, FILE_APPEND);
                } catch (Throwable $e) { /* ignore */ }

                if (!$pricingLocked) {
                    rebuild_order_nightly_prices_multi($pdo, $orderId);
                    apply_nightly_overrides_to_order($pdo, $orderId, $nightlyOverrides, (int)($_SESSION['user_id'] ?? 0));
                } else {
                    /*
                       Even when pricing is locked, the user can still edit a nightly
                       override from the pricing table. Previously this block was skipped,
                       so Update Reservation kept the old override in rel_orders_prices.
                       This applies only the posted nightly override values and does not
                       rebuild rate-plan pricing or rooming.
                    */
                    apply_nightly_overrides_to_order($pdo, $orderId, $nightlyOverrides, (int)($_SESSION['user_id'] ?? 0));
                }

                /*
                   SOURCE-OF-TRUTH SAVE (the fix for "reverts to old rate on Update").
                   Build the preview rows the same way the on-screen preview does -
                   base rates from build_preview_rows_multi(), then the SAME override
                   overlay used for display - and persist those exact night totals.
                   This guarantees what you saved equals what you saw (e.g. an edited
                   160/pp night saves as its computed total, not the rate-plan rate),
                   independent of how the override was passed. Fully guarded so a
                   failure here can never break the save.
                */
                try {
                    $saveRows = build_preview_rows_multi(
                        $pdo,
                        $form['room_rows'],
                        (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
                        $form['check_in'],
                        $form['check_out'],
                        normalize_meal_plan_for_rates($form['meal_plan'] ?? 'HB')
                    );
                    if (!empty($nightlyOverrides)) {
                        $saveRows = apply_nightly_overrides_to_preview_rows($saveRows, $nightlyOverrides);
                    }
                    persist_preview_rows_to_order($pdo, $orderId, $saveRows);
                } catch (Throwable $persistErr) {
                    if (function_exists('error_log')) {
                        error_log('persist_preview_rows_to_order failed (booking saved, totals may need refresh): ' . $persistErr->getMessage());
                    }
                }

                /* TEMP DIAGNOSTIC: capture prices AFTER applying the override. */
                try {
                    $dbgAfter = $pdo->prepare("SELECT date, total, rate_overridden, override_total, override_type FROM rel_orders_prices WHERE order_id = :id ORDER BY date");
                    $dbgAfter->execute([':id' => $orderId]);
                    @file_put_contents(__DIR__ . '/override_debug.log',
                        "    prices_after_apply=" . json_encode($dbgAfter->fetchAll(PDO::FETCH_ASSOC)) . "\n",
                        FILE_APPEND);
                } catch (Throwable $e) { /* ignore */ }

                $unassignedStmt = $pdo->prepare("
                    SELECT COUNT(*)
                    FROM order_rooming
                    WHERE order_id = :order_id
                      AND COALESCE(room_id, 0) <= 0
                ");
                $unassignedStmt->execute([':order_id' => $orderId]);
                $unassignedRoomSlots = (int)$unassignedStmt->fetchColumn();
                if ($unassignedRoomSlots > 0) {
                    /*
                       Do not roll back the reservation simply because a physical room
                       could not be auto-assigned. The booking, rate and room-type demand
                       are valid; leave those slots unassigned so Front Desk can allocate
                       them manually from the calendar/rooming screen.
                    */
                    $_SESSION['reservation_assignment_warning'] = $unassignedRoomSlots
                        . ' room slot(s) were saved but could not be auto-assigned to a physical room. Please assign them manually from the calendar.';
                }

                if (rf_column_exists($pdo, 'orders', 'kids_0_3')) {
                    $pdo->prepare("UPDATE orders SET kids_0_3 = :kids_0_3, updated_on = NOW(), updated_by = :updated_by, updated_at = NOW() WHERE id = :id")->execute([
                        ':kids_0_3'    => (int)$form['kids_0_3'],
                        ':updated_by' => $loggedInUserId > 0 ? $loggedInUserId : null,
                        ':id'          => $orderId,
                    ]);
                }

                /*
                   Keep folio accounting current immediately after saving the booking.
                   Previously, payments.php was the first page to post/recalculate room charges,
                   so reservation_list.php showed wrong/blank totals until Payments was opened.
                */
                rf_rebuild_master_folio_room_charges_from_prices($pdo, $orderId);

                if ($isSaveAction) {
                    post_booking_stage_extras($pdo, $orderId, (int)$form['property_id'], $form['booking_extras']);
                }

                if ((int)$form['waitlist_id'] > 0) {
                    mark_waitlist_converted($pdo, (int)$form['waitlist_id'], $orderId);
                }
                refresh_waitlist_matches($pdo, (int)$form['property_id'], (int)$primaryRoomTypeId);

                $pdo->commit();

                /* -----------------------------------------------------------
                   Nightsbridge availability push (internal-booking path).
                   The NB webhook (new_api_booking.php) already pushes for
                   channel-originated bookings, but bookings created/edited
                   inside RoomZ had no push at all - so channel availability
                   went stale (e.g. a group booking holding rooms of a type
                   that never got re-pushed). Push here, AFTER commit, keyed
                   off the room types this order actually holds (from the
                   rooming rows, not just the header type, so group bookings
                   that span multiple types push every affected type).

                   Fully guarded and non-fatal: a push failure must never
                   break the save - the nightly cron flush will self-correct.
                ----------------------------------------------------------- */
                try {
                    if (!function_exists('nb_run_inventory_push')) {
                        if (!defined('NB_INVENTORY_PUSH_LIB')) {
                            define('NB_INVENTORY_PUSH_LIB', true);
                        }
                        foreach ([
                            __DIR__ . '/../../api/nightsbridge_inventory_push.php',
                            __DIR__ . '/../api/nightsbridge_inventory_push.php',
                        ] as $nbLibPath) {
                            if (is_file($nbLibPath)) {
                                require_once $nbLibPath;
                                break;
                            }
                        }
                    }

                    if (function_exists('nb_run_inventory_push')) {
                        // Only push if this order belongs to the Nightsbridge-
                        // mapped property (the push lib computes availability
                        // for NIGHTSBRIDGE_PROPERTY_ID only).
                        $nbPropertyMatches = !defined('NIGHTSBRIDGE_PROPERTY_ID')
                            || (int)$form['property_id'] === (int)NIGHTSBRIDGE_PROPERTY_ID;

                        // Distinct room types this order holds, from rooming rows.
                        $nbTypeStmt = $pdo->prepare("
                            SELECT DISTINCT room_type_id
                            FROM order_rooming
                            WHERE order_id = :oid
                              AND room_type_id IS NOT NULL
                              AND room_type_id > 0
                        ");
                        $nbTypeStmt->execute([':oid' => $orderId]);
                        $nbRoomTypeIds = array_map('intval', $nbTypeStmt->fetchAll(PDO::FETCH_COLUMN));

                        // Fallback to the header room type if there are no
                        // rooming rows (e.g. a header-only quick capture).
                        if (!$nbRoomTypeIds && $primaryRoomTypeId) {
                            $nbRoomTypeIds = [(int)$primaryRoomTypeId];
                        }

                        if ($nbPropertyMatches && $nbRoomTypeIds) {
                            nb_run_inventory_push($pdo, $nbRoomTypeIds, 'reservation_form', false);
                        }
                    }
                } catch (Throwable $nbPushError) {
                    if (function_exists('error_log')) {
                        error_log('RoomZ->Nightsbridge push after save failed (booking saved OK, nightly flush will correct): ' . $nbPushError->getMessage());
                    }
                }

                /*
                   New reservations automatically open the proforma endpoint in
                   download mode. Editing an existing reservation keeps the old
                   behaviour and returns to the reservation list without creating
                   or downloading another proforma.
                */
                if (!empty($instantCalendarBooking)) {
                    $returnPropertyId = max(0, (int)($_POST['calendar_return_property_id'] ?? $form['property_id'] ?? 0));
                    header('Location: calendar.php?' . http_build_query([
                        'property_id' => $returnPropertyId,
                        'from' => $form['check_in'],
                        'to' => date('Y-m-d', strtotime($form['check_out'] . ' +7 days')),
                        'instant_saved' => $orderId,
                    ]));
                } elseif (!empty($isNewBooking)) {
                    header('Location: /rooms/modules/payments/proforma_invoice.php?order_id=' . $orderId . '&download=1');
                } else {
                    header('Location: reservation_list.php?updated=1');
                }
                exit;
            } catch (Throwable $e) {
                if ($pdo->inTransaction()) {
                    $pdo->rollBack();
                }
                $errors[] = $e->getMessage();
            }
        }
    }

}

$roomTypes = fetch_room_types_by_property($pdo, (int)$form['property_id']);

$roomTypeAvailabilityAll = [];
if ((int)$form['property_id'] > 0 && $form['check_in'] !== '' && $form['check_out'] !== '') {
    foreach ($roomTypes as $rtAvail) {
        $rtId = (int)($rtAvail['id'] ?? 0);
        if ($rtId <= 0) {
            continue;
        }
        $roomTypeAvailabilityAll[$rtId] = count_available_rooms_by_type(
            $pdo,
            (int)$form['property_id'],
            $rtId,
            $form['check_in'],
            $form['check_out'],
            (int)$form['id']
        );
    }
}

$contacts = [];
$selectedAgentId = (int)($form['agent_id'] ?? 0);
if ($selectedAgentId > 0) {
    $contacts = rf_fetch_agent_contacts($pdo, $selectedAgentId);
}

if ($pricingLocked && (int)$form['id'] > 0) {
    $previewRows = build_saved_preview_rows_from_order($pdo, (int)$form['id']);
} else {
    $previewRows = build_preview_rows_multi(
        $pdo,
        $form['room_rows'],
        (int)$form['agent_id'] > 0 ? (int)$form['agent_id'] : null,
        $form['check_in'],
        $form['check_out']
    );
}

if ((int)$form['id'] > 0) {
    $previewRows = apply_saved_nightly_overrides_to_preview_rows($pdo, (int)$form['id'], $previewRows);
}
if ($nightlyOverrides) {
    $previewRows = apply_nightly_overrides_to_preview_rows($previewRows, $nightlyOverrides);
}
$nightlyOverridesForClient = preview_rows_to_nightly_overrides($previewRows);

/*
   TEMP ON-PAGE DIAGNOSTIC. Shows the raw rel_orders_prices values actually
   stored for this booking, plus the computed display totals, so we can see at a
   glance whether the DB holds the edited price or the rate-plan price. Visible
   only when the page is opened with &pxdebug=1. Remove once resolved.
*/
$pxDebugHtml = '';
if (isset($_GET['pxdebug']) && (int)$form['id'] > 0) {
    try {
        $pxStmt = $pdo->prepare("SELECT date, total, rate_overridden, override_total, override_type, override_components FROM rel_orders_prices WHERE order_id = :id ORDER BY date");
        $pxStmt->execute([':id' => (int)$form['id']]);
        $pxRows = $pxStmt->fetchAll(PDO::FETCH_ASSOC);
        $pxOrder = $pdo->prepare("SELECT base_price, amount, totalamount, pricing_locked FROM orders WHERE id = :id");
        $pxOrder->execute([':id' => (int)$form['id']]);
        $pxO = $pxOrder->fetch(PDO::FETCH_ASSOC);
        $pxDebugHtml = '<div style="background:#111;color:#0f0;font-family:monospace;font-size:12px;padding:12px;border-radius:8px;margin:12px 0;white-space:pre-wrap;">'
            . 'BUILD=persist  order=' . (int)$form['id'] . '  pricing_locked=' . (int)($pxO['pricing_locked'] ?? -1) . "\n"
            . 'orders: base_price=' . h((string)($pxO['base_price'] ?? '')) . ' amount=' . h((string)($pxO['amount'] ?? '')) . ' totalamount=' . h((string)($pxO['totalamount'] ?? '')) . "\n"
            . 'rel_orders_prices (what is ACTUALLY stored):' . "\n"
            . h(json_encode($pxRows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
            . '</div>';
    } catch (Throwable $e) {
        $pxDebugHtml = '<div style="background:#300;color:#f88;padding:8px;">pxdebug error: ' . h($e->getMessage()) . '</div>';
    }
}


/*
   Fresh availability map for the current form stay.
   This keeps the room dropdown data-available and JavaScript validation aligned
   with the exact check-in/check-out currently being booked.
*/
$roomTypeAvailabilityCurrent = [];
foreach ($roomTypes as $rtAvailabilityRow) {
    $roomTypeAvailabilityCurrent[(int)$rtAvailabilityRow['id']] = count_available_rooms_by_type(
        $pdo,
        (int)$form['property_id'],
        (int)$rtAvailabilityRow['id'],
        $form['check_in'],
        $form['check_out'],
        (int)$form['id']
    );
}

$previewTotal = 0.0;
foreach ($previewRows as $r) {
    $previewTotal += (float)$r['night_total'];
}

$bookingExtrasInitialTotal = 0.0;
if (!empty($form['booking_extras'])) {
    $servicePriceMapInitial = [];
    foreach ($extraServices as $svc) {
        $servicePriceMapInitial[(int)$svc['id']] = (float)$svc['price_per_person'];
    }
    foreach ($form['booking_extras'] as $extraRow) {
        $svcId = (int)($extraRow['service_id'] ?? 0);
        $qty = (float)($extraRow['quantity'] ?? 0);
        if ($svcId > 0 && $qty > 0 && isset($servicePriceMapInitial[$svcId])) {
            $unit = $servicePriceMapInitial[$svcId];
            $ovr = trim((string)($extraRow['override_price'] ?? ''));
            if ($ovr !== '') {
                $ovrVal = (float)str_replace(',', '', $ovr);
                if ($ovrVal > 0) $unit = $ovrVal;
            }
            $bookingExtrasInitialTotal += $unit * $qty;
        }
    }
}
$bookingGrandInitialTotal = $previewTotal + $bookingExtrasInitialTotal;
$availabilityInfo = [];
$requestedRoomsNow = (int)$form['totalrooms'];
$canSaveDirectNow = true;
$availabilityTotalInventory = 0;
$availabilityTotalAvailable = 0;

$groupedRoomNeeds = group_room_rows_by_type($form['room_rows']);
foreach ($groupedRoomNeeds as $need) {
    /*
       Always recalculate availability for the exact stay currently on the form.
       Do not trust $roomTypeAvailabilityAll here because it can be built before
       date changes or from an older page-load state, causing false "no rooms"
       messages even when the database count shows availability.
    */
    $info = count_available_rooms_by_type(
        $pdo,
        (int)$form['property_id'],
        (int)$need['room_type_id'],
        $form['check_in'],
        $form['check_out'],
        (int)$form['id']
    );
    $availabilityInfo[(int)$need['room_type_id']] = $info;

    $availabilityTotalInventory += (int)($info['total_rooms'] ?? 0);
    $availabilityTotalAvailable += (int)($info['available_rooms'] ?? 0);

    if ((int)$need['rooms_needed'] > (int)$info['available_rooms']) {
        $canSaveDirectNow = false;
    }
}

/*
   Whole-lodge availability (ignores room types) for the current stay.
   Shown as a faint secondary badge and used to prompt a calendar juggle
   when a specific type is full but the lodge still has capacity.
*/
$availabilityLodgeTotal = 0;
$availabilityLodgeAvailable = 0;
if ((int)$form['property_id'] > 0 && $form['check_in'] !== '' && $form['check_out'] !== '') {
    $lodgeInfoNow = count_available_rooms_property_wide(
        $pdo,
        (int)$form['property_id'],
        $form['check_in'],
        $form['check_out'],
        (int)$form['id']
    );
    $availabilityLodgeTotal = (int)($lodgeInfoNow['total_rooms'] ?? 0);
    $availabilityLodgeAvailable = (int)($lodgeInfoNow['available_rooms'] ?? 0);
}

$showWaitlistCallout = !empty($groupedRoomNeeds) && !$canSaveDirectNow;
$notifiedWaitlists = fetch_notified_waitlists($pdo, 5);

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

/* Rooms currently allocated to this booking (edit mode only). */
$currentlyAllocatedRooms = 0;
if ((int)$form['id'] > 0 && !empty($roomingRows)) {
    foreach ($roomingRows as $rrAlloc) {
        if ((int)($rrAlloc['room_id'] ?? 0) > 0) {
            $currentlyAllocatedRooms++;
        }
    }
}

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

<div class="hero-band">
    <div class="row g-2 align-items-center">
        <div class="col-lg-8">
            <div class="page-eyebrow text-white-50"><i class="bi bi-journal-check"></i> Reservations</div>
            <div class="hero-title mt-2"><?php echo $isEdit ? 'Edit Reservation' : 'New Reservation'; ?></div>
<div class="hero-subtitle mt-1">
    Create or edit reservations, room allocations and pricing.
</div>
        </div>
        <div class="col-lg-4 text-lg-end">
            <a href="waitlist.php" class="btn btn-outline-light me-2">
                <i class="bi bi-hourglass-split me-2"></i>Waitlist
            </a>
            <a href="/rooms/modules/reservations/reservation_list.php" class="btn btn-outline-light">
                <i class="bi bi-arrow-left me-2"></i>Back to List
            </a>
        </div>
    </div>
</div>

<?php if ($success): ?>
    <div class="alert alert-success"><?php echo h($success); ?></div>
<?php endif; ?>

<?php if ($errors): ?>
    <div class="alert alert-danger">
        <div class="fw-bold mb-2">Please fix the following:</div>
        <ul class="mb-0">
            <?php foreach ($errors as $err): ?>
                <li><?php echo h($err); ?></li>
            <?php endforeach; ?>
        </ul>
    </div>
<?php endif; ?>

<?php if ($notifiedWaitlists): ?>
    <div class="alert alert-warning d-flex justify-content-between align-items-center flex-wrap gap-2">
        <div>
            <strong><?php echo count($notifiedWaitlists); ?> waitlisted booking(s)</strong> now have matching availability.
            Open the waitlist page to confirm them.
        </div>
        <a href="waitlist.php?show=notified" class="btn btn-sm btn-warning">
            <i class="bi bi-bell me-1"></i>Open Waitlist Alerts
        </a>
    </div>
<?php endif; ?>

<form method="post" class="row g-4" id="reservationForm" novalidate>
    <input type="hidden" name="nightly_overrides_json" id="nightly_overrides_json" value="<?php echo h(json_encode($nightlyOverridesForClient ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>">
    <input type="hidden" name="id" value="<?php echo (int)$form['id']; ?>">
    <input type="hidden" name="current_guest_id" value="<?php echo (int)$currentGuestId; ?>">
    <input type="hidden" name="waitlist_id" value="<?php echo (int)$form['waitlist_id']; ?>">

    <div class="col-xl-8">
        <div class="form-section">
            <div class="form-section-title">
                <i class="bi bi-people-fill"></i>
                Booking Details
            </div>

            <div class="field-group">
                <div class="field-group-title"><i class="bi bi-building"></i> Stay</div>
            <div class="row g-3">
                <div class="col-md-3">
                    <label class="form-label">Property</label>
                    <select name="property_id" id="property_id" class="form-select" required>
                        <option value="">Select property</option>
                        <?php foreach ($properties as $p): ?>
                            <option value="<?php echo (int)$p['id']; ?>" <?php echo (int)$form['property_id'] === (int)$p['id'] ? 'selected' : ''; ?>>
                                <?php echo h($p['name']); ?> - <?php echo h($p['code']); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>

                <div class="col-md-6">
                    <label class="form-label">Guest Name</label>
                    <input type="text" name="guest_name" class="form-control" value="<?php echo h($form['guest_name']); ?>" required>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Guest Nationality</label>
                    <select name="nationality_id" class="form-select" required>
                        <option value="">Select nationality</option>
                        <?php foreach ($nationalities as $nat): ?>
                            <option value="<?php echo (int)$nat['id']; ?>" <?php echo (int)$form['nationality_id'] === (int)$nat['id'] ? 'selected' : ''; ?>>
                                <?php echo h($nat['nationality']); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="col-md-3">
                    <label class="form-label">GroupName</label>
                    <input type="text" name="group_name" class="form-control" maxlength="180" value="<?php echo h($form['group_name']); ?>" placeholder="e.g. Smith Family / ABC Tour Group">
                </div>
                <div class="col-md-3">
                    <label class="form-label">Group Leader</label>
                    <input type="text" name="group_leader" class="form-control" maxlength="180" value="<?php echo h($form['group_leader']); ?>" placeholder="Leader / guide name">
                </div>
                <div class="col-md-3">
                    <label class="form-label">MealPlan <span class="text-danger">*</span></label>
                    <select name="meal_plan" class="form-select" required>
                        <?php
                        /*
                           Built from roomz_meal_plans() so BB/HB/FB stay in step
                           with the normalizer. The previous markup had the BB
                           option's ternary inverted ('' : 'selected'), so BB was
                           pre-selected whenever the saved value was NOT BB -
                           an HB booking reopened showing BB selected.
                        */
                        $currentMealPlan = (string)($form['meal_plan'] ?? '');
                        if ($currentMealPlan === '') $currentMealPlan = 'BB';
                        foreach (roomz_meal_plans() as $mpCode => $mpLabel):
                        ?>
                            <option value="<?php echo htmlspecialchars($mpCode, ENT_QUOTES, 'UTF-8'); ?>"
                                <?php echo $currentMealPlan === $mpCode ? 'selected' : ''; ?>>
                                <?php echo htmlspecialchars($mpLabel, ENT_QUOTES, 'UTF-8'); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
            </div>
            </div>

            <div class="field-group">
                <div class="field-group-title"><i class="bi bi-briefcase"></i> Agent &amp; Contract</div>
            <div class="row g-3">
                <div class="col-md-4">
                    <label class="form-label d-flex justify-content-between align-items-center">
                        <span>Agent</span>
                        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" id="addAgentBtn"><i class="bi bi-plus-circle me-1"></i>New agent</button>
                    </label>
                    <div class="agent-combobox position-relative">
                        <?php
                            $selectedAgentIdVal = (int)($form['agent_id'] ?? 0);
                            $selectedAgentLabel = '';
                            foreach ($agents as $agentOpt) {
                                if ((int)$agentOpt['id'] === $selectedAgentIdVal) {
                                    $an = trim((string)($agentOpt['display_name'] ?? ''));
                                    $selectedAgentLabel = $an !== '' ? $an : ('Agent #' . (int)$agentOpt['id']);
                                    break;
                                }
                            }
                        ?>
                        <!-- Hidden field carries the real value that all existing logic reads -->
                        <input type="hidden" name="agent_id" id="agent_id" value="<?php echo $selectedAgentIdVal > 0 ? (int)$selectedAgentIdVal : ''; ?>">
                        <input type="text" id="agentComboInput" class="form-select" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list"
                               placeholder="Search or select agent..." value="<?php echo h($selectedAgentLabel); ?>"
                               <?php echo $selectedAgentIdVal <= 0 ? 'data-empty="1"' : ''; ?>>
                        <div id="agentComboList" class="agent-combo-list list-group shadow-sm" style="display:none;position:absolute;z-index:2000;width:100%;max-height:260px;overflow-y:auto;top:100%;left:0;"></div>
                    </div>
                    <!-- Inline add-agent panel, hidden until "New agent" is clicked -->
                    <div id="newAgentPanel" class="border rounded-3 p-2 mt-2" style="display:none;background:#f8fafc;">
                        <div class="row g-2">
                            <div class="col-12"><input type="text" id="newAgentFirstname" class="form-control form-control-sm" placeholder="Agent / company name" maxlength="180"></div>
                            <div class="col-12 d-flex gap-2">
                                <button type="button" class="btn btn-primary btn-sm" id="saveNewAgentBtn"><i class="bi bi-check2 me-1"></i>Save agent</button>
                                <button type="button" class="btn btn-outline-secondary btn-sm" id="cancelNewAgentBtn">Cancel</button>
                            </div>
                            <div class="col-12"><div id="newAgentMsg" class="small text-danger"></div></div>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <label class="form-label d-flex justify-content-between align-items-center">
                        <span>Agent Contact</span>
                        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" id="addContactBtn"><i class="bi bi-plus-circle me-1"></i>New contact</button>
                    </label>
                    <select name="contact_id" id="contact_id" class="form-select" required>
                        <option value="">Select contact</option>
                        <?php foreach ($contacts as $contact): ?>
                            <?php
                            $contactName = trim(($contact['firstname'] ?? '') . ' ' . ($contact['lastname'] ?? ''));
                            $contactMeta = [];
                            if (!empty($contact['email'])) $contactMeta[] = $contact['email'];
                            if (!empty($contact['mobile'])) $contactMeta[] = $contact['mobile'];
                            elseif (!empty($contact['phone'])) $contactMeta[] = $contact['phone'];
                            ?>
                            <option value="<?php echo (int)$contact['id']; ?>" <?php echo (int)$form['contact_id'] === (int)$contact['id'] ? 'selected' : ''; ?>>
                                <?php echo h($contactName); ?><?php echo $contactMeta ? ' - ' . h(implode(' / ', $contactMeta)) : ''; ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                    <!-- Inline add-contact panel, hidden until "New contact" is clicked -->
                    <div id="newContactPanel" class="border rounded-3 p-2 mt-2" style="display:none;background:#f8fafc;">
                        <div class="row g-2">
                            <div class="col-6"><input type="text" id="newContactFirstname" class="form-control form-control-sm" placeholder="First name"></div>
                            <div class="col-6"><input type="text" id="newContactLastname" class="form-control form-control-sm" placeholder="Last name"></div>
                            <div class="col-6"><input type="email" id="newContactEmail" class="form-control form-control-sm" placeholder="Email (optional)"></div>
                            <div class="col-6"><input type="text" id="newContactMobile" class="form-control form-control-sm" placeholder="Mobile (optional)"></div>
                            <div class="col-12 d-flex gap-2">
                                <button type="button" class="btn btn-primary btn-sm" id="saveNewContactBtn"><i class="bi bi-check2 me-1"></i>Save contact</button>
                                <button type="button" class="btn btn-outline-secondary btn-sm" id="cancelNewContactBtn">Cancel</button>
                            </div>
                            <div class="col-12"><div id="newContactMsg" class="small text-danger"></div></div>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <label class="form-label">Originating Agent</label>
                    <select name="originating_agent" id="originating_agent" class="form-select">
                        <option value="">Select Agent</option>
                        <?php foreach ($agents as $agent): ?>
                            <?php $agentName = trim((string)($agent['display_name'] ?? '')); ?>
                            <option value="<?php echo (int)$agent['id']; ?>" <?php echo (int)$form['originating_agent'] === (int)$agent['id'] ? 'selected' : ''; ?>>
                                <?php echo h($agentName !== '' ? $agentName : ('Agent #' . $agent['id'])); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
            </div>
            </div>

            <div class="field-group">
                <div class="field-group-title"><i class="bi bi-calendar-range"></i> Dates &amp; Arrival</div>
            <div class="row g-3">
                <div class="col-md-3">
                    <label class="form-label">Check In</label>
                    <input type="date" name="check_in" id="check_in" class="form-control" value="<?php echo h($form['check_in']); ?>" required>
                </div>

                <div class="col-md-3">
                    <label class="form-label">Check Out</label>
                    <input type="date" name="check_out" id="check_out" class="form-control" value="<?php echo h($form['check_out']); ?>" required>
                </div>

                <div class="col-md-3">
                    <label class="form-label">Kids 0-3 yrs <span class="text-muted">(No Charge)</span></label>
                    <input type="number" min="0" name="kids_0_3" id="kids_0_3" class="form-control" value="<?php echo (int)($form['kids_0_3'] ?? 0); ?>">
                    <div class="form-hint">Travelling infants/toddlers, excluded from room pricing.</div>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Expected Arrival Time</label>
                    <input type="time" name="expected_arrival_time" class="form-control" value="<?php echo h($form['expected_arrival_time'] ?? '14:00'); ?>">
                    <div class="form-hint">Default check-in time is 14:00 hrs.</div>
                </div>
            </div>
            </div>

            <div class="row g-3">
                <div class="col-12">
                    <div class="rooms-block">
                    <div class="d-flex justify-content-between align-items-center mb-2">
                        <label class="form-label mb-0 fw-semibold"><i class="bi bi-door-open me-1"></i>Rooms Required</label>
                        <button type="button" class="btn btn-sm btn-outline-primary" id="addRoomRowBtn">
                            <i class="bi bi-plus-circle me-1"></i>Add Room
                        </button>
                    </div>

                    <?php
                    $availabilitySummary = [];
                    foreach ($groupedRoomNeeds as $need) {
                        $rtTitle = '';
                        foreach ($roomTypes as $rt) {
                            if ((int)$rt['id'] === (int)$need['room_type_id']) {
                                $rtTitle = (string)$rt['title'];
                                break;
                            }
                        }
                        $info = $availabilityInfo[(int)$need['room_type_id']] ?? ['available_rooms' => 0];
                        $availabilitySummary[] = ($rtTitle !== '' ? $rtTitle : ('Type #' . (int)$need['room_type_id']))
                            . ': need ' . (int)$need['rooms_needed']
                            . ', available ' . (int)($info['available_rooms'] ?? 0);
                    }
                    ?>

                    <div id="roomRowsWrap">
                        <?php foreach (($form['room_rows'] ?? []) as $idx => $roomRow): ?>
                            <div class="border rounded-3 p-3 mb-3 room-row" data-index="<?php echo (int)$idx; ?>">
                                <div class="row g-3 align-items-end">
                                    <div class="col-lg-4 col-md-6">
                                        <label class="form-label">Room Type</label>
                                        <select name="room_rows[<?php echo (int)$idx; ?>][room_type_id]" class="form-select room-type-select" required>
                                            <option value="">Select room type</option>
                                            <?php foreach ($roomTypes as $roomType): ?>
                                                <option value="<?php echo (int)$roomType['id']; ?>" data-higher-occupancy="<?php echo (int)($roomType['higher_occupancy'] ?? 0); ?>" data-room-title="<?php echo h($roomType['title']); ?>" data-available="<?php echo (int)($roomTypeAvailabilityCurrent[(int)$roomType['id']]['available_rooms'] ?? $roomTypeAvailabilityAll[(int)$roomType['id']]['available_rooms'] ?? 0); ?>" <?php echo (int)$roomRow['room_type_id'] === (int)$roomType['id'] ? 'selected' : ''; ?>>
                                                    <?php echo h($roomType['title']); ?><?php if (room_type_allows_triple_label((string)$roomType['title'], (int)($roomType['higher_occupancy'] ?? 0))): ?> — Triple possible<?php endif; ?>
                                                </option>
                                            <?php endforeach; ?>
                                        </select>
                                    </div>

                                    <div class="col-lg-2 col-md-3">
                                        <label class="form-label">No. Rooms</label>
                                        <input type="number" min="1" name="room_rows[<?php echo (int)$idx; ?>][qty_rooms]" class="form-control room-qty" value="<?php echo (int)($roomRow['qty_rooms'] ?? 1); ?>" required>
                                    </div>

                                    <div class="col-lg-2 col-md-3">
                                        <label class="form-label">Adults</label>
                                        <input type="number" min="0" name="room_rows[<?php echo (int)$idx; ?>][adults]" class="form-control room-adults" value="<?php echo (int)$roomRow['adults']; ?>" required>
                                    </div>

                                    <div class="col-lg-2 col-md-3">
                                        <label class="form-label">Children</label>
                                        <input type="number" min="0" name="room_rows[<?php echo (int)$idx; ?>][children]" class="form-control room-children" value="<?php echo (int)$roomRow['children']; ?>">
                                    </div>

                                    <div class="col-lg-2 col-md-3 text-md-end">
                                        <button type="button" class="btn btn-outline-danger remove-room-row">
                                            <i class="bi bi-trash me-1"></i>Remove
                                        </button>
                                    </div>
                                </div>
                                <div class="room-requirements-box mt-3">
                                    <div class="row g-2 align-items-end">
                                        <div class="col-lg-2 col-md-3">
                                            <label class="form-label">Extra Beds</label>
                                            <input type="number" min="0" name="room_rows[<?php echo (int)$idx; ?>][extra_beds_required]" class="form-control room-extra-beds" value="<?php echo (int)($roomRow['extra_beds_required'] ?? 0); ?>">
                                        </div>
                                        <div class="col-lg-2 col-md-3">
                                            <label class="form-label">Baby Cots</label>
                                            <input type="number" min="0" name="room_rows[<?php echo (int)$idx; ?>][cots_required]" class="form-control room-cots" value="<?php echo (int)($roomRow['cots_required'] ?? 0); ?>">
                                        </div>
                                        <div class="col-lg-2 col-md-3">
                                            <div class="form-check mt-4">
                                                <input class="form-check-input room-wheelchair" type="checkbox" name="room_rows[<?php echo (int)$idx; ?>][wheelchair_room_required]" value="1" id="wheelchair_<?php echo (int)$idx; ?>" <?php echo !empty($roomRow['wheelchair_room_required']) ? 'checked' : ''; ?>>
                                                <label class="form-check-label" for="wheelchair_<?php echo (int)$idx; ?>">Wheelchair</label>
                                            </div>
                                        </div>
                                        <div class="col-lg-2 col-md-3">
                                            <div class="form-check mt-4">
                                                <input class="form-check-input room-interleading" type="checkbox" name="room_rows[<?php echo (int)$idx; ?>][interleading_required]" value="1" id="interleading_<?php echo (int)$idx; ?>" <?php echo !empty($roomRow['interleading_required']) ? 'checked' : ''; ?>>
                                                <label class="form-check-label" for="interleading_<?php echo (int)$idx; ?>">Interleading</label>
                                            </div>
                                        </div>
                                        <input type="hidden" name="room_rows[<?php echo (int)$idx; ?>][special_occasion]" class="room-special-occasion" value="<?php echo h($roomRow['special_occasion'] ?? ''); ?>">
                                        <div class="col-lg-6">
                                            <label class="form-label">Room Setup Notes</label>
                                            <input type="text" maxlength="255" name="room_rows[<?php echo (int)$idx; ?>][room_setup_notes]" class="form-control room-setup-notes" value="<?php echo h($roomRow['room_setup_notes'] ?? ''); ?>" placeholder="Twin beds, extra blanket, crib location">
                                        </div>
                                        <div class="col-lg-6">
                                            <label class="form-label">Housekeeping Notes</label>
                                            <input type="text" maxlength="255" name="room_rows[<?php echo (int)$idx; ?>][housekeeping_notes]" class="form-control room-housekeeping-notes" value="<?php echo h($roomRow['housekeeping_notes'] ?? ''); ?>" placeholder="Prepare extra bed before arrival">
                                        </div>
                                    </div>
                                </div>
                            </div>
                        <?php endforeach; ?>
                    </div>

                    <div class="form-text">
                        Add one row for each requested room type. Enter the number of rooms required for that type, then the total adults and children across those rooms.
                        <?php if ($availabilitySummary): ?>
                            <br>Availability summary: <?php echo h(implode(' | ', $availabilitySummary)); ?>
                        <?php endif; ?>
                    </div>
                    </div>
                </div>








                <div class="col-md-4">
                    <label class="form-label">Special Occasion</label>
                    <select name="occassion_id" class="form-select">
                        <option value="">Select occasion</option>
                        <?php foreach ($occassions as $occ): ?>
                            <option value="<?php echo (int)$occ['id']; ?>" <?php echo (int)$form['occassion_id'] === (int)$occ['id'] ? 'selected' : ''; ?>>
                                <?php echo h($occ['occassion_name']); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="col-md-4">
                    <label class="form-label">Special Occasion Details</label>
                    <input type="text" maxlength="80" id="global_special_occasion" class="form-control" value="<?php echo h($form['room_rows'][0]['special_occasion'] ?? ''); ?>" placeholder="Honeymoon, birthday, anniversary">
                    <div class="form-text"></div>
                </div>
                <div class="col-md-4">
                    <label class="form-label">Voucher No</label>
                    <input type="text" name="voucher_no" class="form-control" value="<?php echo h($form['voucher_no']); ?>">
                </div>

                <div class="col-12">
                    <div class="border rounded-4 p-3 bg-light-subtle">
                        <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
                            <div>
                                <div class="fw-bold"><i class="bi bi-palette me-2"></i>Booking Tracking & Colour Coding</div>
                                <div class="small text-muted">Use these fields to drive reservation-list and planner colours consistently.</div>
                            </div>
                            <?php
                                $derivedColourKey = rf_determine_booking_colour_key($form);
                                $derivedColourStyle = '';
                                try {
                                    $ccStmt = $pdo->prepare("SELECT status_label, hex_color, text_color FROM booking_color_codes WHERE status_key = :key LIMIT 1");
                                    $ccStmt->execute([':key' => $derivedColourKey]);
                                    $ccRow = $ccStmt->fetch(PDO::FETCH_ASSOC) ?: [];
                                    if ($ccRow) {
                                        $derivedColourStyle = 'style="background:' . h($ccRow['hex_color']) . ';color:' . h($ccRow['text_color']) . ';"';
                                    }
                                } catch (Throwable $ignore) { $ccRow = []; }
                            ?>
                            <span class="badge rounded-pill px-3 py-2" <?php echo $derivedColourStyle; ?>>
                                <?php echo h($ccRow['status_label'] ?? ucwords(str_replace('_', ' ', $derivedColourKey))); ?>
                            </span>
                        </div>

                        <div class="row g-3">
                            <?php
                                $currentOrderStatus = (string)($form['booking_status'] ?? '2');
                                $canEditBookingStatus = in_array($currentOrderStatus, ['2', '1', ''], true);
                                $currentOrderStatusLabel = $operationalBookingStatusLabels[$currentOrderStatus] ?? ('Status ' . $currentOrderStatus);
                            ?>
                            <input type="hidden" name="current_order_status" value="<?php echo h($currentOrderStatus); ?>">
                            <div class="col-md-3">
                                <label class="form-label">Booking Status</label>
                                <?php if ($canEditBookingStatus): ?>
                                    <select name="booking_status" class="form-select" required>
                                        <?php foreach ($bookingStatusOptions as $value => $label): ?>
                                            <option value="<?php echo h($value); ?>" <?php echo (string)$form['booking_status'] === (string)$value ? 'selected' : ''; ?>>
                                                <?php echo h($label); ?>
                                            </option>
                                        <?php endforeach; ?>
                                    </select>
                                    <div class="form-hint">Choose whether this reservation is confirmed or provisional.</div>
                                <?php else: ?>
                                    <input type="hidden" name="booking_status" value="<?php echo h($currentOrderStatus); ?>">
                                    <input type="text" class="form-control" value="<?php echo h($currentOrderStatusLabel); ?>" readonly>
                                    <div class="form-hint">Operational statuses are changed from Front Desk actions.</div>
                                <?php endif; ?>
                            </div>

                            <div class="col-md-3">
                                <label class="form-label">Booking Source</label>
                                <select name="booking_source" class="form-select">
                                    <?php foreach ($bookingSourceOptions as $value => $label): ?>
                                        <option value="<?php echo h($value); ?>" <?php echo (string)$form['booking_source'] === (string)$value ? 'selected' : ''; ?>>
                                            <?php echo h($label); ?>
                                        </option>
                                    <?php endforeach; ?>
                                </select>
                            </div>

                            <div class="col-md-3">
                                <label class="form-label d-block">Room Account Posting</label>
                                <div class="form-check form-switch mt-2">
                                    <input class="form-check-input" type="checkbox" role="switch" name="block_room_posting" id="block_room_posting" value="1" <?php echo !empty($form['block_room_posting']) ? 'checked' : ''; ?>>
                                    <label class="form-check-label" for="block_room_posting">Block purchases to room account</label>
                                </div>
                                <div class="form-hint">Use for groups or guests not allowed to charge POS/extras to room.</div>
                            </div>

                            <div class="col-md-3">
                                <label class="form-label">Payment Due</label>
                                <select name="payment_due" class="form-select">
                                    <?php foreach ($paymentDueOptions as $value => $label): ?>
                                        <option value="<?php echo h($value); ?>" <?php echo (string)$form['payment_due'] === (string)$value ? 'selected' : ''; ?>>
                                            <?php echo h($label); ?>
                                        </option>
                                    <?php endforeach; ?>
                                </select>
                                <div class="form-hint">Includes Account, Pay on arrival, Chase payment, Online payment, Active Out.</div>
                            </div>

                            <div class="col-md-3">
                                <label class="form-label">Payment Status</label>
                                <select name="payment_status" class="form-select">
                                    <?php foreach ($paymentStatusOptions as $value => $label): ?>
                                        <option value="<?php echo h($value); ?>" <?php echo (string)$form['payment_status'] === (string)$value ? 'selected' : ''; ?>>
                                            <?php echo h($label); ?>
                                        </option>
                                    <?php endforeach; ?>
                                </select>
                            </div>

                            <div class="col-md-3" style="display:none">
                                <label class="form-label d-block">Invoiced</label>
                                <div class="form-check form-switch mt-2">
                                    <input class="form-check-input" type="checkbox" role="switch" name="invoiced" id="invoiced" value="0" <?php echo !empty($form['invoiced']) ? 'checked' : ''; ?>>
                                    <label class="form-check-label" for="invoiced">Invoice has been issued</label>
                                </div>
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">Invoice Number</label>
                                <input type="text" name="invoice_number" class="form-control" value="<?php echo h($form['invoice_number']); ?>">
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">Fiscal Device ID</label>
                                <input type="text" name="fiscalDeviceId" class="form-control" value="<?php echo h($form['fiscalDeviceId']); ?>">
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">Verification Code</label>
                                <input type="text" name="verificationCode" class="form-control" value="<?php echo h($form['verificationCode']); ?>">
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">Global Number</label>
                                <input type="text" name="globalNumber" class="form-control" value="<?php echo h($form['globalNumber']); ?>">
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">Fiscal Day Number</label>
                                <input type="text" name="fiscalDayNumber" class="form-control" value="<?php echo h($form['fiscalDayNumber']); ?>">
                            </div>

                            <div class="col-md-4" style="display:none">
                                <label class="form-label">QR Code URL</label>
                                <input type="url" name="qrCodeUrl" class="form-control" value="<?php echo h($form['qrCodeUrl']); ?>">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-md-4" style="display:none">
                    <label class="form-label">Waitlist Priority</label>
                    <select name="priority" class="form-select">
                        <option value="0" <?php echo (int)$form['priority'] === 0 ? 'selected' : ''; ?>>Normal</option>
                        <option value="1" <?php echo (int)$form['priority'] === 1 ? 'selected' : ''; ?>>High</option>
                        <option value="2" <?php echo (int)$form['priority'] === 2 ? 'selected' : ''; ?>>Urgent</option>
                    </select>
                </div>                
                <div class="col-md-4">
                    <label class="form-label">Total Rooms</label>
                    <input type="number" id="totalrooms_display" class="form-control" value="<?php echo (int)$form['totalrooms']; ?>" readonly>
                </div>

                <div class="col-md-4">
                    <label class="form-label">Adults</label>
                    <input type="number" id="adults_display" class="form-control" value="<?php echo (int)$form['adults']; ?>" readonly>
                </div>

                <div class="col-md-4">
                    <label class="form-label">Children</label>
                    <input type="number" id="kids_display" class="form-control" value="<?php echo (int)$form['kids']; ?>" readonly>
                </div>





                <div class="col-12">
                    <label class="form-label">Reservation Notes</label>
                    <textarea name="reservation_notes" class="form-control" rows="3"><?php echo h($form['reservation_notes']); ?></textarea>
                </div>

                <div class="col-12">
                    <div class="booking-extras-panel mt-2">
                        <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
                            <div>
                                <div class="fw-bold"><i class="bi bi-bag-plus me-2"></i>Extras at Booking Stage</div>
                                <div class="small text-muted">Add, edit or remove extras here. Each posts when the reservation is saved - to the booking master, the guests' room accounts, or a separate paying agent selected under Others.</div>
                            </div>
                            <button type="button" class="btn btn-sm btn-outline-primary" id="addBookingExtraBtn"><i class="bi bi-plus-circle me-1"></i>Add Extra</button>
                        </div>
                        <div id="bookingExtrasWrap">
                            <?php foreach (($form['booking_extras'] ?? []) as $extraIdx => $extraRow): ?>
                                <div class="booking-extra-row border rounded-3 p-3 mb-3" data-index="<?php echo (int)$extraIdx; ?>" data-transaction-id="<?php echo (int)($extraRow['transaction_id'] ?? 0); ?>">
                                    <div class="row g-3 align-items-end">
                                        <div class="col-md-2"><label class="form-label">Date</label><input type="date" name="booking_extras[<?php echo (int)$extraIdx; ?>][tx_date]" class="form-control extra-date" value="<?php echo h($extraRow['tx_date'] ?? date('Y-m-d')); ?>"></div>
                                        <div class="col-md-3"><label class="form-label">Category</label><select name="booking_extras[<?php echo (int)$extraIdx; ?>][category_id]" class="form-select extra-category"><option value="">Select category</option><?php foreach ($extraCategories as $cat): ?><option value="<?php echo (int)$cat['id']; ?>" <?php echo (int)($extraRow['category_id'] ?? 0) === (int)$cat['id'] ? 'selected' : ''; ?>><?php echo h($cat['category']); ?></option><?php endforeach; ?></select></div>
                                        <div class="col-md-3"><label class="form-label">Service / Description</label><select name="booking_extras[<?php echo (int)$extraIdx; ?>][service_id]" class="form-select extra-service"><option value="">Select category first</option><?php foreach ($extraServices as $svc): ?><option value="<?php echo (int)$svc['id']; ?>" data-category="<?php echo (int)$svc['category']; ?>" data-price="<?php echo h($svc['price_per_person']); ?>" <?php echo (int)($extraRow['service_id'] ?? 0) === (int)$svc['id'] ? 'selected' : ''; ?>><?php echo h($svc['service']); ?></option><?php endforeach; ?></select></div>
                                        <div class="col-md-1"><label class="form-label">Qty</label><input type="number" step="1" min="1" name="booking_extras[<?php echo (int)$extraIdx; ?>][quantity]" class="form-control extra-qty" value="<?php echo h($extraRow['quantity'] ?? 1); ?>"></div>
                                        <div class="col-md-1"><label class="form-label" title="Editable if charged incorrectly">Rate <i class="bi bi-pencil-square text-primary" style="font-size:.7rem;"></i></label><input type="number" step="0.01" min="0" name="booking_extras[<?php echo (int)$extraIdx; ?>][override_price]" class="form-control extra-rate" value="<?php echo h((string)($extraRow['override_price'] ?? '') !== '' ? $extraRow['override_price'] : '0.00'); ?>" title="Leave as-is to use the standard price, or type a corrected rate"></div>
                                        <div class="col-md-1"><label class="form-label">Total</label><input type="text" class="form-control extra-total" value="0.00" readonly></div>
                                        <div class="col-md-1 text-end"><button type="button" class="btn btn-outline-danger remove-booking-extra" title="Delete this extra"><i class="bi bi-trash"></i></button></div><div class="col-12 extra-save-state small text-muted"><i class="bi bi-cloud-check me-1"></i><span>Saved</span></div>
                                        <div class="col-md-4">
                                            <label class="form-label">Charge To</label>
                                            <select name="booking_extras[<?php echo (int)$extraIdx; ?>][charge_to]" class="form-select extra-charge-to">
                                                <?php $ct = strtolower((string)($extraRow['charge_to'] ?? 'master')); if (!in_array($ct, ['master','room','other'], true)) $ct = 'master'; ?>
                                                <option value="master" <?php echo $ct === 'master' ? 'selected' : ''; ?>>Master (Booking Agent)</option>
                                                <option value="room" <?php echo $ct === 'room' ? 'selected' : ''; ?>>Room Account (Guest)</option>
                                                <option value="other" <?php echo $ct === 'other' ? 'selected' : ''; ?>>Others (Different Paying Agent)</option>
                                            </select>
                                        </div>
                                        <div class="col-md-4 extra-payer-agent-wrap" style="<?php echo $ct === 'other' ? '' : 'display:none;'; ?>">
                                            <label class="form-label">Paying Agent</label>
                                            <select name="booking_extras[<?php echo (int)$extraIdx; ?>][payer_agent_id]" class="form-select extra-payer-agent">
                                                <option value="">Select paying agent</option>
                                                <?php foreach ($agents as $payerAgent): $payerName = trim((string)($payerAgent['display_name'] ?? '')); ?>
                                                    <option value="<?php echo (int)$payerAgent['id']; ?>" <?php echo (int)($extraRow['payer_agent_id'] ?? 0) === (int)$payerAgent['id'] ? 'selected' : ''; ?>><?php echo h($payerName !== '' ? $payerName : ('Agent #' . (int)$payerAgent['id'])); ?></option>
                                                <?php endforeach; ?>
                                            </select>
                                        </div>
                                        <div class="col-md-<?php echo $ct === 'other' ? '4' : '8'; ?> extra-note-wrap"><label class="form-label">Note</label><input type="text" name="booking_extras[<?php echo (int)$extraIdx; ?>][note]" class="form-control" placeholder="Optional note, e.g. airport pickup time or activity date" value="<?php echo h($extraRow['note'] ?? ''); ?>"></div>
                                    </div>
                                </div>
                            <?php endforeach; ?>
                        </div>
                        <div class="d-flex justify-content-end mb-3"><div class="badge text-bg-dark fs-6 px-3 py-2">Extras Total: <span id="bookingExtrasTotal"><?php echo display_money_local($bookingExtrasInitialTotal); ?></span></div></div>
                        <div class="alert alert-info mb-0 small">Extras are calculated as <strong>service price × quantity</strong> and posted to the selected payer account after saving.</div>
                    </div>
                </div>

                <div class="col-12" style="display:none">
                    <label class="form-label">Internal Memo</label>
                    <textarea name="internal_memo" class="form-control" rows="3"><?php echo h($form['internal_memo']); ?></textarea>
                </div>

                <div class="col-12" style="display:none">
                    <label class="form-label">Reminder Info</label>
                    <textarea name="reminder_info" class="form-control" rows="3"><?php echo h($form['reminder_info']); ?></textarea>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-4">
        <div class="form-section">
            <div class="form-section-title">
                    <div class="d-flex justify-content-between align-items-center mb-2">
                        
                        <h5 class="mb-0">Pricing Summary</h5>
                    
                        <?php if ($isEdit): ?>
                            <a href="/rooms/modules/payments/proforma_invoice.php?order_id=<?php echo (int)$form['id']; ?>" 
                               target="_blank" 
                               class="btn btn-success btn-lg">Proforma
                            </a>
                        <?php endif; ?>
                    
                    </div>              
            </div>

            <?php
            /*
               Rate contract banner.

               Which contract is pricing this booking is the single most
               consequential thing on the form, and it used to be invisible
               until the nightly preview was scrolled to. It is derived from the
               resolved nightly rows rather than from the agent record, so it
               reflects what pricing ACTUALLY did - including a fall back to
               rack, or a rate derived from a meal supplement.
            */
            $rcBannerPlan   = '';
            $rcBannerSource = '';
            $rcBannerMeal   = '';
            foreach ($previewRows as $rcRow) {
                $rcSrc = (string)($rcRow['rate_source'] ?? '');
                if ($rcSrc === '') continue;
                $rcBannerPlan = (string)($rcRow['rate_plan_title'] ?? '');
                $rcBannerMeal = (string)($rcRow['meal_plan'] ?? ($form['meal_plan'] ?? ''));
                // Worst case wins: one rack night on an agent booking matters
                // more than ten contracted ones.
                $rank = ['agent' => 1, 'agent_derived' => 2, 'public' => 3, 'public_fallback' => 4, 'zero_hold' => 5];
                $cur  = isset($rank[$rcBannerSource]) ? $rank[$rcBannerSource] : 0;
                $new  = isset($rank[$rcSrc]) ? $rank[$rcSrc] : 0;
                if ($new >= $cur) { $rcBannerSource = $rcSrc; }
            }
            ?>
            <div id="rateContractBanner"
                 class="rate-contract-banner mb-3 <?php echo h('rcb-' . ($rcBannerSource !== '' ? $rcBannerSource : 'none')); ?>"
                 data-source="<?php echo h($rcBannerSource); ?>">
                <div class="rcb-icon"><i class="bi bi-file-earmark-text"></i></div>
                <div class="rcb-body">
                    <div class="rcb-label">Rate Contract In Use</div>
                    <div class="rcb-plan" id="rateContractBannerPlan">
                        <?php echo h($rcBannerPlan !== '' ? $rcBannerPlan : 'Not yet priced'); ?>
                    </div>
                    <div class="rcb-note" id="rateContractBannerNote">
                        <?php
                        if ($rcBannerSource === 'agent') {
                            echo 'Agent contract rate &middot; ' . h($rcBannerMeal);
                        } elseif ($rcBannerSource === 'agent_derived') {
                            echo 'Derived from contract base + meal supplement &middot; ' . h($rcBannerMeal);
                        } elseif ($rcBannerSource === 'public_fallback') {
                            echo 'No agent contract for these dates &mdash; RACK RATE applied';
                        } elseif ($rcBannerSource === 'public') {
                            echo 'Public / rack rate &middot; ' . h($rcBannerMeal);
                        } elseif ($rcBannerSource === 'zero_hold') {
                            echo 'No rate resolved &mdash; booking held at zero';
                        } else {
                            echo 'Set an agent and dates, then refresh pricing.';
                        }
                        ?>
                    </div>
                </div>
            </div>

            <div class="mini-stat mb-3"><span>Nights</span><strong id="summaryNights"><?php echo count($previewRows); ?></strong></div>
            <div class="mini-stat mb-3"><span>Total Rooms</span><strong id="summaryTotalRooms"><?php echo (int)$form['totalrooms']; ?></strong></div>
            <div class="mini-stat mb-3"><span>Adults</span><strong id="summaryAdults"><?php echo (int)$form['adults']; ?></strong></div>
            <div class="mini-stat mb-3"><span>Children</span><strong id="summaryChildren"><?php echo (int)$form['kids']; ?></strong></div>
            <div class="mini-stat mb-3"><span>Kids 0-3 yrs</span><strong><span id="summaryKids03"><?php echo (int)($form['kids_0_3'] ?? 0); ?></span> <small class="text-muted">no charge</small></strong></div>
            <div class="mini-stat mb-1"><span>Rooms in Selected Types</span><strong id="summaryTotalInventory"><?php echo (int)$availabilityTotalInventory; ?></strong></div>
            <div class="mb-3" style="opacity:.55;font-size:.78rem;padding-left:.1rem;">
                <span class="text-muted">Rooms in Lodge (all types)</span>
                <span class="badge rounded-pill text-bg-light border ms-1" id="summaryLodgeAvailBadge"
                      style="font-weight:500;"
                      data-lodge-total="<?php echo (int)$availabilityLodgeTotal; ?>"
                      data-lodge-available="<?php echo (int)$availabilityLodgeAvailable; ?>">
                    <?php echo (int)$availabilityLodgeAvailable; ?> free / <?php echo (int)$availabilityLodgeTotal; ?> total
                </span>
                <?php if ((int)$form['id'] > 0): ?>
                    <span class="badge rounded-pill text-bg-light border ms-1" style="font-weight:500;"
                          title="Rooms currently allocated to this booking">
                        <?php echo (int)$currentlyAllocatedRooms; ?> allocated now
                    </span>
                <?php endif; ?>
            </div>
            <div class="mini-stat mb-3">
                <span>Available Rooms</span>
                <strong id="availableRoomsStat" data-available-rooms="<?php echo (int)$availabilityTotalAvailable; ?>">
                    <?php echo (int)$availabilityTotalAvailable; ?>
                </strong>
            </div>
            <div class="mini-stat mb-3"><span>Accommodation Total</span><strong id="summaryAccommodationTotal"><?php echo display_money_local($previewTotal); ?></strong></div>
            <div class="mini-stat mb-3"><span>Extras Total</span><strong id="summaryExtrasTotal"><?php echo display_money_local($bookingExtrasInitialTotal); ?></strong></div>
            <div class="mini-stat mb-3"><span>Grand Total</span><strong id="summaryGrandTotal"><?php echo display_money_local($bookingGrandInitialTotal); ?></strong></div>
            <div class="mini-stat mb-3">
                <span>Pricing Status</span>
                <strong>
                    <?php if ($pricingLocked): ?>
                        <span class="badge text-bg-dark">Locked</span>
                    <?php else: ?>
                        <span class="badge text-bg-success">Open</span>
                    <?php endif; ?>
                </strong>
            </div>

            <?php if ($pricingLocked && $pricingLockedAt !== ''): ?>
                <div class="form-hint mb-3">
                    Pricing was locked on <?php echo h($pricingLockedAt); ?>.
                </div>
            <?php endif; ?>

            <div class="form-hint">
                <?php if ($pricingLocked): ?>
                    Pricing is locked. Notes and non-pricing details can still be updated, but pricing drivers cannot change until you unlock.
                <?php else: ?>
                    Pricing refreshes when property, dates, agent, or the requested room mix changes. The reservation is only saved when you click the save button.
                <?php endif; ?>
            </div>

            <?php if ($showWaitlistCallout): ?>
                <div class="alert alert-warning mt-3">
                    <div class="fw-bold mb-1">No immediate availability for this request.</div>
                    <div class="small mb-2">
                        Requested: <?php echo (int)$requestedRoomsNow; ?> room(s).
                        <?php
                        $warnParts = [];
                        foreach ($groupedRoomNeeds as $need) {
                            $info = $availabilityInfo[(int)$need['room_type_id']] ?? ['available_rooms' => 0];
                            $warnParts[] = 'Type #' . (int)$need['room_type_id'] . ': need ' . (int)$need['rooms_needed'] . ', available ' . (int)($info['available_rooms'] ?? 0);
                        }
                        echo h(implode(' | ', $warnParts));
                        ?>
                    </div>
                    <?php if ($availabilityLodgeAvailable >= $requestedRoomsNow && $requestedRoomsNow > 0): ?>
                        <div class="small fw-semibold mb-2 text-success-emphasis">
                            If you ignore room types you can have <?php echo (int)$availabilityLodgeAvailable; ?> room(s) across the lodge.
                            Juggle the calendar to create availability rather than lose the booking.
                        </div>
                    <?php endif; ?>
                    <div class="small">You can still save the guest to the waitlist and confirm later when a cancellation releases rooms.</div>
                </div>
            <?php endif; ?>

            <?php
            /* Change Dates support: load this booking's allocated rooming rows.
               A date change acts per room (rooming row) via move_booking.php.
               Single-room bookings open the modal directly; multi-room bookings
               let staff pick which room's dates to change. */
            $cdRoomingRows = [];
            if ($isEdit && (int)$form['id'] > 0) {
                try {
                    $cdStmt = $pdo->prepare("
                        SELECT rmg.id AS rooming_id, rmg.room_id,
                               COALESCE(rm.room_no, rmg.room_no) AS room_no,
                               rmg.check_in, rmg.check_out
                        FROM order_rooming rmg
                        LEFT JOIN rooms rm ON rm.id = rmg.room_id
                        WHERE rmg.order_id = :oid
                          AND rmg.room_id > 0
                        ORDER BY rmg.room_no ASC, rmg.id ASC");
                    $cdStmt->execute([':oid' => (int)$form['id']]);
                    $cdRoomingRows = $cdStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
                } catch (Throwable $e) {
                    $cdRoomingRows = [];
                }
            }
            ?>

            <div class="d-grid gap-2 mt-4">
                <button type="button" id="refreshPricingBtn" class="btn btn-light" <?php echo $pricingLocked ? 'disabled' : ''; ?>>
                    <i class="bi bi-arrow-repeat me-2"></i>Refresh Pricing
                </button>
                <?php if ($isEdit && (int)$form['id'] > 0): ?>
                <button type="button" class="btn btn-outline-primary"
                        onclick="openApplyRate(<?php echo (int)$form['id']; ?>, { onDone: function(){ location.reload(); } });">
                    <i class="bi bi-cash-coin me-2"></i>Apply Rate
                </button>
                <?php if (count($cdRoomingRows) === 1): ?>
                <button type="button" class="btn btn-outline-primary"
                        onclick="openChangeDates({
                            roomingId: <?php echo (int)$cdRoomingRows[0]['rooming_id']; ?>,
                            orderId: <?php echo (int)$form['id']; ?>,
                            roomId: <?php echo (int)$cdRoomingRows[0]['room_id']; ?>,
                            roomNo: '<?php echo htmlspecialchars((string)$cdRoomingRows[0]['room_no'], ENT_QUOTES); ?>',
                            guest: '<?php echo htmlspecialchars((string)($form['guest_name'] ?? 'Guest'), ENT_QUOTES); ?>',
                            checkIn: '<?php echo htmlspecialchars(substr((string)$cdRoomingRows[0]['check_in'],0,10), ENT_QUOTES); ?>',
                            checkOut: '<?php echo htmlspecialchars(substr((string)$cdRoomingRows[0]['check_out'],0,10), ENT_QUOTES); ?>',
                            onDone: function(){ location.reload(); }
                        });">
                    <i class="bi bi-calendar-range me-2"></i>Change Dates
                </button>
                <?php elseif (count($cdRoomingRows) > 1): ?>
                <div class="dropdown d-grid">
                    <button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
                        <i class="bi bi-calendar-range me-2"></i>Change Dates
                    </button>
                    <ul class="dropdown-menu w-100">
                        <li><h6 class="dropdown-header">Choose a room to change</h6></li>
                        <?php foreach ($cdRoomingRows as $cdR): ?>
                        <li>
                            <a class="dropdown-item" href="#"
                               onclick="event.preventDefault(); openChangeDates({
                                   roomingId: <?php echo (int)$cdR['rooming_id']; ?>,
                                   orderId: <?php echo (int)$form['id']; ?>,
                                   roomId: <?php echo (int)$cdR['room_id']; ?>,
                                   roomNo: '<?php echo htmlspecialchars((string)$cdR['room_no'], ENT_QUOTES); ?>',
                                   guest: '<?php echo htmlspecialchars((string)($form['guest_name'] ?? 'Guest'), ENT_QUOTES); ?>',
                                   checkIn: '<?php echo htmlspecialchars(substr((string)$cdR['check_in'],0,10), ENT_QUOTES); ?>',
                                   checkOut: '<?php echo htmlspecialchars(substr((string)$cdR['check_out'],0,10), ENT_QUOTES); ?>',
                                   onDone: function(){ location.reload(); }
                               });">
                                Room <?php echo htmlspecialchars((string)$cdR['room_no'], ENT_QUOTES); ?>
                                <span class="text-muted small">(<?php echo htmlspecialchars(substr((string)$cdR['check_in'],0,10), ENT_QUOTES); ?> → <?php echo htmlspecialchars(substr((string)$cdR['check_out'],0,10), ENT_QUOTES); ?>)</span>
                            </a>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                </div>
                <?php endif; ?>
                <?php endif; ?>
                <button type="submit" name="save_reservation" value="1" class="btn btn-primary btn-lg" <?php echo ($showWaitlistCallout && !$isEdit) ? 'disabled' : ''; ?>>
                    <i class="bi bi-save me-2"></i><?php echo $isEdit ? 'Update Reservation' : 'Save Reservation'; ?>
                </button>
                <button type="submit" name="save_waitlist" value="1" class="btn btn-outline-warning btn-lg">
                    <i class="bi bi-hourglass-split me-2"></i><?php echo (int)$form['waitlist_id'] > 0 ? 'Update Waitlist' : 'Save to Waitlist'; ?>
                </button>

                <?php if ($isEdit): ?>
                    <?php if (!$pricingLocked): ?>
                        <button type="submit" name="lock_pricing" value="1" class="btn btn-outline-dark">
                            <i class="bi bi-lock me-2"></i>Lock Pricing
                        </button>
                    <?php else: ?>
                        <button type="submit" name="unlock_pricing" value="1" class="btn btn-outline-warning">
                            <i class="bi bi-unlock me-2"></i>Unlock Pricing
                        </button>
                    <?php endif; ?>
                <?php endif; ?>

                <?php if ($isEdit): ?>
                    <a href="cancel_reservation.php?id=<?php echo (int)$form['id']; ?>" class="btn btn-outline-danger" onclick="return confirm('Cancel this reservation and release availability?');">
                        <i class="bi bi-x-circle me-2"></i>Cancel Reservation
                    </a>
                <?php endif; ?>

                <a href="/rooms/modules/reservations/reservation_list.php" class="btn btn-light">Close</a>
            </div>
        </div>
    </div>

    <div class="col-12">
        <?php echo $pxDebugHtml; ?>
        <div class="card panel-card">
            <div class="card-header d-flex justify-content-between align-items-center">
                <div>
                    <h5 class="mb-0">Nightly Pricing Preview</h5>
                    <small>Each night shows the exact matched rate plan, season, and room-by-room pricing logic.</small>
                    <!-- RoomZ override-fix build: 2026-07-14-directinput (per-night hidden inputs) -->
                    <small class="d-block text-muted" style="font-size:.68rem;opacity:.6;">pricing engine build 2026-07-14-directinput</small>
                    <small class="d-block" id="ovStagedBadge" style="font-size:.7rem;color:#b45309;font-weight:600;">overrides staged: 0</small>
                </div>
            </div>
            <div class="card-body p-0">
                <div class="table-responsive">
                    <table class="table table-modern align-middle mb-0">
                        <thead>
                            <tr>
                                <th>Date</th>
                                <th>Rate Plan</th>
                                <th>Sharing</th>
                                <th>Single</th>
                                <th>Supplement</th>
                                <th>Child %</th>
                                <th>Room Breakdown</th>
                                <th>Adult Total</th>
                                <th>Child Total</th>
                                <th class="text-end">Night Total</th>
                                <th class="text-end">Override</th>
                            </tr>
                        </thead>
                        <tbody id="pricingPreviewBody">
                            <?php if (!$previewRows): ?>
                                <tr>
                                    <td colspan="11" class="text-center text-muted py-4">Enter the booking details to preview pricing.</td>
                                </tr>
                            <?php else: ?>
                                <?php foreach ($previewRows as $row): ?>
                                    <tr>
                                        <td><?php echo h(date('d M Y', strtotime($row['date']))); ?></td>
                                        <td>
                                            <div class="fw-semibold">
                                                <?php echo h($row['rate_plan']); ?>
                                                <?php if (!empty($row['rate_plan_currency'])): ?>
                                                    (<?php echo h($row['rate_plan_currency']); ?>)
                                                <?php endif; ?>
                                            </div>
                                            <div class="small text-muted">
                                                Plan ID: <?php echo (int)($row['rate_plan_id'] ?? 0); ?>
                                                <?php if (!empty($row['rate_plan_season'])): ?>
                                                    &middot; <span class="badge text-bg-<?php echo h(season_badge_class($row['rate_plan_color'] ?? 'secondary')); ?>">
                                                        <?php echo h($row['rate_plan_season']); ?>
                                                      </span>
                                                <?php endif; ?>
                                                <?php
                                                /*
                                                   An agent booking that finds no contract for this night is
                                                   silently charged rack rate. Say so - the Semper migration
                                                   deliberately leaves gaps (skipped dead months), so this is
                                                   a live path, and silent lost margin otherwise.
                                                */
                                                $rfSrc = (string)($row['rate_source'] ?? '');
                                                if ($rfSrc === 'public_fallback'):
                                                ?>
                                                    &middot; <span class="badge text-bg-warning text-dark"
                                                        title="This agent has no contracted rate for this night. The public/rack rate was applied.">
                                                        <i class="bi bi-exclamation-triangle"></i> Rack rate (no agent contract)
                                                      </span>
                                                <?php elseif ($rfSrc === 'agent_derived'): ?>
                                                    <?php
                                                    /* The contract had no plan for the requested meal plan, so the
                                                       rate was derived from the contract base plus a configured
                                                       meal supplement. Not a contracted price - say so. */
                                                    ?>
                                                    &middot; <span class="badge text-bg-info text-dark"
                                                        title="This contract has no rate for the selected meal plan. The contract base rate plus the configured meal supplement was used.">
                                                        <i class="bi bi-calculator"></i>
                                                        Derived<?php if (!empty($row['derived_from_meal_plan'])): ?>
                                                            from <?php echo h($row['derived_from_meal_plan']); ?>
                                                        <?php endif; ?>
                                                        <?php if (!empty($row['derived_supplement'])): ?>
                                                            +<?php echo display_money_local($row['derived_supplement']); ?>
                                                        <?php endif; ?>
                                                      </span>
                                                <?php elseif ($rfSrc === 'agent'): ?>
                                                    &middot; <span class="badge text-bg-success-subtle text-success-emphasis border border-success-subtle"
                                                        title="Priced on the agent's contracted rate for this meal plan.">
                                                        <i class="bi bi-file-earmark-check"></i> Contract rate
                                                      </span>
                                                <?php elseif ($rfSrc === 'zero_hold'): ?>
                                                    &middot; <span class="badge text-bg-danger"
                                                        title="No rate could be resolved for this night. The booking is held at zero and MUST be priced before it is invoiced.">
                                                        <i class="bi bi-x-octagon"></i> No rate found
                                                      </span>
                                                <?php endif; ?>

                                                <?php if (!empty($row['unresolved_meal_plan'])): ?>
                                                    &middot; <span class="badge text-bg-warning text-dark"
                                                        title="The contract holds no rate for this meal plan.">
                                                        <i class="bi bi-exclamation-triangle"></i>
                                                        No <?php echo h($row['unresolved_meal_plan']); ?> in contract
                                                      </span>
                                                <?php endif; ?>
                                            </div>
                                        </td>
                                        <td><?php echo display_money_local($row['sharing_rate']); ?></td>
                                        <td><?php echo display_money_local($row['single_rate']); ?></td>
                                        <td><?php echo display_money_local($row['single_supplement']); ?></td>
                                        <td><?php echo display_money_local($row['child_rate_percent']); ?>%</td>
                                        <td>
                                            <?php if (!empty($row['room_breakdown'])): ?>
                                                <div class="d-grid gap-2">
                                                    <?php foreach ($row['room_breakdown'] as $rb): ?>
                                                        <div class="small border rounded p-2 bg-light btn-room-override" role="button" title="Click to edit this room">
                                                            <div>
                                                                <?php
                                                                    $rbAdults = (int)($rb['adults'] ?? 0);
                                                                    $rbChildren = (int)($rb['children'] ?? 0);
                                                                    $rbDisplayLabel = (string)($rb['display_label'] ?? '');
                                                                    $rbRoomTypeTitle = (string)($rb['room_type_title'] ?? '');
                                                                ?>
                                                                <strong>Room <?php echo (int)$rb['room_no']; ?>:</strong>
                                                                <?php echo $rbAdults; ?>A<?php echo $rbChildren > 0 ? ' + ' . $rbChildren . 'C' : ''; ?>
                                                                <?php if ($rbRoomTypeTitle !== ''): ?>
                                                                    <span class="text-muted">— <?php echo h($rbRoomTypeTitle); ?></span>
                                                                <?php endif; ?>
                                                                <?php if ($rbDisplayLabel !== ''): ?>
                                                                    <span class="badge text-bg-warning ms-2"><i class="bi bi-person-plus"></i> <?php echo h($rbDisplayLabel); ?></span>
                                                                <?php endif; ?>
                                                            </div>
                                                            <div class="text-muted">
                                                                <?php echo h($rb['rate_plan_title']); ?>
                                                                <?php if (!empty($rb['rate_plan_currency'])): ?>
                                                                    (<?php echo h($rb['rate_plan_currency']); ?>)
                                                                <?php endif; ?>
                                                                <?php if (!empty($rb['rate_plan_season'])): ?>
                                                                    ¡¤ <span class="badge text-bg-<?php echo h(season_badge_class($rb['rate_plan_color'] ?? 'secondary')); ?>">
                                                                        <?php echo h($rb['rate_plan_season']); ?>
                                                                      </span>
                                                                <?php endif; ?>
                                                            </div>
                                                            <div>
                                                                <span class="badge text-bg-light border text-dark">
                                                                    <?php echo h($rb['rate_type']); ?>
                                                                </span>
                                                                <span class="ms-2 fw-semibold"><?php echo display_money_local($rb['room_total']); ?></span>
                                                            </div>
                                                        </div>
                                                    <?php endforeach; ?>
                                                </div>
                                            <?php else: ?>
                                                <span class="text-muted">No room breakdown</span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php echo display_money_local($row['adult_total']); ?></td>
                                        <td><?php echo display_money_local($row['child_total']); ?></td>
                                        <td class="text-end fw-bold">
                                            <?php if (!empty($row['rate_overridden'])): ?>
                                                <div class="text-decoration-line-through text-muted small"><?php echo display_money_local($row['original_total'] ?? $row['night_total']); ?></div>
                                                <div class="text-danger"><?php echo display_money_local($row['night_total']); ?></div>
                                            <?php else: ?>
                                                <?php echo display_money_local($row['night_total']); ?>
                                            <?php endif; ?>
                                        </td>
                                        <td class="text-end">
                                            <button type="button"
                                                    class="btn btn-sm <?php echo !empty($row['rate_overridden']) ? 'btn-warning' : 'btn-outline-secondary'; ?> btn-night-override"
                                                    data-date="<?php echo h($row['date']); ?>"
                                                    data-date-label="<?php echo h(date('d M Y', strtotime($row['date']))); ?>"
                                                    data-original-total="<?php echo h((string)($row['original_total'] ?? $row['night_total'])); ?>"
                                                    data-current-total="<?php echo h((string)$row['night_total']); ?>"
                                                    data-override-type="<?php echo h((string)($row['override_type'] ?? 'total')); ?>"
                                                    data-override-value="<?php echo h((string)($row['override_value'] ?? $row['override_total'] ?? '')); ?>"
                                                    data-adult-total="<?php echo h((string)($row['adult_total'] ?? 0)); ?>"
                                                    data-child-total="<?php echo h((string)($row['child_total'] ?? 0)); ?>"
                                                    data-sharing-rate="<?php echo h((string)($row['sharing_rate'] ?? 0)); ?>"
                                                    data-single-rate="<?php echo h((string)($row['single_rate'] ?? 0)); ?>"
                                                    data-room-breakdown="<?php echo h(json_encode($row['room_breakdown'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>"
                                                    data-reason="<?php echo h((string)($row['override_reason'] ?? '')); ?>">
                                                <i class="bi bi-pencil-square me-1"></i><?php echo !empty($row['rate_overridden']) ? 'Edit' : 'Override'; ?>
                                            </button>
                                            <?php if (!empty($row['rate_overridden']) && !empty($row['override_reason'])): ?>
                                                <div class="small text-muted mt-1"><?php echo h($row['override_reason']); ?></div>
                                            <?php endif; ?>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                            <?php endif; ?>
                        </tbody>
                        <?php if ($previewRows): ?>
                            <tfoot>
                                <tr>
                                    <th colspan="10" class="text-end">Grand Total</th>
                                    <th class="text-end" id="previewTotalFooter"><?php echo display_money_local($previewTotal); ?></th>
                                </tr>
                            </tfoot>
                        <?php endif; ?>
                    </table>
                </div>
            </div>
        </div>
    </div>
</form>

<div class="modal fade" id="nightOverrideModal" tabindex="-1" aria-labelledby="nightOverrideModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <div>
                    <h5 class="modal-title" id="nightOverrideModalLabel"><i class="bi bi-pencil-square me-2"></i>Override Night Price</h5>
                    <div class="small text-muted" id="nightOverrideDateLabel">Selected night</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="nightOverrideDate" value="">
                <input type="hidden" id="nightOverrideBaseTotal" value="0">
                <input type="hidden" id="nightOverrideRoomBreakdown" value="[]">

                <div class="small text-muted mb-2" id="nightOverrideMixSummary"></div>

                <div class="table-responsive">
                    <table class="table table-sm align-middle mb-2" id="nightOverrideRoomTable">
                        <thead>
                            <tr>
                                <th style="min-width:150px;">Room</th>
                                <th style="width:130px;">Mode</th>
                                <th style="width:120px;">Rate / Amount</th>
                                <th style="width:110px;" class="text-end">Room total</th>
                            </tr>
                        </thead>
                        <tbody id="nightOverrideRoomRows"><!-- filled by JS --></tbody>
                    </table>
                </div>

                <div class="override-calc-box">
                    <div class="d-flex justify-content-between"><span>Calculated total (current rates)</span><strong id="overrideBasePreview">0.00</strong></div>
                    <div class="d-flex justify-content-between"><span>New night total</span><strong id="overrideFinalPreview" class="fs-5">0.00</strong></div>
                </div>

                <div class="form-check mt-3">
                    <input class="form-check-input" type="checkbox" id="nightOverrideWholeStay">
                    <label class="form-check-label" for="nightOverrideWholeStay">
                        Apply these room rates to <strong>every night</strong> of the stay
                    </label>
                </div>

                <div class="mt-3">
                    <label class="form-label">Reason <span class="text-muted">(optional)</span></label>
                    <textarea id="nightOverrideReason" class="form-control" rows="2" placeholder="Example: tour leader room waived / negotiated per-person rate"></textarea>
                </div>

                <div class="alert alert-info mt-3 mb-0 small">
                    Set each room individually. <strong>Per person</strong> = rate x that room's adults. <strong>Flat total</strong> = the exact room amount (type 0 to waive, e.g. a tour leader). Leave a room on <strong>Keep</strong> to use its calculated rate.
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-outline-danger me-auto" id="clearNightOverrideBtn">Clear Overrides</button>
                <button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-primary" id="applyNightOverrideBtn">Apply</button>
            </div>
        </div>
    </div>
</div>


<?php if (!empty($roomingRows)): ?>
<div class="card panel-card mt-4">
    <div class="card-header">
        <h5 class="mb-0">Rooming</h5>
    </div>
    <div class="card-body p-0">
        <div class="table-responsive">
            <table class="table table-modern mb-0">
                <thead>
                    <tr>
                        <th>Room Slot</th>
                        <th>Requested Type</th>
                        <th>Adults</th>
                        <th>Children</th>
                        <th>Assigned Room</th>
                        <th>Stay</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($roomingRows as $rr): ?>
                        <tr>
                            <td><?php echo (int)$rr['room_no']; ?></td>
                            <td><?php echo h($rr['room_type_title'] ?? ('Type #' . (int)($rr['room_type_id'] ?? 0))); ?></td>
                            <td><?php echo (int)$rr['adults']; ?></td>
                            <td><?php echo (int)$rr['children']; ?></td>
                            <td><?php echo !empty($rr['actual_room_no']) ? h($rr['actual_room_no']) : 'Unassigned'; ?></td>
                            <td><?php echo h($rr['check_in']); ?> to <?php echo h($rr['check_out']); ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
    </div>
</div>
<?php endif; ?>

<style>
/* ---------- Field groups ----------------------------------------------
   Booking Details was one flat grid of a dozen unrelated inputs - property
   next to meal plan next to agent next to dates - so nothing read as
   belonging to anything. These group it into Stay / Agent & Contract /
   Dates & Arrival / Rooms, which is the order the work actually happens in.
------------------------------------------------------------------------ */
.field-group {
    position: relative;
    padding: .9rem 1rem 1rem;
    margin-bottom: .9rem;
    border: 1px solid #e9ecef;
    border-radius: .6rem;
    background: #fdfdfe;
}
.field-group:last-child { margin-bottom: 0; }
.field-group-title {
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: #6c757d;
    margin-bottom: .7rem;
    display: flex;
    align-items: center;
    gap: .4rem;
}
.field-group-title i { font-size: .9rem; opacity: .75; }
/* Rooms sits INSIDE the main grid rather than in its own field-group: the
   grid continues past it (booking status, extras, memos), so wrapping it in
   a group closed the row early and knocked the Pricing Summary column out
   of the layout entirely - it dropped below the form instead of sitting
   right. Styled to match the groups without breaking the row. */
.rooms-block {
    padding: .9rem 1rem 1rem;
    border: 1px solid #e9ecef;
    border-radius: .6rem;
    background: #fbfcfd;
}

/* The agent group carries the pricing consequence, so give it a spine. */
.field-group:nth-of-type(2) { border-left: 3px solid #cfe2ff; }

@media (max-width: 767.98px) {
    .field-group { padding: .75rem .7rem .8rem; }
}

/* ---------- Rate contract banner ---------------------------------------
   Which contract is pricing the booking, shown where the money is.

   Deliberately NOT a blinking element: content that flashes is an
   accessibility failure (WCAG 2.2.2 requires it be stoppable, and it is a
   trigger for vestibular disorders), and staff stop seeing it within a day.
   Instead it pulses ONCE when the contract changes - movement draws the eye,
   then gets out of the way - and carries a permanent colour that says what
   kind of rate is being used.
------------------------------------------------------------------------ */
.rate-contract-banner {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: .75rem .9rem;
    border-radius: .6rem;
    border: 1px solid transparent;
    border-left-width: 4px;
    background: #f8f9fa;
    transition: background-color .25s ease, border-color .25s ease;
}
.rate-contract-banner .rcb-icon {
    font-size: 1.15rem;
    line-height: 1.2;
    opacity: .85;
}
.rate-contract-banner .rcb-label {
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 700;
    opacity: .7;
}
.rate-contract-banner .rcb-plan {
    font-weight: 700;
    font-size: .98rem;
    line-height: 1.25;
    margin-top: .1rem;
    word-break: break-word;
}
.rate-contract-banner .rcb-note {
    font-size: .76rem;
    opacity: .85;
    margin-top: .15rem;
}

/* Agent contract - the good case. */
.rate-contract-banner.rcb-agent {
    background: #e8f6ed; border-color: #b7e0c6; border-left-color: #198754; color: #0f5132;
}
/* Derived from a meal supplement - correct, but not a contracted price. */
.rate-contract-banner.rcb-agent_derived {
    background: #e7f3f8; border-color: #b6dfec; border-left-color: #0dcaf0; color: #055160;
}
/* Rack on an agent booking - lost margin. Loudest of the non-error states. */
.rate-contract-banner.rcb-public_fallback {
    background: #fff4e0; border-color: #ffe0a3; border-left-color: #fd7e14; color: #7a4100;
}
/* No agent on the booking - rack is correct here. */
.rate-contract-banner.rcb-public {
    background: #f1f3f5; border-color: #dde1e5; border-left-color: #6c757d; color: #3f474e;
}
/* Nothing resolved. Must not reach an invoice. */
.rate-contract-banner.rcb-zero_hold {
    background: #fdeaea; border-color: #f5c2c2; border-left-color: #dc3545; color: #842029;
}
.rate-contract-banner.rcb-none {
    background: #f8f9fa; border-color: #e9ecef; border-left-color: #ced4da; color: #6c757d;
}

/* One pulse on change, then still. Honours reduced-motion. */
@keyframes rcbPulse {
    0%   { transform: scale(1);     box-shadow: 0 0 0 0 rgba(13,110,253,.45); }
    50%  { transform: scale(1.025); box-shadow: 0 0 0 .5rem rgba(13,110,253,0); }
    100% { transform: scale(1);     box-shadow: 0 0 0 0 rgba(13,110,253,0); }
}
.rate-contract-banner.rcb-changed {
    animation: rcbPulse .55s ease-out 2;
}
@media (prefers-reduced-motion: reduce) {
    .rate-contract-banner.rcb-changed { animation: none; }
}


.hero-band{padding:12px 18px!important;margin-bottom:12px!important;border-radius:14px!important;}
.hero-band .hero-title{font-size:1.45rem!important;line-height:1.12!important;margin-top:2px!important;}
.hero-band .hero-subtitle{font-size:.84rem!important;margin-top:2px!important;line-height:1.25!important;}
.hero-band .page-eyebrow{font-size:.72rem!important;letter-spacing:.06em!important;}
.hero-band .btn{padding:.38rem .72rem!important;border-radius:10px!important;font-size:.82rem!important;}
.room-requirements-box{background:#f8fafc;border:1px dashed #cbd5e1;border-radius:14px;padding:10px 12px;}
.room-requirements-box .form-label{font-size:.75rem;font-weight:700;color:#475569;margin-bottom:.2rem;}
@media (max-width: 768px){.hero-band{padding:10px 12px!important}.hero-band .hero-title{font-size:1.2rem!important}.hero-band .hero-subtitle{display:none!important}}
.booking-extras-panel{border:1px solid #e6edf5;background:#fbfdff;border-radius:18px;padding:16px;}
.booking-extra-row{background:#fff;transition:border-color .18s ease,box-shadow .18s ease;}
.booking-extra-row.extra-saving{border-color:#f59e0b!important;box-shadow:0 0 0 3px rgba(245,158,11,.10);}
.booking-extra-row.extra-saved{border-color:#10b981!important;}
.booking-extra-row.extra-error{border-color:#dc2626!important;box-shadow:0 0 0 3px rgba(220,38,38,.10);}
.extra-save-state{min-height:20px;}
/* Agent combobox: make the text input read like a select with a caret */
#agentComboInput{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;padding-right:2.25rem;cursor:text;}
.agent-combo-list .list-group-item{cursor:pointer;padding:.4rem .75rem;font-size:.95rem;}
.agent-combo-list .list-group-item.active{background:#0d6efd;border-color:#0d6efd;color:#fff;}
.booking-extra-row .form-label{font-size:.82rem;font-weight:600;}
.override-calc-box{border:1px solid #e5eaf0;background:#fbfdff;border-radius:16px;padding:12px 14px;}
.override-calc-box strong{font-weight:800;}

.status-provisional{background:#9CA3AF;color:#fff;}
.status-confirmed{background:#FACC15;color:#111827;}
.status-checkedin{background:#22C55E;color:#fff;}
.status-pay-arrival{background:#8B5CF6;color:#fff;}
.status-chase-payment{background:#EC4899;color:#fff;}
.status-online-payment{background:#F97316;color:#fff;}
.status-active-out{background:#DC2626;color:#fff;}
.status-invoiced{background:#FDBA74;color:#111827;}
.status-checkedout{background:#3B82F6;color:#fff;}
.status-cancelled{background:#6B7280;color:#fff;}
.status-no-show{background:#92400E;color:#fff;}

</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
    const form = document.getElementById('reservationForm');

    function syncGlobalSpecialOccasionToRows() {
        const globalSpecial = document.getElementById('global_special_occasion');
        if (!globalSpecial) return;
        document.querySelectorAll('.room-special-occasion').forEach(function (input) {
            input.value = globalSpecial.value || '';
        });
    }

    const globalSpecialOccasion = document.getElementById('global_special_occasion');
    if (globalSpecialOccasion) {
        globalSpecialOccasion.addEventListener('input', syncGlobalSpecialOccasionToRows);
        syncGlobalSpecialOccasionToRows();
    }
    if (!form) return;

    const nightlyOverridesInput = document.getElementById('nightly_overrides_json');
    let nightlyOverrides = {};
    try {
        nightlyOverrides = nightlyOverridesInput && nightlyOverridesInput.value ? JSON.parse(nightlyOverridesInput.value) : {};
    } catch (e) {
        nightlyOverrides = {};
    }

    function syncNightlyOverridesInput() {
        if (nightlyOverridesInput) nightlyOverridesInput.value = JSON.stringify(nightlyOverrides || {});
        if (typeof rebuildDirectOverrideInputs === 'function') rebuildDirectOverrideInputs();
        try {
            var _b = document.getElementById('ovStagedBadge');
            if (_b) _b.textContent = 'overrides staged: ' + Object.keys(nightlyOverrides || {}).length;
        } catch (e) {}

        /*
           Reliability fallback for PHP save:
           also post each active nightly override as normal form inputs.
           This protects the save even if the JSON hidden value is stale/blocked.
        */
        form.querySelectorAll('.nightly-override-hidden-line').forEach(function (el) { el.remove(); });
        let idx = 0;
        Object.keys(nightlyOverrides || {}).forEach(function (date) {
            const ov = nightlyOverrides[date];
            if (!ov || !ov.enabled) return;
            const type = ov.type || ov.override_type || 'total';
            /*
               Per-room overrides cannot be represented as a single flat value -
               they need the whole rooms map, which the JSON hidden field already
               carries. Emitting a flat fallback line here would let PHP's
               array_replace clobber the good JSON entry with a room-less one,
               which is exactly what made per-room edits snap back after save.
               So skip the fallback line for per_room and rely on the JSON.
            */
            if (type === 'per_room') return;
            const value = (ov.value !== undefined && ov.value !== null && ov.value !== '')
                ? ov.value
                : (ov.override_value !== undefined && ov.override_value !== null && ov.override_value !== '' ? ov.override_value : ov.override_total);
            if (value === undefined || value === null || value === '') return;

            const fields = {
                date: date,
                type: type,
                value: value,
                override_value: value,
                override_total: type === 'total' ? value : (ov.override_total || ov.final_total || ''),
                final_total: type === 'total' ? value : (ov.final_total || ov.override_total || ''),
                original_total: ov.original_total || '',
                reason: ov.reason || ov.override_reason || ''
            };
            Object.keys(fields).forEach(function (key) {
                const input = document.createElement('input');
                input.type = 'hidden';
                input.className = 'nightly-override-hidden-line';
                input.name = 'nightly_override_lines[' + idx + '][' + key + ']';
                input.value = fields[key];
                form.appendChild(input);
            });
            idx++;
        });
    }

    function calculateOverrideResult(row, ov) {
        const base = parseFloat(row.original_total || row.night_total || 0) || 0;
        const adultBase = parseFloat(row.adult_total || 0) || 0;
        const childBase = parseFloat(row.child_total || 0) || 0;
        const type = (ov && (ov.type || ov.override_type)) || 'total';
        let value = parseFloat((ov && (ov.value ?? ov.override_value ?? ov.override_total ?? ov.extra_bed)) || 0);
        if (isNaN(value) || value < 0) value = 0;

        const breakdown = Array.isArray(row.room_breakdown) ? row.room_breakdown : [];
        let sharingAdults = 0;
        let singleRooms = 0;
        let childCount = 0;

        breakdown.forEach(function (rb) {
            const a = parseInt(rb.adults || 0, 10) || 0;
            const c = parseInt(rb.children || 0, 10) || 0;
            if (a >= 2) sharingAdults += a;
            if (a === 1) singleRooms += 1;
            childCount += c;
        });

        if (childCount <= 0) childCount = parseInt(row.kids_count || row.children || 0, 10) || 0;

        let finalTotal = base;
        let detail = '';
        let adultTotal = adultBase;
        let childTotal = childBase;

        if (type === 'total') {
            finalTotal = value;
            detail = 'Full night total override';
        } else if (type === 'extra_bed') {
            finalTotal = base + value;
            detail = 'Extra bed / triple supplement +' + formatDisplayMoney(value);
        } else if (type === 'sharing') {
            const oldRate = parseFloat(row.sharing_rate || 0) || 0;
            const delta = (value - oldRate) * sharingAdults;
            finalTotal = base + delta;
            adultTotal = Math.max(0, adultBase + delta);
            detail = 'Sharing rate override: ' + formatDisplayMoney(value) + ' x ' + sharingAdults;
        } else if (type === 'single') {
            const oldRate = parseFloat(row.single_rate || 0) || 0;
            const count = Math.max(1, singleRooms);
            const delta = (value - oldRate) * count;
            finalTotal = base + delta;
            adultTotal = Math.max(0, adultBase + delta);
            detail = 'Single rate override: ' + formatDisplayMoney(value) + ' x ' + count;
        } else if (type === 'child') {
            const newChildTotal = value * childCount;
            const delta = newChildTotal - childBase;
            finalTotal = base + delta;
            childTotal = Math.max(0, newChildTotal);
            detail = 'Child rate override: ' + formatDisplayMoney(value) + ' x ' + childCount;
        }

        finalTotal = Math.max(0, finalTotal);
        return {type, value, finalTotal, detail, adjustment: finalTotal - base, adultTotal, childTotal};
    }

    /*
       Component override: reprice a night from independent Sharing / Single /
       Child rates (any subset filled), repricing each room by its occupancy.
       Rooms with 2+ adults use the sharing rate per adult; single-adult rooms
       use the single rate per room; children use the child rate per child.
       A blank component means "keep the current rate for that component".
       Returns the recomputed night total plus the per-component detail we
       persist (as a "total" override carrying its breakdown for re-display).
    */
    /*
       PER-ROOM OVERRIDE MODEL
       nightlyOverrides[date] = {
           enabled: true,
           reason: '...',
           rooms: { "1": {mode:'perPerson'|'flat', value:Number}, "2": {...} },
           final_total: Number
       }
       A room absent from `rooms` (or mode 'keep') uses its calculated rate.
    */

    // Compute a room's overridden total given its breakdown entry + a room override.
    function computeRoomTotal(rb, roomOv) {
        const a = parseInt(rb.adults || 0, 10) || 0;
        const baseTotal = parseFloat(rb.room_total || 0) || 0;
        if (!roomOv || roomOv.mode === 'keep' || roomOv.value === null || roomOv.value === undefined || roomOv.value === '') {
            return baseTotal;
        }
        const v = parseFloat(roomOv.value);
        if (isNaN(v) || v < 0) return baseTotal;
        if (roomOv.mode === 'flat') {
            return Math.round(v * 100) / 100;         // flat room amount (0 = waived)
        }
        // perPerson: rate x adults in this room
        return Math.round((v * Math.max(1, a)) * 100) / 100;
    }

    // Recompute the whole night from its per-room overrides. Returns the new
    // breakdown (with room_total repriced) + the night total + a detail string.
    function computeNightFromRoomOverrides(row, ov) {
        const breakdown = Array.isArray(row.room_breakdown) ? row.room_breakdown : [];
        const rooms = (ov && ov.rooms) ? ov.rooms : {};
        let total = 0;
        const bits = [];
        const newBreakdown = breakdown.map(function (rb) {
            const out = Object.assign({}, rb);
            const key = String(rb.room_no);
            const roomOv = rooms[key];
            const rt = computeRoomTotal(rb, roomOv);
            out.room_total = rt;
            total += rt;
            if (roomOv && roomOv.mode && roomOv.mode !== 'keep' && roomOv.value !== '' && roomOv.value !== null && roomOv.value !== undefined) {
                if (roomOv.mode === 'flat') {
                    bits.push('Room ' + key + (parseFloat(roomOv.value) === 0 ? ' waived' : ' ' + formatDisplayMoney(roomOv.value)));
                } else {
                    bits.push('Room ' + key + ' ' + formatDisplayMoney(roomOv.value) + '/pp');
                }
            }
            return out;
        });
        return {
            finalTotal: Math.round(total * 100) / 100,
            breakdown: newBreakdown,
            detail: bits.length ? bits.join(' | ') : 'No room changes'
        };
    }

    function currentOverrideRowContext() {
        let breakdown = [];
        try { breakdown = JSON.parse(document.getElementById('nightOverrideRoomBreakdown').value || '[]'); } catch (e) { breakdown = []; }
        return {
            date: document.getElementById('nightOverrideDate').value,
            original_total: parseFloat(document.getElementById('nightOverrideBaseTotal').value || 0) || 0,
            night_total: parseFloat(document.getElementById('nightOverrideBaseTotal').value || 0) || 0,
            room_breakdown: breakdown
        };
    }

    // Read the per-room editor rows in the modal into a rooms map.
    function readOverrideRoomInputs() {
        const rooms = {};
        document.querySelectorAll('#nightOverrideRoomRows tr[data-room-no]').forEach(function (tr) {
            const key = tr.getAttribute('data-room-no');
            const modeEl = tr.querySelector('.ov-room-mode');
            const valEl = tr.querySelector('.ov-room-value');
            const mode = modeEl ? modeEl.value : 'keep';
            const raw = valEl ? (valEl.value || '').trim() : '';
            if (mode === 'keep' || raw === '') {
                return; // not overridden
            }
            const v = parseFloat(raw);
            if (isNaN(v) || v < 0) return;
            rooms[key] = {mode: mode, value: v};
        });
        return rooms;
    }
    function applyOverrideToRow(row) {
        if (!row || !row.date) return row;
        const original = parseFloat(row.original_total || row.night_total || 0) || 0;
        row.original_total = original;
        const ov = nightlyOverrides[row.date];
        if (ov && ov.enabled && ov.rooms && Object.keys(ov.rooms).length > 0) {
            const calc = computeNightFromRoomOverrides(row, ov);
            row.rate_overridden = 1;
            row.override_type = 'per_room';
            row.override_total = calc.finalTotal;
            row.final_total = calc.finalTotal;
            row.override_reason = ov.reason || '';
            row.override_detail = calc.detail;
            row.night_total = calc.finalTotal;
            // Repaint each room card with its overridden amount.
            row.room_breakdown = calc.breakdown;
            row.room_overrides = ov.rooms;
        } else {
            row.rate_overridden = 0;
            row.override_total = null;
            row.override_value = null;
            row.override_type = '';
            row.override_reason = '';
            row.override_detail = '';
            row.night_total = original;
        }
        return row;
    }

    function recalcPreviewFooterFromRows() {
        let total = 0;
        document.querySelectorAll('#pricingPreviewBody tr[data-date]').forEach(function (tr) {
            total += parseFloat(tr.getAttribute('data-night-total') || 0) || 0;
        });
        const footer = document.getElementById('previewTotalFooter');
        if (footer) footer.textContent = formatDisplayMoney(total);
        const acc = document.getElementById('summaryAccommodationTotal');
        if (acc) acc.textContent = formatDisplayMoney(total);
        const grand = document.getElementById('summaryGrandTotal');
        if (grand) grand.textContent = formatDisplayMoney(total + calculateExtrasTotal());
    }

    const propertyField = form.querySelector('[name="property_id"]');
    const agentField = form.querySelector('[name="agent_id"]');
    const contactField = form.querySelector('[name="contact_id"]');
    const roomRowsWrap = document.getElementById('roomRowsWrap');
    const addRoomRowBtn = document.getElementById('addRoomRowBtn');
    const pricingLocked = <?php echo (int)$pricingLocked; ?>;
    let availabilityByRoomType = <?php echo json_encode($roomTypeAvailabilityCurrent ?: $roomTypeAvailabilityAll, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '{}'; ?>;
    const initialContactId = '<?php echo (int)($form["contact_id"] ?? 0); ?>';
    /*
       One-shot override consumed by the agent 'change' handler. When set to a
       contact id, the next change event keeps that contact instead of the
       field's live value; set to '' to force-clear (used when the user picks a
       genuinely different agent). null means "keep whatever is selected".
    */
    let agentChangePreserveContactId = null;
    // Agents for the searchable combobox (single control filtering this list).
    let agentComboData = <?php echo json_encode(array_map(function ($a) {
        $an = trim((string)($a['display_name'] ?? ''));
        return ['id' => (int)$a['id'], 'label' => $an !== '' ? $an : ('Agent #' . (int)$a['id'])];
    }, $agents), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '[]'; ?>;
    const consultantsByAgent = <?php echo json_encode($consultantsByAgent, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '{}'; ?>;
    /*
       The saved contact for an existing booking, looked up by id irrespective of
       which agent it is currently linked to. This is the safety net for the
       "editing loses the contact" bug: if the consultant was re-linked to another
       agent (or the data drifted) the contact would vanish from the agent-scoped
       list when the dropdown is rebuilt on load. We inject it as a fallback option
       so the saved selection always survives a reopen.
    */
    const initialContactRecord = <?php
        $initialContactRecord = null;
        $initialContactIdPhp = (int)($form['contact_id'] ?? 0);
        if ($initialContactIdPhp > 0) {
            try {
                $icStmt = $pdo->prepare("SELECT id, firstname, lastname, email, mobile, phone FROM consultants WHERE id = :id LIMIT 1");
                $icStmt->execute([':id' => $initialContactIdPhp]);
                $icRow = $icStmt->fetch(PDO::FETCH_ASSOC);
                if ($icRow) {
                    $initialContactRecord = [
                        'id'        => (int)$icRow['id'],
                        'firstname' => (string)($icRow['firstname'] ?? ''),
                        'lastname'  => (string)($icRow['lastname'] ?? ''),
                        'email'     => (string)($icRow['email'] ?? ''),
                        'mobile'    => (string)($icRow['mobile'] ?? ''),
                        'phone'     => (string)($icRow['phone'] ?? ''),
                    ];
                }
            } catch (Throwable $e) {
                $initialContactRecord = null;
            }
        }
        echo json_encode($initialContactRecord, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: 'null';
    ?>;

    function toPositiveInt(value, fallback) {
        const n = parseInt(value, 10);
        return isNaN(n) || n < 0 ? (fallback || 0) : n;
    }

    function setFieldValue(id, value) {
        const el = document.getElementById(id);
        if (!el) return;
        if ('value' in el) {
            el.value = value;
        } else {
            el.textContent = value;
        }
    }

    function calculateRoomSummaryTotals() {
        let totalRooms = 0;
        let totalAdults = 0;
        let totalChildren = 0;

        if (!roomRowsWrap) {
            return { totalRooms: 0, totalAdults: 0, totalChildren: 0 };
        }

        roomRowsWrap.querySelectorAll('.room-row').forEach(function (row) {
            const qtyEl = row.querySelector('.room-qty');
            const adultsEl = row.querySelector('.room-adults');
            const childrenEl = row.querySelector('.room-children');

            totalRooms += Math.max(1, toPositiveInt(qtyEl ? qtyEl.value : 1, 1));
            totalAdults += toPositiveInt(adultsEl ? adultsEl.value : 0, 0);
            totalChildren += toPositiveInt(childrenEl ? childrenEl.value : 0, 0);
        });

        return {
            totalRooms: totalRooms,
            totalAdults: totalAdults,
            totalChildren: totalChildren
        };
    }

    function refreshRoomSummaryTotals() {
        const totals = calculateRoomSummaryTotals();

        setFieldValue('totalrooms_display', totals.totalRooms);
        setFieldValue('adults_display', totals.totalAdults);
        setFieldValue('kids_display', totals.totalChildren);

        setFieldValue('summaryTotalRooms', totals.totalRooms);
        setFieldValue('summaryAdults', totals.totalAdults);
        setFieldValue('summaryChildren', totals.totalChildren);

        return totals;
    }


    function getAllSelectedRoomTypes() {
        const selected = [];
        roomRowsWrap.querySelectorAll('.room-type-select').forEach(function (selectEl) {
            const val = (selectEl.value || '').trim();
            if (val !== '') {
                selected.push(val);
            }
        });
        return selected;
    }

    function getAvailableRoomTypeValues() {
        const firstSelect = roomRowsWrap.querySelector('.room-type-select');
        if (!firstSelect) return [];

        return Array.from(firstSelect.options)
            .map(function (opt) { return (opt.value || '').trim(); })
            .filter(function (val) { return val !== ''; });
    }

    function getSelectedRoomTypeValues() {
        const vals = [];
        roomRowsWrap.querySelectorAll('.room-type-select').forEach(function (selectEl) {
            const val = (selectEl.value || '').trim();
            if (val !== '' && !vals.includes(val)) {
                vals.push(val);
            }
        });
        return vals;
    }

    function refreshAddRoomButtonState() {
        if (!addRoomRowBtn) return;

        const allTypes = getAvailableRoomTypeValues();
        const selectedTypes = getSelectedRoomTypeValues();
        const exhausted = allTypes.length > 0 && selectedTypes.length >= allTypes.length;

        addRoomRowBtn.disabled = exhausted;

        if (exhausted) {
            addRoomRowBtn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>All room types added';
            addRoomRowBtn.classList.remove('btn-outline-primary');
            addRoomRowBtn.classList.add('btn-outline-secondary');
        } else {
            addRoomRowBtn.innerHTML = '<i class="bi bi-plus-circle me-1"></i>Add Room';
            addRoomRowBtn.classList.remove('btn-outline-secondary');
            addRoomRowBtn.classList.add('btn-outline-primary');
        }
    }

    function refreshRoomTypeFilters() {
        const allSelected = getAllSelectedRoomTypes();

        roomRowsWrap.querySelectorAll('.room-row').forEach(function (row) {
            const currentSelect = row.querySelector('.room-type-select');
            if (!currentSelect) return;

            const currentValue = (currentSelect.value || '').trim();

            Array.from(currentSelect.options).forEach(function (opt) {
                if (!opt.value) {
                    opt.disabled = false;
                    opt.hidden = false;
                    return;
                }

                const pickedSomewhereElse = allSelected.includes(opt.value) && opt.value !== currentValue;
                opt.disabled = pickedSomewhereElse;
                opt.hidden = pickedSomewhereElse;
            });
        });

        refreshAddRoomButtonState();
    }

    function reindexRoomRows() {
        const rows = roomRowsWrap.querySelectorAll('.room-row');

        rows.forEach(function (row, idx) {
            row.dataset.index = idx;

            const roomType = row.querySelector('.room-type-select');
            const qtyRooms = row.querySelector('.room-qty');
            const adults = row.querySelector('.room-adults');
            const children = row.querySelector('.room-children');
            const extraBeds = row.querySelector('.room-extra-beds');
            const cots = row.querySelector('.room-cots');
            const wheelchair = row.querySelector('.room-wheelchair');
            const interleading = row.querySelector('.room-interleading');
            const setupNotes = row.querySelector('.room-setup-notes');
            const housekeepingNotes = row.querySelector('.room-housekeeping-notes');
            const specialOccasion = row.querySelector('.room-special-occasion');

            if (roomType) roomType.name = 'room_rows[' + idx + '][room_type_id]';
            if (qtyRooms) qtyRooms.name = 'room_rows[' + idx + '][qty_rooms]';
            if (adults) adults.name = 'room_rows[' + idx + '][adults]';
            if (children) children.name = 'room_rows[' + idx + '][children]';
            if (extraBeds) extraBeds.name = 'room_rows[' + idx + '][extra_beds_required]';
            if (cots) cots.name = 'room_rows[' + idx + '][cots_required]';
            if (wheelchair) { wheelchair.name = 'room_rows[' + idx + '][wheelchair_room_required]'; wheelchair.id = 'wheelchair_' + idx; const label = row.querySelector('label[for^="wheelchair_"]'); if (label) label.setAttribute('for', wheelchair.id); }
            if (interleading) { interleading.name = 'room_rows[' + idx + '][interleading_required]'; interleading.id = 'interleading_' + idx; const label = row.querySelector('label[for^="interleading_"]'); if (label) label.setAttribute('for', interleading.id); }
            if (setupNotes) setupNotes.name = 'room_rows[' + idx + '][room_setup_notes]';
            if (housekeepingNotes) housekeepingNotes.name = 'room_rows[' + idx + '][housekeeping_notes]';
            if (specialOccasion) specialOccasion.name = 'room_rows[' + idx + '][special_occasion]';
        });

        refreshRoomTypeFilters();
        refreshRoomSummaryTotals();
    }

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


    function contactOptionLabel(contact) {
        const name = [contact.firstname || '', contact.lastname || ''].join(' ').trim() || ('Contact #' + (contact.id || ''));
        const meta = [];
        if (contact.email) meta.push(contact.email);
        if (contact.mobile) meta.push(contact.mobile);
        else if (contact.phone) meta.push(contact.phone);
        return name + (meta.length ? ' - ' + meta.join(' / ') : '');
    }

    function renderAgentContacts(rows, selectedContactId) {
        let list = (rows || []).slice();

        /*
           If we are restoring a saved booking and the saved contact is not in the
           agent-scoped list, splice it in so the selection is never lost. Without
           this, reopening a booking whose consultant has since been re-linked to a
           different agent would blank the contact field.
        */
        const wantId = String(selectedContactId || '');
        if (wantId && !list.some(function (c) { return String(c.id || '') === wantId; })) {
            if (initialContactRecord && String(initialContactRecord.id || '') === wantId) {
                list.unshift(initialContactRecord);
            }
        }

        let html = '<option value="">Select contact</option>';
        list.forEach(function (contact) {
            const selected = wantId === String(contact.id || '') ? ' selected' : '';
            html += '<option value="' + escapeHtml(contact.id || '') + '"' + selected + '>' + escapeHtml(contactOptionLabel(contact)) + '</option>';
        });

        if (list.length === 0) {
            html = '<option value="">No contacts linked to this agent</option>';
        }

        contactField.innerHTML = html;

        // Guarantee the DOM value reflects the intended selection.
        if (wantId) {
            contactField.value = wantId;
            if (String(contactField.value) !== wantId) {
                // Option genuinely absent (e.g. record could not be loaded) - leave unset.
            }
        }
    }

    async function loadAgentContacts(agentId, selectedContactId) {
        if (!contactField) return;

        const cleanAgentId = String(agentId || '').trim();
        console.log('Agent contact loader fired. agent_id=', cleanAgentId);
        contactField.innerHTML = '<option value="">Loading contacts...</option>';

        if (!cleanAgentId) {
            contactField.innerHTML = '<option value="">Select contact</option>';
            return;
        }

        /*
           Primary source: consultants preloaded by agent_id from PHP.
           This avoids AJAX/path/cache issues and uses the exact schema:
           consultants.agent_id = agents.id
        */
        if (consultantsByAgent && Object.prototype.hasOwnProperty.call(consultantsByAgent, cleanAgentId)) {
            renderAgentContacts(consultantsByAgent[cleanAgentId] || [], selectedContactId);
            return;
        }

        /* AJAX fallback only for unexpected dynamically-added agents. Still no all-consultants fallback. */
        try {
            const url = new URL(window.location.href);
            url.search = '';
            url.searchParams.set('ajax', 'agent_contacts');
            url.searchParams.set('agent_id', cleanAgentId);

            const resp = await fetch(url.toString(), {
                credentials: 'same-origin',
                headers: { 'Accept': 'application/json' }
            });
            const json = await resp.json();

            if (!json.ok) {
                contactField.innerHTML = '<option value="">No contacts found</option>';
                alert(json.message || 'Could not load contacts for the selected agent.');
                return;
            }

            renderAgentContacts(json.rows || [], selectedContactId);
        } catch (e) {
            contactField.innerHTML = '<option value="">No contacts linked to this agent</option>';
            console.warn('Could not load agent contacts', e);
        }
    }
    function getRoomTypeOptionsHtml() {
        const first = roomRowsWrap.querySelector('.room-type-select');
        return first ? first.innerHTML : '<option value="">Select room type</option>';
    }

    function getAvailableForRoomType(roomTypeId, optionEl) {
        const key = String(roomTypeId || '');
        if (key !== '' && availabilityByRoomType && availabilityByRoomType[key]) {
            return parseInt(availabilityByRoomType[key].available_rooms || '0', 10) || 0;
        }
        if (optionEl && optionEl.hasAttribute('data-available')) {
            return parseInt(optionEl.getAttribute('data-available') || '0', 10) || 0;
        }
        return null;
    }

    function getSelectedOptionMeta(selectEl) {
        if (!selectEl) return {higherOccupancy: 0, roomTitle: '', availableRooms: 0};
        const opt = selectEl.options[selectEl.selectedIndex];
        if (!opt) return {higherOccupancy: 0, roomTitle: '', availableRooms: 0};
        return {
            higherOccupancy: parseInt(opt.getAttribute('data-higher-occupancy') || '0', 10) || 0,
            roomTitle: opt.getAttribute('data-room-title') || opt.textContent || '',
            availableRooms: getAvailableForRoomType(selectEl.value, opt)
        };
    }

    function validateRoomRow(row) {
        const roomType = row.querySelector('.room-type-select');
        const qtyRooms = row.querySelector('.room-qty');
        const adults = row.querySelector('.room-adults');
        const children = row.querySelector('.room-children');
        if (!roomType || !qtyRooms || !adults || !children) return;

        const qty = parseInt(qtyRooms.value || '0', 10) || 0;
        const adultCount = parseInt(adults.value || '0', 10) || 0;
        const childCount = parseInt(children.value || '0', 10) || 0;
        const totalPax = adultCount + childCount;
        const meta = getSelectedOptionMeta(roomType);
        const higherOccupancy = meta.higherOccupancy;

        const availabilityKnown = meta.availableRooms !== null && meta.availableRooms !== undefined && meta.availableRooms !== '';
        const availableRooms = availabilityKnown ? (parseInt(meta.availableRooms || '0', 10) || 0) : 0;
        let maxRooms = totalPax > 0 ? totalPax : 0;
        if (availabilityKnown) {
            maxRooms = maxRooms > 0 ? Math.min(maxRooms, availableRooms) : availableRooms;
        }

        if (maxRooms > 0) {
            qtyRooms.max = String(maxRooms);
        } else if (availabilityKnown && (roomType.value || '').trim() !== '') {
            qtyRooms.max = '0';
        } else {
            qtyRooms.removeAttribute('max');
        }

        if (higherOccupancy > 0 && qty > 0) {
            adults.max = String(higherOccupancy * qty);
        } else {
            adults.removeAttribute('max');
        }

        if (availabilityKnown && availableRooms > 0 && qty > availableRooms) {
            qtyRooms.setCustomValidity('Only ' + availableRooms + ' room(s) are available for ' + (meta.roomTitle || 'this room type') + '.');
        } else if (availabilityKnown && availableRooms <= 0 && (roomType.value || '').trim() !== '') {
            qtyRooms.setCustomValidity('No rooms are available for ' + (meta.roomTitle || 'this room type') + ' for the selected dates.');
        } else if (qty > totalPax && totalPax > 0) {
            qtyRooms.setCustomValidity('Number of rooms cannot be more than total adults + children.');
        } else {
            qtyRooms.setCustomValidity('');
        }

        if (higherOccupancy > 0 && qty > 0 && totalPax > (higherOccupancy * qty)) {
            children.setCustomValidity('Total guests cannot exceed ' + higherOccupancy + ' per room x ' + qty + ' room(s) = ' + (higherOccupancy * qty) + '.');
        } else {
            children.setCustomValidity('');
        }
    }

    function money2(value) {
        const n = parseFloat(value || 0);
        return (isNaN(n) ? 0 : n).toFixed(2);
    }

    function formatDisplayMoney(value) {
        const n = parseFloat(value || 0);
        return (isNaN(n) ? 0 : n).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
    }

    function renderPricingRows(rows) {
        const tbody = document.getElementById('pricingPreviewBody');
        if (!tbody) return;

        if (!rows || !rows.length) {
            tbody.innerHTML = '<tr><td colspan="11" class="text-center text-muted py-4">Enter the booking details to preview pricing.</td></tr>';
            return;
        }

        // Cache the current rows so "apply to whole stay" can reach every night.
        window.__previewRowsCache = rows;

        tbody.innerHTML = rows.map(function (row) {
            row = applyOverrideToRow(row);
            const breakdown = (row.room_breakdown || []).length
                ? '<div class="d-grid gap-2">' + row.room_breakdown.map(function (rb) {
                    const label = rb.display_label ? '<span class="badge text-bg-warning ms-2"><i class="bi bi-person-plus"></i> ' + escapeHtml(rb.display_label) + '</span>' : '';
                    const roomType = rb.room_type_title ? '<span class="text-muted">— ' + escapeHtml(rb.room_type_title) + '</span>' : '';
                    const season = rb.rate_plan_season ? ' • <span class="badge text-bg-secondary">' + escapeHtml(rb.rate_plan_season) + '</span>' : '';
                    return '<div class="small border rounded p-2 bg-light btn-room-override" role="button" title="Click to edit this room">'
                        + '<div><strong>Room ' + escapeHtml(rb.room_no) + ':</strong> ' + escapeHtml(rb.adults || 0) + 'A' + ((parseInt(rb.children || 0, 10) > 0) ? ' + ' + escapeHtml(rb.children) + 'C' : '') + ' ' + roomType + label + '</div>'
                        + '<div class="text-muted">' + escapeHtml(rb.rate_plan_title || '') + (rb.rate_plan_currency ? ' (' + escapeHtml(rb.rate_plan_currency) + ')' : '') + season + '</div>'
                        + '<div><span class="badge text-bg-light border text-dark">' + escapeHtml(rb.rate_type || '') + '</span><span class="ms-2 fw-semibold">' + formatDisplayMoney(rb.room_total || 0) + '</span></div>'
                        + '</div>';
                }).join('') + '</div>'
                : '<span class="text-muted">No room breakdown</span>';

            const d = row.date ? new Date(row.date + 'T00:00:00') : null;
            const dateLabel = d && !isNaN(d.getTime()) ? d.toLocaleDateString(undefined, {day:'2-digit', month:'short', year:'numeric'}) : escapeHtml(row.date || '');
            const season = row.rate_plan_season ? ' • <span class="badge text-bg-secondary">' + escapeHtml(row.rate_plan_season) + '</span>' : '';

            /*
               Effective sharing display: when a per-room "per person" override
               is active for this night, show the overridden per-person rate here
               (with the base rate struck through) instead of the untouched
               rate-plan sharing rate. Previously this column always showed the
               base rate, which made a saved per-person edit look like it had
               reverted even though the night total was correct.
            */
            let sharingCell = formatDisplayMoney(row.sharing_rate || 0);
            const roomOv = row.room_overrides || (row.override_components && row.override_components.per_room) || null;
            if (roomOv && typeof roomOv === 'object') {
                let ppRate = null;
                Object.keys(roomOv).forEach(function (k) {
                    const o = roomOv[k];
                    if (o && o.mode === 'perPerson' && o.value !== undefined && o.value !== null && o.value !== '') {
                        ppRate = parseFloat(o.value);
                    }
                });
                if (ppRate !== null && !isNaN(ppRate) && Math.abs(ppRate - (parseFloat(row.sharing_rate) || 0)) > 0.001) {
                    sharingCell = '<span class="text-decoration-line-through text-muted small">' + formatDisplayMoney(row.sharing_rate || 0) + '</span> '
                        + '<span class="text-danger fw-semibold">' + formatDisplayMoney(ppRate) + '</span>';
                }
            }

            return '<tr data-date="' + escapeHtml(row.date || '') + '" data-night-total="' + escapeHtml(row.night_total || 0) + '">'
                + '<td>' + dateLabel + '</td>'
                + '<td><div class="fw-semibold">' + escapeHtml(row.rate_plan || '') + (row.rate_plan_currency ? ' (' + escapeHtml(row.rate_plan_currency) + ')' : '') + '</div><div class="small text-muted">Plan ID: ' + escapeHtml(row.rate_plan_id || 0) + season + '</div></td>'
                + '<td>' + sharingCell + '</td>'
                + '<td>' + formatDisplayMoney(row.single_rate || 0) + '</td>'
                + '<td>' + formatDisplayMoney(row.single_supplement || 0) + '</td>'
                + '<td>' + formatDisplayMoney(row.child_rate_percent || 0) + '%</td>'
                + '<td>' + breakdown + '</td>'
                + '<td>' + formatDisplayMoney(row.adult_total || 0) + '</td>'
                + '<td>' + formatDisplayMoney(row.child_total || 0) + '</td>'
                + '<td class="text-end fw-bold">' + (row.rate_overridden ? '<div class="text-decoration-line-through text-muted small">' + formatDisplayMoney(row.original_total || 0) + '</div><div class="text-danger">' + formatDisplayMoney(row.night_total || 0) + '</div>' : formatDisplayMoney(row.night_total || 0)) + '</td>'
                + '<td class="text-end"><button type="button" class="btn btn-sm ' + (row.rate_overridden ? 'btn-warning' : 'btn-outline-secondary') + ' btn-night-override" data-date="' + escapeHtml(row.date || '') + '" data-date-label="' + dateLabel + '" data-original-total="' + escapeHtml(row.original_total || row.night_total || 0) + '" data-current-total="' + escapeHtml(row.night_total || 0) + '" data-override-type="' + escapeHtml(row.override_type || 'total') + '" data-override-value="' + escapeHtml(row.override_value ?? row.override_total ?? '') + '" data-adult-total="' + escapeHtml(row.adult_total || 0) + '" data-child-total="' + escapeHtml(row.child_total || 0) + '" data-sharing-rate="' + escapeHtml(row.sharing_rate || 0) + '" data-single-rate="' + escapeHtml(row.single_rate || 0) + '" data-room-breakdown="' + escapeHtml(JSON.stringify(row.room_breakdown || [])) + '" data-reason="' + escapeHtml(row.override_reason || '') + '"><i class="bi bi-pencil-square me-1"></i>' + (row.rate_overridden ? 'Edit' : 'Override') + '</button>' + (row.rate_overridden ? '<div class="small text-muted mt-1">' + escapeHtml(row.override_detail || row.override_reason || '') + '</div>' : '') + '</td>'
                + '</tr>';
        }).join('');
        recalcPreviewFooterFromRows();
    }

    function updateSummary(json) {
        const setText = function (id, value) {
            const el = document.getElementById(id);
            if (el) el.textContent = value;
        };

        /*
           Rate contract banner. Pulses ONCE when the contract or the kind of
           rate changes, then sits still - a permanently blinking element is an
           accessibility failure and gets tuned out within a day anyway.
        */
        function updateRateContractBanner(rc) {
            const banner = document.getElementById('rateContractBanner');
            if (!banner) return;

            const planEl = document.getElementById('rateContractBannerPlan');
            const noteEl = document.getElementById('rateContractBannerNote');

            const source = (rc && rc.source) ? String(rc.source) : '';
            const plan   = (rc && rc.plan)   ? String(rc.plan)   : '';
            const meal   = (rc && rc.meal_plan) ? String(rc.meal_plan) : '';

            let note;
            if (source === 'agent') {
                note = 'Agent contract rate' + (meal ? ' \u00b7 ' + meal : '');
            } else if (source === 'agent_derived') {
                note = 'Derived from contract base + meal supplement'
                     + (rc && rc.derived_from ? ' (from ' + rc.derived_from + ')' : '')
                     + (meal ? ' \u00b7 ' + meal : '');
            } else if (source === 'public_fallback') {
                note = 'No agent contract for these dates \u2014 RACK RATE applied';
            } else if (source === 'public') {
                note = 'Public / rack rate' + (meal ? ' \u00b7 ' + meal : '');
            } else if (source === 'zero_hold') {
                note = 'No rate resolved \u2014 booking held at zero';
            } else {
                note = 'Set an agent and dates, then refresh pricing.';
            }
            if (rc && rc.unresolved) {
                note += ' \u00b7 no ' + rc.unresolved + ' rate in contract';
            }

            const prev = banner.getAttribute('data-source') || '';
            const prevPlan = planEl ? planEl.textContent.trim() : '';
            const nextPlan = plan !== '' ? plan : 'Not yet priced';
            const changed = (prev !== source) || (prevPlan !== nextPlan);

            if (planEl) planEl.textContent = nextPlan;
            if (noteEl) noteEl.textContent = note;

            banner.className = 'rate-contract-banner mb-3 rcb-' + (source !== '' ? source : 'none');
            banner.setAttribute('data-source', source);

            if (changed) {
                banner.classList.remove('rcb-changed');
                // Force reflow so the animation restarts on a repeat change.
                void banner.offsetWidth;
                banner.classList.add('rcb-changed');
                window.setTimeout(function () {
                    banner.classList.remove('rcb-changed');
                }, 1300);
            }
        }
        setText('summaryNights', json.nights || 0);
        updateRateContractBanner(json.rate_contract || null);
        const roomTotals = calculateRoomSummaryTotals();
        const totalRooms = json.total_rooms !== undefined ? json.total_rooms : roomTotals.totalRooms;
        const adults = json.adults !== undefined ? json.adults : roomTotals.totalAdults;
        const children = json.children !== undefined ? json.children : roomTotals.totalChildren;

        setText('summaryTotalRooms', totalRooms || 0);
        setText('summaryAdults', adults || 0);
        setText('summaryChildren', children || 0);
        setFieldValue('totalrooms_display', totalRooms || 0);
        setFieldValue('adults_display', adults || 0);
        setFieldValue('kids_display', children || 0);
        setText('summaryKids03', json.kids_0_3 || 0);
        setText('summaryTotalInventory', json.total_inventory || 0);
        setText('summaryAccommodationTotal', formatDisplayMoney(json.accommodation_total || 0));
        setText('summaryExtrasTotal', formatDisplayMoney(json.extras_total || 0));
        setText('summaryGrandTotal', formatDisplayMoney(json.grand_total || 0));
        setText('bookingExtrasTotal', formatDisplayMoney(json.extras_total || calculateExtrasTotal()));
        setText('previewTotalFooter', formatDisplayMoney(json.accommodation_total || 0));
        if (json.availability_by_type) {
            availabilityByRoomType = json.availability_by_type;
            roomRowsWrap.querySelectorAll('.room-row').forEach(validateRoomRow);
        }

        const avail = document.getElementById('availableRoomsStat');
        if (avail) {
            avail.textContent = json.available_rooms || 0;
            avail.dataset.availableRooms = json.available_rooms || 0;
        }

        const lodgeBadge = document.getElementById('summaryLodgeAvailBadge');
        if (lodgeBadge && json.lodge_total !== undefined) {
            const lodgeAvail = json.lodge_available || 0;
            const lodgeTotal = json.lodge_total || 0;
            lodgeBadge.textContent = lodgeAvail + ' free / ' + lodgeTotal + ' total';
            lodgeBadge.dataset.lodgeAvailable = lodgeAvail;
            lodgeBadge.dataset.lodgeTotal = lodgeTotal;
        }
    }

    function calculateExtrasTotal() {
        let sum = 0;
        const wrap = document.getElementById('bookingExtrasWrap');
        if (!wrap) return 0;
        wrap.querySelectorAll('.booking-extra-row').forEach(function (row) {
            const svc = row.querySelector('.extra-service');
            const qty = row.querySelector('.extra-qty');
            const rateEl = row.querySelector('.extra-rate');
            const selected = svc ? svc.options[svc.selectedIndex] : null;
            const master = selected ? parseFloat(selected.getAttribute('data-price') || 0) : 0;
            // Prefer the (possibly corrected) rate the user sees; fall back to master price.
            let price = master;
            if (rateEl) {
                const typed = parseFloat(String(rateEl.value || '').replace(/,/g, ''));
                if (!isNaN(typed) && typed > 0) price = typed;
            }
            const q = parseFloat(qty ? qty.value || 0 : 0);
            if (!isNaN(price) && !isNaN(q)) sum += price * q;
        });
        return sum;
    }


    // Build the per-room editor rows inside the modal from a breakdown +
    // any existing per-room overrides for this night.
    function buildOverrideRoomRows(breakdown, existingRooms) {
        const tbody = document.getElementById('nightOverrideRoomRows');
        if (!tbody) return;
        tbody.innerHTML = '';
        (Array.isArray(breakdown) ? breakdown : []).forEach(function (rb) {
            const key = String(rb.room_no);
            const a = parseInt(rb.adults || 0, 10) || 0;
            const c = parseInt(rb.children || 0, 10) || 0;
            const label = (rb.room_type_title || rb.rate_type || 'Room');
            const existing = (existingRooms && existingRooms[key]) ? existingRooms[key] : null;
            const mode = existing ? existing.mode : 'keep';
            const val = existing && existing.value !== undefined && existing.value !== null ? existing.value : '';

            const tr = document.createElement('tr');
            tr.setAttribute('data-room-no', key);
            tr.innerHTML =
                '<td><strong>Room ' + escapeHtml(key) + '</strong> '
                    + '<span class="text-muted small">' + a + 'A' + (c > 0 ? ' + ' + c + 'C' : '')
                    + (label ? ' &middot; ' + escapeHtml(label) : '') + '</span></td>'
                + '<td>'
                    + '<select class="form-select form-select-sm ov-room-mode">'
                        + '<option value="keep"' + (mode === 'keep' ? ' selected' : '') + '>Keep</option>'
                        + '<option value="perPerson"' + (mode === 'perPerson' ? ' selected' : '') + '>Per person</option>'
                        + '<option value="flat"' + (mode === 'flat' ? ' selected' : '') + '>Flat total</option>'
                    + '</select>'
                + '</td>'
                + '<td>'
                    + '<input type="number" step="0.01" min="0" class="form-control form-control-sm ov-room-value" '
                        + 'value="' + escapeHtml(val === '' ? '' : val) + '" '
                        + (mode === 'keep' ? 'disabled ' : '') + 'placeholder="0.00">'
                + '</td>'
                + '<td class="text-end ov-room-total" data-base-total="' + escapeHtml(rb.room_total || 0) + '">'
                    + formatDisplayMoney(rb.room_total || 0)
                + '</td>';
            tbody.appendChild(tr);
        });

        // Wire live recompute on each row.
        tbody.querySelectorAll('tr[data-room-no]').forEach(function (tr) {
            const modeEl = tr.querySelector('.ov-room-mode');
            const valEl = tr.querySelector('.ov-room-value');
            if (modeEl) modeEl.addEventListener('change', function () {
                if (valEl) valEl.disabled = (modeEl.value === 'keep');
                updateOverrideModalPreview();
            });
            if (valEl) valEl.addEventListener('input', updateOverrideModalPreview);
        });
    }

    function updateOverrideModalPreview() {
        const row = currentOverrideRowContext();
        const rooms = readOverrideRoomInputs();
        const calc = computeNightFromRoomOverrides(row, {rooms: rooms});

        // Update each row's shown room total.
        const map = {};
        calc.breakdown.forEach(function (rb) { map[String(rb.room_no)] = rb.room_total; });
        document.querySelectorAll('#nightOverrideRoomRows tr[data-room-no]').forEach(function (tr) {
            const key = tr.getAttribute('data-room-no');
            const cell = tr.querySelector('.ov-room-total');
            if (cell && map[key] !== undefined) cell.textContent = formatDisplayMoney(map[key]);
        });

        const mix = document.getElementById('nightOverrideMixSummary');
        if (mix) mix.textContent = (Array.isArray(row.room_breakdown) ? row.room_breakdown.length : 0) + ' room(s) this night. Edit any of them below.';

        const baseEl = document.getElementById('overrideBasePreview');
        const finalEl = document.getElementById('overrideFinalPreview');
        if (baseEl) baseEl.textContent = formatDisplayMoney(row.original_total);
        if (finalEl) finalEl.textContent = formatDisplayMoney(calc.finalTotal);
        return calc;
    }

    document.addEventListener('click', function (e) {
        const btn = e.target.closest('.btn-night-override');
        if (!btn) return;
        const modalEl = document.getElementById('nightOverrideModal');
        if (!modalEl) return;
        const date = btn.getAttribute('data-date') || '';
        const ov = nightlyOverrides[date] || {};
        document.getElementById('nightOverrideDate').value = date;
        document.getElementById('nightOverrideDateLabel').textContent = btn.getAttribute('data-date-label') || date;
        document.getElementById('nightOverrideBaseTotal').value = btn.getAttribute('data-original-total') || btn.getAttribute('data-current-total') || 0;
        document.getElementById('nightOverrideRoomBreakdown').value = btn.getAttribute('data-room-breakdown') || '[]';

        let breakdown = [];
        try { breakdown = JSON.parse(btn.getAttribute('data-room-breakdown') || '[]'); } catch (err) { breakdown = []; }

        buildOverrideRoomRows(breakdown, ov.rooms || null);
        document.getElementById('nightOverrideReason').value = ov.reason !== undefined ? ov.reason : (btn.getAttribute('data-reason') || '');
        const wholeStayEl = document.getElementById('nightOverrideWholeStay');
        if (wholeStayEl) wholeStayEl.checked = false;

        updateOverrideModalPreview();
        bootstrap.Modal.getOrCreateInstance(modalEl).show();
    });

    // If focusing a specific room (click a room card), open the night modal.
    document.addEventListener('click', function (e) {
        const roomBtn = e.target.closest('.btn-room-override');
        if (!roomBtn) return;
        const nightBtn = roomBtn.closest('tr').querySelector('.btn-night-override');
        if (nightBtn) nightBtn.click();
    });

    function collectRoomsFromModal() {
        return readOverrideRoomInputs();
    }

    const applyNightOverrideBtn = document.getElementById('applyNightOverrideBtn');
    if (applyNightOverrideBtn) {
        applyNightOverrideBtn.addEventListener('click', function () {
            const date = document.getElementById('nightOverrideDate').value;
            const reason = document.getElementById('nightOverrideReason').value || '';
            if (!date) return;

            const rooms = collectRoomsFromModal();
            if (Object.keys(rooms).length === 0) {
                // No room overridden -> treat as clear for this night.
                if (nightlyOverrides[date]) delete nightlyOverrides[date];
                syncNightlyOverridesInput();
                bootstrap.Modal.getOrCreateInstance(document.getElementById('nightOverrideModal')).hide();
                submitPreview();
                return;
            }

            const row = currentOverrideRowContext();
            const calc = computeNightFromRoomOverrides(row, {rooms: rooms});
            const baseTotal = parseFloat(document.getElementById('nightOverrideBaseTotal').value || 0) || 0;

            const overrideObj = {
                enabled: true,
                type: 'per_room',
                rooms: rooms,
                original_total: baseTotal.toFixed(2),
                override_total: calc.finalTotal.toFixed(2),
                final_total: calc.finalTotal.toFixed(2),
                reason: reason,
                override_detail: calc.detail
            };
            nightlyOverrides[date] = overrideObj;

            // Apply to the whole stay if requested: copy the same room map to
            // every night that has the same set of room numbers.
            const wholeStayEl = document.getElementById('nightOverrideWholeStay');
            if (wholeStayEl && wholeStayEl.checked) {
                applyRoomOverridesToWholeStay(date, rooms, reason);
            }

            syncNightlyOverridesInput();
            bootstrap.Modal.getOrCreateInstance(document.getElementById('nightOverrideModal')).hide();
            submitPreview();
        });
    }

    /*
       BULLETPROOF: mirror every staged override into its own hidden <input>
       physically inside the form, as JSON per night. These named inputs are
       submitted no matter what happens to the central nightlyOverrides object
       or the single JSON field. The server reads nightly_override_full[] first.
    */
    function rebuildDirectOverrideInputs() {
        try {
            form.querySelectorAll('.nightly-override-full').forEach(function (el) { el.remove(); });
            let i = 0;
            Object.keys(nightlyOverrides || {}).forEach(function (date) {
                const ov = nightlyOverrides[date];
                if (!ov || !ov.enabled) return;
                const input = document.createElement('input');
                input.type = 'hidden';
                input.className = 'nightly-override-full';
                input.name = 'nightly_override_full[' + i + ']';
                input.value = JSON.stringify(Object.assign({ date: date }, ov));
                form.appendChild(input);
                i++;
            });
        } catch (e) { /* ignore */ }
    }

    // Copy a night's per-room overrides onto every other night whose room
    // layout (set of room numbers) matches, so the same rates apply per stay.
    function applyRoomOverridesToWholeStay(sourceDate, rooms, reason) {
        const rows = window.__previewRowsCache || [];
        rows.forEach(function (r) {
            const d = r.date;
            if (!d || d === sourceDate) return;
            const bd = Array.isArray(r.room_breakdown) ? r.room_breakdown : [];
            // Only apply if this night has all the room numbers being overridden.
            const keys = {};
            bd.forEach(function (rb) { keys[String(rb.room_no)] = true; });
            const applicable = Object.keys(rooms).every(function (k) { return keys[k]; });
            if (!applicable) return;
            const calc = computeNightFromRoomOverrides(
                {room_breakdown: bd, original_total: r.original_total || r.night_total},
                {rooms: rooms}
            );
            nightlyOverrides[d] = {
                enabled: true,
                type: 'per_room',
                rooms: JSON.parse(JSON.stringify(rooms)),
                original_total: String(r.original_total || r.night_total || 0),
                override_total: calc.finalTotal.toFixed(2),
                final_total: calc.finalTotal.toFixed(2),
                reason: reason,
                override_detail: calc.detail
            };
        });
    }

    const clearNightOverrideBtn = document.getElementById('clearNightOverrideBtn');
    if (clearNightOverrideBtn) {
        clearNightOverrideBtn.addEventListener('click', function () {
            const date = document.getElementById('nightOverrideDate').value;
            if (date && nightlyOverrides[date]) delete nightlyOverrides[date];
            syncNightlyOverridesInput();
            bootstrap.Modal.getOrCreateInstance(document.getElementById('nightOverrideModal')).hide();
            submitPreview();
        });
    }


    function syncOpenNightOverrideModalBeforeSubmit() {
        const modalEl = document.getElementById('nightOverrideModal');
        if (!modalEl || !modalEl.classList.contains('show')) {
            syncNightlyOverridesInput();
            return;
        }
        const dateEl = document.getElementById('nightOverrideDate');
        if (!dateEl || !dateEl.value) {
            syncNightlyOverridesInput();
            return;
        }
        const rooms = readOverrideRoomInputs();
        if (Object.keys(rooms).length === 0) {
            syncNightlyOverridesInput();
            return;
        }
        const row = currentOverrideRowContext();
        const calc = computeNightFromRoomOverrides(row, {rooms: rooms});
        const baseTotal = parseFloat(document.getElementById('nightOverrideBaseTotal').value || 0) || 0;
        nightlyOverrides[dateEl.value] = {
            enabled: true,
            type: 'per_room',
            rooms: rooms,
            original_total: baseTotal.toFixed(2),
            override_total: calc.finalTotal.toFixed(2),
            final_total: calc.finalTotal.toFixed(2),
            reason: (document.getElementById('nightOverrideReason') || {}).value || '',
            override_detail: calc.detail
        };
        syncNightlyOverridesInput();
    }

    function reservationRequiredValue(selector) {
        const el = form.querySelector(selector);
        return el ? String(el.value || '').trim() : '';
    }

    function firstBlankRoomTypeRow() {
        if (!roomRowsWrap) return 1;
        const rows = roomRowsWrap.querySelectorAll('.room-row');
        if (!rows.length) return 1;
        for (let i = 0; i < rows.length; i++) {
            const select = rows[i].querySelector('.room-type-select');
            if (!select || !String(select.value || '').trim()) {
                return i + 1;
            }
        }
        return 0;
    }

    function validateReservationBeforeSubmit(event) {
        const submitter = event.submitter || document.activeElement;
        const isSaveLike = submitter && (
            submitter.name === 'save_reservation' ||
            submitter.name === 'save_waitlist' ||
            submitter.name === 'preview_only'
        );

        if (!isSaveLike) {
            return true;
        }

        const missing = [];

        if (!reservationRequiredValue('[name="guest_name"]')) {
            missing.push('Guest name');
        }
        if (!reservationRequiredValue('[name="agent_id"]')) {
            missing.push('Agent');
        }
        if (!reservationRequiredValue('[name="contact_id"]')) {
            missing.push('Agent contact');
        }
        if (!reservationRequiredValue('[name="meal_plan"]')) {
            missing.push('MealPlan');
        }

        const blankRoomRow = firstBlankRoomTypeRow();
        if (blankRoomRow > 0) {
            missing.push('Room type on row ' + blankRoomRow);
        }

        if (missing.length) {
            event.preventDefault();
            event.stopPropagation();
            alert('Please complete the following before saving:\n\n- ' + missing.join('\n- '));
            return false;
        }

        return true;
    }

    // Track which submit button initiated the submission so the overlay can
    // show the right message (a real booking save vs waitlist vs pricing lock).
    var lastSubmitter = null;
    function messageForButton(name) {
        if (name === 'save_waitlist') return 'Please wait, saving to waitlist';
        if (name === 'lock_pricing') return 'Please wait, locking pricing';
        if (name === 'unlock_pricing') return 'Please wait, unlocking pricing';
        return 'Please wait, saving booking';
    }
    form.querySelectorAll('button[type="submit"]').forEach(function (btn) {
        btn.addEventListener('click', function () { lastSubmitter = btn; });
    });

    var savingOverlayReleased = false;

    /*
       GUARANTEED sync of nightly overrides into the hidden field.
       Root cause of "override reverts on save": the POST arrived with an empty
       nightly_overrides_json (server saw []), meaning the field wasn't updated
       before submit. We now force the write in the capture phase (runs before
       every other submit handler) and again on any save-button interaction, so
       the field can never submit stale/empty while nightlyOverrides holds data.
    */
    function forceSyncNightlyOverrides() {
        try {
            if (nightlyOverridesInput) {
                nightlyOverridesInput.value = JSON.stringify(nightlyOverrides || {});
            }
            if (typeof rebuildDirectOverrideInputs === 'function') rebuildDirectOverrideInputs();
            var n = Object.keys(nightlyOverrides || {}).length;
            console.log('[RoomZ] forceSync overrides staged =', n, nightlyOverridesInput ? nightlyOverridesInput.value : '(no field)');
            var badge = document.getElementById('ovStagedBadge');
            if (badge) badge.textContent = 'overrides staged: ' + n;
        } catch (e) { /* ignore */ }
    }
    form.addEventListener('submit', forceSyncNightlyOverrides, true); // capture phase, before others

    form.addEventListener('submit', function (event) {
        // If the browser is blocking submission for native required-field reasons
        // let it be; our validator handles the custom checks.
        if (validateReservationBeforeSubmit(event) === false) {
            hideSavingOverlay();
            return;
        }
        syncOpenNightOverrideModalBeforeSubmit();

        // If something already called preventDefault (e.g. a modal sync), don't
        // show the overlay because navigation isn't happening.
        if (event.defaultPrevented) {
            hideSavingOverlay();
            return;
        }

        // Already released (our own re-dispatch below) - let it submit for real.
        if (savingOverlayReleased) {
            return;
        }

        // Show the overlay and hold it visible briefly so a fast save/redirect
        // doesn't hide it before the user can see it. Then submit for real.
        var submitter = event.submitter || lastSubmitter;
        var name = submitter ? (submitter.getAttribute('name') || '') : 'save_reservation';
        event.preventDefault();
        showSavingOverlay(messageForButton(name));

        // Preserve which button was pressed by injecting a hidden field, since
        // a programmatic submit() loses the submitter's name/value.
        if (name) {
            var hidden = form.querySelector('input[type="hidden"][data-saving-submitter="1"]');
            if (!hidden) {
                hidden = document.createElement('input');
                hidden.type = 'hidden';
                hidden.setAttribute('data-saving-submitter', '1');
                form.appendChild(hidden);
            }
            hidden.name = name;
            hidden.value = submitter ? (submitter.getAttribute('value') || '1') : '1';
        }

        savingOverlayReleased = true;
        setTimeout(function () {
            if (typeof form.requestSubmit === 'function') {
                form.requestSubmit();
            } else {
                form.submit();
            }
        }, 650);
    });

    function showSavingOverlay(message) {
        // Build the overlay fresh in JS and append to <body> so no page CSS,
        // stacking context, or markup placement can hide it.
        var existing = document.getElementById('reservationSavingOverlayJS');
        if (existing) existing.parentNode.removeChild(existing);

        var o = document.createElement('div');
        o.id = 'reservationSavingOverlayJS';
        o.style.cssText = [
            'position:fixed',
            'top:0','left:0','right:0','bottom:0',
            'width:100vw','height:100vh',
            'z-index:2147483647',
            'background:rgba(15,23,42,0.6)',
            'display:flex',
            'align-items:center',
            'justify-content:center',
            'margin:0','padding:0'
        ].join(';');

        var box = document.createElement('div');
        box.style.cssText = [
            'background:#ffffff',
            'border-radius:18px',
            'padding:30px 40px',
            'text-align:center',
            'box-shadow:0 24px 60px rgba(15,23,42,0.4)',
            'max-width:340px',
            'font-family:inherit'
        ].join(';');

        box.innerHTML =
            '<div style="width:48px;height:48px;margin:0 auto;border:5px solid #dbeafe;'
            + 'border-top-color:#2563eb;border-radius:50%;animation:rzspin 0.8s linear infinite;"></div>'
            + '<div style="margin-top:18px;font-weight:700;font-size:1.05rem;color:#0f172a;">'
            + (message || 'Please wait, saving booking') + '</div>'
            + '<div style="margin-top:6px;color:#64748b;font-size:.85rem;">This may take a few moments…</div>'
            + '<div style="margin-top:14px;font-weight:800;letter-spacing:.04em;color:#2563eb;font-size:.8rem;">RoomZ PMS</div>';

        // Inject keyframes once.
        if (!document.getElementById('rzspin-style')) {
            var st = document.createElement('style');
            st.id = 'rzspin-style';
            st.textContent = '@keyframes rzspin{to{transform:rotate(360deg)}}';
            document.head.appendChild(st);
        }

        o.appendChild(box);
        document.body.appendChild(o);
        void o.offsetWidth; // force paint
    }

    function hideSavingOverlay() {
        var js = document.getElementById('reservationSavingOverlayJS');
        if (js) js.parentNode.removeChild(js);
        var o = document.getElementById('reservationSavingOverlay');
        if (o) o.style.display = 'none';
    }

    // If the user navigates back to this page from bfcache, ensure the overlay
    // is not left showing over a restored form.
    window.addEventListener('pageshow', function () {
        var o = document.getElementById('reservationSavingOverlay');
        if (o) o.style.display = 'none';
    });

    let previewAbort = null;
    async function runAjaxPreview() {
        if (pricingLocked) return;
        if (previewAbort) previewAbort.abort();
        previewAbort = new AbortController();

        const body = new FormData(form);
        const pricingTable = document.querySelector('.table-responsive');
        if (pricingTable) pricingTable.classList.add('ajax-preview-loading');

        try {
            const resp = await fetch(window.location.pathname + '?ajax=rate_preview', {
                method: 'POST',
                body: body,
                credentials: 'same-origin',
                signal: previewAbort.signal
            });
            const json = await resp.json();
            if (!json.ok && json.errors && json.errors.length) {
                const tbody = document.getElementById('pricingPreviewBody');
                if (tbody) {
                    tbody.innerHTML = '<tr><td colspan="11" class="text-danger text-center py-4">' + escapeHtml(json.errors.join(' | ')) + '</td></tr>';
                }
            } else {
                renderPricingRows(json.rows || []);
            }
            updateSummary(json);
            recalcPreviewFooterFromRows();
        } catch (e) {
            if (e.name !== 'AbortError') {
                console.warn('Rate preview failed', e);
            }
        } finally {
            if (pricingTable) pricingTable.classList.remove('ajax-preview-loading');
        }
    }

    let timer = null;
    function submitPreview() {
        if (pricingLocked) return;
        clearTimeout(timer);
        timer = setTimeout(runAjaxPreview, 300);
    }

    function attachRowEvents(row) {
        const roomType = row.querySelector('.room-type-select');
        const removeBtn = row.querySelector('.remove-room-row');

        if (roomType) {
            roomType.addEventListener('change', function () {
                refreshRoomTypeFilters();
                validateRoomRow(row);
                refreshRoomSummaryTotals();
                submitPreview();
            });
        }

        row.querySelectorAll('input').forEach(function (input) {
            input.addEventListener('change', function () {
                validateRoomRow(row);
                refreshRoomSummaryTotals();
                submitPreview();
            });
            input.addEventListener('input', function () {
                validateRoomRow(row);
                refreshRoomSummaryTotals();
                submitPreview();
            });
        });

        if (removeBtn) {
            removeBtn.addEventListener('click', function () {
                const rows = roomRowsWrap.querySelectorAll('.room-row');
                if (rows.length <= 1) return;

                row.remove();
                reindexRoomRows();
                submitPreview();
            });
        }

        validateRoomRow(row);
    }

    function addRoomRow() {
        const allTypes = getAvailableRoomTypeValues();
        const selectedTypes = getSelectedRoomTypeValues();

        if (allTypes.length > 0 && selectedTypes.length >= allTypes.length) {
            refreshAddRoomButtonState();
            return;
        }

        const idx = roomRowsWrap.querySelectorAll('.room-row').length;

        const div = document.createElement('div');
        div.className = 'border rounded-3 p-3 mb-3 room-row';
        div.dataset.index = idx;

        div.innerHTML = `
            <div class="row g-3 align-items-end">
                <div class="col-lg-4 col-md-6">
                    <label class="form-label">Room Type</label>
                    <select name="room_rows[${idx}][room_type_id]" class="form-select room-type-select" required>
                        ${getRoomTypeOptionsHtml()}
                    </select>
                </div>

                <div class="col-lg-2 col-md-3">
                    <label class="form-label">No. Rooms</label>
                    <input type="number" min="1" name="room_rows[${idx}][qty_rooms]" class="form-control room-qty" value="1" required>
                </div>

                <div class="col-lg-2 col-md-3">
                    <label class="form-label">Adults</label>
                    <input type="number" min="0" name="room_rows[${idx}][adults]" class="form-control room-adults" value="1" required>
                </div>

                <div class="col-lg-2 col-md-3">
                    <label class="form-label">Children</label>
                    <input type="number" min="0" name="room_rows[${idx}][children]" class="form-control room-children" value="0">
                </div>

                <div class="col-lg-2 col-md-3 text-md-end">
                    <button type="button" class="btn btn-outline-danger remove-room-row">
                        <i class="bi bi-trash me-1"></i>Remove
                    </button>
                </div>
            </div>
            <div class="room-requirements-box mt-3">
                <div class="row g-2 align-items-end">
                    <div class="col-lg-2 col-md-3">
                        <label class="form-label">Extra Beds</label>
                        <input type="number" min="0" name="room_rows[${idx}][extra_beds_required]" class="form-control room-extra-beds" value="0">
                    </div>
                    <div class="col-lg-2 col-md-3">
                        <label class="form-label">Baby Cots</label>
                        <input type="number" min="0" name="room_rows[${idx}][cots_required]" class="form-control room-cots" value="0">
                    </div>
                    <div class="col-lg-2 col-md-3">
                        <div class="form-check mt-4">
                            <input class="form-check-input room-wheelchair" type="checkbox" name="room_rows[${idx}][wheelchair_room_required]" value="1" id="wheelchair_${idx}">
                            <label class="form-check-label" for="wheelchair_${idx}">Wheelchair</label>
                        </div>
                    </div>
                    <div class="col-lg-2 col-md-3">
                        <div class="form-check mt-4">
                            <input class="form-check-input room-interleading" type="checkbox" name="room_rows[${idx}][interleading_required]" value="1" id="interleading_${idx}">
                            <label class="form-check-label" for="interleading_${idx}">Interleading</label>
                        </div>
                    </div>
                    <input type="hidden" name="room_rows[${idx}][special_occasion]" class="room-special-occasion" value="">
                    <div class="col-lg-6">
                        <label class="form-label">Room Setup Notes</label>
                        <input type="text" maxlength="255" name="room_rows[${idx}][room_setup_notes]" class="form-control room-setup-notes" placeholder="Twin beds, extra blanket, crib location">
                    </div>
                    <div class="col-lg-6">
                        <label class="form-label">Housekeeping Notes</label>
                        <input type="text" maxlength="255" name="room_rows[${idx}][housekeeping_notes]" class="form-control room-housekeeping-notes" placeholder="Prepare extra bed before arrival">
                    </div>
                </div>
            </div>
        `;

        roomRowsWrap.appendChild(div);
        syncGlobalSpecialOccasionToRows();
        attachRowEvents(div);
        reindexRoomRows();
        refreshRoomTypeFilters();
        refreshRoomSummaryTotals();

        const newSelect = div.querySelector('.room-type-select');
        if (newSelect) newSelect.focus();
    }

    async function loadRoomTypes(propertyId) {
        if (!propertyId) {
            roomRowsWrap.querySelectorAll('.room-type-select').forEach(function (selectEl) {
                selectEl.innerHTML = '<option value="">Select room type</option>';
            });
            refreshRoomTypeFilters();
            refreshRoomSummaryTotals();
            return;
        }

        try {
            const resp = await fetch(window.location.pathname + '?ajax=room_types&property_id=' + encodeURIComponent(propertyId), {
                credentials: 'same-origin'
            });
            const json = await resp.json();

            let html = '<option value="">Select room type</option>';
            (json.rows || []).forEach(function (row) {
                const title = String(row.title || '');
                const higher = parseInt(row.higher_occupancy || 0, 10) || 0;
                const triplePossible = higher >= 3 && title.toLowerCase().indexOf('family') === -1;
                const label = title + (triplePossible ? ' — Triple possible' : '');
                html += '<option value="' + row.id + '" data-higher-occupancy="' + higher + '" data-room-title="' + escapeHtml(title) + '">' + escapeHtml(label) + '</option>';
            });

            roomRowsWrap.querySelectorAll('.room-type-select').forEach(function (selectEl) {
                const current = selectEl.value;
                selectEl.innerHTML = html;
                if (current) {
                    selectEl.value = current;
                }
            });

            refreshRoomTypeFilters();
            refreshRoomSummaryTotals();
        } catch (e) {
            roomRowsWrap.querySelectorAll('.room-type-select').forEach(function (selectEl) {
                selectEl.innerHTML = '<option value="">Select room type</option>';
            });
            refreshRoomTypeFilters();
            refreshRoomSummaryTotals();
        }
    }

    roomRowsWrap.querySelectorAll('.room-row').forEach(attachRowEvents);
    refreshRoomSummaryTotals();

    if (addRoomRowBtn) {
        addRoomRowBtn.addEventListener('click', function () {
            addRoomRow();
        });
    }

    if (propertyField) {
        propertyField.addEventListener('change', async function () {
            await loadRoomTypes(this.value);
            submitPreview();
        });
    }

    if (agentField) {
        agentField.addEventListener('change', async function () {
            console.log('Agent dropdown changed. selected agent_id=', this.value);
            /*
               Preserve the contact already chosen if it still belongs to this
               agent. Previously this always passed '' which wiped a valid
               contact every time a change event fired (combobox re-selection,
               programmatic dispatch on edit load, etc.) - that was the
               "contact keeps dropping when I edit a booking" bug.
            */
            const keepContactId = agentChangePreserveContactId !== null
                ? agentChangePreserveContactId
                : (contactField ? String(contactField.value || '') : '');
            agentChangePreserveContactId = null; // consume one-shot override
            await loadAgentContacts(this.value, keepContactId);
            const originatingAgent = document.getElementById('originating_agent');
            if (originatingAgent && !originatingAgent.value) {
                originatingAgent.value = this.value || '';
            }
            submitPreview();
        });
    }

    if (agentField && agentField.value && contactField) {
        loadAgentContacts(agentField.value, initialContactId);
    }

    /* ------------------------------------------------------------------ *
     *  Single searchable agent combobox + inline "add agent / add contact" *
     * ------------------------------------------------------------------ */

    // One control: a text input that filters a dropdown list and writes the
    // chosen id into the hidden #agent_id field. All existing logic reads that
    // hidden field (agentField), so nothing downstream changes.
    const agentComboInput = document.getElementById('agentComboInput');
    const agentComboList = document.getElementById('agentComboList');
    let agentComboActiveIndex = -1;
    let agentComboFiltered = [];

    function agentLabelById(id) {
        const found = agentComboData.find(function (a) { return String(a.id) === String(id); });
        return found ? found.label : '';
    }

    function closeAgentCombo() {
        if (!agentComboList) return;
        agentComboList.style.display = 'none';
        agentComboActiveIndex = -1;
        if (agentComboInput) agentComboInput.setAttribute('aria-expanded', 'false');
    }

    function renderAgentComboList(term) {
        if (!agentComboList) return;
        const t = (term || '').trim().toLowerCase();
        agentComboFiltered = agentComboData.filter(function (a) {
            return t === '' || a.label.toLowerCase().indexOf(t) !== -1;
        });

        let html = '';
        if (agentComboFiltered.length === 0) {
            html = '<button type="button" class="list-group-item list-group-item-action disabled text-muted">No agents match</button>';
        } else {
            agentComboFiltered.forEach(function (a, i) {
                html += '<button type="button" class="list-group-item list-group-item-action agent-combo-item' +
                        (i === agentComboActiveIndex ? ' active' : '') + '" data-id="' + a.id + '">' +
                        escapeHtml(a.label) + '</button>';
            });
        }
        agentComboList.innerHTML = html;
        agentComboList.style.display = 'block';
        if (agentComboInput) agentComboInput.setAttribute('aria-expanded', 'true');
    }

    function chooseAgent(id, label) {
        if (!agentField) return;
        const prev = String(agentField.value || '');
        const next = id ? String(id) : '';
        /*
           Only force-clear the contact when the agent actually changes. Picking
           the same agent again (or the combobox re-affirming the current agent)
           must keep the existing contact selection.
        */
        agentChangePreserveContactId = (prev === next && next !== '')
            ? (contactField ? String(contactField.value || '') : null)
            : '';
        agentField.value = next;
        if (agentComboInput) {
            agentComboInput.value = label || agentLabelById(id) || '';
            agentComboInput.removeAttribute('data-empty');
        }
        closeAgentCombo();
        agentField.dispatchEvent(new Event('change')); // loads contacts, syncs originating agent, previews
    }

    if (agentComboInput && agentComboList && agentField) {
        // Typing filters the list.
        agentComboInput.addEventListener('input', function () {
            agentComboActiveIndex = -1;
            renderAgentComboList(this.value);
            // Typing invalidates any previous concrete selection until one is picked.
            agentField.value = '';
        });

        // Focus / click opens the full list.
        agentComboInput.addEventListener('focus', function () {
            renderAgentComboList(this.getAttribute('data-empty') ? '' : this.value);
        });

        // Keyboard navigation.
        agentComboInput.addEventListener('keydown', function (e) {
            if (agentComboList.style.display === 'none' && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
                renderAgentComboList(this.value);
                return;
            }
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                agentComboActiveIndex = Math.min(agentComboActiveIndex + 1, agentComboFiltered.length - 1);
                renderAgentComboList(this.value);
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                agentComboActiveIndex = Math.max(agentComboActiveIndex - 1, 0);
                renderAgentComboList(this.value);
            } else if (e.key === 'Enter') {
                e.preventDefault();
                const pick = agentComboFiltered[agentComboActiveIndex] || agentComboFiltered[0];
                if (pick) chooseAgent(pick.id, pick.label);
            } else if (e.key === 'Escape') {
                closeAgentCombo();
            }
        });

        // Click a result.
        agentComboList.addEventListener('click', function (e) {
            const item = e.target.closest('.agent-combo-item');
            if (!item) return;
            chooseAgent(item.getAttribute('data-id'), item.textContent);
        });

        // Click outside closes; restore the label if the field was left half-typed.
        document.addEventListener('click', function (e) {
            if (!e.target.closest('.agent-combobox')) {
                if (agentField.value) {
                    agentComboInput.value = agentLabelById(agentField.value);
                } else if (agentComboInput.value.trim() !== '' && agentComboData.length) {
                    // Left typing without choosing -> clear to avoid a phantom value.
                    agentComboInput.value = '';
                }
                closeAgentCombo();
            }
        });
    }

    // Add a brand-new agent to the combobox data and the originating-agent select.
    function injectAgentOption(id, label) {
        if (!agentComboData.some(function (a) { return String(a.id) === String(id); })) {
            agentComboData.push({ id: Number(id), label: label });
        }
        const originating = document.getElementById('originating_agent');
        if (originating && !Array.from(originating.options).some(function (o) { return String(o.value) === String(id); })) {
            const opt = document.createElement('option');
            opt.value = id;
            opt.textContent = label;
            originating.appendChild(opt);
        }
    }

    const addAgentBtn = document.getElementById('addAgentBtn');
    const newAgentPanel = document.getElementById('newAgentPanel');
    const newAgentFirstname = document.getElementById('newAgentFirstname');
    const saveNewAgentBtn = document.getElementById('saveNewAgentBtn');
    const cancelNewAgentBtn = document.getElementById('cancelNewAgentBtn');
    const newAgentMsg = document.getElementById('newAgentMsg');

    if (addAgentBtn && newAgentPanel) {
        addAgentBtn.addEventListener('click', function () {
            newAgentPanel.style.display = (newAgentPanel.style.display === 'none') ? 'block' : 'none';
            if (newAgentPanel.style.display === 'block' && newAgentFirstname) {
                if (agentComboInput && agentComboInput.value.trim() && !agentField.value) newAgentFirstname.value = agentComboInput.value.trim();
                newAgentFirstname.focus();
            }
        });
    }
    if (cancelNewAgentBtn) {
        cancelNewAgentBtn.addEventListener('click', function () {
            if (newAgentPanel) newAgentPanel.style.display = 'none';
            if (newAgentMsg) newAgentMsg.textContent = '';
        });
    }
    if (saveNewAgentBtn) {
        saveNewAgentBtn.addEventListener('click', async function () {
            if (newAgentMsg) newAgentMsg.textContent = '';
            const name = (newAgentFirstname ? newAgentFirstname.value : '').trim();
            if (!name) { if (newAgentMsg) newAgentMsg.textContent = 'Enter an agent name.'; return; }
            saveNewAgentBtn.disabled = true;
            try {
                const url = new URL(window.location.href);
                url.search = '';
                url.searchParams.set('ajax', 'create_agent');
                const body = new URLSearchParams();
                body.set('firstname', name);
                const resp = await fetch(url.toString(), { method: 'POST', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() });
                const json = await resp.json();
                if (!json.ok) { if (newAgentMsg) newAgentMsg.textContent = json.message || 'Could not add agent.'; return; }
                injectAgentOption(json.id, name);
                chooseAgent(json.id, name); // sets hidden value + combobox label, fires change
                if (newAgentPanel) newAgentPanel.style.display = 'none';
                if (newAgentFirstname) newAgentFirstname.value = '';
            } catch (e) {
                if (newAgentMsg) newAgentMsg.textContent = 'Network error adding agent.';
            } finally {
                saveNewAgentBtn.disabled = false;
            }
        });
    }

    const addContactBtn = document.getElementById('addContactBtn');
    const newContactPanel = document.getElementById('newContactPanel');
    const saveNewContactBtn = document.getElementById('saveNewContactBtn');
    const cancelNewContactBtn = document.getElementById('cancelNewContactBtn');
    const newContactMsg = document.getElementById('newContactMsg');

    if (addContactBtn && newContactPanel) {
        addContactBtn.addEventListener('click', function () {
            if (!agentField.value) {
                if (newContactMsg) { newContactPanel.style.display = 'block'; newContactMsg.textContent = 'Select an agent first.'; }
                return;
            }
            newContactPanel.style.display = (newContactPanel.style.display === 'none') ? 'block' : 'none';
            if (newContactPanel.style.display === 'block') {
                if (newContactMsg) newContactMsg.textContent = '';
                const fn = document.getElementById('newContactFirstname');
                if (fn) fn.focus();
            }
        });
    }
    if (cancelNewContactBtn) {
        cancelNewContactBtn.addEventListener('click', function () {
            if (newContactPanel) newContactPanel.style.display = 'none';
            if (newContactMsg) newContactMsg.textContent = '';
        });
    }
    if (saveNewContactBtn) {
        saveNewContactBtn.addEventListener('click', async function () {
            if (newContactMsg) newContactMsg.textContent = '';
            if (!agentField.value) { if (newContactMsg) newContactMsg.textContent = 'Select an agent first.'; return; }
            const fn = (document.getElementById('newContactFirstname') || {}).value || '';
            const ln = (document.getElementById('newContactLastname') || {}).value || '';
            const em = (document.getElementById('newContactEmail') || {}).value || '';
            const mo = (document.getElementById('newContactMobile') || {}).value || '';
            if (!fn.trim() && !ln.trim()) { if (newContactMsg) newContactMsg.textContent = 'Enter a contact name.'; return; }
            saveNewContactBtn.disabled = true;
            try {
                const url = new URL(window.location.href);
                url.search = '';
                url.searchParams.set('ajax', 'create_consultant');
                const body = new URLSearchParams();
                body.set('agent_id', agentField.value);
                body.set('firstname', fn.trim());
                body.set('lastname', ln.trim());
                body.set('email', em.trim());
                body.set('mobile', mo.trim());
                const resp = await fetch(url.toString(), { method: 'POST', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() });
                const json = await resp.json();
                if (!json.ok) { if (newContactMsg) newContactMsg.textContent = json.message || 'Could not add contact.'; return; }
                // Register in the in-memory map so the loader treats it as known.
                const key = String(agentField.value);
                if (!consultantsByAgent[key]) consultantsByAgent[key] = [];
                consultantsByAgent[key].push({ id: json.id, firstname: json.firstname, lastname: json.lastname, email: json.email, mobile: json.mobile, phone: '' });
                renderAgentContacts(consultantsByAgent[key], String(json.id));
                if (newContactPanel) newContactPanel.style.display = 'none';
                ['newContactFirstname', 'newContactLastname', 'newContactEmail', 'newContactMobile'].forEach(function (id) { const el = document.getElementById(id); if (el) el.value = ''; });
                submitPreview();
            } catch (e) {
                if (newContactMsg) newContactMsg.textContent = 'Network error adding contact.';
            } finally {
                saveNewContactBtn.disabled = false;
            }
        });
    }

    ['check_in', 'check_out', 'meal_plan'].forEach(function (name) {
        const field = form.querySelector('[name="' + name + '"]');
        if (!field) return;
        field.addEventListener('change', submitPreview);
        field.addEventListener('input', submitPreview);
    });

    const bookingExtrasWrap = document.getElementById('bookingExtrasWrap');
    const addBookingExtraBtn = document.getElementById('addBookingExtraBtn');
    const bookingExtraCategoryOptions = <?php echo json_encode(array_map(function ($cat) { return ['id' => (int)$cat['id'], 'category' => (string)$cat['category']]; }, $extraCategories), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '[]'; ?>;
    const bookingExtraServices = <?php echo json_encode(array_map(function ($svc) { return ['id' => (int)$svc['id'], 'service' => (string)$svc['service'], 'category' => (int)$svc['category'], 'price' => (float)$svc['price_per_person']]; }, $extraServices), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '[]'; ?>;
    const bookingExtraAgents = <?php echo json_encode(array_map(function($a){ $name=trim((string)($a['display_name']??'')); return ['id'=>(int)$a['id'],'name'=>$name!==''?$name:('Agent #'.(int)$a['id'])]; }, $agents), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES) ?: '[]'; ?>;
    function buildCategoryOptions(selected){ let html='<option value="">Select category</option>'; bookingExtraCategoryOptions.forEach(function(cat){ html+='<option value="'+cat.id+'"'+(String(selected||'')===String(cat.id)?' selected':'')+'>'+escapeHtml(cat.category)+'</option>'; }); return html; }
    function buildServiceOptions(categoryId, selected){ let html='<option value="">'+(categoryId?'Select service':'Select category first')+'</option>'; bookingExtraServices.forEach(function(svc){ if(String(svc.category)!==String(categoryId||'')) return; html+='<option value="'+svc.id+'" data-category="'+svc.category+'" data-price="'+svc.price+'"'+(String(selected||'')===String(svc.id)?' selected':'')+'>'+escapeHtml(svc.service)+'</option>'; }); return html; }
    function reindexBookingExtras(){ if(!bookingExtrasWrap) return; bookingExtrasWrap.querySelectorAll('.booking-extra-row').forEach(function(row,idx){ row.dataset.index=idx; const date=row.querySelector('.extra-date'), cat=row.querySelector('.extra-category'), svc=row.querySelector('.extra-service'), qty=row.querySelector('.extra-qty'), rate=row.querySelector('.extra-rate'), chargeTo=row.querySelector('.extra-charge-to'), payer=row.querySelector('.extra-payer-agent'), note=row.querySelector('input[name*="[note]"]'); if(date) date.name='booking_extras['+idx+'][tx_date]'; if(cat) cat.name='booking_extras['+idx+'][category_id]'; if(svc) svc.name='booking_extras['+idx+'][service_id]'; if(qty) qty.name='booking_extras['+idx+'][quantity]'; if(rate) rate.name='booking_extras['+idx+'][override_price]'; if(chargeTo) chargeTo.name='booking_extras['+idx+'][charge_to]'; if(payer) payer.name='booking_extras['+idx+'][payer_agent_id]'; if(note) note.name='booking_extras['+idx+'][note]'; }); }
    function refreshBookingExtraTotals(){ const extrasTotal = calculateExtrasTotal(); const accommodationText = (document.getElementById('summaryAccommodationTotal') || {}).textContent || '0'; const accommodation = parseFloat(String(accommodationText).replace(/,/g,'')) || 0; const extrasEl=document.getElementById('bookingExtrasTotal'); if(extrasEl) extrasEl.textContent=formatDisplayMoney(extrasTotal); const summaryExtras=document.getElementById('summaryExtrasTotal'); if(summaryExtras) summaryExtras.textContent=formatDisplayMoney(extrasTotal); const grand=document.getElementById('summaryGrandTotal'); if(grand) grand.textContent=formatDisplayMoney(accommodation + extrasTotal); }
    // Master (catalogue) price for the currently selected service in a row.
    function bookingExtraMasterPrice(row){ const svc=row.querySelector('.extra-service'); if(!svc) return 0; const selected=svc.options[svc.selectedIndex]; const p=selected?parseFloat(selected.getAttribute('data-price')||0):0; return isNaN(p)?0:p; }
    // Recompute only the total from the (possibly edited) rate + qty. Flags the
    // rate visually when it differs from the master price so mischarges stand out.
    function recomputeBookingExtraTotal(row){ const qty=row.querySelector('.extra-qty'), rate=row.querySelector('.extra-rate'), total=row.querySelector('.extra-total'); if(!qty||!rate||!total) return; const q=parseFloat(qty.value||0); const r=parseFloat(String(rate.value||'0').replace(/,/g,'')); const rr=isNaN(r)?0:r; total.value=money2(rr*(isNaN(q)?0:q)); const master=bookingExtraMasterPrice(row); if(master>0 && Math.abs(rr-master)>0.001){ rate.classList.add('border-warning'); rate.classList.add('text-danger'); } else { rate.classList.remove('border-warning'); rate.classList.remove('text-danger'); } refreshBookingExtraTotals(); }
    // Full refresh when category/service changes: rebuild service list and reset
    // the rate to the master price (a new service invalidates any prior override),
    // unless the user has explicitly touched the rate for this row.
    function refreshBookingExtraRow(row, resetRate){ const cat=row.querySelector('.extra-category'), svc=row.querySelector('.extra-service'), rate=row.querySelector('.extra-rate'); if(!cat||!svc||!rate) return; const current=svc.value; svc.innerHTML=buildServiceOptions(cat.value,current); if(current) svc.value=current; const master=bookingExtraMasterPrice(row); const rateTouched=row.dataset.rateTouched==='1'; const currentRate=parseFloat(String(rate.value||'0').replace(/,/g,''))||0; if(resetRate || (!rateTouched && currentRate<=0)){ rate.value=money2(master); } recomputeBookingExtraTotal(row); }
    function buildAgentOptions(selected){ let html='<option value="">Select paying agent</option>'; bookingExtraAgents.forEach(function(a){ html+='<option value="'+a.id+'"'+(String(a.id)===String(selected||'')?' selected':'')+'>'+escapeHtml(a.name)+'</option>'; }); return html; }
    function refreshExtraPayerVisibility(row){ const charge=row.querySelector('.extra-charge-to'), wrap=row.querySelector('.extra-payer-agent-wrap'), payer=row.querySelector('.extra-payer-agent'), noteWrap=row.querySelector('.extra-note-wrap'); const show=charge&&charge.value==='other'; if(wrap) wrap.style.display=show?'':'none'; if(payer) payer.required=!!show; if(noteWrap){ noteWrap.classList.toggle('col-md-4',!!show); noteWrap.classList.toggle('col-md-8',!show); } }
    const instantExtrasOrderId = <?php echo (int)($form['id'] ?? 0); ?>;
    let instantExtrasTimer = null;
    let instantExtrasSaving = false;
    let instantExtrasQueued = false;
    function collectBookingExtras(){ const rows=[]; if(!bookingExtrasWrap) return rows; bookingExtrasWrap.querySelectorAll('.booking-extra-row').forEach(function(row){ const cat=row.querySelector('.extra-category'),svc=row.querySelector('.extra-service'),qty=row.querySelector('.extra-qty'),date=row.querySelector('.extra-date'),rate=row.querySelector('.extra-rate'),charge=row.querySelector('.extra-charge-to'),payer=row.querySelector('.extra-payer-agent'),note=row.querySelector('input[name*="[note]"]'); if(!cat||!svc) return; rows.push({category_id:cat.value,service_id:svc.value,quantity:qty?qty.value:1,tx_date:date?date.value:'',override_price:rate?rate.value:'',charge_to:charge?charge.value:'master',payer_agent_id:payer?payer.value:'',note:note?note.value:''}); }); return rows; }
    function setExtraGridState(kind,message){ if(!bookingExtrasWrap) return; bookingExtrasWrap.querySelectorAll('.booking-extra-row').forEach(function(row){ row.classList.remove('extra-saving','extra-saved','extra-error'); if(kind) row.classList.add('extra-'+kind); const state=row.querySelector('.extra-save-state'); if(state){ const icon=kind==='saving'?'bi-cloud-arrow-up':(kind==='error'?'bi-exclamation-triangle':'bi-cloud-check'); state.innerHTML='<i class="bi '+icon+' me-1"></i><span>'+escapeHtml(message||'')+'</span>'; } }); }
    async function saveBookingExtrasInstant(){ if(!instantExtrasOrderId) return; if(instantExtrasSaving){ instantExtrasQueued=true; return; } instantExtrasSaving=true; setExtraGridState('saving','Saving changes…'); try{ const body=new URLSearchParams(); body.set('order_id',String(instantExtrasOrderId)); body.set('extras_json',JSON.stringify(collectBookingExtras())); const response=await fetch('reservation_form.php?ajax=save_booking_extras',{method:'POST',credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'},body:body}); const data=await response.json(); if(!response.ok||!data.ok) throw new Error(data.message||'Unable to save extras.'); setExtraGridState('saved',data.message||'Saved'); setTimeout(function(){ bookingExtrasWrap&&bookingExtrasWrap.querySelectorAll('.booking-extra-row').forEach(function(r){r.classList.remove('extra-saved');}); },1400); }catch(err){ setExtraGridState('error',err.message||'Save failed'); }finally{ instantExtrasSaving=false; if(instantExtrasQueued){ instantExtrasQueued=false; saveBookingExtrasInstant(); } } }
    function queueBookingExtrasInstantSave(delay){ if(!instantExtrasOrderId) return; clearTimeout(instantExtrasTimer); instantExtrasTimer=setTimeout(saveBookingExtrasInstant,typeof delay==='number'?delay:550); }
    function attachBookingExtraEvents(row){ const cat=row.querySelector('.extra-category'), svc=row.querySelector('.extra-service'), qty=row.querySelector('.extra-qty'), rate=row.querySelector('.extra-rate'), date=row.querySelector('.extra-date'), chargeTo=row.querySelector('.extra-charge-to'), payer=row.querySelector('.extra-payer-agent'), note=row.querySelector('input[name*="[note]"]'), rem=row.querySelector('.remove-booking-extra'); if(payer&&payer.options.length<=1) payer.innerHTML=buildAgentOptions(payer.value); refreshExtraPayerVisibility(row); if(cat) cat.addEventListener('change',function(){ if(svc) svc.value=''; row.dataset.rateTouched='0'; refreshBookingExtraRow(row, true); submitPreview(); queueBookingExtrasInstantSave(250); }); if(svc) svc.addEventListener('change',function(){ row.dataset.rateTouched='0'; refreshBookingExtraRow(row, true); submitPreview(); queueBookingExtrasInstantSave(250); }); if(qty) qty.addEventListener('input',function(){ recomputeBookingExtraTotal(row); submitPreview(); queueBookingExtrasInstantSave(); }); if(rate){ rate.addEventListener('input',function(){ row.dataset.rateTouched='1'; recomputeBookingExtraTotal(row); submitPreview(); queueBookingExtrasInstantSave(); }); } if(date) date.addEventListener('change',function(){ queueBookingExtrasInstantSave(150); }); if(chargeTo){ chargeTo.addEventListener('change',function(){ refreshExtraPayerVisibility(row); submitPreview(); queueBookingExtrasInstantSave(150); }); } if(payer){ payer.addEventListener('change',function(){ queueBookingExtrasInstantSave(150); }); } if(note){ note.addEventListener('input',function(){ queueBookingExtrasInstantSave(); }); } if(rem) rem.addEventListener('click',function(){ if(!confirm('Delete this booking extra? This will update the folio immediately.')) return; row.remove(); reindexBookingExtras(); refreshBookingExtraTotals(); submitPreview(); saveBookingExtrasInstant(); }); const seededRate=parseFloat(String(rate?rate.value:'0').replace(/,/g,''))||0; if(seededRate>0){ row.dataset.rateTouched='1'; refreshBookingExtraRow(row, false); } else { refreshBookingExtraRow(row, true); } }

    function addBookingExtraRow(){ if(!bookingExtrasWrap) return; const idx=bookingExtrasWrap.querySelectorAll('.booking-extra-row').length; const today=new Date().toISOString().slice(0,10); const row=document.createElement('div'); row.className='booking-extra-row border rounded-3 p-3 mb-3'; row.dataset.index=idx; row.innerHTML=`<div class="row g-3 align-items-end"><div class="col-md-2"><label class="form-label">Date</label><input type="date" name="booking_extras[${idx}][tx_date]" class="form-control extra-date" value="${today}"></div><div class="col-md-3"><label class="form-label">Category</label><select name="booking_extras[${idx}][category_id]" class="form-select extra-category">${buildCategoryOptions('')}</select></div><div class="col-md-3"><label class="form-label">Service / Description</label><select name="booking_extras[${idx}][service_id]" class="form-select extra-service"><option value="">Select category first</option></select></div><div class="col-md-1"><label class="form-label">Qty</label><input type="number" min="1" name="booking_extras[${idx}][quantity]" class="form-control extra-qty" value="1"></div><div class="col-md-1"><label class="form-label" title="Editable if charged incorrectly">Rate <i class="bi bi-pencil-square text-primary" style="font-size:.7rem;"></i></label><input type="number" step="0.01" min="0" name="booking_extras[${idx}][override_price]" class="form-control extra-rate" value="0.00"></div><div class="col-md-1"><label class="form-label">Total</label><input type="text" class="form-control extra-total" value="0.00" readonly></div><div class="col-md-1 text-end"><label class="form-label">&nbsp;</label><button type="button" class="btn btn-outline-danger remove-booking-extra"><i class="bi bi-trash"></i></button></div><div class="col-md-4"><label class="form-label">Charge To</label><select name="booking_extras[${idx}][charge_to]" class="form-select extra-charge-to"><option value="master" selected>Master (Booking Agent)</option><option value="room">Room Account (Guest)</option><option value="other">Others (Different Paying Agent)</option></select></div><div class="col-md-4 extra-payer-agent-wrap" style="display:none"><label class="form-label">Paying Agent</label><select name="booking_extras[${idx}][payer_agent_id]" class="form-select extra-payer-agent">${buildAgentOptions('')}</select></div><div class="col-md-8 extra-note-wrap"><label class="form-label">Note</label><input type="text" name="booking_extras[${idx}][note]" class="form-control" placeholder="Optional note, e.g. airport pickup time or activity date"></div><div class="col-12 extra-save-state small text-muted"><i class="bi bi-cloud-check me-1"></i><span>${instantExtrasOrderId ? 'Select a service to save' : 'Will save with reservation'}</span></div></div>`; bookingExtrasWrap.appendChild(row); attachBookingExtraEvents(row); reindexBookingExtras(); refreshBookingExtraTotals(); submitPreview(); }

    if(bookingExtrasWrap){ bookingExtrasWrap.querySelectorAll('.booking-extra-row').forEach(attachBookingExtraEvents); }
    if(addBookingExtraBtn){ addBookingExtraBtn.addEventListener('click', addBookingExtraRow); }
    const refreshPricingBtn = document.getElementById('refreshPricingBtn');
    if(refreshPricingBtn){ refreshPricingBtn.addEventListener('click', runAjaxPreview); }
    refreshBookingExtraTotals();
    refreshRoomTypeFilters();
    refreshAddRoomButtonState();
    submitPreview();
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
    const checkIn = document.getElementById('check_in');
    const checkOut = document.getElementById('check_out');

    if (!checkIn || !checkOut) return;

    function addOneDay(dateStr) {
        const d = new Date(dateStr);
        if (isNaN(d.getTime())) return null;
        d.setDate(d.getDate() + 1);
        return d.toISOString().split('T')[0];
    }

    function enforceCheckout() {
        if (!checkIn.value) return;

        const minCheckout = addOneDay(checkIn.value);

        // set HTML min attribute
        checkOut.min = minCheckout;

        // auto-correct if invalid
        if (!checkOut.value || checkOut.value <= checkIn.value) {
            checkOut.value = minCheckout;
        }
    }

    // when check-in changes
    checkIn.addEventListener('change', enforceCheckout);

    // also validate if user manually edits checkout
    checkOut.addEventListener('change', function () {
        if (!checkIn.value || !checkOut.value) return;

        if (checkOut.value <= checkIn.value) {
            alert('Check-out must be at least 1 day after check-in');
            checkOut.value = addOneDay(checkIn.value);
        }
    });

    // run on page load (important for edit mode)
    enforceCheckout();
});
</script>
<!-- Saving overlay: shown on submit so a slow save never looks like a hang -->
<div id="reservationSavingOverlay" style="display:none;position:fixed;inset:0;
     z-index:20060;background:rgba(15,23,42,0.55);backdrop-filter:blur(3px);
     align-items:center;justify-content:center;">
    <div style="background:#fff;border-radius:18px;padding:28px 36px;text-align:center;
         box-shadow:0 24px 60px rgba(15,23,42,0.35);max-width:340px;">
        <div class="spinner-border text-primary" style="width:3.2rem;height:3.2rem;border-width:.35rem;" role="status">
            <span class="visually-hidden">Saving…</span>
        </div>
        <div id="reservationSavingMessage" style="margin-top:18px;font-weight:700;font-size:1.05rem;color:#0f172a;">
            Please wait, saving booking
        </div>
        <div style="margin-top:6px;color:#64748b;font-size:.85rem;">This may take a few moments…</div>
        <div style="margin-top:14px;font-weight:800;letter-spacing:.04em;color:#2563eb;font-size:.8rem;">
            <i class="bi bi-buildings me-1"></i>RoomZ PMS
        </div>
    </div>
</div>
<?php require_once __DIR__ . '/apply_rate_widget.php'; ?>
<?php require_once __DIR__ . '/change_dates_widget.php'; ?>
<?php require_once __DIR__ . '/../../includes/footer.php'; ?>