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.







