fix the capacity and NaN participants issue
This commit is contained in:
70
apps/web/app/api/booking-types/[id]/available-dates/route.ts
Normal file
70
apps/web/app/api/booking-types/[id]/available-dates/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
102
apps/web/app/api/booking-types/[id]/time-slots/route.ts
Normal file
102
apps/web/app/api/booking-types/[id]/time-slots/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -28,8 +28,8 @@ const CustomerDetails: React.FC<CustomerDetailsProps> = ({
|
||||
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 (
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
|
||||
@@ -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<TimeSlotSelectorProps> = ({
|
||||
<div className="font-medium">
|
||||
{formattedTime}
|
||||
</div>
|
||||
{slot.availableCapacity !== undefined && slot.availableCapacity > 0 && (
|
||||
<div className="text-xs mt-1 text-green-600">
|
||||
{slot.availableCapacity} spots left
|
||||
</div>
|
||||
)}
|
||||
{!isAvailable && (
|
||||
<div className="text-xs mt-1">Unavailable</div>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user