'use client'; import { useState, useEffect, use } from 'react'; import { Calendar, Clock, User, Phone, Mail, ChevronLeft, ChevronRight, Check, Scissors, MapPin, Star, Loader } from 'lucide-react'; import { showToast } from '@/lib/toast'; import ToastContainer from '@/components/Toast'; export default function PublicBookingPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = use(params); const [step, setStep] = useState(1); const [selectedService, setSelectedService] = useState(null); const [selectedProfessional, setSelectedProfessional] = useState(null); const [selectedDate, setSelectedDate] = useState(''); const [selectedTime, setSelectedTime] = useState(null); const [clientName, setClientName] = useState(''); const [clientPhone, setClientPhone] = useState(''); const [clientEmail, setClientEmail] = useState(''); const [booked, setBooked] = useState(false); const [slots, setSlots] = useState([]); const [loadingDb, setLoadingDb] = useState(true); const [dbServices, setDbServices] = useState([]); const [dbProfessionals, setDbProfessionals] = useState([]); const [activeBookingInfo, setActiveBookingInfo] = useState(null); // Dados do tenant vindos do banco const [establishment, setEstablishment] = useState({ name: '', type: 'barbearia', description: '', phone: '', address: '', rating: 5.0, reviews: 0, allowOnlineBooking: true, bookingAdvanceMin: 60, bookingAdvanceMaxDays: 30 }); const [profileSchedules, setProfileSchedules] = useState({ dailySchedules: null, profSchedules: {}, interval: 30, startTime: '09:00', endTime: '19:00' }); // Algoritmo dinâmico de fatiamento de grade multi-turno const generateSlotsForShifts = (shifts: { start: string; end: string }[], intervalMin: number) => { const generated: string[] = []; for (const shift of shifts) { if (!shift.start || !shift.end) continue; const [sh, sm] = shift.start.split(':').map(Number); const [eh, em] = shift.end.split(':').map(Number); let currentMinutes = sh * 60 + sm; const endMinutes = eh * 60 + em; while (currentMinutes < endMinutes) { const h = Math.floor(currentMinutes / 60); const m = currentMinutes % 60; const timeStr = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; if (!generated.includes(timeStr)) { generated.push(timeStr); } currentMinutes += intervalMin; } } return generated.sort(); }; const generateSlots = (startTimeStr: string, endTimeStr: string, intervalMin: number) => { return generateSlotsForShifts([{ start: startTimeStr, end: endTimeStr }], intervalMin); }; useEffect(() => { const fetchData = async () => { try { const [profileRes, agendaRes, servicesRes] = await Promise.all([ fetch(`/api/tenant/profile?slug=${slug}`), fetch(`/api/tenant/agenda?slug=${slug}`), fetch(`/api/tenant/services?slug=${slug}`) ]); const savedBooking = localStorage.getItem(`agendapro_active_booking_${slug}`); if (savedBooking) { try { const parsed = JSON.parse(savedBooking); const statusRes = await fetch(`/api/tenant/agenda/appointment?id=${parsed.id}`); if (statusRes.ok) { const apt = await statusRes.json(); if (apt.status === 'pending' || apt.status === 'confirmed') { setActiveBookingInfo(parsed); setBooked(true); } else { localStorage.removeItem(`agendapro_active_booking_${slug}`); } } else { localStorage.removeItem(`agendapro_active_booking_${slug}`); } } catch (e) { localStorage.removeItem(`agendapro_active_booking_${slug}`); } } // Carregar perfil do tenant if (profileRes.ok) { const profile = await profileRes.json(); const fullAddress = [profile.address, profile.city, profile.state].filter(Boolean).join(', '); setEstablishment({ name: profile.name || slug, type: profile.businessType || 'barbearia', description: profile.description || '', phone: profile.phone || '', address: fullAddress || '', rating: 5.0, reviews: 0, allowOnlineBooking: profile.allowOnlineBooking !== false, bookingAdvanceMin: profile.bookingAdvanceMin || 60, bookingAdvanceMaxDays: profile.bookingAdvanceMaxDays || 30, }); setProfileSchedules({ dailySchedules: profile.dailySchedules || null, profSchedules: profile.profSchedules || {}, interval: profile.interval || 30, startTime: profile.startTime || '09:00', endTime: profile.endTime || '19:00' }); // Slots iniciais setSlots(generateSlots(profile.startTime || '09:00', profile.endTime || '19:00', profile.interval || 30)); // Aplicar cor do tema if (profile.themeColor) { document.documentElement.style.setProperty('--accent', profile.themeColor); document.documentElement.style.setProperty('--accent-glow', `${profile.themeColor}26`); } } else { setSlots(generateSlots('09:00', '19:00', 30)); } if (agendaRes.ok) { const data = await agendaRes.json(); setDbProfessionals(data.professionals || []); } if (servicesRes.ok) { const data = await servicesRes.json(); setDbServices(data || []); } } catch (err) { console.error('Erro ao buscar dados do banco:', err); setSlots(generateSlots('09:00', '19:00', 30)); } finally { setLoadingDb(false); } }; fetchData(); }, [slug]); // Recalcular horários quando mudar data ou profissional useEffect(() => { if (!selectedDate) return; const dateObj = new Date(selectedDate + 'T12:00:00'); const dayOfWeek = dateObj.getDay(); const interval = profileSchedules.interval || 30; let dayConfig = null; if (selectedProfessional && profileSchedules.profSchedules?.[selectedProfessional]) { dayConfig = profileSchedules.profSchedules[selectedProfessional][dayOfWeek]; } if (!dayConfig && profileSchedules.dailySchedules) { dayConfig = profileSchedules.dailySchedules[dayOfWeek]; } if (dayConfig) { if (!dayConfig.active) { setSlots([]); return; } if (Array.isArray(dayConfig.shifts) && dayConfig.shifts.length > 0) { setSlots(generateSlotsForShifts(dayConfig.shifts, interval)); return; } } setSlots(generateSlots(profileSchedules.startTime || '09:00', profileSchedules.endTime || '19:00', interval)); }, [selectedDate, selectedProfessional, profileSchedules]); const today = new Date(); const maxDays = establishment.bookingAdvanceMaxDays || 30; const dates = Array.from({ length: maxDays }, (_, i) => { const d = new Date(today); d.setDate(d.getDate() + i); return d; }); const svc = dbServices.find(s => s.id === selectedService); const prof = dbProfessionals.find(p => p.id === selectedProfessional); const canNext = () => { if (step === 1) return !!selectedService; if (step === 2) return !!selectedProfessional; if (step === 3) return !!selectedDate && !!selectedTime; if (step === 4) return clientName.trim() && clientPhone.trim(); return false; }; const [showPhoneModal, setShowPhoneModal] = useState(false); const handleBook = async () => { if (!clientName.trim()) { showToast('Por favor, informe o seu nome completo.', 'warning'); return; } if (!clientPhone.trim()) { showToast('É necessário informar o seu telefone para confirmar o agendamento!', 'error'); setShowPhoneModal(true); return; } const [startHour, startMin] = selectedTime ? selectedTime.split(':').map(Number) : [9, 0]; try { const res = await fetch('/api/tenant/agenda', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug, professionalId: selectedProfessional || '1', client: clientName, phone: clientPhone, email: clientEmail, service: svc?.name || 'Serviço', price: svc?.price || 0, duration: svc?.duration || 30, startHour, startMin, status: 'pending', date: selectedDate, }) }); if (res.ok) { const aptData = await res.json(); localStorage.setItem(`agendapro_active_booking_${slug}`, JSON.stringify({ id: aptData.id, service: svc?.name, prof: prof?.name, date: selectedDate, time: selectedTime, price: svc?.price })); setBooked(true); } else { const errData = await res.json(); showToast(errData.error || 'Erro ao realizar o agendamento.', 'warning'); } } catch (e) { console.error(e); } }; const cancelBooking = async () => { if (!confirm('Tem certeza que deseja cancelar este agendamento?')) return; try { const idToCancel = activeBookingInfo?.id; if (!idToCancel) return; const res = await fetch('/api/tenant/agenda', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: idToCancel, status: 'cancelled' }) }); if (res.ok) { showToast('Agendamento cancelado com sucesso.', 'success'); localStorage.removeItem(`agendapro_active_booking_${slug}`); setActiveBookingInfo(null); setBooked(false); setStep(1); setSelectedTime(null); } } catch (e) { console.error(e); showToast('Erro ao cancelar agendamento.', 'error'); } }; if (booked) { const renderSvcName = activeBookingInfo?.service || svc?.name; const renderProfName = activeBookingInfo?.prof || prof?.name; const renderDate = activeBookingInfo?.date || selectedDate; const renderTime = activeBookingInfo?.time || selectedTime; const renderPrice = activeBookingInfo?.price !== undefined ? activeBookingInfo.price : svc?.price; return (

