626 lines
31 KiB
TypeScript
626 lines
31 KiB
TypeScript
'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<string | null>(null);
|
||
const [selectedProfessional, setSelectedProfessional] = useState<string | null>(null);
|
||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||
const [selectedTime, setSelectedTime] = useState<string | null>(null);
|
||
const [clientName, setClientName] = useState('');
|
||
const [clientPhone, setClientPhone] = useState('');
|
||
const [clientEmail, setClientEmail] = useState('');
|
||
const [booked, setBooked] = useState(false);
|
||
const [slots, setSlots] = useState<string[]>([]);
|
||
const [loadingDb, setLoadingDb] = useState(true);
|
||
|
||
const [dbServices, setDbServices] = useState<any[]>([]);
|
||
const [dbProfessionals, setDbProfessionals] = useState<any[]>([]);
|
||
const [activeBookingInfo, setActiveBookingInfo] = useState<any>(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<any>({
|
||
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 (
|
||
<div style={{ minHeight: '100vh', background: 'var(--bg-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}>
|
||
<div style={{ textAlign: 'center', maxWidth: '480px', animation: 'slideUp 0.5s ease', width: '100%' }}>
|
||
<div style={{
|
||
width: 80, height: 80, borderRadius: '50%', background: 'var(--success-bg)', display: 'flex',
|
||
alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px', border: '3px solid var(--success)'
|
||
}}>
|
||
<Check size={40} color="var(--success)" />
|
||
</div>
|
||
<h1 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '12px' }}>Agendamento Confirmado! 🎉</h1>
|
||
<p style={{ color: 'var(--text-secondary)', fontSize: '16px', marginBottom: '32px' }}>
|
||
Seu horário está reservado e ativo.
|
||
</p>
|
||
<div style={{
|
||
background: 'var(--bg-card)', border: '1px solid var(--border-color)',
|
||
borderRadius: 'var(--radius-lg)', padding: '24px', textAlign: 'left', marginBottom: '20px'
|
||
}}>
|
||
<div style={{ display: 'grid', gap: '16px' }}>
|
||
<div><span style={{ fontSize: '12px', color: 'var(--text-muted)', textTransform: 'uppercase' }}>Serviço</span><div style={{ fontWeight: 600 }}>{renderSvcName}</div></div>
|
||
<div><span style={{ fontSize: '12px', color: 'var(--text-muted)', textTransform: 'uppercase' }}>Profissional</span><div style={{ fontWeight: 600 }}>{renderProfName}</div></div>
|
||
<div className="grid-2">
|
||
<div><span style={{ fontSize: '12px', color: 'var(--text-muted)', textTransform: 'uppercase' }}>Data</span><div style={{ fontWeight: 600 }}>{renderDate ? new Date(renderDate + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }) : ''}</div></div>
|
||
<div><span style={{ fontSize: '12px', color: 'var(--text-muted)', textTransform: 'uppercase' }}>Horário</span><div style={{ fontWeight: 600 }}>{renderTime}</div></div>
|
||
</div>
|
||
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ color: 'var(--text-secondary)' }}>Total</span>
|
||
<span style={{ fontSize: '24px', fontWeight: 800, color: 'var(--accent)' }}>R$ {renderPrice?.toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button className="btn btn-outline" style={{ width: '100%', borderColor: 'var(--danger)', color: 'var(--danger)' }} onClick={cancelBooking}>
|
||
Cancelar este agendamento
|
||
</button>
|
||
|
||
<p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '20px' }}>
|
||
Você não pode realizar outro agendamento enquanto este estiver ativo.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!establishment.allowOnlineBooking && !booked) {
|
||
return (
|
||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-primary)', color: 'white' }}>
|
||
<div style={{ maxWidth: '440px', textAlign: 'center', padding: '40px', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-color)', boxShadow: 'var(--shadow-lg)' }}>
|
||
<div style={{ width: 80, height: 80, borderRadius: '50%', background: 'var(--danger-bg)', color: 'var(--danger)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}>
|
||
<Calendar size={40} />
|
||
</div>
|
||
<h2 style={{ fontSize: '24px', fontWeight: 800, marginBottom: '16px', color: 'var(--text-primary)' }}>Agendamento Indisponível</h2>
|
||
<p style={{ color: 'var(--text-secondary)' }}>O agendamento online está temporariamente desativado para este estabelecimento. Por favor, entre em contato diretamente pelo WhatsApp ou telefone.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{ minHeight: '100vh', background: 'var(--bg-primary)' }}>
|
||
{/* HEADER */}
|
||
<header style={{
|
||
background: 'var(--gradient-primary)', padding: '32px 24px', textAlign: 'center',
|
||
position: 'relative', overflow: 'hidden'
|
||
}}>
|
||
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1), transparent 60%)' }} />
|
||
<div style={{ position: 'relative', maxWidth: '600px', margin: '0 auto' }}>
|
||
<div style={{ fontSize: '32px', marginBottom: '8px' }}>✂️</div>
|
||
<h1 style={{ fontSize: '28px', fontWeight: 900, letterSpacing: '-1px', marginBottom: '6px' }}>{establishment.name}</h1>
|
||
<p style={{ fontSize: '14px', opacity: 0.9, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
|
||
<MapPin size={14} /> {establishment.address}
|
||
</p>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginTop: '8px' }}>
|
||
<Star size={14} fill="white" />
|
||
<span style={{ fontWeight: 700 }}>{establishment.rating}</span>
|
||
<span style={{ opacity: 0.8 }}>({establishment.reviews} avaliações)</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* STEPPER */}
|
||
<div style={{ maxWidth: '700px', margin: '0 auto', padding: '32px 20px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginBottom: '40px' }}>
|
||
{['Serviço', 'Profissional', 'Data e Hora', 'Seus Dados'].map((label, i) => (
|
||
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<div style={{
|
||
width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: '13px', fontWeight: 700,
|
||
background: step > 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 ? <Check size={16} /> : i + 1}
|
||
</div>
|
||
<span style={{
|
||
fontSize: '13px', fontWeight: step === i + 1 ? 600 : 400,
|
||
color: step === i + 1 ? 'var(--text-primary)' : 'var(--text-muted)',
|
||
display: i < 3 ? 'inline' : 'inline'
|
||
}}>{label}</span>
|
||
{i < 3 && <div style={{ width: '24px', height: '2px', background: step > i + 1 ? 'var(--success)' : 'var(--bg-elevated)', borderRadius: '1px' }} />}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* STEP 1: SELECT SERVICE */}
|
||
{step === 1 && (
|
||
<div className="slide-up">
|
||
<h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '20px' }}>Escolha o serviço</h2>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||
{dbServices.map(s => (
|
||
<div key={s.id} onClick={() => 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'
|
||
}}>
|
||
<div>
|
||
<div style={{ fontWeight: 700, fontSize: '15px', marginBottom: '4px' }}>{s.name}</div>
|
||
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{s.desc || s.description || 'Serviço profissional'}</div>
|
||
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||
<Clock size={12} /> {s.duration} min
|
||
</div>
|
||
</div>
|
||
<div style={{ textAlign: 'right' }}>
|
||
<div style={{ fontSize: '20px', fontWeight: 800, color: 'var(--accent)' }}>R$ {Number(s.price).toFixed(2)}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* STEP 2: SELECT PROFESSIONAL */}
|
||
{step === 2 && (
|
||
<div className="slide-up">
|
||
<h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '20px' }}>Escolha o profissional</h2>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||
{dbProfessionals.map(p => (
|
||
<div key={p.id} onClick={() => 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'
|
||
}}>
|
||
<div className="avatar avatar-lg">{p.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}</div>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 700, fontSize: '15px' }}>{p.name}</div>
|
||
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{p.role || 'Barbeiro/Profissional'}</div>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', color: 'var(--warning)' }}>
|
||
<Star size={14} fill="var(--warning)" /> <span style={{ fontWeight: 700 }}>{p.rating || '5.0'}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* STEP 3: SELECT DATE & TIME */}
|
||
{step === 3 && (
|
||
<div className="slide-up">
|
||
<h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '20px' }}>Escolha data e horário</h2>
|
||
|
||
<div style={{ marginBottom: '24px' }}>
|
||
<h3 style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-secondary)', marginBottom: '12px' }}>
|
||
<Calendar size={14} style={{ display: 'inline', marginRight: '6px' }} />Selecione a data
|
||
</h3>
|
||
<div style={{ display: 'flex', gap: '8px', overflowX: 'auto', paddingBottom: '8px' }}>
|
||
{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 (
|
||
<div key={iso} onClick={() => { 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)'
|
||
}}>
|
||
<div style={{ fontSize: '11px', textTransform: 'uppercase', opacity: 0.7, marginBottom: '4px' }}>{dayName}</div>
|
||
<div style={{ fontSize: '20px', fontWeight: 800 }}>{d.getDate()}</div>
|
||
<div style={{ fontSize: '11px', opacity: 0.7 }}>{d.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '')}</div>
|
||
{isToday && <div style={{ fontSize: '9px', fontWeight: 700, color: isSelected ? 'white' : 'var(--accent)', marginTop: '2px' }}>HOJE</div>}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{selectedDate && (
|
||
<div>
|
||
<h3 style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-secondary)', marginBottom: '12px' }}>
|
||
<Clock size={14} style={{ display: 'inline', marginRight: '6px' }} />Horários disponíveis
|
||
</h3>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(90px, 1fr))', gap: '8px' }}>
|
||
{(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 (
|
||
<div key={slot} onClick={() => 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}
|
||
</div>
|
||
)})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* STEP 4: CLIENT DATA */}
|
||
{step === 4 && (
|
||
<div className="slide-up">
|
||
<h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '20px' }}>Seus dados</h2>
|
||
|
||
<div style={{
|
||
background: 'var(--bg-card)', border: '1px solid var(--border-color)',
|
||
borderRadius: 'var(--radius-lg)', padding: '20px', marginBottom: '24px'
|
||
}}>
|
||
<h3 style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: '12px' }}>Resumo do agendamento</h3>
|
||
<div style={{ display: 'grid', gap: '8px', fontSize: '14px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: 'var(--text-secondary)' }}>Serviço</span><span style={{ fontWeight: 600 }}>{svc?.name}</span></div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: 'var(--text-secondary)' }}>Profissional</span><span style={{ fontWeight: 600 }}>{prof?.name}</span></div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: 'var(--text-secondary)' }}>Data</span><span style={{ fontWeight: 600 }}>{selectedDate ? new Date(selectedDate + 'T12:00:00').toLocaleDateString('pt-BR') : ''}</span></div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: 'var(--text-secondary)' }}>Horário</span><span style={{ fontWeight: 600 }}>{selectedTime}</span></div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: 'var(--text-secondary)' }}>Duração</span><span style={{ fontWeight: 600 }}>{svc?.duration} min</span></div>
|
||
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '8px', display: 'flex', justifyContent: 'space-between' }}>
|
||
<span style={{ fontWeight: 600 }}>Total</span>
|
||
<span style={{ fontSize: '20px', fontWeight: 800, color: 'var(--accent)' }}>R$ {svc?.price.toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label className="form-label"><User size={14} style={{ display: 'inline', marginRight: 4 }} />Nome completo *</label>
|
||
<input className="form-input" placeholder="Seu nome" value={clientName} onChange={e => setClientName(e.target.value)} />
|
||
</div>
|
||
<div className="grid-2">
|
||
<div className="form-group">
|
||
<label className="form-label"><Phone size={14} style={{ display: 'inline', marginRight: 4 }} />Telefone / WhatsApp *</label>
|
||
<input className="form-input" placeholder="(00) 00000-0000" value={clientPhone} onChange={e => setClientPhone(e.target.value)} />
|
||
</div>
|
||
<div className="form-group">
|
||
<label className="form-label"><Mail size={14} style={{ display: 'inline', marginRight: 4 }} />E-mail (opcional)</label>
|
||
<input className="form-input" placeholder="seu@email.com" value={clientEmail} onChange={e => setClientEmail(e.target.value)} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* NAVIGATION BUTTONS */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '32px', gap: '12px' }}>
|
||
{step > 1 ? (
|
||
<button className="btn btn-secondary" onClick={() => setStep(s => s - 1)}>
|
||
<ChevronLeft size={16} /> Voltar
|
||
</button>
|
||
) : <div />}
|
||
{step < 4 ? (
|
||
<button className="btn btn-primary" disabled={!canNext()} onClick={() => setStep(s => s + 1)}
|
||
style={{ opacity: canNext() ? 1 : 0.5 }}>
|
||
Próximo <ChevronRight size={16} />
|
||
</button>
|
||
) : (
|
||
<button className="btn btn-primary" onClick={handleBook}
|
||
style={{ padding: '12px 32px', fontSize: '16px' }}>
|
||
<Check size={18} /> Confirmar Agendamento
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* MODAL POP-UP TELEFONE OBRIGATÓRIO */}
|
||
{showPhoneModal && (
|
||
<div className="modal-overlay" onClick={() => setShowPhoneModal(false)}>
|
||
<div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: '420px', textAlign: 'center' }}>
|
||
<div className="modal-header" style={{ justifyContent: 'center', borderBottom: 'none', paddingBottom: 0 }}>
|
||
<div style={{ width: 60, height: 60, borderRadius: '50%', background: 'rgba(245, 158, 11, 0.15)', color: 'var(--warning)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto' }}>
|
||
<Phone size={30} />
|
||
</div>
|
||
</div>
|
||
<div className="modal-body">
|
||
<h3 style={{ fontSize: '20px', fontWeight: 800, marginBottom: '12px' }}>Telefone / WhatsApp Obrigatório 📱</h3>
|
||
<p style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '20px', lineHeight: 1.5 }}>
|
||
Por favor, informe o seu número de telefone ou WhatsApp para confirmarmos e enviarmos a nota do seu agendamento.
|
||
</p>
|
||
<div className="form-group" style={{ textAlign: 'left', marginBottom: '20px' }}>
|
||
<label className="form-label">Seu Telefone / WhatsApp *</label>
|
||
<input
|
||
className="form-input"
|
||
placeholder="(00) 00000-0000"
|
||
value={clientPhone}
|
||
autoFocus
|
||
onChange={e => setClientPhone(e.target.value)}
|
||
/>
|
||
</div>
|
||
<button
|
||
className="btn btn-primary"
|
||
style={{ width: '100%', padding: '12px', fontSize: '15px' }}
|
||
onClick={() => {
|
||
if (!clientPhone.trim()) {
|
||
showToast('Por favor, digite seu número de telefone.', 'warning');
|
||
return;
|
||
}
|
||
setShowPhoneModal(false);
|
||
handleBook();
|
||
}}
|
||
>
|
||
<Check size={16} /> Confirmar e Agendar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* FOOTER */}
|
||
<footer style={{ textAlign: 'center', padding: '32px 20px', borderTop: '1px solid var(--border-color)', marginTop: '40px' }}>
|
||
<p style={{ fontSize: '13px', color: 'var(--text-muted)' }}>
|
||
Powered by <span style={{ fontWeight: 700, color: 'var(--accent)' }}>AgendaPRO</span>
|
||
</p>
|
||
</footer>
|
||
<ToastContainer />
|
||
</div>
|
||
);
|
||
}
|