241 lines
8.3 KiB
TypeScript
241 lines
8.3 KiB
TypeScript
'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 = (date: string) => {
|
||
setSelectedDate(date);
|
||
setSelectedTimeSlot('');
|
||
|
||
// Get time slots for the selected date from availableSlotsByDate
|
||
const slotsForDate = availableSlotsByDate[date] || [];
|
||
console.log(`Time slots for ${date}:`, slotsForDate);
|
||
|
||
// Convert to TimeSlot format for TimeSlotSelector
|
||
const formattedTimeSlots: TimeSlot[] = slotsForDate.map((slot, index) => ({
|
||
id: `slot-${date}-${index}`,
|
||
start_time: slot.start_time,
|
||
end_time: slot.end_time,
|
||
is_active: true,
|
||
max_capacity: 8,
|
||
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; |