// ============================================================ // 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 ( { onEdit(appt); onClose(); }}>Editar {['confirmed','pending'].includes(appt.status) && ( Cancelar )} Fechar >} > {/* 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 ( Cancelar handleSave(true)} style={{ color:'#25D366', borderColor:'#25D366' }}> Salvar e enviar WhatsApp handleSave(false)}>Salvar >} > {/* Client selector */} Cliente * { 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 */} Serviço * { set('serviceId',e.target.value); set('professionalId',''); }} style={{ width:'100%', padding:'9px 12px', borderRadius:8, fontSize:13, border:`1.5px solid ${errors.serviceId?'#EF4444':'#E2E8F0'}`, outline:'none', color:'#0A1F44', cursor:'pointer' }}> Selecione... {window.JF.SERVICES.map(s => ( {s.name} · {s.duration}min · {UAM.formatCurrency(s.price)} ))} {errors.serviceId && {errors.serviceId}} Profissional * set('professionalId',e.target.value)} style={{ width:'100%', padding:'9px 12px', borderRadius:8, fontSize:13, border:`1.5px solid ${errors.professionalId?'#EF4444':'#E2E8F0'}`, outline:'none', color:'#0A1F44', cursor:'pointer' }}> Selecione... {availableProfs.map(p => ( {p.name} — {p.role} ))} {errors.professionalId && {errors.professionalId}} {/* Date + Time */} set('date',v)} required error={errors.date} icon="Calendar" /> Horário * 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 => ( set('time', slot)} style={{ padding:'2px 8px', borderRadius:20, fontSize:11, fontWeight:600, background: form.time===slot ? '#FF6B35' : '#F0FDF4', color: form.time===slot ? '#fff' : '#15803D', border:`1px solid ${form.time===slot ? '#FF6B35' : '#BBF7D0'}`, cursor:'pointer', }}>{slot} ))} )} {/* Status + Payment */} set('status',v)} options={[ {value:'confirmed',label:'Confirmado'}, {value:'pending',label:'Pendente'}, {value:'completed',label:'Realizado'}, {value:'cancelled',label:'Cancelado'}, {value:'no_show',label:'Falta'}, ]} /> 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 */} set('notes',v)} placeholder="Informações adicionais sobre o agendamento..." rows={2} /> {/* Summary */} {form.serviceId && form.date && form.time && ( {selectedService && (<>{selectedService.name} · {selectedService.duration}min · {UAM.formatCurrency(selectedService.price)}>)} {form.date && form.time && (<>Início: {UAM.formatDate(form.date+'T00:00','dd/MM/yyyy')} às {form.time} · Término previsto: {form.time && selectedService && (() => { const [h,m] = form.time.split(':').map(Number); const end = UAM.addMinutes(new Date(2026,0,1,h,m), selectedService.duration); return `${String(end.getHours()).padStart(2,'0')}:${String(end.getMinutes()).padStart(2,'0')}`; })()}>)} )} ); }; Object.assign(window, { ApptDetailModal, NewApptModal });
{appt.sessionNotes}