diff --git a/apps/pocketbase/pb_data/auxiliary.db b/apps/pocketbase/pb_data/auxiliary.db index 515babc..b0d5595 100644 Binary files a/apps/pocketbase/pb_data/auxiliary.db and b/apps/pocketbase/pb_data/auxiliary.db differ diff --git a/apps/pocketbase/pb_data/data.db b/apps/pocketbase/pb_data/data.db index a58edb1..6165769 100644 Binary files a/apps/pocketbase/pb_data/data.db and b/apps/pocketbase/pb_data/data.db differ diff --git a/apps/pocketbase/pb_migrations/1756248097_updated_bookingTypes.js b/apps/pocketbase/pb_migrations/1756248097_updated_bookingTypes.js new file mode 100644 index 0000000..c7df331 --- /dev/null +++ b/apps/pocketbase/pb_migrations/1756248097_updated_bookingTypes.js @@ -0,0 +1,29 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_43114331") + + // add field + collection.fields.addAt(13, new Field({ + "hidden": false, + "id": "select2363381545", + "maxSelect": 1, + "name": "type", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "workshop", + "rental" + ] + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_43114331") + + // remove field + collection.fields.removeById("select2363381545") + + return app.save(collection) +}) diff --git a/apps/web/app/api/booking-types/[id]/available-dates/route.ts b/apps/web/app/api/booking-types/[id]/available-dates/route.ts new file mode 100644 index 0000000..9b9e43a --- /dev/null +++ b/apps/web/app/api/booking-types/[id]/available-dates/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { bookingApi } from '@/lib/pocketbase'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: bookingTypeId } = await params; + + // Get booking type details to get the actual capacity + const bookingTypes = await bookingApi.getBookingTypes(); + const bookingType = bookingTypes.find(bt => bt.id === bookingTypeId); + + if (!bookingType) { + return NextResponse.json({ error: 'Booking type not found' }, { status: 404 }); + } + + const bookingCapacity = bookingType.booking_capacity; + + // Get time slots for this booking type + const timeSlots = await bookingApi.getTimeSlotsForBookingType(bookingTypeId); + + // Generate all available dates based on recurrence patterns + const availableSlotsByDate = await bookingApi.generateAvailableTimeSlots(timeSlots); + + // Get all dates and check capacity for each + const availableDates: string[] = []; + + for (const date of Object.keys(availableSlotsByDate)) { + // Get bookings for this date + const bookings = await bookingApi.getBookingsForDate(date, [bookingTypeId]); + + // Check if any time slots have capacity available + const slotsForDate = availableSlotsByDate[date]; + const hasAvailableSlots = slotsForDate.some(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); + return bookingStart < slotEnd && bookingEnd > slotStart; + }); + + const totalParticipants = overlappingBookings.reduce((sum, booking) => + sum + (booking.participants_count || 0), 0 + ); + + return totalParticipants < bookingCapacity; + }); + + if (hasAvailableSlots) { + availableDates.push(date); + } + } + + return NextResponse.json({ + bookingTypeId, + availableDates: availableDates.sort() + }); + + } catch (error) { + console.error('Error fetching available dates:', error); + return NextResponse.json( + { error: 'Failed to fetch available dates' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/apps/web/app/api/booking-types/[id]/time-slots/route.ts b/apps/web/app/api/booking-types/[id]/time-slots/route.ts new file mode 100644 index 0000000..1b19f3b --- /dev/null +++ b/apps/web/app/api/booking-types/[id]/time-slots/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { bookingApi } from '@/lib/pocketbase'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: bookingTypeId } = await params; + const { searchParams } = new URL(request.url); + const date = searchParams.get('date'); + + if (!date) { + return NextResponse.json({ error: 'Date parameter required' }, { status: 400 }); + } + + // Get booking type details to get the actual capacity + const bookingTypes = await bookingApi.getBookingTypes(); + const bookingType = bookingTypes.find(bt => bt.id === bookingTypeId); + + if (!bookingType) { + return NextResponse.json({ error: 'Booking type not found' }, { status: 404 }); + } + + const bookingCapacity = bookingType.booking_capacity; + + // Get time slots for this booking type + const timeSlots = await bookingApi.getTimeSlotsForBookingType(bookingTypeId); + + // Generate available slots for all dates + const availableSlotsByDate = await bookingApi.generateAvailableTimeSlots(timeSlots); + const slotsForDate = availableSlotsByDate[date] || []; + + if (slotsForDate.length === 0) { + return NextResponse.json({ + date, + bookingTypeId, + timeSlots: [] + }); + } + + // Get existing bookings for this date + const bookings = await bookingApi.getBookingsForDate(date, [bookingTypeId]); + console.log(`\n=== Bookings for ${date} and booking type ${bookingTypeId} ===`); + console.log('Number of bookings found:', bookings.length); + bookings.forEach((booking, i) => { + console.log(`Booking ${i+1}: ${booking.start_time} - ${booking.end_time}, participants: ${booking.participants_count}`); + }); + + // Calculate capacity for each slot + const slotsWithCapacity = slotsForDate.map((slot, index) => { + const slotStart = new Date(slot.start_time); + const slotEnd = new Date(slot.end_time); + + console.log(`\n=== Checking slot ${slot.start_time} - ${slot.end_time} ===`); + console.log('Available bookings for date:', bookings.length); + + const overlappingBookings = bookings.filter(booking => { + const bookingStart = new Date(booking.start_time); + const bookingEnd = new Date(booking.end_time); + const overlaps = bookingStart < slotEnd && bookingEnd > slotStart; + + console.log(`Booking ${booking.start_time} - ${booking.end_time}: overlaps = ${overlaps}`); + return overlaps; + }); + + const totalParticipants = overlappingBookings.reduce((sum, booking) => + sum + (booking.participants_count || 0), 0 + ); + + const availableCapacity = Math.max(0, bookingCapacity - totalParticipants); + + console.log(`Total participants: ${totalParticipants}, Capacity: ${bookingCapacity}, Available: ${availableCapacity}`); + + return { + id: `slot-${date}-${index}`, + start_time: slot.start_time, + end_time: slot.end_time, + availableCapacity, + totalBookings: totalParticipants, + maxCapacity: bookingCapacity, + is_active: availableCapacity > 0 + }; + }); + + // Filter out fully booked slots + const availableTimeSlots = slotsWithCapacity.filter(slot => slot.availableCapacity > 0); + + return NextResponse.json({ + date, + bookingTypeId, + timeSlots: availableTimeSlots + }); + + } catch (error) { + console.error('Error fetching time slots:', error); + return NextResponse.json( + { error: 'Failed to fetch time slots' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/apps/web/components/BookingForm.tsx b/apps/web/components/BookingForm.tsx index 98f2829..e433f95 100644 --- a/apps/web/components/BookingForm.tsx +++ b/apps/web/components/BookingForm.tsx @@ -4,7 +4,6 @@ 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'; @@ -27,7 +26,6 @@ const BookingInterface = () => { const [bookingTypes, setBookingTypes] = useState([]); const [timeSlots, setTimeSlots] = useState([]); const [availableDates, setAvailableDates] = useState([]); - const [availableSlotsByDate, setAvailableSlotsByDate] = useState<{ [date: string]: { start_time: string; end_time: string }[] }>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -100,22 +98,23 @@ const BookingInterface = () => { // Set participants count to booking type's minimum capacity const bookingType = bookingTypes.find(bt => bt.id === typeId); if (bookingType) { - setParticipantsCount(bookingType.min_capacity); + setParticipantsCount(bookingType.min_participants_capacity); } - // Fetch time slots for the selected booking type + // Fetch available dates from server API (with capacity pre-calculated) try { - const timeSlots = await bookingApi.getTimeSlotsForBookingType(typeId); + const response = await fetch(`/api/booking-types/${typeId}/available-dates`); + const data = await response.json(); - // 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); + if (response.ok) { + setAvailableDates(data.availableDates); + } else { + console.error('Error fetching available dates:', data.error); + setAvailableDates([]); + } } catch (error) { - console.error('Error fetching time slots:', error); + console.error('Error fetching available dates:', error); + setAvailableDates([]); } }; @@ -123,63 +122,37 @@ const BookingInterface = () => { 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 } = {}; + // Fetch time slots with capacity from server API try { - const bookings = await bookingApi.getBookingsForDate(date, [selectedBookingType]); + const response = await fetch(`/api/booking-types/${selectedBookingType}/time-slots?date=${date}`); + const data = await response.json(); - // Count overlapping bookings for each time slot - slotsForDate.forEach(slot => { - const slotStart = new Date(slot.start_time); - const slotEnd = new Date(slot.end_time); + if (response.ok) { + // Convert server response to TimeSlot format + const formattedTimeSlots: TimeSlot[] = data.timeSlots.map((slot: any) => ({ + id: slot.id, + start_time: slot.start_time, + end_time: slot.end_time, + is_active: slot.is_active, + booking_capacity: slot.maxCapacity, + booking_types: [selectedBookingType], + is_reccuring: false, + recurrence_pattern: undefined, + resources: [], + created: new Date().toISOString(), + updated: new Date().toISOString(), + availableCapacity: slot.availableCapacity // Add capacity info for UI + })); - 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); + setTimeSlots(formattedTimeSlots); + } else { + console.error('Error fetching time slots:', data.error); + setTimeSlots([]); + } } catch (error) { - console.error('Error fetching bookings for date:', error); + console.error('Error fetching time slots:', error); + setTimeSlots([]); } - - // 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 ( diff --git a/apps/web/components/CustomerDetails.tsx b/apps/web/components/CustomerDetails.tsx index 912fea3..0e3cb8c 100644 --- a/apps/web/components/CustomerDetails.tsx +++ b/apps/web/components/CustomerDetails.tsx @@ -28,8 +28,8 @@ const CustomerDetails: React.FC = ({ const t = useTranslations('bookingForm'); // Calculate min and max based on booking type - const minParticipants = bookingType.min_capacity; - const maxParticipants = bookingType.max_capacity; + const minParticipants = bookingType.min_participants_capacity; + const maxParticipants = bookingType.max_participants_capacity; return (

