Реалізація системи бронювання/запису на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація системи бронювання/запису на сайті
Складна
~2-4 тижні
Часті питання

Наші компетенції:

Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація системи бронювання та запису на веб-сайті

Система бронювання — це управління часовими слотами, які неможливо продати двічі. Клініка, перукарня, коворкінг, прокат обладнання, ресторан — скрізь одна логіка: ресурс (фахівець, стіл, переговірна, автомобіль) доступний у конкретний час, і цей слот повинен бути займаний рівно одним бронюванням. Технічно завдання близьке до обліку складських залишків, але з часовим виміром замість кількісного.

Концептуальна модель

Чотири сутності:

Resource — те, що бронюється (майстер, кабінет, інвентарна одиниця) Schedule — робочий розклад ресурсу (години роботи, вихідні, винятки) Slot — часовий відрізок доступності (може генеруватися з розпису або задаватися явно) Booking — бронювання слота під конкретного клієнта

Схема даних

CREATE TABLE bookable_resources (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(255) NOT NULL,
    type        VARCHAR(50) NOT NULL,   -- staff | room | equipment | table
    capacity    INTEGER NOT NULL DEFAULT 1,  -- для групових записів
    is_active   BOOLEAN NOT NULL DEFAULT true,
    meta        JSONB NOT NULL DEFAULT '{}'
);

CREATE TABLE resource_schedules (
    id          BIGSERIAL PRIMARY KEY,
    resource_id BIGINT NOT NULL REFERENCES bookable_resources(id),
    day_of_week SMALLINT,           -- 0=нд, 1=пн ... 6=сб; NULL = конкретна дата
    date        DATE,               -- для винятків з тижневого розпису
    is_working  BOOLEAN NOT NULL DEFAULT true,
    opens_at    TIME NOT NULL,
    closes_at   TIME NOT NULL,
    slot_duration INTEGER NOT NULL DEFAULT 60  -- хвилини
);

CREATE TABLE bookings (
    id              BIGSERIAL PRIMARY KEY,
    resource_id     BIGINT NOT NULL REFERENCES bookable_resources(id),
    service_id      BIGINT REFERENCES services(id),
    user_id         BIGINT REFERENCES users(id),
    client_name     VARCHAR(255) NOT NULL,
    client_phone    VARCHAR(50),
    client_email    VARCHAR(255),
    starts_at       TIMESTAMP NOT NULL,
    ends_at         TIMESTAMP NOT NULL,
    status          VARCHAR(50) NOT NULL DEFAULT 'confirmed',
                    -- pending | confirmed | cancelled | no_show | completed
    notes           TEXT,
    cancel_reason   TEXT,
    reminder_sent   BOOLEAN NOT NULL DEFAULT false,
    created_at      TIMESTAMP NOT NULL DEFAULT NOW(),

    -- Виключаємо перехрестя на рівні БД
    EXCLUDE USING gist (
        resource_id WITH =,
        tsrange(starts_at, ends_at, '[)') WITH &&
    ) WHERE (status NOT IN ('cancelled', 'no_show'))
);

EXCLUDE USING gist — обмеження PostgreSQL на перетин часових діапазонів. Вимагає розширення btree_gist. Це єдиний надійний спосіб виключити подвійне бронювання на рівні бази даних.

CREATE EXTENSION IF NOT EXISTS btree_gist;

Генерація доступних слотів

Слоти не зберігаються в базі — вони обчислюються з розпису мінус існуючі бронювання:

class SlotGenerator
{
    public function getAvailableSlots(
        BookableResource $resource,
        int $serviceDurationMinutes,
        Carbon $date
    ): Collection
    {
        $schedule = $this->getScheduleForDate($resource, $date);

        if (!$schedule || !$schedule->is_working) {
            return collect();
        }

        $slots = collect();
        $current = $date->copy()->setTimeFromTimeString($schedule->opens_at);
        $closes  = $date->copy()->setTimeFromTimeString($schedule->closes_at);
        $duration = CarbonInterval::minutes($serviceDurationMinutes);

        while ($current->copy()->add($duration)->lte($closes)) {
            $slots->push($current->copy());
            $current->addMinutes($schedule->slot_duration);
        }

        // Видаляємо слоти, які перехрещуються з існуючими бронюваннями
        $existingBookings = Booking::where('resource_id', $resource->id)
            ->whereDate('starts_at', $date)
            ->whereNotIn('status', ['cancelled', 'no_show'])
            ->get();

        return $slots->filter(function (Carbon $slot) use ($existingBookings, $duration) {
            $slotEnd = $slot->copy()->add($duration);

            return $existingBookings->every(function (Booking $booking) use ($slot, $slotEnd) {
                return $slotEnd->lte($booking->starts_at) || $slot->gte($booking->ends_at);
            });
        })->values();
    }
}

