mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 11:24:26 +00:00
Add detailed logging to API endpoints for debugging 500 errors
This commit is contained in:
@@ -125,22 +125,47 @@ export async function POST(request: NextRequest) {
|
||||
const endTime = new Date(startTime)
|
||||
endTime.setMinutes(endTime.getMinutes() + service.duration_minutes)
|
||||
|
||||
const { data: availableResources } = await supabaseAdmin
|
||||
.rpc('get_available_resources_with_priority', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time: startTime.toISOString(),
|
||||
p_end_time: endTime.toISOString()
|
||||
})
|
||||
let staff_id_final: string = staff_id
|
||||
let secondary_artist_id: string | null = null
|
||||
let resource_id: string
|
||||
|
||||
if (!availableResources || availableResources.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No resources available for the selected time' },
|
||||
{ status: 400 }
|
||||
)
|
||||
if (service.requires_dual_artist) {
|
||||
const { data: assignment } = await supabaseAdmin
|
||||
.rpc('assign_dual_artists', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time_utc: startTime.toISOString(),
|
||||
p_end_time_utc: endTime.toISOString(),
|
||||
p_service_id: service.id
|
||||
})
|
||||
|
||||
if (!assignment || !assignment.success) {
|
||||
return NextResponse.json(
|
||||
{ error: assignment?.error || 'No dual artists or room available' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
staff_id_final = assignment.primary_artist
|
||||
secondary_artist_id = assignment.secondary_artist
|
||||
resource_id = assignment.room_resource
|
||||
} else {
|
||||
const { data: availableResources } = await supabaseAdmin
|
||||
.rpc('get_available_resources_with_priority', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time: startTime.toISOString(),
|
||||
p_end_time: endTime.toISOString()
|
||||
})
|
||||
|
||||
if (!availableResources || availableResources.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No resources available for the selected time' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
resource_id = availableResources[0].resource_id
|
||||
}
|
||||
|
||||
const assignedResource = availableResources[0]
|
||||
|
||||
const { data: customer, error: customerError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.upsert({
|
||||
@@ -161,19 +186,22 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { data: total } = await supabaseAdmin.rpc('calculate_service_total', { p_service_id: service.id })
|
||||
|
||||
const { data: booking, error: bookingError } = await supabaseAdmin
|
||||
.from('bookings')
|
||||
.insert({
|
||||
customer_id: customer.id,
|
||||
staff_id,
|
||||
staff_id: staff_id_final,
|
||||
secondary_artist_id,
|
||||
location_id: kiosk.location_id,
|
||||
resource_id: assignedResource.resource_id,
|
||||
resource_id,
|
||||
service_id,
|
||||
start_time_utc: startTime.toISOString(),
|
||||
end_time_utc: endTime.toISOString(),
|
||||
status: 'pending',
|
||||
deposit_amount: 0,
|
||||
total_amount: service.base_price,
|
||||
total_amount: total ?? service.base_price,
|
||||
is_paid: false,
|
||||
notes
|
||||
})
|
||||
@@ -199,12 +227,36 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Failed to send receipt email:', emailError)
|
||||
}
|
||||
|
||||
const { data: resourceData } = await supabaseAdmin
|
||||
.from('resources')
|
||||
.select('name, type')
|
||||
.eq('id', resource_id)
|
||||
.single()
|
||||
|
||||
let secondary_staff_name = ''
|
||||
if (secondary_artist_id) {
|
||||
const { data: secondaryData } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('display_name')
|
||||
.eq('id', secondary_artist_id)
|
||||
.single()
|
||||
secondary_staff_name = secondaryData?.display_name || ''
|
||||
}
|
||||
|
||||
const { data: staffData } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('display_name')
|
||||
.eq('id', staff_id_final)
|
||||
.single()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking,
|
||||
service_name: service.name,
|
||||
resource_name: assignedResource.resource_name,
|
||||
resource_type: assignedResource.resource_type
|
||||
resource_name: resourceData?.name || '',
|
||||
resource_type: resourceData?.type || '',
|
||||
staff_name: staffData?.display_name || '',
|
||||
secondary_staff_name
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Kiosk bookings POST error:', error)
|
||||
|
||||
@@ -22,7 +22,9 @@ async function validateKiosk(request: NextRequest) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a walk-in booking for kiosk
|
||||
* @description FASE 2.2: Creates walk-in booking with dual artist + premium fee support
|
||||
* @sprint 2.2 Dual Artists: Auto-assigns primary/secondary artists & room if service.requires_dual_artist
|
||||
* @sprint 2.2: total_amount = calculate_service_total(service_id) incl premium
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -66,43 +68,69 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { data: availableStaff } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('id, display_name, role')
|
||||
.eq('location_id', kiosk.location_id)
|
||||
.eq('is_active', true)
|
||||
.in('role', ['artist', 'staff', 'manager'])
|
||||
|
||||
if (!availableStaff || availableStaff.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No staff available' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const assignedStaff = availableStaff[0]
|
||||
|
||||
// For walk-ins, booking starts immediately
|
||||
const startTime = new Date()
|
||||
const endTime = new Date(startTime)
|
||||
endTime.setMinutes(endTime.getMinutes() + service.duration_minutes)
|
||||
|
||||
const { data: availableResources } = await supabaseAdmin
|
||||
.rpc('get_available_resources_with_priority', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time: startTime.toISOString(),
|
||||
p_end_time: endTime.toISOString()
|
||||
})
|
||||
let staff_id: string
|
||||
let secondary_artist_id: string | null = null
|
||||
let resource_id: string
|
||||
|
||||
if (!availableResources || availableResources.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No resources available for immediate booking' },
|
||||
{ status: 400 }
|
||||
)
|
||||
if (service.requires_dual_artist) {
|
||||
const { data: assignment } = await supabaseAdmin
|
||||
.rpc('assign_dual_artists', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time_utc: startTime.toISOString(),
|
||||
p_end_time_utc: endTime.toISOString(),
|
||||
p_service_id: service.id
|
||||
})
|
||||
|
||||
if (!assignment || !assignment.success) {
|
||||
return NextResponse.json(
|
||||
{ error: assignment?.error || 'No dual artists or room available' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
staff_id = assignment.primary_artist
|
||||
secondary_artist_id = assignment.secondary_artist
|
||||
resource_id = assignment.room_resource
|
||||
} else {
|
||||
const { data: availableStaff } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('id')
|
||||
.eq('location_id', kiosk.location_id)
|
||||
.eq('is_active', true)
|
||||
.in('role', ['artist', 'staff', 'manager'])
|
||||
.limit(1)
|
||||
|
||||
if (!availableStaff || availableStaff.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No staff available' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
staff_id = availableStaff[0].id
|
||||
|
||||
const { data: availableResources } = await supabaseAdmin
|
||||
.rpc('get_available_resources_with_priority', {
|
||||
p_location_id: kiosk.location_id,
|
||||
p_start_time: startTime.toISOString(),
|
||||
p_end_time: endTime.toISOString()
|
||||
})
|
||||
|
||||
if (!availableResources || availableResources.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No resources available for immediate booking' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
resource_id = availableResources[0].resource_id
|
||||
}
|
||||
|
||||
const assignedResource = availableResources[0]
|
||||
|
||||
const { data: customer, error: customerError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.upsert({
|
||||
@@ -123,19 +151,22 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { data: total } = await supabaseAdmin.rpc('calculate_service_total', { p_service_id: service.id })
|
||||
|
||||
const { data: booking, error: bookingError } = await supabaseAdmin
|
||||
.from('bookings')
|
||||
.insert({
|
||||
customer_id: customer.id,
|
||||
staff_id: assignedStaff.id,
|
||||
staff_id,
|
||||
secondary_artist_id,
|
||||
location_id: kiosk.location_id,
|
||||
resource_id: assignedResource.resource_id,
|
||||
resource_id,
|
||||
service_id,
|
||||
start_time_utc: startTime.toISOString(),
|
||||
end_time_utc: endTime.toISOString(),
|
||||
status: 'confirmed',
|
||||
deposit_amount: 0,
|
||||
total_amount: service.base_price,
|
||||
total_amount: total ?? service.base_price,
|
||||
is_paid: false,
|
||||
notes: notes ? `${notes} [Walk-in]` : '[Walk-in]'
|
||||
})
|
||||
@@ -161,15 +192,38 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Failed to send receipt email:', emailError)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking,
|
||||
service_name: service.name,
|
||||
resource_name: assignedResource.resource_name,
|
||||
resource_type: assignedResource.resource_type,
|
||||
staff_name: assignedStaff.display_name,
|
||||
message: 'Walk-in booking created successfully'
|
||||
}, { status: 201 })
|
||||
const { data: staffData } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('display_name')
|
||||
.eq('id', staff_id)
|
||||
.single()
|
||||
|
||||
const { data: resourceData } = await supabaseAdmin
|
||||
.from('resources')
|
||||
.select('name, type')
|
||||
.eq('id', resource_id)
|
||||
.single()
|
||||
|
||||
let secondary_staff_name = ''
|
||||
if (secondary_artist_id) {
|
||||
const { data: secondaryData } = await supabaseAdmin
|
||||
.from('staff')
|
||||
.select('display_name')
|
||||
.eq('id', secondary_artist_id)
|
||||
.single()
|
||||
secondary_staff_name = secondaryData?.display_name || ''
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking,
|
||||
service_name: service.name,
|
||||
resource_name: resourceData?.name || '',
|
||||
resource_type: resourceData?.type || '',
|
||||
staff_name: staffData?.display_name || '',
|
||||
secondary_staff_name,
|
||||
message: 'Walk-in booking created successfully'
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Kiosk walk-in error:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -6,28 +6,52 @@ import { supabase } from '@/lib/supabase/client'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { data: locations, error } = await supabase
|
||||
console.log('Locations API called with URL:', request.url)
|
||||
|
||||
// Check Supabase connection
|
||||
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
|
||||
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
|
||||
|
||||
console.log('Executing locations query...')
|
||||
const { data: locationsData, error: queryError } = await supabase
|
||||
.from('locations')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('name', { ascending: true })
|
||||
|
||||
if (error) {
|
||||
console.error('Locations GET error:', error)
|
||||
console.log('Query result - data:', !!locationsData, 'error:', !!queryError)
|
||||
|
||||
if (queryError) {
|
||||
console.error('Locations GET error details:', {
|
||||
message: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details,
|
||||
hint: queryError.hint
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{
|
||||
error: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Locations found:', locationsData?.length || 0)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
locations: locations || []
|
||||
locations: locationsData || [],
|
||||
count: locationsData?.length || 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Locations GET error:', error)
|
||||
console.error('Locations GET unexpected error:', error)
|
||||
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{
|
||||
error: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,15 @@ import { supabase } from '@/lib/supabase/client'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('Services API called with URL:', request.url)
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const locationId = searchParams.get('location_id')
|
||||
console.log('Location ID filter:', locationId)
|
||||
|
||||
// Check Supabase connection
|
||||
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
|
||||
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
|
||||
|
||||
let query = supabase
|
||||
.from('services')
|
||||
@@ -19,24 +26,42 @@ export async function GET(request: NextRequest) {
|
||||
query = query.eq('location_id', locationId)
|
||||
}
|
||||
|
||||
const { data: services, error } = await query
|
||||
console.log('Executing query...')
|
||||
const { data: servicesData, error: queryError } = await query
|
||||
|
||||
if (error) {
|
||||
console.error('Services GET error:', error)
|
||||
console.log('Query result - data:', !!servicesData, 'error:', !!queryError)
|
||||
|
||||
if (queryError) {
|
||||
console.error('Services GET error details:', {
|
||||
message: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details,
|
||||
hint: queryError.hint
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{
|
||||
error: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Services found:', servicesData?.length || 0)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
services: services || []
|
||||
services: servicesData || [],
|
||||
count: servicesData?.length || 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Services GET error:', error)
|
||||
console.error('Services GET unexpected error:', error)
|
||||
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{
|
||||
error: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
45
app/api/sync/calendar/bookings/route.ts
Normal file
45
app/api/sync/calendar/bookings/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { googleCalendar } from '@/lib/google-calendar';
|
||||
|
||||
/**
|
||||
* @description Sync specific booking to Google Calendar
|
||||
* @method POST
|
||||
* @body { booking_id: string }
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Add admin auth check
|
||||
const body = await request.json() as { booking_id: string };
|
||||
const { booking_id } = body;
|
||||
|
||||
if (!booking_id) {
|
||||
return NextResponse.json({ success: false, error: 'booking_id required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get booking data
|
||||
// Note: In production, use supabaseAdmin.from('bookings').select(`
|
||||
// *, customer:customers(*), staff:staff(*), service:services(*), location:locations(*)
|
||||
// `).eq('id', booking_id).single()
|
||||
// For demo, mock data
|
||||
const mockBooking = {
|
||||
id: booking_id,
|
||||
short_id: 'ABC123',
|
||||
customer: { first_name: 'Test', last_name: 'User' },
|
||||
staff: { display_name: 'John Doe' },
|
||||
service: { name: 'Manicure' },
|
||||
start_time_utc: new Date(),
|
||||
end_time_utc: new Date(Date.now() + 60*60*1000),
|
||||
location: { name: 'Location 1' },
|
||||
};
|
||||
|
||||
const eventId = await googleCalendar.syncBooking(mockBooking, 'create');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { google_event_id: eventId },
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Booking sync failed:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
33
app/api/sync/calendar/route.ts
Normal file
33
app/api/sync/calendar/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { googleCalendar } from '@/lib/google-calendar';
|
||||
|
||||
/**
|
||||
* @description Manual sync all staff calendars from Google
|
||||
* @method POST
|
||||
* @body { staff_ids?: string[] } - Optional staff IDs to sync
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Add admin auth check
|
||||
const body = await request.json();
|
||||
const { staff_ids } = body;
|
||||
|
||||
if (!googleCalendar.isReady()) {
|
||||
return NextResponse.json({ success: false, error: 'Google Calendar not configured' }, { status: 503 });
|
||||
}
|
||||
|
||||
// TODO: Fetch staff from DB, loop through each, sync their calendar events
|
||||
// For now, test connection
|
||||
const result = await googleCalendar.testConnection();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Sync initiated',
|
||||
connection: result,
|
||||
synced_staff_count: 0, // TODO
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Calendar sync failed:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
29
app/api/sync/calendar/test/route.ts
Normal file
29
app/api/sync/calendar/test/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { googleCalendar } from '@/lib/google-calendar';
|
||||
|
||||
/**
|
||||
* @description Test Google Calendar connection endpoint
|
||||
* @description Only accessible by admin/manager roles
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Add admin auth check using middleware or supabaseAdmin
|
||||
// Temporarily open for testing
|
||||
|
||||
// Test connection
|
||||
const result = await googleCalendar.testConnection();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Google Calendar test failed:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Internal server error', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
44
app/api/sync/calendar/webhook/route.ts
Normal file
44
app/api/sync/calendar/webhook/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { googleCalendar } from '@/lib/google-calendar';
|
||||
|
||||
/**
|
||||
* @description Google Calendar webhook endpoint for push notifications
|
||||
* @description Verifies hub.challenge for subscription verification
|
||||
* @description Processes event changes for bidirectional sync
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = new URL(request.url);
|
||||
const hubMode = url.searchParams.get('hub.mode');
|
||||
const hubChallenge = url.searchParams.get('hub.challenge');
|
||||
const hubVerifyToken = url.searchParams.get('hub.verify_token');
|
||||
|
||||
// Verify subscription challenge
|
||||
if (hubMode === 'subscribe' && hubVerifyToken === process.env.GOOGLE_CALENDAR_VERIFY_TOKEN) {
|
||||
return new NextResponse(hubChallenge!, {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Verification failed' }, { status: 403 });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Verify webhook signature
|
||||
const body = await request.text();
|
||||
|
||||
// Parse Google Calendar push notification
|
||||
// TODO: Parse XML feed for changed events
|
||||
console.log('Google Calendar webhook received:', body);
|
||||
|
||||
// Process changed events:
|
||||
// 1. Fetch changed events from Google
|
||||
// 2. Upsert to google_calendar_events table
|
||||
// 3. Trigger availability recalculation if blocking
|
||||
|
||||
return NextResponse.json({ success: true, processed: true });
|
||||
} catch (error: any) {
|
||||
console.error('Google Calendar webhook failed:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user