mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 14:24:27 +00:00
feat: Implementar API de reservas para clientes
- Crear route /api/bookings con POST y GET - Validar disponibilidad de staff y recursos - Crear cliente si no existe - Generar short_id para reservas - Utilizar RPC functions de disponibilidad - Agregar utilidad generateShortId
This commit is contained in:
314
app/api/bookings/route.ts
Normal file
314
app/api/bookings/route.ts
Normal file
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/utils/short-id.ts
Normal file
11
lib/utils/short-id.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { supabaseAdmin } from '@/lib/supabase/client'
|
||||||
|
|
||||||
|
export async function generateShortId(): Promise<string> {
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user