Реалізація календаря доступності для бронювання на сайті
Календар доступності — центральний UI-елемент системи бронювання. Показує, які дати відкриті для запису, які зайняті, які заблоковані. Від коректності відображення залежить користувацький досвід та кількість помилкових бронювань.
Логіка доступності
Доступність слота визначається перетином кількох джерел:
interface AvailabilitySlot {
datetime: Date;
available: boolean;
reason?: 'booked' | 'blocked' | 'outside_hours' | 'capacity_full';
capacity?: number; // для группового бронювання
remaining?: number;
}
class AvailabilityService {
async getAvailability(resourceId: number, from: Date, to: Date): Promise<AvailabilitySlot[]> {
const [schedule, bookings, blocks] = await Promise.all([
this.getWorkingSchedule(resourceId), // години роботи
this.getBookings(resourceId, from, to),
this.getBlocks(resourceId, from, to), // ручні блокування
]);
return this.generateSlots(from, to, schedule, bookings, blocks);
}
}
Компонент календаря (React)
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { startOfMonth, endOfMonth, eachDayOfInterval, format, isSameDay } from 'date-fns';
import { uk } from 'date-fns/locale';
interface DayAvailability {
date: string;
hasSlots: boolean;
allBooked: boolean;
}
export function AvailabilityCalendar({ resourceId, onDateSelect }: Props) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const { data: availability } = useQuery({
queryKey: ['availability', resourceId, format(currentMonth, 'yyyy-MM')],
queryFn: () => fetchMonthAvailability(resourceId, currentMonth),
staleTime: 60_000,
});
const days = eachDayOfInterval({
start: startOfMonth(currentMonth),
end: endOfMonth(currentMonth),
});
return (
<div className="grid grid-cols-7 gap-1">
{/* Заголовки днів тижня */}
{['Пн','Вт','Ср','Чт','Пт','Сб','Нд'].map(d => (
<div key={d} className="text-center text-xs text-gray-500 py-2">{d}</div>
))}
{/* Дні місяця */}
{days.map(day => {
const dateStr = format(day, 'yyyy-MM-dd');
const dayData = availability?.find(a => a.date === dateStr);
const isToday = isSameDay(day, new Date());
return (
<button
key={dateStr}
disabled={!dayData?.hasSlots || dayData?.allBooked}
onClick={() => onDateSelect(day)}
className={cn(
'aspect-square rounded-lg text-sm font-medium transition-colors',
isToday && 'ring-2 ring-blue-500',
dayData?.hasSlots && !dayData?.allBooked
? 'bg-green-50 text-green-700 hover:bg-green-100'
: 'bg-gray-50 text-gray-300 cursor-not-allowed'
)}
>
{format(day, 'd')}
</button>
);
})}
</div>
);
}
Оптимізація завантаження
Доступність завантажується помісячно та кешується 60 секунд. При виборі дати завантажуються часові слоти конкретного дня. Інвалідація кеша через WebSocket-подію при створенні нового бронювання.
Синхронізація кількох ресурсів
Для сервісів з кількома спеціалістами/залами — вибір конкретного ресурсу або показ першого доступного слота серед усіх:
async function getFirstAvailableSlot(date: Date, serviceId: number): Promise<Slot | null> {
const resources = await getServiceResources(serviceId);
const slots = await Promise.all(
resources.map(r => getAvailableSlots(r.id, date))
);
return slots.flat().sort((a, b) => a.datetime - b.datetime)[0] ?? null;
}
Терміни
Календар доступності з API та компонентом: 4–6 робочих днів.







