// Booking flow: 4 steps — service, professional, date+time, confirm. // Disponibilidade e confirmação batem no backend real (Postgres), não mais // em localStorage — ver backend/src/routes/bookings.routes.ts. const { useState, useMemo, useEffect } = React; const SERVICES = [ {id:'corte', name:'Corte & Finalização', meta:'45 min · Salão de Beleza', price:'R$ 180'}, {id:'coloracao', name:'Coloração Premium', meta:'2h 30 min · Salão de Beleza', price:'R$ 480'}, {id:'tratamento', name:'Tratamento Capilar', meta:'1h 15 min · Salão de Beleza', price:'R$ 240'}, ]; const PROS = [ {id:'angella', name:'Angella Barros', meta:'Beleza Capilar · Colorista'}, {id:'aline', name:'Aline Maria', meta:'Terapeuta · Bem-estar'}, {id:'qualquer',name:'Qualquer especialista', meta:'Primeira disponibilidade'}, ]; const SLOTS = ['09:00','10:00','11:00','13:00','14:30','16:00','17:30','19:00']; const MONTH_NAMES = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']; const DOW = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; function Booking({showToast, only, proId}) { const store = useStore(); // sessões de massoterapia vivem no painel (aba Massoterapia) — entram aqui automaticamente const MASSAGENS = (store.wellness?.sessions || []).map(s => ({id:s.id, name:s.name, meta:`${s.time} · Massoterapia`, price:s.price})); const ALL = [...SERVICES, ...MASSAGENS]; const SERVICE_LIST = only ? ALL.filter(s => s.meta.includes(only)) : ALL; const PRO_LIST = proId ? PROS.filter(p => p.id === proId) : PROS; const [step, setStep] = useState(0); const [service, setService] = useState(null); const [pro, setPro] = useState(proId ? PROS.find(p => p.id === proId) : null); const [date, setDate] = useState(null); const [time, setTime] = useState(null); const [client, setClient] = useState({name:'', phone:'', note:''}); const [saved, setSaved] = useState(null); const today = new Date(); today.setHours(0,0,0,0); const [view, setView] = useState({y: today.getFullYear(), m: today.getMonth()}); const cells = useMemo(() => { const first = new Date(view.y, view.m, 1); const start = first.getDay(); const days = new Date(view.y, view.m+1, 0).getDate(); const out = []; for (let i=0;i today.getFullYear() || (view.y === today.getFullYear() && view.m > today.getMonth()); const fmt = (d) => d ? `${d.getDate()} de ${MONTH_NAMES[d.getMonth()].toLowerCase()}` : null; const isPast = (d) => d < today; const isSun = (d) => d.getDay() === 0; // closed Sundays const reset = () => { setStep(0); setService(null); setPro(proId ? PROS.find(p => p.id === proId) : null); setDate(null); setTime(null); setClient({name:'', phone:'', note:''}); setSaved(null); }; const isoOf = (d) => d ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` : ''; const proName = pro ? (pro.id === 'qualquer' ? 'Angella Barros' : pro.name) : ''; // horários já ocupados para a data e a especialista escolhidas — consulta real no backend const [taken, setTaken] = useState(new Set()); const [loadingSlots, setLoadingSlots] = useState(false); useEffect(() => { const iso = isoOf(date); if (!iso) { setTaken(new Set()); return; } let cancelled = false; setLoadingSlots(true); const qs = new URLSearchParams({date: iso, ...(proName ? {pro: proName} : {})}); fetch(`/api/bookings/availability?${qs}`) .then(r => r.json()) .then(data => { if (!cancelled) setTaken(new Set(data.taken || [])); }) .catch(() => { if (!cancelled) setTaken(new Set()); }) .finally(() => { if (!cancelled) setLoadingSlots(false); }); return () => { cancelled = true; }; }, [date, proName]); const digits = (s) => (s||'').replace(/\D/g,''); const maskFone = (v) => { const d = digits(v).slice(0,11); if (d.length <= 10) return d.replace(/(\d{0,2})(\d{0,4})(\d{0,4})/, (m,a,b,c) => [a && `(${a}`, a.length===2 ? ') ' : '', b, c && `-${c}`].join('')); return d.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3'); }; const dadosOk = client.name.trim().length > 2 && digits(client.phone).length >= 10; const waStudio = () => { const num = digits(store.settings.contact.whatsapp); const txt = [ 'Olá! Acabei de agendar pelo site:', `• Serviço: ${service?.name}`, `• Com: ${proName}`, `• Quando: ${fmt(date)} às ${time}`, `• Nome: ${client.name}`, `• WhatsApp: ${client.phone}`, client.note ? `• Observação: ${client.note}` : '', saved ? `• Código: ${saved.code}` : '', ].filter(Boolean).join('\n'); return `https://wa.me/${num}?text=${encodeURIComponent(txt)}`; }; const [confirming, setConfirming] = useState(false); const [confirmError, setConfirmError] = useState(''); const confirm = async () => { if (!date || !time || !dadosOk || confirming) return; setConfirming(true); setConfirmError(''); try { const res = await fetch('/api/bookings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ clientName: client.name.trim(), clientPhone: digits(client.phone), service: service.name, pro: proName, date: isoOf(date), time, price: Number(String(service.price).replace(/\D/g,'')) || 0, channel: 'site', notes: client.note.trim() || undefined, }), }); if (res.status === 409) { const err = await res.json().catch(() => ({})); setConfirmError(err.error || 'Esse horário acabou de ficar indisponível. Escolha outro.'); setTaken(t => new Set([...t, time])); setStep(2); return; } if (!res.ok) throw new Error('booking failed'); const { booking } = await res.json(); setSaved({...booking, client: client.name.trim(), phone: digits(client.phone), note: client.note.trim()}); showToast && showToast('Reserva registrada — confirmação a caminho'); setStep(4); } catch (e) { setConfirmError('Não foi possível registrar a reserva agora. Tente novamente ou fale pelo WhatsApp.'); } finally { setConfirming(false); } }; return (
Agendamento Online

Reserve seu momento

Escolha serviço, especialista e horário. A confirmação sai na hora pelo WhatsApp do studio.

0}/> 1}/> 2}/> 3}/>
Seg–Sex 9h–20h · Sáb 9h–17h
{step !== 4 && (

{['Escolha o serviço','Escolha a especialista','Escolha data & horário','Seus dados'][step]}

{step>0 && }
)} {step===0 && (
{SERVICE_LIST.map(s => ( ))}
)} {step===1 && (
{PRO_LIST.map(p => ( ))}
)} {step===2 && (
{MONTH_NAMES[view.m]} {view.y}
{DOW.map(d =>
{d}
)} {cells.map((d,i) => { if (!d) return
; const dis = isPast(d) || isSun(d); const sel = date && d.toDateString() === date.toDateString(); const isToday = d.toDateString() === today.toDateString(); return ( ); })}
{SLOTS.map((s) => ( ))}
)} {step===3 && (