diff --git a/app/api/bookings/[id]/route.ts b/app/api/bookings/[id]/route.ts new file mode 100644 index 0000000..4ad34b2 --- /dev/null +++ b/app/api/bookings/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const bookingId = params.id + const body = await request.json() + const { status } = body + + if (!status) { + return NextResponse.json( + { error: 'Missing required field: status' }, + { status: 400 } + ) + } + + const validStatuses = ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'] + if (!validStatuses.includes(status)) { + return NextResponse.json( + { error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }, + { status: 400 } + ) + } + + const { data: booking, error: updateError } = await supabaseAdmin + .from('bookings') + .update({ status }) + .eq('id', bookingId) + .select() + .single() + + if (updateError || !booking) { + return NextResponse.json( + { error: updateError?.message || 'Failed to update booking' }, + { status: 400 } + ) + } + + return NextResponse.json({ + success: true, + booking + }) + } catch (error) { + console.error('Update booking error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/hq/page.tsx b/app/hq/page.tsx new file mode 100644 index 0000000..a9f3e92 --- /dev/null +++ b/app/hq/page.tsx @@ -0,0 +1,403 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Badge } from '@/components/ui/badge' +import { format } from 'date-fns' +import { Calendar, Clock, MapPin, Users, CheckCircle, XCircle, AlertCircle } from 'lucide-react' + +interface Booking { + id: string + short_id: string + status: string + start_time_utc: string + end_time_utc: string + notes: string | null + is_paid: boolean + customer: { + id: string + email: string + first_name: string | null + last_name: string | null + phone: string | null + } + service: { + id: string + name: string + duration_minutes: number + base_price: number + } + resource?: { + id: string + name: string + type: string + } + staff: { + id: string + display_name: string + } + location: { + id: string + name: string + } +} + +interface Location { + id: string + name: string +} + +interface Staff { + staff_id: string + staff_name: string + role: string + work_hours_start: string | null + work_hours_end: string | null + work_days: string | null + location_id: string +} + +export default function HQDashboard() { + const [locations, setLocations] = useState([]) + const [staffList, setStaffList] = useState([]) + const [bookings, setBookings] = useState([]) + const [selectedLocation, setSelectedLocation] = useState('') + const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd')) + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState<'calendar' | 'bookings' | 'staff'>('calendar') + + useEffect(() => { + fetchLocations() + }, []) + + useEffect(() => { + if (selectedLocation) { + fetchBookings() + fetchStaff() + } + }, [selectedLocation, selectedDate]) + + const fetchLocations = async () => { + try { + const response = await fetch('/api/admin/locations', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('admin_enrollment_key') || ''}` + } + }) + const data = await response.json() + if (data.locations) { + setLocations(data.locations) + if (data.locations.length > 0) { + setSelectedLocation(data.locations[0].id) + } + } + } catch (error) { + console.error('Failed to fetch locations:', error) + } + } + + const fetchBookings = async () => { + if (!selectedLocation) return + + setLoading(true) + try { + const startDate = new Date(selectedDate).toISOString() + const endDate = new Date(selectedDate) + endDate.setDate(endDate.getDate() + 1) + + const response = await fetch(`/api/bookings?location_id=${selectedLocation}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('admin_enrollment_key') || ''}` + } + }) + const data = await response.json() + if (data.bookings) { + const filtered = data.bookings.filter((b: Booking) => { + const bookingDate = new Date(b.start_time_utc) + return bookingDate >= new Date(startDate) && bookingDate < new Date(endDate) + }) + setBookings(filtered) + } + } catch (error) { + console.error('Failed to fetch bookings:', error) + } finally { + setLoading(false) + } + } + + const fetchStaff = async () => { + if (!selectedLocation) return + + try { + const startTime = new Date(selectedDate + 'T00:00:00.000Z').toISOString() + const endTime = new Date(selectedDate + 'T23:59:59.999Z').toISOString() + + const response = await fetch(`/api/availability/staff?location_id=${selectedLocation}&start_time_utc=${startTime}&end_time_utc=${endTime}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('admin_enrollment_key') || ''}` + } + }) + const data = await response.json() + if (data.staff) { + setStaffList(data.staff) + } + } catch (error) { + console.error('Failed to fetch staff:', error) + } + } + + const updateBookingStatus = async (bookingId: string, newStatus: string) => { + try { + const response = await fetch(`/api/bookings/${bookingId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('admin_enrollment_key') || ''}` + }, + body: JSON.stringify({ status: newStatus }) + }) + + if (response.ok) { + setBookings(bookings.map(b => + b.id === bookingId ? { ...b, status: newStatus } : b + )) + } + } catch (error) { + console.error('Failed to update booking:', error) + } + } + + const getStatusBadge = (status: string) => { + const config = { + pending: { icon: AlertCircle, label: 'Pending', color: 'bg-yellow-100 text-yellow-800' }, + confirmed: { icon: Clock, label: 'Confirmed', color: 'bg-blue-100 text-blue-800' }, + completed: { icon: CheckCircle, label: 'Completed', color: 'bg-green-100 text-green-800' }, + cancelled: { icon: XCircle, label: 'Cancelled', color: 'bg-red-100 text-red-800' }, + no_show: { icon: XCircle, label: 'No Show', color: 'bg-gray-100 text-gray-800' } + } + + const { icon: Icon, label, color } = config[status as keyof typeof config] || config.pending + + return ( + + + {label} + + ) + } + + return ( +
+
+
+

+ HQ Dashboard +

+

+ Operations management and scheduling +

+
+ +
+
+
+ + +
+ +
+ + setSelectedDate(e.target.value)} + /> +
+
+
+ + setActiveTab(v as any)} className="space-y-6"> + + Calendar + Bookings + Staff + + + + + + Daily Schedule + + {loading ? 'Loading...' : `${bookings.length} bookings for ${format(new Date(selectedDate), 'PPP')}`} + + + +
+ {bookings.length === 0 ? ( +
+ No bookings for this date +
+ ) : ( + bookings + .sort((a, b) => new Date(a.start_time_utc).getTime() - new Date(b.start_time_utc).getTime()) + .map((booking) => ( +
+
+
+
+ {getStatusBadge(booking.status)} + + {format(new Date(booking.start_time_utc), 'HH:mm')} - {format(new Date(booking.end_time_utc), 'HH:mm')} + +
+

{booking.service.name}

+

+ {booking.customer.first_name} {booking.customer.last_name} ({booking.customer.email}) +

+
+
+ + {booking.staff.display_name} +
+
+ + {booking.resource?.name || 'Not assigned'} +
+
+ {booking.notes && ( +

+ Note: {booking.notes} +

+ )} +
+
+

+ ${booking.service.base_price.toFixed(2)} +

+

+ {booking.service.duration_minutes} min +

+
+
+
+ )) + )} +
+
+
+
+ + + + + Bookings List + + All bookings for selected date + + + +
+ {bookings.length === 0 ? ( +
+ No bookings found +
+ ) : ( + bookings.map((booking) => ( +
+
+
+
+ + {booking.short_id} + + {getStatusBadge(booking.status)} +
+

{booking.service.name}

+

+ {booking.customer.first_name} {booking.customer.last_name} +

+
+
+

+ {format(new Date(booking.start_time_utc), 'HH:mm')} +

+

+ {booking.staff.display_name} +

+
+
+
+ )) + )} +
+
+
+
+ + + + + Staff Availability + + Available staff for selected date + + + +
+ {staffList.length === 0 ? ( +
+ No staff available +
+ ) : ( + staffList.map((staff) => ( +
+
+
+
+

{staff.staff_name}

+ + {staff.role} + +
+
+
+ + {staff.work_hours_start || 'N/A'} - {staff.work_hours_end || 'N/A'} +
+
+
+
+ + Available + +
+
+
+ )) + )} +
+
+
+
+
+
+
+ ) +}