Реализация расписания видеоконсультаций на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация расписания видеоконсультаций на сайте
Средняя
~5 рабочих дней
Часто задаваемые вопросы

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

Этапы разработки

Последние работы

  • 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

Разработка системы записи на видеоконсультации

Система записи — это не просто форма. Нужно отображать реальную занятость специалиста, предотвращать двойное бронирование, учитывать часовые пояса, управлять отменами и переносами, синхронизировать с Google Calendar или Outlook.

Структура доступности

-- Стандартное расписание (повторяющееся)
CREATE TABLE availability_schedules (
  id UUID PRIMARY KEY,
  specialist_id UUID REFERENCES specialists(id),
  day_of_week SMALLINT NOT NULL,  -- 1=Mon ... 7=Sun
  start_time TIME NOT NULL,
  end_time TIME NOT NULL,
  is_active BOOLEAN DEFAULT true
);

-- Исключения (отпуск, праздники, блокировка дня)
CREATE TABLE availability_overrides (
  id UUID PRIMARY KEY,
  specialist_id UUID REFERENCES specialists(id),
  date DATE NOT NULL,
  type VARCHAR(50),  -- 'blocked' | 'custom_hours'
  start_time TIME,
  end_time TIME
);

-- Бронирования
CREATE TABLE bookings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  specialist_id UUID REFERENCES specialists(id),
  client_id UUID REFERENCES users(id),
  starts_at TIMESTAMPTZ NOT NULL,
  ends_at TIMESTAMPTZ NOT NULL,
  status VARCHAR(50) DEFAULT 'confirmed',
  -- 'confirmed' | 'cancelled' | 'rescheduled' | 'no_show' | 'completed'
  cancel_reason TEXT,
  google_event_id VARCHAR(255),
  created_at TIMESTAMPTZ DEFAULT now()
);

Алгоритм свободных слотов

async function getAvailableSlots(
  specialistId: string,
  date: string,  // YYYY-MM-DD
  durationMinutes: number,
  userTimezone: string
): Promise<Array<{ start: string; end: string }>> {
  const localDate = new Date(`${date}T00:00:00`);
  const dayOfWeek = getISODayOfWeek(localDate);  // 1-7

  // 1. Стандартное расписание на этот день
  const schedule = await db.query<{ start_time: string; end_time: string }>(
    `SELECT start_time, end_time FROM availability_schedules
     WHERE specialist_id = $1 AND day_of_week = $2 AND is_active = true`,
    [specialistId, dayOfWeek]
  );

  if (!schedule.rows.length) return [];

  // 2. Проверить переопределения (отпуск, кастомные часы)
  const override = await db.query(
    `SELECT * FROM availability_overrides
     WHERE specialist_id = $1 AND date = $2`,
    [specialistId, date]
  );

  if (override.rows[0]?.type === 'blocked') return [];

  const workStart = override.rows[0]?.start_time ?? schedule.rows[0].start_time;
  const workEnd = override.rows[0]?.end_time ?? schedule.rows[0].end_time;

  // 3. Занятые интервалы
  const booked = await db.query<{ starts_at: string; ends_at: string }>(
    `SELECT starts_at, ends_at FROM bookings
     WHERE specialist_id = $1
       AND DATE(starts_at AT TIME ZONE $3) = $2
       AND status = 'confirmed'`,
    [specialistId, date, userTimezone]
  );

  // 4. Сгенерировать слоты
  const slots: Array<{ start: string; end: string }> = [];
  let current = parseTime(date, workStart, userTimezone);
  const end = parseTime(date, workEnd, userTimezone);

  while (current < end) {
    const slotEnd = addMinutes(current, durationMinutes);
    if (slotEnd > end) break;

    const isBusy = booked.rows.some(b =>
      current < new Date(b.ends_at) && slotEnd > new Date(b.starts_at)
    );

    if (!isBusy) {
      slots.push({
        start: current.toISOString(),
        end: slotEnd.toISOString(),
      });
    }

    current = addMinutes(current, durationMinutes);
  }

  return slots;
}

API бронирования с защитой от двойного бронирования

