// ============================================================ // JuFlow — Appointment Modal (New / Edit / Detail) // ============================================================ const { useState: useStAM, useMemo: useMemoAM, useEffect: useEfAM } = React; const UAM = window.JFUtils; // ─── APPOINTMENT DETAIL MODAL ───────────────────────────── const ApptDetailModal = ({ appt, onClose, onEdit, onCancel, addToast }) => { if (!appt) return null; const client = UAM.getClient(appt.clientId); const service = UAM.getService(appt.serviceId); const prof = UAM.getProfessional(appt.professionalId); const endTime = UAM.addMinutes(appt.datetime, appt.duration); const handleCancel = () => { onCancel(appt.id); addToast('Agendamento cancelado.', 'info'); onClose(); }; return ( {['confirmed','pending'].includes(appt.status) && ( )} } >
{/* Status banner */}
{UAM.statusLabel(appt.status)}
{/* Client + Professional */}
Cliente
{client?.name}
{client?.phone}
Profissional
{prof?.name}
{prof?.role}
{/* Service details */}
Serviço
{[ { label:'Serviço', value:service?.name, icon:'Scissors' }, { label:'Data', value:UAM.formatDate(appt.datetime,'dd/MM/yyyy'), icon:'Calendar' }, { label:'Horário', value:`${UAM.formatTime(appt.datetime)} – ${UAM.formatTime(endTime)}`, icon:'Clock' }, { label:'Duração', value:`${appt.duration} min`, icon:'Timer' }, { label:'Valor', value:UAM.formatCurrency(appt.price), icon:'DollarSign' }, { label:'Pagamento', value:appt.paymentMethod || '—', icon:'CreditCard' }, ].map(item => (
{item.label}
{item.value}
))}
{/* Session notes */} {appt.sessionNotes && (
Observações da sessão

{appt.sessionNotes}

)}
); }; // ─── NEW / EDIT APPOINTMENT MODAL ───────────────────────── const NewApptModal = ({ open, onClose, initial = {}, appointments, onSave, addToast }) => { const emptyForm = { clientId: '', serviceId: '', professionalId: '', date: UAM.formatDate(initial.datetime || window.JF.TODAY, 'yyyy-MM-dd'), time: initial.datetime ? UAM.formatTime(initial.datetime) : '09:00', status: 'confirmed', notes: '', paymentMethod: 'Pix', }; const [form, setForm] = useStAM({ ...emptyForm }); const [errors, setErrors] = useStAM({}); const [clientSearch, setClientSearch] = useStAM(''); const [showClientDD, setShowClientDD] = useStAM(false); useEfAM(() => { if (open) { setForm({ ...emptyForm, date: UAM.formatDate(initial.datetime || window.JF.TODAY, 'yyyy-MM-dd'), time: initial.datetime ? UAM.formatTime(initial.datetime) : '09:00', clientId: initial.clientId || '', serviceId: initial.serviceId || '', professionalId: initial.professionalId || '', status: initial.status || 'confirmed', }); setErrors({}); if (initial.clientId) { const c = UAM.getClient(initial.clientId); setClientSearch(c?.name || ''); } else setClientSearch(''); } }, [open]); const set = (key, val) => setForm(f => ({ ...f, [key]:val })); const selectedService = useMemoAM(() => window.JF.SERVICES.find(s => s.id === Number(form.serviceId)), [form.serviceId]); const availableProfs = useMemoAM(() => selectedService ? window.JF.PROFESSIONALS.filter(p => selectedService.professionals.includes(p.id)) : window.JF.PROFESSIONALS, [selectedService]); // Suggested available slots const suggestedSlots = useMemoAM(() => { if (!form.date) return []; const day = new Date(form.date + 'T00:00:00'); const dur = selectedService?.duration || 60; const dayAppts = appointments.filter(a => UAM.isSameDay(a.datetime, day) && a.status !== 'cancelled'); const slots = []; for (let h = 8; h <= 18; h++) { for (let m of [0, 30]) { const slotStart = h * 60 + m; const slotEnd = slotStart + dur; const conflict = dayAppts.some(a => { const as = UAM.minutesFromMidnight(a.datetime); const ae = as + a.duration; return !(slotEnd <= as || slotStart >= ae); }); if (!conflict) { slots.push(`${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`); if (slots.length >= 6) break; } } if (slots.length >= 6) break; } return slots; }, [form.date, form.serviceId, appointments]); const filteredClients = useMemoAM(() => { if (!clientSearch) return window.JF.CLIENTS.slice(0, 8); const q = clientSearch.toLowerCase(); return window.JF.CLIENTS.filter(c => c.name.toLowerCase().includes(q) || c.phone.includes(q)).slice(0, 8); }, [clientSearch]); const validate = () => { const e = {}; if (!form.clientId) e.clientId = 'Selecione um cliente'; if (!form.serviceId) e.serviceId = 'Selecione um serviço'; if (!form.professionalId) e.professionalId = 'Selecione um profissional'; if (!form.date) e.date = 'Informe a data'; if (!form.time) e.time = 'Informe o horário'; setErrors(e); return Object.keys(e).length === 0; }; const handleSave = (sendWhatsApp = false) => { if (!validate()) return; const svc = UAM.getService(Number(form.serviceId)); const dt = new Date(form.date + 'T' + form.time + ':00'); const newAppt = { id: Date.now(), clientId: Number(form.clientId), serviceId: Number(form.serviceId), professionalId: Number(form.professionalId), datetime: dt.toISOString(), duration: svc?.duration || 60, price: svc?.price || 0, status: form.status, notes: form.notes, sessionNotes: '', paymentMethod: form.paymentMethod, }; onSave(newAppt); addToast(sendWhatsApp ? 'Agendamento salvo e lembrete enviado pelo WhatsApp!' : 'Agendamento salvo com sucesso!', 'success'); onClose(); }; return ( } >
{/* Client selector */}
{ setClientSearch(e.target.value); setShowClientDD(true); set('clientId',''); }} onFocus={() => setShowClientDD(true)} placeholder="Buscar cliente pelo nome ou telefone..." style={{ width:'100%', boxSizing:'border-box', padding:'9px 12px 9px 34px', borderRadius:8, fontSize:13, border:`1.5px solid ${errors.clientId ? '#EF4444' : '#E2E8F0'}`, outline:'none', color:'#0A1F44', }} /> {form.clientId && ( )}
{errors.clientId && {errors.clientId}} {showClientDD && (
{filteredClients.map(c => (
{ setClientSearch(c.name); set('clientId', String(c.id)); setShowClientDD(false); }} style={{ display:'flex', alignItems:'center', gap:10, padding:'9px 14px', cursor:'pointer', fontSize:13, }} onMouseEnter={e => e.currentTarget.style.background='#F9FAFB'} onMouseLeave={e => e.currentTarget.style.background='#fff'} >
{c.name}
{c.phone} · Última visita: {c.lastVisit ? UAM.formatDate(c.lastVisit) : '—'}
))}
{ setClientSearch('Novo Cliente'); set('clientId','new'); setShowClientDD(false); }} style={{ padding:'9px 14px', fontSize:13, color:'#FF6B35', fontWeight:600, cursor:'pointer', borderTop:'1px solid #F0F4F8', display:'flex', alignItems:'center', gap:8, }} onMouseEnter={e => e.currentTarget.style.background='#FFF0EA'} onMouseLeave={e => e.currentTarget.style.background='#fff'} > Cadastrar novo cliente
)}
{/* Service + Professional */}
{errors.serviceId && {errors.serviceId}}
{errors.professionalId && {errors.professionalId}}
{/* Date + Time */}
set('date',v)} required error={errors.date} icon="Calendar" />
set('time',e.target.value)} style={{ width:'100%', boxSizing:'border-box', padding:'9px 12px', borderRadius:8, fontSize:13, border:`1.5px solid ${errors.time?'#EF4444':'#E2E8F0'}`, outline:'none', color:'#0A1F44' }} /> {suggestedSlots.length > 0 && (
Disponíveis: {suggestedSlots.map(slot => ( ))}
)}
{/* Status + Payment */}
set('paymentMethod',v)} options={[ {value:'Pix',label:'Pix'}, {value:'Cartão de Crédito',label:'Cartão de Crédito'}, {value:'Cartão de Débito',label:'Cartão de Débito'}, {value:'Dinheiro',label:'Dinheiro'}, {value:'Pendente',label:'Pendente'}, ]} />
{/* Notes */}