Онлайн-запись — это задача распределения ограниченных временных интервалов между людьми. Календарная сетка показывает расписание, но сама по себе не предотвращает двойную бронь. Чтобы прототип работал надёжно, нужно описать длительность услуги, календарь ресурса, правила доступности, временную зону, удержание слота, подтверждение и отмену.
Сначала определи, что именно бронируется
Для учебной записи на консультацию выбери ресурс: конкретный специалист, кабинет или общее оборудование. У услуги есть длительность и, возможно, буфер до/после приёма. Слот — не просто строка 14:00: это интервал времени и ресурс, который нельзя занять дважды, если система не допускает одновременные брони.
Раздели три времени:
- время события — конкретный момент, когда проходит встреча;
- часовой пояс отображения — как показать этот момент человеку;
- местная дата/время будущего расписания — например, «каждый понедельник в 10:00 по времени кабинета».
Правило для времени, которое дважды возникает или исчезает
При переходе на летнее время некоторые местные часы не существуют, а при обратном переходе могут встретиться дважды. MDN показывает оба случая и отдельно предупреждает о выборе поведения конструктора Date: при повторе выбирается более ранний момент, а пропущенное время сдвигается вперёд на длину разрыва (MDN: Date components and time zones). Для записи не наследуй это поведение молча. Если клиент выбирает несуществующие 02:30, обозначь время недоступным и предложи ближайший разрешённый вариант, который пользователь подтвердит. Если 01:30 случается дважды, покажи два варианта с разными смещениями/подписями и попроси выбрать один. Сохраняй IANA-зону расписания и выбранный однозначный момент; для повторяющихся сеансов дополнительно храни локальное правило и зону, чтобы следующий перевод часов не изменил обещанный час автоматически.
Для разовой встречи сохраняй однозначный момент и зону, в которой его обещали. Для повторяющегося расписания храни локальные правила и идентификатор зоны, иначе смена смещения времени может сдвинуть ожидаемые часы. В JavaScript Date представляет момент времени в миллисекундах относительно UTC, но методы компонентов даты могут использовать локальную зону среды выполнения; MDN отдельно описывает это поведение (MDN Date). Intl.DateTimeFormat позволяет форматировать дату с заданными locale/options, включая timeZone (MDN Intl.DateTimeFormat). Не путай формат отображения с правилами доступности слотов.
Модель брони
Для каждой записи нужны как минимум resource_id, starts_at, ends_at, customer_contact и status. Статусы стоит определить заранее, например pending, confirmed, cancelled, expired. Запись pending может быть временным удержанием, но у неё должна быть понятная граница: сколько она живёт и как освобождается, если клиент не подтвердил встречу.
Рабочий поток:
- Сервер выдаёт свободные слоты на выбранную дату и ресурс.
- Клиент выбирает один из них.
- Сервер атомарно проверяет доступность и создаёт удержание/бронь.
- Клиент получает ясный ответ: слот удержан, подтверждён или уже занят.
- Подтверждение отправляется только после фактической операции и нужного внешнего сигнала.
Не считай список, который браузер получил несколько минут назад, гарантией свободного места. Другой пользователь мог подтвердить этот же слот между загрузкой и отправкой формы.
Гонка: два клиента нажали одновременно
Представь один кабинет и слот 10:00–10:30. Клиент A и клиент B открыли страницу, оба видят свободное время. Если оба запроса сначала читают наличие свободного слота, а затем независимо вставляют запись, проверки могут пройти одновременно. Поэтому повторная проверка должна быть частью атомарной серверной операции с ограничением на пересечение интервалов либо транзакцией/блокировкой, подходящей выбранной БД.
PostgreSQL документирует range types для представления интервалов, в том числе включённые/исключённые границы и ограничения на ranges (PostgreSQL range types). Документация constraints включает exclusion constraints, которые могут применяться к конфликтующим интервалам в подходящей схеме (constraints). Эти механизмы не включаются автоматически: схему, границы интервала и поведение при конфликте нужно спроектировать и проверить.
Для теста отправь два конкурентных запроса на один resource и одинаковый интервал. Ожидаемый исход: только допустимое число подтверждённых записей; проигравший получает «слот уже занят» и обновлённый список. Проверь соседние слоты и границу ровно в момент конца предыдущей встречи: интервал 10:00–10:30 обычно может соседствовать с 10:30–11:00, если выбраны полуоткрытые границы, но конкретное правило задаёт владелец продукта.
Отмена и подтверждение
Отмена должна менять состояние существующей записи, а не удалять её без следа, если нужен журнал. Определи, кто имеет право отменить, до какого момента и как пользователь узнает результат. Не обещай автоматическое уведомление, пока интеграция реально не отправляет сообщение. Если подтверждение вручную, покажи «запрос получен, ждём подтверждения», а не «вы записаны».
Попроси ИИ сначала перечислить edge cases: часовой пояс клиента отличается от зоны кабинета; конец месяца; переход летнего времени в поддерживаемой юрисдикции; слот уже занят; два запроса одновременно; ошибка отправки подтверждения; повторная отправка формы; отмена после подтверждения. Затем пусть предложит модель и тесты. Код здесь не запускался: реализация зависит от выбранной БД и календарной библиотеки.
Критерий прототипа
Надёжный прототип демонстрирует выбранное правило: человек видит согласованное время и зону; сервер повторно проверяет ресурс и интервал; конфликт не создаёт запрещённую двойную бронь; статусы и сообщения соответствуют факту; reload показывает серверное состояние. Пока эти проверки не пройдены, интерфейс календаря показывает лишь макет доступности, а не подтверждённую систему записи.
