Реализация выбора времени и даты для бронирования
Выбор времени и даты — двухшаговый UI: сначала дата в календаре, затем доступные временные слоты. Ключевые требования: отображение только реально свободного времени, обновление в реальном времени при одновременных бронированиях, корректная работа с часовыми поясами.
Временные слоты
function TimeSlotPicker({ resourceId, date, onSelect }: Props) {
const { data: slots, isLoading } = useQuery({
queryKey: ['slots', resourceId, format(date, 'yyyy-MM-dd')],
queryFn: () => fetchTimeSlots(resourceId, date),
});
if (isLoading) return <SlotsSkeleton />;
const grouped = groupBy(slots, slot =>
slot.datetime.getHours() < 13 ? 'morning' : 'afternoon'
);
return (
<div className="space-y-4">
{grouped.morning && (
<div>
<p className="text-sm text-gray-500 mb-2">Утро</p>
<div className="grid grid-cols-4 gap-2">
{grouped.morning.map(slot => (
<button
key={slot.id}
disabled={!slot.available}
onClick={() => onSelect(slot)}
className={cn(
'py-2 rounded-lg text-sm border transition',
slot.available
? 'border-blue-200 hover:bg-blue-50 hover:border-blue-500'
: 'border-gray-100 text-gray-300 cursor-not-allowed'
)}
>
{format(slot.datetime, 'HH:mm')}
</button>
))}
</div>
</div>
)}
</div>
);
}
Часовые пояса
Сервер хранит время в UTC. Клиент отображает в локальном времени пользователя:
// Конвертация серверного UTC в локальное время пользователя
const localSlot = utcToZonedTime(slot.datetime_utc, userTimezone);
const displayTime = format(localSlot, 'HH:mm', { timeZone: userTimezone });
Для мультирегиональных сервисов — явный выбор часового пояса клиентом.
Защита от двойного бронирования
Пессимистическая блокировка: слот временно резервируется на 10 минут при попытке бронирования, снимается при отмене или завершении процесса.
Время реализации: 2–3 рабочих дня.







