timeslots API
This commit is contained in:
15
apps/web/lib/bookingService.ts
Normal file
15
apps/web/lib/bookingService.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { bookingApi } from './pocketbase';
|
||||
import { BookingType } from '@/types/bookings';
|
||||
|
||||
export class BookingService {
|
||||
static async getBookingTypes(): Promise<BookingType[]> {
|
||||
try {
|
||||
const bookingTypes = await bookingApi.getBookingTypes();
|
||||
return bookingTypes.filter(type => type.is_active);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch booking types:', error);
|
||||
throw new Error('Unable to load booking options. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
apps/web/lib/i18n.ts
Normal file
16
apps/web/lib/i18n.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
// This typically corresponds to the `[locale]` segment
|
||||
let locale = await requestLocale;
|
||||
|
||||
// Ensure that a valid locale is used
|
||||
if (!locale || !['en', 'ka'].includes(locale)) {
|
||||
locale = 'en';
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import { BookingType, Resource, TimeSlot, Booking } from '@/types/booking';
|
||||
import { BookingType, TimeSlot } from '@/types/bookings';
|
||||
|
||||
// Initialize PocketBase client
|
||||
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || 'http://127.0.0.1:8090');
|
||||
@@ -8,7 +8,6 @@ export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || 'http
|
||||
pb.autoCancellation(false);
|
||||
|
||||
// API functions for booking system
|
||||
|
||||
export const bookingApi = {
|
||||
// Get all active booking types
|
||||
async getBookingTypes(): Promise<BookingType[]> {
|
||||
@@ -24,121 +23,103 @@ export const bookingApi = {
|
||||
}
|
||||
},
|
||||
|
||||
// Get available resources
|
||||
async getResources(): Promise<Resource[]> {
|
||||
// Get time slots for a specific booking type
|
||||
async getTimeSlotsForBookingType(bookingTypeId: string): Promise<TimeSlot[]> {
|
||||
try {
|
||||
const records = await pb.collection('resources').getFullList<Resource>({
|
||||
filter: 'is_active = true',
|
||||
sort: 'type',
|
||||
});
|
||||
return records;
|
||||
} catch (error) {
|
||||
console.error('Error fetching resources:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
const today = new Date();
|
||||
const oneMonthFromToday = new Date();
|
||||
oneMonthFromToday.setMonth(today.getMonth() + 1);
|
||||
|
||||
// Get available time slots for a specific booking type and date range
|
||||
async getAvailableTimeSlots(
|
||||
bookingTypeId: string,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<TimeSlot[]> {
|
||||
try {
|
||||
const startDateStr = startDate.toISOString().split('T')[0];
|
||||
const endDateStr = endDate.toISOString().split('T')[0];
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
const oneMonthStr = oneMonthFromToday.toISOString().split('T')[0];
|
||||
|
||||
const records = await pb.collection('timeSlots').getFullList<TimeSlot>({
|
||||
filter: `is_active = true && booking_types ~ "${bookingTypeId}" && start_time >= "${startDateStr}" && start_time <= "${endDateStr}"`,
|
||||
filter: `is_active = true && booking_types ~ "${bookingTypeId}" && start_time >= "${todayStr}" && end_time <= "${oneMonthStr}"`,
|
||||
sort: 'start_time',
|
||||
});
|
||||
return records;
|
||||
} catch (error) {
|
||||
console.error('Error fetching time slots:', error);
|
||||
console.error('Error fetching time slots for booking type:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Check if a time slot has availability
|
||||
async checkTimeSlotAvailability(
|
||||
timeSlotId: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
participantsCount: number
|
||||
): Promise<{ available: boolean; currentBookings: number }> {
|
||||
try {
|
||||
// Get existing confirmed bookings for this time slot
|
||||
const existingBookings = await pb.collection('bookings').getFullList<Booking>({
|
||||
filter: `status = "confirmed" && start_time >= "${startTime}" && end_time <= "${endTime}"`,
|
||||
});
|
||||
// Generate available time slots grouped by date
|
||||
generateAvailableTimeSlots(timeSlots: TimeSlot[]): { [date: string]: { start_time: string; end_time: string }[] } {
|
||||
const today = new Date();
|
||||
const oneMonthFromToday = new Date();
|
||||
oneMonthFromToday.setMonth(today.getMonth() + 1);
|
||||
|
||||
// Get the time slot to check max capacity
|
||||
const timeSlot = await pb.collection('timeSlots').getOne<TimeSlot>(timeSlotId);
|
||||
// Step 1: Generate massive array of individual time slots with specific dates
|
||||
const allTimeSlots: { start_time: string; end_time: string }[] = [];
|
||||
|
||||
const currentBookings = existingBookings.reduce((sum, booking) =>
|
||||
sum + (booking.participants_count || 0), 0
|
||||
);
|
||||
timeSlots.forEach(slot => {
|
||||
if (!slot.is_reccuring || !slot.recurrence_pattern) {
|
||||
// Handle non-recurring slots - use as is
|
||||
allTimeSlots.push({
|
||||
start_time: slot.start_time,
|
||||
end_time: slot.end_time
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const availableSpots = timeSlot.max_capacity - currentBookings;
|
||||
const available = availableSpots >= participantsCount;
|
||||
// Handle recurring slots - generate dates based on recurrence pattern
|
||||
const pattern = slot.recurrence_pattern;
|
||||
const endDate = pattern.end_date ? new Date(pattern.end_date) : oneMonthFromToday;
|
||||
const finalEndDate = endDate < oneMonthFromToday ? endDate : oneMonthFromToday;
|
||||
|
||||
return {
|
||||
available,
|
||||
currentBookings,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking availability:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// Extract time from original start_time and end_time
|
||||
const originalStartTime = new Date(slot.start_time);
|
||||
const originalEndTime = new Date(slot.end_time);
|
||||
const startHours = originalStartTime.getHours();
|
||||
const startMinutes = originalStartTime.getMinutes();
|
||||
const endHours = originalEndTime.getHours();
|
||||
const endMinutes = originalEndTime.getMinutes();
|
||||
|
||||
// Create a new booking
|
||||
async createBooking(bookingData: {
|
||||
booking_type: string;
|
||||
customer_name: string;
|
||||
customer_email: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
participants_count: number;
|
||||
internal_notes?: string;
|
||||
}): Promise<Booking> {
|
||||
try {
|
||||
const record = await pb.collection('bookings').create<Booking>({
|
||||
...bookingData,
|
||||
status: 'confirmed',
|
||||
payment_status: 'not_required', // We'll handle payment logic later
|
||||
payment_required: false,
|
||||
});
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error creating booking:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
let currentDate = new Date(today);
|
||||
|
||||
while (currentDate <= finalEndDate) {
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
// Convert Sunday (0) to 6, Monday (1) to 0, etc. to match pattern format
|
||||
const patternDay = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
|
||||
if (pattern.days && pattern.days.includes(patternDay)) {
|
||||
// Create specific datetime for this occurrence
|
||||
const specificStartTime = new Date(currentDate);
|
||||
specificStartTime.setHours(startHours, startMinutes, 0, 0);
|
||||
|
||||
const specificEndTime = new Date(currentDate);
|
||||
specificEndTime.setHours(endHours, endMinutes, 0, 0);
|
||||
|
||||
allTimeSlots.push({
|
||||
start_time: specificStartTime.toISOString(),
|
||||
end_time: specificEndTime.toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Get booking by cancellation token (for cancellation flow)
|
||||
async getBookingByToken(token: string): Promise<Booking | null> {
|
||||
try {
|
||||
const records = await pb.collection('bookings').getFullList<Booking>({
|
||||
filter: `cancellation_token = "${token}"`,
|
||||
});
|
||||
return records[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching booking by token:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
// Step 2: Group time slots by date (without time)
|
||||
const availableSlotsByDate: { [date: string]: { start_time: string; end_time: string }[] } = {};
|
||||
|
||||
allTimeSlots.forEach(timeSlot => {
|
||||
const dateStr = timeSlot.start_time.split('T')[0];
|
||||
|
||||
if (dateStr) {
|
||||
if (!availableSlotsByDate[dateStr]) {
|
||||
availableSlotsByDate[dateStr] = [];
|
||||
}
|
||||
availableSlotsByDate[dateStr].push(timeSlot);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel booking
|
||||
async cancelBooking(bookingId: string): Promise<boolean> {
|
||||
try {
|
||||
await pb.collection('bookings').update(bookingId, {
|
||||
status: 'cancelled',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error cancelling booking:', error);
|
||||
return false;
|
||||
}
|
||||
// Step 3: Sort time slots within each date
|
||||
Object.keys(availableSlotsByDate).forEach(date => {
|
||||
availableSlotsByDate[date]?.sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||
});
|
||||
|
||||
return availableSlotsByDate;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user