diff --git a/app/api/bookings/route.ts b/app/api/bookings/route.ts new file mode 100644 index 0000000..37e5baf --- /dev/null +++ b/app/api/bookings/route.ts @@ -0,0 +1,314 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' +import { generateShortId } from '@/lib/utils/short-id' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { + customer_email, + customer_phone, + customer_first_name, + customer_last_name, + service_id, + location_id, + start_time_utc, + notes + } = body + + if (!customer_email || !service_id || !location_id || !start_time_utc) { + return NextResponse.json( + { error: 'Missing required fields: customer_email, service_id, location_id, start_time_utc' }, + { status: 400 } + ) + } + + const startTime = new Date(start_time_utc) + + if (isNaN(startTime.getTime())) { + return NextResponse.json( + { error: 'Invalid start_time_utc format' }, + { status: 400 } + ) + } + + const { data: location, error: locationError } = await supabaseAdmin + .from('locations') + .select('id, name, timezone') + .eq('id', location_id) + .single() + + if (locationError || !location) { + return NextResponse.json( + { error: 'Location not found' }, + { status: 404 } + ) + } + + const { data: service, error: serviceError } = await supabaseAdmin + .from('services') + .select('*, location_id') + .eq('id', service_id) + .single() + + if (serviceError || !service) { + return NextResponse.json( + { error: 'Service not found' }, + { status: 404 } + ) + } + + if (service.location_id !== location_id) { + return NextResponse.json( + { error: 'Service does not belong to the specified location' }, + { status: 400 } + ) + } + + const endTime = new Date(startTime) + endTime.setMinutes(endTime.getMinutes() + service.duration_minutes) + + const endTimeUtc = endTime.toISOString() + + const { data: availableStaff, error: staffError } = await supabaseAdmin.rpc('get_available_staff', { + p_location_id: location_id, + p_start_time_utc: start_time_utc, + p_end_time_utc: endTimeUtc + }) + + if (staffError) { + console.error('Error checking staff availability:', staffError) + return NextResponse.json( + { error: 'Failed to check staff availability' }, + { status: 500 } + ) + } + + if (!availableStaff || availableStaff.length === 0) { + return NextResponse.json( + { error: 'No staff available for the selected time' }, + { status: 409 } + ) + } + + const assignedStaff = availableStaff[0] + + const { data: availableResources, error: resourcesError } = await supabaseAdmin.rpc('get_available_resources_with_priority', { + p_location_id: location_id, + p_start_time: start_time_utc, + p_end_time: endTimeUtc, + p_service_id: service_id + }) + + if (resourcesError) { + console.error('Error checking resource availability:', resourcesError) + return NextResponse.json( + { error: 'Failed to check resource availability' }, + { status: 500 } + ) + } + + if (!availableResources || availableResources.length === 0) { + return NextResponse.json( + { error: 'No resources available for the selected time' }, + { status: 409 } + ) + } + + const assignedResource = availableResources[0] + + const { data: customer, error: customerError } = await supabaseAdmin + .from('customers') + .upsert({ + email: customer_email, + phone: customer_phone || null, + first_name: customer_first_name || null, + last_name: customer_last_name || null + }, { + onConflict: 'email', + ignoreDuplicates: false + }) + .select() + .single() + + if (customerError || !customer) { + console.error('Error creating customer:', customerError) + return NextResponse.json( + { error: 'Failed to create customer' }, + { status: 500 } + ) + } + + const shortId = await generateShortId() + + const { data: booking, error: bookingError } = await supabaseAdmin + .from('bookings') + .insert({ + customer_id: customer.id, + service_id, + location_id, + staff_id: assignedStaff.staff_id, + resource_id: assignedResource.resource_id, + short_id: shortId, + status: 'pending', + start_time_utc: start_time_utc, + end_time_utc: endTimeUtc, + is_paid: false, + notes + }) + .select(` + id, + short_id, + status, + start_time_utc, + end_time_utc, + notes, + service ( + id, + name, + duration_minutes, + base_price + ), + resource ( + id, + name, + type + ), + staff ( + id, + display_name + ), + location ( + id, + name, + address, + timezone + ) + `) + .single() + + if (bookingError || !booking) { + console.error('Error creating booking:', bookingError) + return NextResponse.json( + { error: bookingError?.message || 'Failed to create booking' }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + booking + }, { status: 201 }) + } catch (error) { + console.error('Create booking error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const customerId = searchParams.get('customer_id') + const shortId = searchParams.get('short_id') + const locationId = searchParams.get('location_id') + const status = searchParams.get('status') + + if (!customerId && !shortId && !locationId) { + return NextResponse.json( + { error: 'At least one of customer_id, short_id, or location_id is required' }, + { status: 400 } + ) + } + + let query = supabaseAdmin + .from('bookings') + .select(` + id, + short_id, + status, + start_time_utc, + end_time_utc, + notes, + is_paid, + created_at, + service ( + id, + name, + duration_minutes, + base_price, + category + ), + resource ( + id, + name, + type + ), + staff ( + id, + display_name + ), + location ( + id, + name, + address, + timezone + ), + customer ( + id, + email, + first_name, + last_name, + phone + ) + `) + + if (customerId) { + query = query.eq('customer_id', customerId) + } + + if (locationId) { + query = query.eq('location_id', locationId) + } + + if (status) { + query = query.eq('status', status) + } + + if (!shortId) { + query = query.order('start_time_utc', { ascending: false }) + } + + const { data: bookings, error } = shortId + ? await query.eq('short_id', shortId).single() + : await query + + if (error) { + console.error('Bookings GET error:', error) + return NextResponse.json( + { error: error.message }, + { status: 400 } + ) + } + + if (shortId && (!bookings || (Array.isArray(bookings) && bookings.length === 0))) { + return NextResponse.json( + { error: 'Booking not found' }, + { status: 404 } + ) + } + + return NextResponse.json({ + success: true, + bookings: shortId ? [bookings] : (bookings || []) + }) + } catch (error) { + console.error('Bookings GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/lib/utils/short-id.ts b/lib/utils/short-id.ts new file mode 100644 index 0000000..6596718 --- /dev/null +++ b/lib/utils/short-id.ts @@ -0,0 +1,11 @@ +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function generateShortId(): Promise { + const { data, error } = await supabaseAdmin.rpc('generate_short_id') + + if (error) { + throw new Error(`Failed to generate short_id: ${error.message}`) + } + + return data as string +}