Атомарне створення бронювання

Обмеження EXCLUDE USING gist ловить конкурентні вставки, але потрібно обробляти винятки PostgreSQL:

class BookingService
{
    public function create(array $data): Booking
    {
        try {
            return DB::transaction(function () use ($data) {
                $booking = Booking::create([
                    'resource_id'   => $data['resource_id'],
                    'starts_at'     => $data['starts_at'],
                    'ends_at'       => Carbon::parse($data['starts_at'])
                                        ->addMinutes($data['duration']),
                    'client_name'   => $data['client_name'],
                    'client_phone'  => $data['client_phone'],
                    'client_email'  => $data['client_email'],
                    'service_id'    => $data['service_id'] ?? null,
                    'status'        => 'confirmed',
                ]);

                BookingConfirmed::dispatch($booking);

                return $booking;
            });
        } catch (QueryException $e) {
            // Код 23P01 — порушення обмеження виключення в PostgreSQL
            if (str_contains($e->getMessage(), '23P01')) {
                throw new SlotAlreadyBookedException($data['starts_at']);
            }
            throw $e;
        }
    }
}

Управління розпису

Розпис — ієрархічний: тижневий шаблон перекривається конкретними датами (свята, відпустки, особливі години):

private function getScheduleForDate(BookableResource $resource, Carbon $date): ?ResourceSchedule
{
    // Спочатку шукаємо конкретну дату (виняток)
    $specific = $resource->schedules()
        ->whereDate('date', $date)
        ->first();

    if ($specific) {
        return $specific;
    }

    // Потім — шаблон дня тижня
    return $resource->schedules()
        ->where('day_of_week', $date->dayOfWeek)
        ->whereNull('date')
        ->first();
}

Нагадування

Автоматичні нагадування зменшують відсоток no-show:

// Запускається кожну годину
class SendBookingReminders
{
    public function handle(): void
    {
        $upcoming = Booking::where('status', 'confirmed')
            ->where('reminder_sent', false)
            ->whereBetween('starts_at', [
                now()->addHours(23),
                now()->addHours(25),
            ])
            ->get();

        foreach ($upcoming as $booking) {
            SendBookingReminder::dispatch($booking);
            $booking->update(['reminder_sent' => true]);
        }
    }
}

Нагадування — за 24 години через SMS та email. Можна додати друге нагадування за 2 години. Канал залежить від інфраструктури: SMS через SMSC.ru, Twilio; email через Mailgun, SES, SMTP.

Скасування та перенесення

Політика скасування налаштовується: безплатно за 24+ години, штраф або заборона — менше 2 годин. При скасуванні — автоматичне сповіщення фахівцю та клієнту.

Перенесення — це скасування + нове бронювання. Важливо: при перенесенні старий слот звільняється атомарно зі створенням нового в одній транзакції.

Віджет запису на веб-сайті

Триетапний процес:

  1. Вибір послуги — список з тривалістю та ціною
  2. Вибір часу — календар з доступними слотами (завантажується через AJAX)
  3. Контактні дані — ім'я, телефон, email, примітка

Слоти завантажуються за запитом при виборі дати:

GET /api/bookings/availability?resource_id=1&service_id=3&date=2025-04-15

Відповідь — масив доступних часових міток. Фронтенд відображує кнопки-слоти.

Адміністративний календар

Для адміністратора/фахівця — вид розпису у форматі тижневого календаря. Реалізується через FullCalendar (JS-бібліотека) з користувацьким джерелом подій:

GET /api/admin/bookings/calendar?resource_id=1&start=2025-04-14&end=2025-04-21

Повертає события у форматі FullCalendar, включаючи блокування та перерви.

Терміни реалізації

  • Базова модель: ресурси + розпис + бронювання + обмеження конфлікту: 4–6 днів
  • Генерація слотів + API для віджета: 2–3 дні
  • Віджет запису (фронтенд): 3–4 дні
  • Нагадування + сповіщення про скасування: 1–2 дні
  • Адміністративний календар: 2–3 дні
  • Онлайн-платіж при бронюванні (передплата або депозит): +3–4 дні

Повна система: 2–4 тижні.