Setting up a room availability calendar in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages

Room Availability Calendar Setup in 1C-Bitrix

A hotel misses bookings not because there's no demand, but because guests don't see available dates in real time. Standard Bitrix catalog can't work with availability dates — this isn't its task. Booking calendar for rooms requires separate architecture: period occupancy storage, date intersection logic, and visual component.

Occupancy Period Storage

The central system element is the bookings table:

CREATE TABLE custom_room_bookings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    room_id INT NOT NULL,               -- Infoblock element ID (room)
    order_id INT,                       -- Bitrix order link
    guest_name VARCHAR(255),
    check_in DATE NOT NULL,
    check_out DATE NOT NULL,
    status ENUM('pending','confirmed','cancelled') DEFAULT 'pending',
    created_at DATETIME,
    INDEX idx_room_dates (room_id, check_in, check_out),
    INDEX idx_dates (check_in, check_out)
);

Room availability check for period is built on interval intersection query:

SELECT id FROM custom_room_bookings
WHERE room_id = :room_id
  AND status != 'cancelled'
  AND check_in < :check_out
  AND check_out > :check_in
LIMIT 1;

If query returns row — room is occupied for requested period.

ORM Wrapper in D7

namespace Custom\Hotel;

class BookingTable extends \Bitrix\Main\ORM\Data\DataManager {
    public static function getTableName(): string { return 'custom_room_bookings'; }

    public static function isRoomAvailable(int $roomId, string $checkIn, string $checkOut): bool {
        $result = static::getList([
            'filter' => [
                '=ROOM_ID' => $roomId,
                '!=STATUS' => 'cancelled',
                '<CHECK_IN' => $checkOut,
                '>CHECK_OUT' => $checkIn,
            ],
            'limit' => 1,
        ]);
        return !$result->fetch();
    }

    public static function getOccupiedDates(int $roomId, string $month): array {
        // Returns array of occupied dates for calendar
        $from = date('Y-m-01', strtotime($month));
        $to = date('Y-m-t', strtotime($month));

        $bookings = static::getList([
            'filter' => [
                '=ROOM_ID' => $roomId,
                '!=STATUS' => 'cancelled',
                '<CHECK_IN' => $to,
                '>CHECK_OUT' => $from,
            ],
        ]);

        $dates = [];
        while ($booking = $bookings->fetch()) {
            $current = strtotime($booking['CHECK_IN']);
            $end = strtotime($booking['CHECK_OUT']);
            while ($current < $end) {
                $dates[] = date('Y-m-d', $current);
                $current = strtotime('+1 day', $current);
            }
        }

        return array_unique($dates);
    }
}

Calendar Visual Component

For display use either ready library (Flatpickr, Pikaday, Air Datepicker) or custom. Flatpickr is optimal: light (16 KB), supports date ranges, simple styling.

Flatpickr configuration with occupied dates:

async function initBookingCalendar(roomId) {
    const response = await fetch(`/api/hotel/availability/?room_id=${roomId}&months=3`);
    const { occupiedDates } = await response.json();

    flatpickr('#date-range-picker', {
        mode: 'range',
        minDate: 'today',
        dateFormat: 'Y-m-d',
        locale: 'en',
        disable: occupiedDates,
        onChange: function(selectedDates) {
            if (selectedDates.length === 2) {
                const nights = Math.round(
                    (selectedDates[1] - selectedDates[0]) / 86400000
                );
                updatePricePreview(roomId, selectedDates[0], selectedDates[1], nights);
            }
        }
    });
}

API Endpoint for Availability Data

AJAX controller returns occupied dates for requested period:

class HotelAvailabilityController extends \Bitrix\Main\Engine\Controller {

    public function getAction(int $roomId, int $months = 2): array {
        $occupiedDates = [];
        $current = new \DateTime();

        for ($m = 0; $m < $months; $m++) {
            $monthStr = $current->format('Y-m');
            $dates = BookingTable::getOccupiedDates($roomId, $monthStr);
            $occupiedDates = array_merge($occupiedDates, $dates);
            $current->modify('+1 month');
        }

        return ['occupiedDates' => array_unique($occupiedDates)];
    }
}

Cache response — 5 minutes via \Bitrix\Main\Data\Cache, invalidate on new booking.

Date-Based Pricing

Hotels often apply seasonal rates. Room price stored not in trade offer (doesn't support periods) but in separate rates table:

CREATE TABLE custom_room_rates (
    id INT AUTO_INCREMENT PRIMARY KEY,
    room_id INT NOT NULL,
    rate_from DATE NOT NULL,
    rate_to DATE NOT NULL,
    price_per_night DECIMAL(10,2) NOT NULL,
    INDEX idx_room_period (room_id, rate_from, rate_to)
);

When dates selected, JavaScript requests total cost via separate endpoint, summing prices per night by active rates.

Integration with Bitrix Orders

Completed booking creates entry in custom_room_bookings and simultaneously creates order in sale module. This keeps single sales journal and allows using standard payment and notification mechanisms.

Execution Timeline

Scope Timeline
Storage + API + Flatpickr 2–3 days
Seasonal rates + cost preview +1–2 days
Order integration + notifications +1–2 days
Channel Manager sync (OTA) separate task

Availability calendar is the foundation of online booking system. This is where customer makes purchase decision.