Розробка форми запису на консультацію 1С-Бітрікс
Форма запису на консультацію — це не просто форма зворотного зв'язку. Користувач має обрати зручний час, спеціаліста, тему — і одразу отримати підтвердження. Основна технічна складність: відображення реального розкладу (незайнятих слотів) і блокування обраного часу від паралельних записів. На 1С-Бітрікс це вирішується через HL-блоки для розкладу та транзакційне блокування при створенні запису.
Модель даних
Спеціалісти (b_hl_consultants):
class ConsultantTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_consultants'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('USER_ID'), // Посилання на b_user
new StringField('NAME'),
new StringField('SPECIALIZATION'),
new IntegerField('PHOTO_ID'), // b_file
new StringField('SCHEDULE_JSON'), // Робочі дні та години
new IntegerField('SLOT_DURATION'), // Тривалість слоту в хвилинах
new BooleanField('IS_ACTIVE', ['values' => [false, true]]),
];
}
}
Слоти розкладу (b_hl_booking_slots):
class BookingSlotTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_booking_slots'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('CONSULTANT_ID'),
new DatetimeField('SLOT_START'),
new DatetimeField('SLOT_END'),
new EnumField('STATUS', ['values' => ['FREE', 'BOOKED', 'BLOCKED']]),
new IntegerField('BOOKING_ID'), // Посилання на запис, якщо BOOKED
];
}
}
Записи (b_hl_bookings):
class BookingTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_bookings'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('CONSULTANT_ID'),
new IntegerField('SLOT_ID'),
new StringField('CLIENT_NAME'),
new StringField('CLIENT_PHONE'),
new StringField('CLIENT_EMAIL'),
new StringField('TOPIC'),
new StringField('COMMENT'),
new EnumField('STATUS', ['values' => ['PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED']]),
new StringField('CANCEL_TOKEN'), // Для скасування за посиланням
new DatetimeField('CREATED_AT'),
];
}
}
Генерація слотів
Слоти генеруються агентом або при першому запиті. Розклад спеціаліста — JSON з робочими годинами:
{
"1": {"start": "09:00", "end": "18:00"},
"2": {"start": "09:00", "end": "18:00"},
"3": {"start": "10:00", "end": "16:00"},
"4": {"start": "09:00", "end": "18:00"},
"5": {"start": "09:00", "end": "17:00"}
}
Генерація слотів на 30 днів уперед:
class SlotGenerator
{
public function generateForConsultant(int $consultantId, int $daysAhead = 30): void
{
$consultant = ConsultantTable::getByPrimary($consultantId)->fetch();
$schedule = json_decode($consultant['SCHEDULE_JSON'], true);
$duration = (int)$consultant['SLOT_DURATION']; // наприклад, 60 хвилин
for ($day = 0; $day <= $daysAhead; $day++) {
$date = new \DateTime("+{$day} days");
$weekDay = $date->format('N'); // 1=Пн, 7=Нд
if (!isset($schedule[$weekDay])) {
continue; // Вихідний
}
$daySchedule = $schedule[$weekDay];
$slotStart = new \DateTime($date->format('Y-m-d') . ' ' . $daySchedule['start']);
$slotEnd = new \DateTime($date->format('Y-m-d') . ' ' . $daySchedule['end']);
// Перевірити, що слот ще не створено
$existingCount = BookingSlotTable::getCount([
'>=SLOT_START' => $slotStart,
'<SLOT_START' => $slotEnd,
'CONSULTANT_ID' => $consultantId,
]);
if ($existingCount > 0) {
continue;
}
$current = clone $slotStart;
while ($current < $slotEnd) {
$next = clone $current;
$next->modify("+{$duration} minutes");
if ($next > $slotEnd) break;
BookingSlotTable::add([
'CONSULTANT_ID' => $consultantId,
'SLOT_START' => \Bitrix\Main\Type\DateTime::createFromPhp($current),
'SLOT_END' => \Bitrix\Main\Type\DateTime::createFromPhp($next),
'STATUS' => 'FREE',
]);
$current = $next;
}
}
}
}
Відображення вільних слотів
AJAX-запит повертає вільні слоти для обраної дати та спеціаліста:
// /local/ajax/booking_slots.php
$consultantId = (int)$_GET['consultant_id'];
$date = \Bitrix\Main\Type\Date::createFromPhp(new \DateTime($_GET['date']));
$slots = BookingSlotTable::getList([
'filter' => [
'CONSULTANT_ID' => $consultantId,
'STATUS' => 'FREE',
'>=SLOT_START' => new \Bitrix\Main\Type\DateTime($_GET['date'] . ' 00:00:00'),
'<SLOT_START' => new \Bitrix\Main\Type\DateTime($_GET['date'] . ' 23:59:59'),
],
'order' => ['SLOT_START' => 'ASC'],
'select' => ['ID', 'SLOT_START', 'SLOT_END'],
])->fetchAll();
$result = array_map(fn($s) => [
'id' => $s['ID'],
'start' => date('H:i', strtotime($s['SLOT_START'])),
'end' => date('H:i', strtotime($s['SLOT_END'])),
], $slots);
echo json_encode($result);
Створення запису з блокуванням
Паралельні запити можуть забронювати один слот двічі. Рішення — оптимістичне блокування через UPDATE ... WHERE STATUS = 'FREE' і перевірка зачеплених рядків:
// /local/ajax/booking_create.php
$slotId = (int)$data['slot_id'];
// Спроба забронювати слот через умовне оновлення
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
// Перевірити що слот FREE
$slot = BookingSlotTable::getByPrimary($slotId, ['select' => ['ID', 'STATUS']])->fetch();
if (!$slot || $slot['STATUS'] !== 'FREE') {
$connection->rollbackTransaction();
echo json_encode(['error' => 'Цей слот вже зайнятий. Будь ласка, оберіть інший час.']);
exit;
}
// Позначити як BOOKED
BookingSlotTable::update($slotId, ['STATUS' => 'BOOKED']);
// Створити запис
$addResult = BookingTable::add([
'CONSULTANT_ID' => $data['consultant_id'],
'SLOT_ID' => $slotId,
'CLIENT_NAME' => htmlspecialchars($data['name']),
'CLIENT_PHONE' => htmlspecialchars($data['phone']),
'CLIENT_EMAIL' => htmlspecialchars($data['email']),
'TOPIC' => htmlspecialchars($data['topic'] ?? ''),
'STATUS' => 'CONFIRMED',
'CANCEL_TOKEN' => bin2hex(random_bytes(16)),
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
// Оновити BOOKING_ID у слоті
BookingSlotTable::update($slotId, ['BOOKING_ID' => $addResult->getId()]);
$connection->commitTransaction();
// Надіслати підтвердження
sendBookingConfirmation($addResult->getId());
echo json_encode(['success' => true, 'booking_id' => $addResult->getId()]);
} catch (\Exception $e) {
$connection->rollbackTransaction();
echo json_encode(['error' => 'Помилка при створенні запису']);
}
Сповіщення
При створенні запису — два листи:
- Клієнту: підтвердження з датою, часом, іменем спеціаліста та посиланням для скасування.
- Спеціалісту: сповіщення про новий запис.
Посилання для скасування: /consultation/cancel/?token={CANCEL_TOKEN}. Обробник знаходить запис за токеном, переводить у статус CANCELLED і звільняє слот.
Терміни розробки
| Варіант | Склад | Термін |
|---|---|---|
| Один спеціаліст | Слоти, форма, підтвердження по email | 4–6 днів |
| Кілька спеціалістів | Вибір спеціаліста, управління розкладом | 7–10 днів |
| З особистим кабінетом | ОК спеціаліста, скасування, перенос, історія | 12–18 днів |







