// ============================================================ // JuFlow — Agenda (Daily / Weekly / Monthly views) // ============================================================ const { useState: useStA, useMemo: useMemoA, useCallback: useCbA, useRef: useRefA, useEffect: useEfA } = React; const UA = window.JFUtils; const START_HOUR = 7; const END_HOUR = 21; const PX_PER_MIN = 1.8; // pixels per minute → 60min=108px, 30min=54px const timeToY = (date) => { const d = new Date(date); const mins = (d.getHours() - START_HOUR) * 60 + d.getMinutes(); return Math.max(0, mins * PX_PER_MIN); }; const TOTAL_HEIGHT = (END_HOUR - START_HOUR) * 60 * PX_PER_MIN; // Generate time labels (30-min increments) const TIME_LABELS = []; for (let h = START_HOUR; h <= END_HOUR; h++) { TIME_LABELS.push({ label: `${String(h).padStart(2,'0')}:00`, mins: (h - START_HOUR) * 60 }); if (h < END_HOUR) TIME_LABELS.push({ label: `${String(h).padStart(2,'0')}:30`, mins: (h - START_HOUR) * 60 + 30 }); } // ─── Appointment card for grid views ────────────────────── const ApptCard = ({ appt, onOpen, compact = false }) => { const [hov, setHov] = useState(false); const client = UA.getClient(appt.clientId); const service = UA.getService(appt.serviceId); const prof = UA.getProfessional(appt.professionalId); const color = prof?.color || '#FF6B35'; const endTime = UA.addMinutes(appt.datetime, appt.duration); const isCancelled = appt.status === 'cancelled' || appt.status === 'no_show'; return (
onOpen(appt)} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{ background: isCancelled ? '#F9FAFB' : UA.hexToRgba(color, hov ? 0.2 : 0.12), borderLeft: `3px solid ${isCancelled ? '#D1D5DB' : color}`, borderRadius: 8, padding: compact ? '3px 6px' : '6px 10px', cursor: 'pointer', transition: 'all .15s', boxShadow: hov ? `0 4px 14px ${UA.hexToRgba(color, 0.3)}` : 'none', transform: hov ? 'translateY(-1px)' : 'none', overflow: 'hidden', opacity: isCancelled ? 0.6 : 1, height: '100%', boxSizing: 'border-box', }} > {!compact && (
{UA.formatTime(appt.datetime)} – {UA.formatTime(endTime)}
)}
{client?.name || '—'}
{!compact && (
{service?.name}
)} {!compact && (
{prof?.name}
)}
); }; // ─── DAILY VIEW ─────────────────────────────────────────── const DailyView = ({ date, appointments, onOpen, onNewAtTime }) => { const dayAppts = useMemoA(() => UA.getAppointmentsForDay(appointments, date) .filter(a => a.status !== 'cancelled') .sort((a,b) => new Date(a.datetime) - new Date(b.datetime)) , [appointments, date]); // Compute overlapping columns const positioned = useMemoA(() => { const groups = []; dayAppts.forEach(appt => { const start = new Date(appt.datetime).getTime(); const end = start + appt.duration * 60000; let placed = false; for (const group of groups) { const overlaps = group.some(g => { const gs = new Date(g.datetime).getTime(); const ge = gs + g.duration * 60000; return start < ge && end > gs; }); if (!overlaps) { group.push(appt); placed = true; break; } } if (!placed) groups.push([appt]); }); const result = {}; groups.forEach((group, col) => { group.forEach(appt => { result[appt.id] = { col, totalCols: groups.length }; }); }); return result; }, [dayAppts]); const colWidth = 100 / Math.max(1, Object.values(positioned).reduce((m,v) => Math.max(m, v.totalCols), 1)); return (
{/* Time column */}
{TIME_LABELS.filter(t => t.label.endsWith(':00')).map(t => (
{t.label}
))}
{/* Grid */}
{/* Hour lines */} {TIME_LABELS.map(t => (
))} {/* Click-to-create zones */} {TIME_LABELS.map(t => (
onNewAtTime(date, t.mins)} style={{ position:'absolute', top: t.mins * PX_PER_MIN, left:0, right:0, height: 30 * PX_PER_MIN, cursor:'pointer', zIndex:1, }} onMouseEnter={e => { e.currentTarget.style.background='rgba(255,107,53,.04)'; }} onMouseLeave={e => { e.currentTarget.style.background='transparent'; }} /> ))} {/* Appointment cards */} {dayAppts.map(appt => { const pos = positioned[appt.id] || { col:0, totalCols:1 }; const top = timeToY(appt.datetime); const height = Math.max(appt.duration * PX_PER_MIN - 4, 32); const pct = 100 / pos.totalCols; return (
); })} {/* "Now" line */} {UA.isToday(date) && (() => { const now = window.JF.TODAY; const y = (now.getHours() - START_HOUR) * 60 * PX_PER_MIN + now.getMinutes() * PX_PER_MIN; return y >= 0 && y <= TOTAL_HEIGHT ? (
) : null; })()}
); }; // ─── WEEKLY VIEW ────────────────────────────────────────── const WeeklyView = ({ date, appointments, onOpen, onNewAtTime }) => { const weekDays = useMemoA(() => UA.getWeekDays(date), [date]); const DAY_NAMES = ['Seg','Ter','Qua','Qui','Sex','Sáb','Dom']; return (
{/* Time column */}
{TIME_LABELS.filter(t => t.label.endsWith(':00')).map(t => (
{t.label}
))}
{/* Day columns */} {weekDays.map((day, di) => { const dayAppts = UA.getAppointmentsForDay(appointments, day).filter(a => a.status !== 'cancelled'); const isToday = UA.isToday(day); return (
{/* Day header */}
{DAY_NAMES[di]} {day.getDate()}
{/* Time grid */}
{TIME_LABELS.map(t => (
))} {TIME_LABELS.map(t => (
onNewAtTime(day, t.mins)} style={{ position:'absolute', top: t.mins * PX_PER_MIN, left:0, right:0, height:30*PX_PER_MIN, cursor:'pointer', zIndex:1 }} onMouseEnter={e => e.currentTarget.style.background='rgba(255,107,53,.04)'} onMouseLeave={e => e.currentTarget.style.background='transparent'} /> ))} {dayAppts.map(appt => { const top = timeToY(appt.datetime); const height = Math.max(appt.duration * PX_PER_MIN - 3, 24); return (
); })}
); })}
); }; // ─── MONTHLY VIEW ───────────────────────────────────────── const MonthlyView = ({ date, appointments, onDayClick }) => { const calDays = useMemoA(() => UA.getMonthCalendar(date), [date]); const DAY_NAMES = ['Seg','Ter','Qua','Qui','Sex','Sáb','Dom']; return (
{/* Week headers */}
{DAY_NAMES.map(d => (
{d}
))}
{/* Calendar grid */}
{calDays.map(({ date:d, currentMonth }, i) => { const dayAppts = appointments.filter(a => UA.isSameDay(a.datetime, d) && a.status !== 'cancelled'); const isToday = UA.isToday(d); const profColors = [...new Set(dayAppts.map(a => UA.getProfessional(a.professionalId)?.color).filter(Boolean))]; return (
onDayClick(d)} style={{ border:'1px solid #F0F4F8', padding:'8px 6px', cursor:'pointer', transition:'background .1s', background: isToday ? '#FFF7F3' : currentMonth ? '#fff' : '#FAFAFA', opacity: currentMonth ? 1 : 0.5, display:'flex', flexDirection:'column', gap:4, minHeight:80, }} onMouseEnter={e => e.currentTarget.style.background = isToday ? '#FFF0EA' : '#F9FAFB'} onMouseLeave={e => e.currentTarget.style.background = isToday ? '#FFF7F3' : currentMonth ? '#fff' : '#FAFAFA'} >
{d.getDate()}
{dayAppts.length > 0 && (
{dayAppts.slice(0, 3).map(a => { const prof = UA.getProfessional(a.professionalId); const client = UA.getClient(a.clientId); return (
{UA.formatTime(a.datetime)} {client?.name?.split(' ')[0]}
); })} {dayAppts.length > 3 && (
+{dayAppts.length-3} mais
)}
)}
); })}
); }; // ─── PROFESSIONAL FILTER CHIPS ──────────────────────────── const ProfChips = ({ selected, onChange }) => (
{window.JF.PROFESSIONALS.map(p => ( ))}
); // ─── MAIN AGENDA COMPONENT ──────────────────────────────── const Agenda = ({ appointments, onOpenNew, onOpenAppt, date, onDateChange, view, onViewChange }) => { const [profFilter, setProfFilter] = useStA(null); const [statusFilter, setStatusFilter] = useStA('all'); const scrollRef = useRefA(null); // Scroll to current time on load useEfA(() => { if (scrollRef.current && view === 'daily') { const now = window.JF.TODAY; const y = (now.getHours() - START_HOUR) * 60 * PX_PER_MIN; scrollRef.current.scrollTop = Math.max(0, y - 100); } }, [view]); const filteredAppts = useMemoA(() => { let list = appointments; if (profFilter) list = list.filter(a => a.professionalId === profFilter); if (statusFilter !== 'all') list = list.filter(a => a.status === statusFilter); return list; }, [appointments, profFilter, statusFilter]); const handleNewAtTime = (day, mins) => { const h = Math.floor(mins / 60) + START_HOUR; const m = mins % 60; const dt = new Date(day); dt.setHours(h, m, 0, 0); onOpenNew({ datetime: dt.toISOString() }); }; const navDate = (dir) => { const delta = view==='daily' ? 1 : view==='weekly' ? 7 : 30; onDateChange(UA.addDays(date, dir * delta)); }; const weekDays = UA.getWeekDays(date); const headerLabel = view === 'monthly' ? UA.formatMonthYear(date) : view === 'weekly' ? `${UA.formatDateShort(weekDays[0])} – ${UA.formatDateShort(weekDays[6])}, ${date.getFullYear()}` : UA.formatDateLong(date); return (
{/* Toolbar */}
{/* View toggle */}
{['daily','weekly','monthly'].map(v => ( ))}
{/* Date navigation */}
{headerLabel}
{/* Status filter */}
{/* Professional chips */}
{/* Calendar area */}
{view === 'daily' && } {view === 'weekly' && } {view === 'monthly' && { onDateChange(d); onViewChange('daily'); }} />}
); }; Object.assign(window, { Agenda });