Files
vitrify-me/apps/web/components/BookingForm.tsx
2025-08-27 02:08:23 +04:00

278 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslations } from 'next-intl';
import { BookingService } from '@/lib/bookingService';
import { bookingApi } from '@/lib/pocketbase';
import { BookingType, TimeSlot } from '@/types/bookings';
import BookingTypeSelector from './BookingTypeSelector';
import DateSelector from './DateSelector';
import TimeSlotSelector from './TimeSlotSelector';
import CustomerDetails from './CustomerDetails';
import BookingSummary from './BookingSummary';
interface FormData {
customerName: string;
customerEmail: string;
participantsCount: number;
}
const BookingInterface = () => {
const t = useTranslations('bookingForm');
const [selectedDate, setSelectedDate] = useState('');
const [selectedTimeSlot, setSelectedTimeSlot] = useState('');
const [selectedBookingType, setSelectedBookingType] = useState('');
const [participantsCount, setParticipantsCount] = useState(1);
const [bookingTypes, setBookingTypes] = useState<BookingType[]>([]);
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const [availableDates, setAvailableDates] = useState<string[]>([]);
const [availableSlotsByDate, setAvailableSlotsByDate] = useState<{ [date: string]: { start_time: string; end_time: string }[] }>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { register, handleSubmit, formState: { errors, isValid }, setValue, trigger } = useForm<FormData>({
mode: 'onChange',
defaultValues: {
customerName: '',
customerEmail: '',
participantsCount: 1
}
});
const selectedBookingTypeData = bookingTypes.find(bt => bt.id === selectedBookingType);
// Fetch booking types on mount
useEffect(() => {
const fetchBookingTypes = async () => {
try {
setLoading(true);
setError(null);
const types = await BookingService.getBookingTypes();
setBookingTypes(types);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load booking types');
} finally {
setLoading(false);
}
};
fetchBookingTypes();
}, []);
// Available dates are now handled in handleBookingTypeChange
// Time slots are now handled in handleDateChange
useEffect(() => {
setValue('participantsCount', participantsCount);
trigger('participantsCount');
}, [participantsCount, setValue, trigger]);
const onSubmit = (data: FormData) => {
const selectedTimeSlotData = timeSlots.find(slot => slot.id === selectedTimeSlot);
const formData = {
bookingTypeId: selectedBookingType,
customerName: data.customerName,
customerEmail: data.customerEmail,
startTime: selectedDate && selectedTimeSlotData
? `${selectedDate}T${selectedTimeSlotData.start_time}`
: '',
endTime: selectedDate && selectedTimeSlotData
? `${selectedDate}T${selectedTimeSlotData.end_time}`
: '',
participantsCount: data.participantsCount,
};
console.log(formData);
};
const handleParticipantsCountChange = (count: number) => {
setParticipantsCount(count);
};
const handleBookingTypeChange = async (typeId: string) => {
setSelectedBookingType(typeId);
setSelectedDate('');
setSelectedTimeSlot('');
setTimeSlots([]);
// Set participants count to booking type's minimum capacity
const bookingType = bookingTypes.find(bt => bt.id === typeId);
if (bookingType) {
setParticipantsCount(bookingType.min_capacity);
}
// Fetch time slots for the selected booking type
try {
const timeSlots = await bookingApi.getTimeSlotsForBookingType(typeId);
// Generate available time slots grouped by date
const generatedSlotsByDate = bookingApi.generateAvailableTimeSlots(timeSlots);
const availableDatesFromAPI = Object.keys(generatedSlotsByDate).sort();
// Update available dates and slots data for the date selector
setAvailableDates(availableDatesFromAPI);
setAvailableSlotsByDate(generatedSlotsByDate);
} catch (error) {
console.error('Error fetching time slots:', error);
}
};
const handleDateChange = async (date: string) => {
setSelectedDate(date);
setSelectedTimeSlot('');
// Get time slots for the selected date from availableSlotsByDate
const slotsForDate = availableSlotsByDate[date] || [];
// Get bookings for this date filtered by booking type IDs
let bookingOverlapCounts: { [key: string]: number } = {};
try {
const bookings = await bookingApi.getBookingsForDate(date, [selectedBookingType]);
// Count overlapping bookings for each time slot
slotsForDate.forEach(slot => {
const slotStart = new Date(slot.start_time);
const slotEnd = new Date(slot.end_time);
const overlappingBookings = bookings.filter(booking => {
const bookingStart = new Date(booking.start_time);
const bookingEnd = new Date(booking.end_time);
// Check if bookings overlap with time slot
return bookingStart < slotEnd && bookingEnd > slotStart;
});
const totalParticipants = overlappingBookings.reduce((sum, booking) =>
sum + (booking.participants_count || 0), 0
);
const key = `${slot.start_time}-${slot.end_time}`;
bookingOverlapCounts[key] = totalParticipants;
});
console.log('Booking overlap counts:', bookingOverlapCounts);
} catch (error) {
console.error('Error fetching bookings for date:', error);
}
// Convert to TimeSlot format and filter out fully booked slots
const bookingTypeCapacity = selectedBookingTypeData?.booking_capacity || 8;
const availableTimeSlots = slotsForDate.filter(slot => {
const key = `${slot.start_time}-${slot.end_time}`;
const overlappingCount = bookingOverlapCounts[key] || 0;
return overlappingCount < bookingTypeCapacity;
});
const formattedTimeSlots: TimeSlot[] = availableTimeSlots.map((slot, index) => ({
id: `slot-${date}-${index}`,
start_time: slot.start_time,
end_time: slot.end_time,
is_active: true,
booking_capacity: bookingTypeCapacity,
booking_types: [selectedBookingType],
is_reccuring: false,
recurrence_pattern: undefined,
resources: [],
created: new Date().toISOString(),
updated: new Date().toISOString()
}));
setTimeSlots(formattedTimeSlots);
};
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('title')}</h1>
<p className="text-gray-600">{t('subtitle')}</p>
</div>
<div className="space-y-8">
{/* Loading State */}
{loading && (
<div className="bg-white rounded-xl p-6 shadow-sm text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-600 mt-2">{t('loading')}</p>
</div>
)}
{/* Error State */}
{error && (
<div className="bg-white rounded-xl p-6 shadow-sm text-center">
<div className="text-red-500 mb-2"></div>
<p className="text-gray-800 font-medium mb-2">{t('noBookings')}</p>
<p className="text-gray-600 text-sm">{error}</p>
</div>
)}
{/* Booking Type Selection */}
{!loading && !error && bookingTypes.length === 0 && (
<div className="bg-white rounded-xl p-6 shadow-sm text-center">
<p className="text-gray-800">{t('noBookings')}</p>
<p className="text-gray-600 text-sm mt-2">{t('checkBackLater')}</p>
</div>
)}
{!loading && !error && bookingTypes.length > 0 && (
<BookingTypeSelector
bookingTypes={bookingTypes}
selectedBookingType={selectedBookingType}
onBookingTypeChange={handleBookingTypeChange}
/>
)}
{/* Date Selection */}
{selectedBookingType && (
<DateSelector
selectedDate={selectedDate}
availableDates={availableDates}
onDateChange={handleDateChange}
/>
)}
{/* Time Slot Selection */}
{selectedDate && (
<TimeSlotSelector
timeSlots={timeSlots}
selectedTimeSlot={selectedTimeSlot}
onTimeSlotChange={setSelectedTimeSlot}
/>
)}
{/* Customer Details */}
{selectedTimeSlot && selectedBookingTypeData && (
<form onSubmit={handleSubmit(onSubmit)}>
<CustomerDetails
register={register}
errors={errors}
participantsCount={participantsCount}
onParticipantsCountChange={handleParticipantsCountChange}
bookingType={selectedBookingTypeData}
/>
{/* Summary & Submit */}
{isValid && (
<div className="mt-8">
<BookingSummary
selectedBookingTypeData={selectedBookingTypeData}
selectedDate={selectedDate}
selectedTimeSlot={selectedTimeSlot}
timeSlots={timeSlots}
participantsCount={participantsCount}
onSubmit={handleSubmit(onSubmit)}
/>
</div>
)}
</form>
)}
</div>
</div>
</div>
);
};
export default BookingInterface;