A medical clinic website with a 'Leave a request and we'll call you back' form loses up to 60% conversion — users want to choose a specific doctor, a specific time, and get immediate confirmation. Online booking with slot selection is a standard for any modern clinic, and the best implementations use documented 1C-Bitrix components that ensure security and scalability. Implementing on 1C-Bitrix allows you to link the schedule to the MIS or manage it within the system if there is no MIS. Our engineers, certified by Bitrix, have deployed such solutions in 20+ clinics, reducing appointment time by an average of 50%.
Savings from implementation: clients reduce call center workload by 40% and achieve project payback in 6–8 months. Compared to a custom PHP solution, implementation speed on 1C-Bitrix is 2–3 times faster, and maintenance costs are 30% lower. Ready-made components and the platform API speed up development: built-in authorization, notifications, personal account, and attack protection.
Technical Implementation of Online Booking
The source of the schedule is a key point when designing. Let's consider two options.
Option A: Schedule in Bitrix. The clinic administrator manages the doctor's schedule through the Bitrix interface. Appointments are stored in Bitrix and can be transferred to the MIS (or not — for clinics without an MIS). Suitable for small clinics without complex MIS.
Option B: Schedule from the MIS. Bitrix synchronizes the schedule from the MIS every N minutes. Appointments are created via the MIS API. The site is just the interface; master data is in the MIS.
We further describe Option A — standalone schedule in Bitrix.
Table Structure
-- Doctor's working time template
CREATE TABLE local_doctor_schedule_template (
ID INT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL, -- ID of the infoblock element "Doctors"
DAY_OF_WEEK TINYINT NOT NULL, -- 1=Mon, 7=Sun
TIME_FROM TIME NOT NULL,
TIME_TO TIME NOT NULL,
SLOT_DURATION INT DEFAULT 30, -- minutes per appointment
ACTIVE CHAR(1) DEFAULT 'Y'
);
-- Specific slots (generated from template)
CREATE TABLE local_doctor_slots (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_DATE DATE NOT NULL,
SLOT_TIME TIME NOT NULL,
STATUS ENUM('free','reserved','booked','blocked') DEFAULT 'free',
APPOINTMENT_ID BIGINT,
INDEX idx_doctor_date (DOCTOR_ID, SLOT_DATE, STATUS)
);
-- Patient appointments
CREATE TABLE local_appointments (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_ID BIGINT NOT NULL,
USER_ID INT, -- NULL for unregistered users
PATIENT_NAME VARCHAR(200),
PATIENT_PHONE VARCHAR(20),
PATIENT_EMAIL VARCHAR(200),
SERVICE_ID INT, -- Service (infoblock of services)
COMMENT TEXT,
STATUS ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending',
CREATED_AT DATETIME,
CONFIRMED_AT DATETIME,
CANCELLED_AT DATETIME
);
Slot Generation from Template
An agent run daily generates slots for the next 30 days:
function GenerateDoctorSlots(): string
{
$targetDate = (new \DateTime())->modify('+30 days');
$today = new \DateTime();
$templates = LocalDoctorScheduleTemplateTable::getList([
'filter' => ['ACTIVE' => 'Y'],
'select' => ['DOCTOR_ID', 'DAY_OF_WEEK', 'TIME_FROM', 'TIME_TO', 'SLOT_DURATION'],
]);
while ($tpl = $templates->fetch()) {
$date = clone $today;
while ($date <= $targetDate) {
if ((int)$date->format('N') === (int)$tpl['DAY_OF_WEEK']) {
generateSlotsForDay($tpl, $date);
}
$date->modify('+1 day');
}
}
return __FUNCTION__ . '();';
}
function generateSlotsForDay(array $tpl, \DateTime $date): void
{
$from = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_FROM']);
$to = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_TO']);
$interval = new \DateInterval('PT' . $tpl['SLOT_DURATION'] . 'M');
$current = clone $from;
while ($current < $to) {
// Avoid duplicates
$exists = LocalDoctorSlotsTable::getCount([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
]);
if (!$exists) {
LocalDoctorSlotsTable::add([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
'STATUS' => 'free',
]);
}
$current->add($interval);
}
}
Booking Component
The component /local/components/local/appointment.booking/ is built on the Component 2.0 with steps:
Step 1 — Select doctor/specialization. Filter by specialization from the doctors infoblock. AJAX updates the doctor list.
Step 2 — Select date and time. Calendar with highlighted available dates. When a date is selected, an AJAX request fetches available slots:
// AJAX handler /local/ajax/get-slots.php
$doctorId = (int)$_POST['doctor_id'];
$date = $_POST['date']; // Y-m-d
$slots = LocalDoctorSlotsTable::getList([
'filter' => [
'DOCTOR_ID' => $doctorId,
'SLOT_DATE' => $date,
'STATUS' => 'free',
],
'order' => ['SLOT_TIME' => 'ASC'],
'select' => ['ID', 'SLOT_TIME'],
])->fetchAll();
header('Content-Type: application/json');
echo json_encode(['slots' => $slots]);
Step 3 — Patient form. Name, phone, email, comment. For authorized users, data is populated from the profile. Phone number validation.
Step 4 — Confirmation and booking.
public function bookSlot(int $slotId, array $patientData, int $serviceId = 0): int
{
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
// Atomic slot reservation
$connection->queryExecute("
UPDATE local_doctor_slots
SET STATUS = 'reserved'
WHERE ID = ? AND STATUS = 'free'
", [$slotId]);
if ($connection->getAffectedRowsCount() === 0) {
throw new \RuntimeException('This slot is already taken');
}
$appointmentId = LocalAppointmentsTable::add([
'DOCTOR_ID' => $this->getSlotDoctorId($slotId),
'SLOT_ID' => $slotId,
'USER_ID' => $patientData['user_id'] ?? null,
'PATIENT_NAME' => $patientData['name'],
'PATIENT_PHONE' => $patientData['phone'],
'PATIENT_EMAIL' => $patientData['email'],
'SERVICE_ID' => $serviceId,
'COMMENT' => $patientData['comment'] ?? '',
'STATUS' => 'confirmed',
])->getId();
// Update slot status and link to appointment
LocalDoctorSlotsTable::update($slotId, [
'STATUS' => 'booked',
'APPOINTMENT_ID' => $appointmentId,
]);
$connection->commitTransaction();
// Notifications outside transaction
$this->sendConfirmationSms($patientData['phone'], $appointmentId);
$this->sendConfirmationEmail($patientData['email'], $appointmentId);
return $appointmentId;
} catch (\Exception $e) {
$connection->rollbackTransaction();
throw $e;
}
}
The transaction with UPDATE ... WHERE STATUS = 'free' and checking affectedRows protects against race conditions when two users book the same slot simultaneously. The atomic UPDATE locks the row at the database level. In case of conflict, the second request throws an exception. This is a standard pattern for handling concurrent bookings.
How We Solve the Double Booking Problem
Atomic UPDATE with affectedRows check inside a transaction. If two users submit requests simultaneously, only one successfully updates the slot. The second gets an error 'This slot is already taken'. Additionally, we lock the row in the database for the transaction duration — this guarantees consistency.
How to Cancel an Appointment from the Personal Account
The patient can cancel an appointment no later than N hours before the appointment:
public function cancelAppointment(int $appointmentId, int $userId): void
{
$appointment = LocalAppointmentsTable::getById($appointmentId)->fetch();
if (!$appointment || (int)$appointment['USER_ID'] !== $userId) {
throw new \RuntimeException('Appointment not found');
}
$slot = LocalDoctorSlotsTable::getById($appointment['SLOT_ID'])->fetch();
$slotDateTime = new \DateTime($slot['SLOT_DATE'] . ' ' . $slot['SLOT_TIME']);
if ($slotDateTime <= (new \DateTime())->modify('+2 hours')) {
throw new \RuntimeException('Cancellation is allowed no later than 2 hours before the appointment');
}
LocalAppointmentsTable::update($appointmentId, ['STATUS' => 'cancelled']);
LocalDoctorSlotsTable::update($appointment['SLOT_ID'], ['STATUS' => 'free', 'APPOINTMENT_ID' => null]);
}
Comparison of Integration Options
| Characteristic | Schedule in Bitrix | Schedule from MIS |
|---|---|---|
| Implementation complexity | Low — 3–5 weeks | High — 6–10 weeks |
| Data management | Via Bitrix admin | Via MIS, Bitrix only interface |
| Autonomy | Full | Dependent on MIS |
| Suitable for | Clinics without MIS or with simple MIS | Clinics with existing MIS |
| Development costs | 50% lower | Higher due to integration |
What's Included in the Work
- Design of table structure and agents
- Development and installation of the booking component (select doctor → date → slot → form → confirmation)
- Configuration of slot generation agent
- AJAX handlers for dynamic slot updates
- Implementation of race condition protection
- Integration of SMS/email notifications and reminders
- Patient personal account with history and cancellation
- API and administration documentation
- Administrator training (2–4 hours)
- Technical support for 30 days after launch
Implementation Process and Timeline
- Analysis — study current schedule, doctor workload, MIS (if any). Estimate volume: on average, 30 doctors generate 600 slots per day.
- Design — agree data structure, slot logic. Create interface prototype.
- Implementation — write code, configure components. Enable slot generation agent for 30 days ahead.
- Testing — test on real scenarios: double booking, rescheduling, cancellation. Perform load testing up to 1000 concurrent requests.
- Deployment — deploy to production, configure access rights. Monitor logs for first 24 hours.
- Training — show administrators the interface and backup procedures. Provide documentation.
Timeline: 3–5 weeks for standalone system without MIS. 6–10 weeks with MIS integration.
Contact us for a precise timeline estimate for your project. Get a free consultation from a certified 1C-Bitrix engineer.