app.post('/api/bookings', authenticate, async (req, res) => {
  const { specialistId, startsAt, durationMinutes } = req.body;
  const endsAt = addMinutes(new Date(startsAt), durationMinutes);

  try {
    const booking = await db.transaction(async (trx) => {
      // Пессимистичная блокировка — исключить race condition
      const conflict = await trx.query(
        `SELECT id FROM bookings
         WHERE specialist_id = $1
           AND status = 'confirmed'
           AND tstzrange(starts_at, ends_at) && tstzrange($2::timestamptz, $3::timestamptz)
         FOR UPDATE NOWAIT`,
        [specialistId, startsAt, endsAt.toISOString()]
      );

      if (conflict.rows.length > 0) {
        throw Object.assign(new Error('Slot taken'), { code: 'CONFLICT' });
      }

      const [booking] = await trx.query(
        `INSERT INTO bookings (specialist_id, client_id, starts_at, ends_at)
         VALUES ($1, $2, $3, $4) RETURNING *`,
        [specialistId, req.user.id, startsAt, endsAt.toISOString()]
      );

      return booking;
    });

    // Создать событие в Google Calendar
    await syncToGoogleCalendar(booking);

    // Отправить подтверждения
    await sendBookingConfirmation(booking, req.user);
    await notifySpecialist(booking, req.user);

    // Запланировать напоминания
    await scheduleReminders(booking);

    res.json(booking);
  } catch (err: any) {
    if (err.code === 'CONFLICT') {
      return res.status(409).json({ error: 'Slot is no longer available' });
    }
    throw err;
  }
});

Синхронизация с Google Calendar

import { google } from 'googleapis';

async function syncToGoogleCalendar(booking: Booking) {
  const specialist = await db.specialists.findById(booking.specialist_id);
  if (!specialist.google_calendar_token) return;

  const oauth2Client = new google.auth.OAuth2(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET
  );
  oauth2Client.setCredentials(specialist.google_calendar_token);

  const calendar = google.calendar({ version: 'v3', auth: oauth2Client });

  const client = await db.users.findById(booking.client_id);

  const event = await calendar.events.insert({
    calendarId: 'primary',
    requestBody: {
      summary: `Консультация с ${client.name}`,
      start: { dateTime: booking.starts_at.toISOString() },
      end: { dateTime: booking.ends_at.toISOString() },
      attendees: [{ email: client.email }],
      conferenceData: {
        createRequest: { requestId: booking.id },
      },
    },
    conferenceDataVersion: 1,
  });

  await db.bookings.update(booking.id, { google_event_id: event.data.id });
}

Компонент выбора времени

function BookingCalendar({ specialistId, durationMinutes }) {
  const [selectedDate, setSelectedDate] = useState<Date | null>(null);
  const [slots, setSlots] = useState<Slot[]>([]);
  const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);

  useEffect(() => {
    if (!selectedDate) return;
    fetch(`/api/specialists/${specialistId}/slots?date=${formatDate(selectedDate)}&duration=${durationMinutes}`)
      .then(r => r.json())
      .then(setSlots);
  }, [selectedDate]);

  return (
    <div className="grid grid-cols-2 gap-8">
      <CalendarPicker
        value={selectedDate}
        onChange={setSelectedDate}
        minDate={new Date()}
        maxDate={addDays(new Date(), 60)}
        disabledDates={/* выходные и блокированные дни */}
      />

      {selectedDate && (
        <div>
          <p className="font-semibold mb-3">{formatDate(selectedDate, 'd MMMM')}</p>
          {slots.length === 0 ? (
            <p className="text-gray-500">Нет доступных слотов</p>
          ) : (
            <div className="grid grid-cols-3 gap-2">
              {slots.map(slot => (
                <button
                  key={slot.start}
                  onClick={() => setSelectedSlot(slot)}
                  className={`py-2 text-sm rounded-lg border transition ${
                    selectedSlot?.start === slot.start
                      ? 'border-blue-600 bg-blue-50 text-blue-700'
                      : 'border-gray-200 hover:border-blue-400'
                  }`}
                >
                  {formatTime(slot.start)}
                </button>
              ))}
            </div>
          )}
          {selectedSlot && (
            <button onClick={confirmBooking} className="mt-4 btn-primary w-full">
              Записаться на {formatTime(selectedSlot.start)}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

Сроки

Система записи с календарём доступности, защитой от двойного бронирования, напоминаниями и синхронизацией с Google Calendar — 1.5–2 недели.