Setting up pickup time selection 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
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Configuring Pickup Time Slot Selection in 1C-Bitrix

Customers want to arrive at a specific time — the store wants to distribute the flow evenly. Without slotted pickup scheduling, everyone shows up at noon and a queue forms. 1C-Bitrix's standard order form does not include time slot selection for pickup — this is custom functionality built on top of order properties and a JavaScript slot-selection component.

Storing Time Slots

Slots are stored in a custom data store: either in b_user_field_* as delivery service settings, or in a dedicated table. For flexibility — a dedicated table:

CREATE TABLE b_pickup_slot (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    STORE_ID INT NOT NULL,
    SLOT_DATE DATE NOT NULL,
    SLOT_TIME_FROM TIME NOT NULL,
    SLOT_TIME_TO TIME NOT NULL,
    CAPACITY INT NOT NULL DEFAULT 10,      -- maximum orders per slot
    BOOKED INT NOT NULL DEFAULT 0,         -- already booked
    ACTIVE CHAR(1) DEFAULT 'Y',
    INDEX idx_store_date (STORE_ID, SLOT_DATE),
    INDEX idx_active (ACTIVE)
);

Slots are populated a week in advance via a 1C-Bitrix agent that generates slots according to each location's operating schedule.

Fetching Available Slots via AJAX

// /ajax/pickup-slots.php
\Bitrix\Main\Loader::includeModule('main');

$storeId = (int)($_GET['store_id'] ?? 0);
$date    = $_GET['date'] ?? date('Y-m-d');

if (!$storeId) {
    echo json_encode(['error' => 'store_id required']);
    exit;
}

$connection = \Bitrix\Main\Application::getConnection();
$slots = $connection->query("
    SELECT
        ID,
        DATE_FORMAT(SLOT_TIME_FROM, '%H:%i') as TIME_FROM,
        DATE_FORMAT(SLOT_TIME_TO, '%H:%i') as TIME_TO,
        CAPACITY - BOOKED as AVAILABLE
    FROM b_pickup_slot
    WHERE STORE_ID = ? AND SLOT_DATE = ? AND ACTIVE = 'Y'
      AND BOOKED < CAPACITY
    ORDER BY SLOT_TIME_FROM
", [$storeId, $date])->fetchAll();

header('Content-Type: application/json');
echo json_encode(['slots' => $slots]);

Order Properties for Storing a Slot

Store → Settings → Order Properties → Add:

  • PICKUP_SLOT_ID — type "Number", ID of the selected slot
  • PICKUP_DATE — type "String", date in DD.MM.YYYY format
  • PICKUP_TIME — type "String", interval "10:00–11:00"

Slot Selection Component in the Checkout Form

// In the checkout component template
document.addEventListener('DOMContentLoaded', function() {
    const storeSelect = document.getElementById('pickup-store');
    const dateInput   = document.getElementById('pickup-date');
    const slotList    = document.getElementById('slot-list');

    function loadSlots() {
        const storeId = storeSelect.value;
        const date    = dateInput.value;
        if (!storeId || !date) return;

        slotList.innerHTML = '<li>Loading...</li>';

        fetch('/ajax/pickup-slots/?store_id=' + storeId + '&date=' + date)
            .then(r => r.json())
            .then(data => {
                slotList.innerHTML = '';
                if (!data.slots || !data.slots.length) {
                    slotList.innerHTML = '<li>No available slots</li>';
                    return;
                }
                data.slots.forEach(slot => {
                    const li = document.createElement('li');
                    li.className = 'slot-option';
                    li.dataset.slotId = slot.ID;
                    li.innerHTML =
                        slot.TIME_FROM + '–' + slot.TIME_TO +
                        ' <span class="available">(' + slot.AVAILABLE + ' spots)</span>';
                    li.addEventListener('click', () => selectSlot(slot));
                    slotList.appendChild(li);
                });
            });
    }

    function selectSlot(slot) {
        document.querySelectorAll('.slot-option').forEach(el => el.classList.remove('active'));
        document.querySelector('[data-slot-id="' + slot.ID + '"]').classList.add('active');

        // Populate hidden order property fields
        document.querySelector('[name="PICKUP_SLOT_ID"]').value = slot.ID;
        document.querySelector('[name="PICKUP_TIME"]').value =
            slot.TIME_FROM + '–' + slot.TIME_TO;
    }

    storeSelect.addEventListener('change', loadSlots);
    dateInput.addEventListener('change', loadSlots);
});

Booking a Slot at Order Placement

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale', 'OnSaleOrderSaved',
    function (\Bitrix\Main\Event $event) {
        $order = $event->getParameter('ENTITY');
        if (!$order->isNew()) {
            return;
        }

        $slotIdProp = $order->getPropertyCollection()
            ->getItemByOrderPropertyCode('PICKUP_SLOT_ID');
        $slotId = $slotIdProp ? (int)$slotIdProp->getValue() : 0;

        if (!$slotId) {
            return;
        }

        // Atomic BOOKED increment with CAPACITY check
        $connection = \Bitrix\Main\Application::getConnection();
        $affected = $connection->queryExecute("
            UPDATE b_pickup_slot
            SET BOOKED = BOOKED + 1
            WHERE ID = ? AND BOOKED < CAPACITY
        ", [$slotId]);

        if ($connection->getAffectedRowsCount() === 0) {
            // Slot is full — notify the manager
            // In production, return an error before saving the order
        }
    }
);

On order cancellation — decrement BOOKED:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale', 'OnSaleOrderCanceled',
    function (\Bitrix\Main\Event $event) {
        $order = $event->getParameter('ENTITY');
        $slotIdProp = $order->getPropertyCollection()
            ->getItemByOrderPropertyCode('PICKUP_SLOT_ID');
        $slotId = $slotIdProp ? (int)$slotIdProp->getValue() : 0;

        if ($slotId) {
            $connection = \Bitrix\Main\Application::getConnection();
            $connection->queryExecute(
                "UPDATE b_pickup_slot SET BOOKED = GREATEST(0, BOOKED - 1) WHERE ID = ?",
                [$slotId]
            );
        }
    }
);

Implementation Timeline

Slot table, schedule generation agent, AJAX endpoint, JS selection component, saving to order properties with atomic booking — 2–3 business days.