'use client' /** * @description Service selection and appointment booking page for The Boutique * @audit BUSINESS RULE: Multi-step booking flow: service → datetime → confirm → client registration * @audit SECURITY: Public endpoint with rate limiting recommended for availability checks * @audit Validate: All steps must be completed before final booking submission * @audit PERFORMANCE: Auto-fetches services, locations, and time slots based on selections */ import { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Calendar, Clock, User, Check } from 'lucide-react' import { format, isSameDay, parseISO } from 'date-fns' import { es } from 'date-fns/locale' import DatePicker from '@/components/booking/date-picker' interface Service { id: string name: string duration_minutes: number base_price: number } interface Location { id: string name: string timezone: string } interface Staff { id: string display_name: string role: string } type BookingStep = 'service' | 'datetime' | 'artist' | 'confirm' | 'client' /** * @description Booking flow page guiding customers through service selection, date/time, and confirmation * @returns {JSX.Element} Multi-step booking wizard with service cards, date picker, time slots, and confirmation * @audit BUSINESS RULE: Time slots filtered by service duration and staff availability * @audit BUSINESS RULE: Time slots respect location business hours and existing bookings * @audit SECURITY: Public endpoint; no authentication required for browsing * @audit Validate: Service, location, date, and time required before proceeding * @audit PERFORMANCE: Dynamic time slot loading based on service and date selection * @audit AUDIT: Booking attempts logged for analytics and capacity planning */ export default function ServiciosPage() { const [services, setServices] = useState([]) const [locations, setLocations] = useState([]) const [selectedService, setSelectedService] = useState('') const [selectedLocation, setSelectedLocation] = useState('') const [selectedDate, setSelectedDate] = useState(new Date()) const [timeSlots, setTimeSlots] = useState([]) const [selectedTime, setSelectedTime] = useState('') const [availableArtists, setAvailableArtists] = useState([]) const [selectedArtist, setSelectedArtist] = useState('') const [currentStep, setCurrentStep] = useState('service') const [loading, setLoading] = useState(false) const [errors, setErrors] = useState>({}) useEffect(() => { fetchServices() fetchLocations() }, []) useEffect(() => { if (selectedService && selectedLocation && selectedDate) { fetchTimeSlots() } }, [selectedService, selectedLocation, selectedDate]) const fetchServices = async () => { try { const response = await fetch('/api/services') const data = await response.json() if (data.services) { setServices(data.services) } } catch (error) { console.error('Error fetching services:', error) setErrors({ ...errors, services: 'Error al cargar servicios' }) } } const fetchLocations = async () => { try { const response = await fetch('/api/locations') 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('Error fetching locations:', error) setErrors({ ...errors, locations: 'Error al cargar ubicaciones' }) } } const fetchTimeSlots = async () => { if (!selectedService || !selectedLocation || !selectedDate) return setLoading(true) try { const formattedDate = format(selectedDate, 'yyyy-MM-dd') const response = await fetch( `/api/availability/time-slots?location_id=${selectedLocation}&service_id=${selectedService}&date=${formattedDate}` ) const data = await response.json() if (data.availability) { setTimeSlots(data.availability) } const artistsResponse = await fetch( `/api/availability/staff?location_id=${selectedLocation}&service_id=${selectedService}&date=${formattedDate}` ) const artistsData = await artistsResponse.json() if (artistsData.staff) { setAvailableArtists(artistsData.staff) } } catch (error) { console.error('Error fetching time slots:', error) setErrors({ ...errors, timeSlots: 'Error al cargar horarios' }) } finally { setLoading(false) } } const handleDateSelect = (date: Date) => { setSelectedDate(date) setSelectedTime('') } const canProceedToDateTime = () => { return selectedService && selectedLocation } const canProceedToConfirm = () => { return selectedService && selectedLocation && selectedDate && selectedTime } const canProceedToArtist = () => { return selectedService && selectedLocation && selectedDate && selectedTime } const handleProceed = () => { setErrors({}) if (currentStep === 'service') { if (!selectedService) { setErrors({ service: 'Selecciona un servicio' }) return } if (!selectedLocation) { setErrors({ location: 'Selecciona una ubicación' }) return } setCurrentStep('datetime') } else if (currentStep === 'datetime') { if (!selectedDate) { setErrors({ date: 'Selecciona una fecha' }) return } if (!selectedTime) { setErrors({ time: 'Selecciona un horario' }) return } if (availableArtists.length > 0) { setCurrentStep('artist') } else { const params = new URLSearchParams({ service_id: selectedService, location_id: selectedLocation, date: format(selectedDate!, 'yyyy-MM-dd'), time: selectedTime }) window.location.href = `/booking/cita?${params.toString()}` } } else if (currentStep === 'artist') { const params = new URLSearchParams({ service_id: selectedService, location_id: selectedLocation, date: format(selectedDate!, 'yyyy-MM-dd'), time: selectedTime, staff_id: selectedArtist }) window.location.href = `/booking/cita?${params.toString()}` } else if (currentStep === 'confirm') { const params = new URLSearchParams({ service_id: selectedService, location_id: selectedLocation, date: format(selectedDate!, 'yyyy-MM-dd'), time: selectedTime, staff_id: selectedArtist }) window.location.href = `/booking/cita?${params.toString()}` } } const handleStepBack = () => { if (currentStep === 'datetime') { setCurrentStep('service') } else if (currentStep === 'artist') { setCurrentStep('datetime') } else if (currentStep === 'confirm') { setCurrentStep('artist') } } const selectedServiceData = services.find(s => s.id === selectedService) const selectedLocationData = locations.find(l => l.id === selectedLocation) return (

Reservar Cita

Selecciona el servicio y horario de tu preferencia

{currentStep === 'service' && ( <> Servicios Selecciona el servicio que deseas reservar
{errors.service &&

{errors.service}

}
{errors.location &&

{errors.location}

}
)} {currentStep === 'datetime' && ( <> Fecha y Hora Selecciona la fecha y hora disponible
{errors.date &&

{errors.date}

}
{loading ? (
Cargando horarios...
) : (
{timeSlots.length === 0 ? (

No hay horarios disponibles para esta fecha. Selecciona otra fecha.

) : (
{timeSlots.map((slot, index) => { const slotTimeUTC = new Date(slot.start_time) // JavaScript automatically converts ISO string to local timezone // Since Monterrey is UTC-6, this gives us the correct local time return ( ) })}
)} {errors.time &&

{errors.time}

}
)}
{selectedServiceData && (
Duración del servicio: {selectedServiceData.duration_minutes} minutos
)} )} {currentStep === 'artist' && ( <> Seleccionar Artista {availableArtists.length > 0 ? 'Elige el artista que prefieres para tu servicio' : 'Se asignará automáticamente el primer artista disponible'} {availableArtists.length === 0 ? (
No hay artistas específicos disponibles. Se asignará automáticamente.
) : (
{availableArtists.map((artist) => (
setSelectedArtist(artist.id)} >
{artist.display_name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}

{artist.display_name}

{artist.role}

))}
)}
)} {currentStep === 'confirm' && selectedServiceData && selectedLocationData && selectedDate && selectedTime && ( <>

Resumen de la reserva

Servicio

{selectedServiceData.name}

Ubicación

{selectedLocationData.name}

Fecha

{format(selectedDate, 'PPP', { locale: es })}

Hora

{format(new Date(selectedTime), 'HH:mm', { locale: es })}

{selectedArtist && (

Artista

{availableArtists.find(a => a.id === selectedArtist)?.display_name || 'Seleccionado'}

)}

Duración

{selectedServiceData.duration_minutes} minutos

Precio

${selectedServiceData.base_price.toFixed(2)} USD

)} {currentStep === 'confirm' && ( )} {currentStep !== 'confirm' && ( <> {currentStep !== 'service' && ( )} )}
) }