diff --git a/apps/web/components/TimeSlotSelector.tsx b/apps/web/components/TimeSlotSelector.tsx index e07d589..56d262b 100644 --- a/apps/web/components/TimeSlotSelector.tsx +++ b/apps/web/components/TimeSlotSelector.tsx @@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'; import { TimeSlot } from '@/types/bookings'; interface TimeSlotSelectorProps { - timeSlots: TimeSlot[]; + timeSlots: (TimeSlot & { availableCapacity?: number })[]; selectedTimeSlot: string; onTimeSlotChange: (slotId: string) => void; loading?: boolean; @@ -85,6 +85,11 @@ const TimeSlotSelector: React.FC = ({
{formattedTime}
+ {slot.availableCapacity !== undefined && slot.availableCapacity > 0 && ( +
+ {slot.availableCapacity} spots left +
+ )} {!isAvailable && (
Unavailable
)} diff --git a/apps/web/types/bookings.ts b/apps/web/types/bookings.ts index 974305e..9ed78aa 100644 --- a/apps/web/types/bookings.ts +++ b/apps/web/types/bookings.ts @@ -16,6 +16,7 @@ export interface Resource extends BaseRecord { // Booking Type entity export interface BookingType extends BaseRecord { name: string; + type: 'workshop' | 'rental'; display_name: string; description?: string; requires_payment?: boolean;