Agendamento Confirmado! 🎉

Seu horário está reservado e ativo.

Serviço
{renderSvcName}
Profissional
{renderProfName}
Data
{renderDate ? new Date(renderDate + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }) : ''}
Horário
{renderTime}
Total R$ {renderPrice?.toFixed(2)}

Você não pode realizar outro agendamento enquanto este estiver ativo.

); } if (!establishment.allowOnlineBooking && !booked) { return (

Agendamento Indisponível

O agendamento online está temporariamente desativado para este estabelecimento. Por favor, entre em contato diretamente pelo WhatsApp ou telefone.

); } return (
{/* HEADER */}
✂️

{establishment.name}

{establishment.address}

{establishment.rating} ({establishment.reviews} avaliações)
{/* STEPPER */}
{['Serviço', 'Profissional', 'Data e Hora', 'Seus Dados'].map((label, i) => (
i + 1 ? 'var(--success)' : step === i + 1 ? 'var(--accent)' : 'var(--bg-elevated)', color: step >= i + 1 ? 'white' : 'var(--text-muted)', transition: 'all 0.3s' }}> {step > i + 1 ? : i + 1}
{label} {i < 3 &&
i + 1 ? 'var(--success)' : 'var(--bg-elevated)', borderRadius: '1px' }} />}
))}
{/* STEP 1: SELECT SERVICE */} {step === 1 && (

Escolha o serviço

{dbServices.map(s => (
setSelectedService(s.id)} style={{ padding: '18px 20px', borderRadius: 'var(--radius-md)', cursor: 'pointer', background: selectedService === s.id ? 'var(--accent-glow)' : 'var(--bg-card)', border: `2px solid ${selectedService === s.id ? 'var(--accent)' : 'var(--border-color)'}`, transition: 'all 0.2s', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{s.name}
{s.desc || s.description || 'Serviço profissional'}
{s.duration} min
R$ {Number(s.price).toFixed(2)}
))}
)} {/* STEP 2: SELECT PROFESSIONAL */} {step === 2 && (

Escolha o profissional

{dbProfessionals.map(p => (
setSelectedProfessional(p.id)} style={{ padding: '20px', borderRadius: 'var(--radius-md)', cursor: 'pointer', background: selectedProfessional === p.id ? 'var(--accent-glow)' : 'var(--bg-card)', border: `2px solid ${selectedProfessional === p.id ? 'var(--accent)' : 'var(--border-color)'}`, transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: '16px' }}>
{p.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
{p.name}
{p.role || 'Barbeiro/Profissional'}
{p.rating || '5.0'}
))}
)} {/* STEP 3: SELECT DATE & TIME */} {step === 3 && (

Escolha data e horário

Selecione a data

{dates.map(d => { const iso = d.toISOString().split('T')[0]; const isSelected = selectedDate === iso; const isToday = d.toDateString() === today.toDateString(); const dayName = d.toLocaleDateString('pt-BR', { weekday: 'short' }).replace('.', ''); return (
{ setSelectedDate(iso); setSelectedTime(null); }} style={{ minWidth: '64px', padding: '12px 8px', borderRadius: 'var(--radius-md)', cursor: 'pointer', textAlign: 'center', flexShrink: 0, transition: 'all 0.2s', background: isSelected ? 'var(--accent)' : 'var(--bg-card)', border: `2px solid ${isSelected ? 'var(--accent)' : 'var(--border-color)'}`, color: isSelected ? 'white' : 'var(--text-primary)' }}>
{dayName}
{d.getDate()}
{d.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '')}
{isToday &&
HOJE
}
); })}
{selectedDate && (

Horários disponíveis

{(slots.length > 0 ? slots : ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00']).map(slot => { const isToday = selectedDate === today.toISOString().split('T')[0]; const [sh, sm] = slot.split(':').map(Number); const slotMinutes = sh * 60 + sm; const currentMinutes = today.getHours() * 60 + today.getMinutes(); const isPastOrTooSoon = isToday && (slotMinutes < currentMinutes + (establishment.bookingAdvanceMin || 60)); if (isPastOrTooSoon) return null; return (
setSelectedTime(slot)} style={{ padding: '12px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', textAlign: 'center', fontWeight: 600, fontSize: '14px', transition: 'all 0.2s', background: selectedTime === slot ? 'var(--accent)' : 'var(--bg-card)', border: `1px solid ${selectedTime === slot ? 'var(--accent)' : 'var(--border-color)'}`, color: selectedTime === slot ? 'white' : 'var(--text-primary)' }}> {slot}
)})}
)}
)} {/* STEP 4: CLIENT DATA */} {step === 4 && (

Seus dados

Resumo do agendamento

Serviço{svc?.name}
Profissional{prof?.name}
Data{selectedDate ? new Date(selectedDate + 'T12:00:00').toLocaleDateString('pt-BR') : ''}
Horário{selectedTime}
Duração{svc?.duration} min
Total R$ {svc?.price.toFixed(2)}
setClientName(e.target.value)} />
setClientPhone(e.target.value)} />
setClientEmail(e.target.value)} />
)} {/* NAVIGATION BUTTONS */}
{step > 1 ? ( ) :
} {step < 4 ? ( ) : ( )}
{/* MODAL POP-UP TELEFONE OBRIGATÓRIO */} {showPhoneModal && (
setShowPhoneModal(false)}>
e.stopPropagation()} style={{ maxWidth: '420px', textAlign: 'center' }}>

Telefone / WhatsApp Obrigatório 📱

Por favor, informe o seu número de telefone ou WhatsApp para confirmarmos e enviarmos a nota do seu agendamento.

setClientPhone(e.target.value)} />
)} {/* FOOTER */}

Powered by AgendaPRO

); }