Development of a form for booking a consultation with 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
    1177
  • 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

Consultation Booking Form Development on 1C-Bitrix

A consultation booking form is not just a feedback form. The user must choose a convenient time, a specialist, and a topic — and immediately receive confirmation. The main technical challenge is displaying real availability (unbooked slots) and blocking the selected time from parallel bookings. On 1C-Bitrix, this is solved via HL-blocks for scheduling and transactional locking when creating a booking.

Data Model

Specialists (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'),        // Reference to b_user
            new StringField('NAME'),
            new StringField('SPECIALIZATION'),
            new IntegerField('PHOTO_ID'),       // b_file
            new StringField('SCHEDULE_JSON'),   // Working days and hours
            new IntegerField('SLOT_DURATION'),  // Slot duration in minutes
            new BooleanField('IS_ACTIVE', ['values' => [false, true]]),
        ];
    }
}

Schedule slots (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'),    // Reference to booking, if BOOKED
        ];
    }
}

Bookings (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'),   // For cancellation by link
            new DatetimeField('CREATED_AT'),
        ];
    }
}

Slot Generation

Slots are generated by an agent or on first request. The specialist's schedule is a JSON with working hours:

{
  "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"}
}

Generating slots for 30 days ahead:

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']; // e.g. 60 minutes

        for ($day = 0; $day <= $daysAhead; $day++) {
            $date    = new \DateTime("+{$day} days");
            $weekDay = $date->format('N'); // 1=Mon, 7=Sun

            if (!isset($schedule[$weekDay])) {
                continue; // Day off
            }

            $daySchedule = $schedule[$weekDay];
            $slotStart   = new \DateTime($date->format('Y-m-d') . ' ' . $daySchedule['start']);
            $slotEnd     = new \DateTime($date->format('Y-m-d') . ' ' . $daySchedule['end']);

            // Check that the slot has not already been created
            $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;
            }
        }
    }
}

Displaying Free Slots

An AJAX request returns free slots for the selected date and specialist:

// /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);

Creating a Booking with Locking

Parallel requests may book the same slot twice. The solution is optimistic locking via UPDATE ... WHERE STATUS = 'FREE' and checking the number of affected rows:

// /local/ajax/booking_create.php
$slotId = (int)$data['slot_id'];

// Attempt to book the slot via conditional update
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();

try {
    // Check that the slot is FREE
    $slot = BookingSlotTable::getByPrimary($slotId, ['select' => ['ID', 'STATUS']])->fetch();
    if (!$slot || $slot['STATUS'] !== 'FREE') {
        $connection->rollbackTransaction();
        echo json_encode(['error' => 'This slot is already taken. Please choose another time.']);
        exit;
    }

    // Mark as BOOKED
    BookingSlotTable::update($slotId, ['STATUS' => 'BOOKED']);

    // Create booking
    $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(),
    ]);

    // Update BOOKING_ID in the slot
    BookingSlotTable::update($slotId, ['BOOKING_ID' => $addResult->getId()]);

    $connection->commitTransaction();

    // Send confirmation
    sendBookingConfirmation($addResult->getId());

    echo json_encode(['success' => true, 'booking_id' => $addResult->getId()]);

} catch (\Exception $e) {
    $connection->rollbackTransaction();
    echo json_encode(['error' => 'Error creating booking']);
}

Notifications

When a booking is created — two emails are sent:

  1. To the client: confirmation with the date, time, specialist name, and a cancellation link.
  2. To the specialist: notification of a new booking.

Cancellation link: /consultation/cancel/?token={CANCEL_TOKEN}. The handler finds the booking by token, changes its status to CANCELLED, and frees the slot.

Development Timeline

Option Scope Timeline
Single specialist Slots, form, email confirmation 4–6 days
Multiple specialists Specialist selection, schedule management 7–10 days
With personal account Specialist's dashboard, cancellation, rescheduling, history 12–18 days