/* ============================================================ AGENDA — Calendário (dia / semana / mês) ============================================================ */ function CalendarScreen(ctx) { const { terms, appts, services, team, clients, openNewAppt, openAppt, statusMap, settings, addAppt, toast } = ctx; const DB = window.DB; const [view, setView] = React.useState(() => { const saved = localStorage.getItem('ag-calendar-view'); return ['dia', 'semana', 'mes'].includes(saved) ? saved : (window.innerWidth <= 820 ? 'dia' : 'semana'); }); const [anchor, setAnchor] = React.useState(new Date(DB.today)); // data de referência const user = ctx.user || {}; const isPlainUser = user.role === 'user'; const isAdmin = user.role === 'admin'; const isPro = user.role === 'pro'; const userProId = user.proId ? Number(user.proId) : 0; const resolveCalendarFilter = () => { if (isPlainUser) return 'mine'; const requested = ctx.calPro && ctx.calPro !== 'default' ? ctx.calPro : (settings.calendar_default_scope || 'mine'); if (requested === 'mine' || requested === 'all') return requested; const specificMatch = String(requested).match(/^pro:(\d+)$/); const requestedProId = specificMatch ? Number(specificMatch[1]) : Number(requested); return requestedProId > 0 && team.some((p) => Number(p.id) === requestedProId && !p.archived) ? requestedProId : 'mine'; }; const [proFilter, setProFilter] = React.useState(resolveCalendarFilter); // pré-filtro via configuração ou "Ver agenda" const [clientFilter, setClientFilter] = React.useState('all'); const [openGroup, setOpenGroup] = React.useState(null); const [freeSlotsOpen, setFreeSlotsOpen] = React.useState(false); const [printOpen, setPrintOpen] = React.useState(false); const [draggingId, setDraggingId] = React.useState(null); const dragMovedRef = React.useRef(false); const dragOffsetYRef = React.useRef(0); // relógio vivo: a linha de "agora" anda sozinha (atualiza a cada 30s, sem recarregar) const [now, setNow] = React.useState(() => new Date()); React.useEffect(() => { localStorage.setItem('ag-calendar-view', view); }, [view]); React.useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []); React.useEffect(() => { setProFilter(resolveCalendarFilter()); }, [ctx.calPro, settings.calendar_default_scope, isPlainUser, team]); React.useEffect(() => { if (clientFilter !== 'all' && !clients.some((c) => Number(c.id) === Number(clientFilter))) setClientFilter('all'); }, [clients, clientFilter]); const readHour = (value, fallback) => { const m = String(value || '').match(/^(\d{1,2})(?::\d{2})?$/); const h = m ? Number(m[1]) : fallback; return Number.isFinite(h) ? Math.max(0, Math.min(23, h)) : fallback; }; const H_START = readHour(settings.calendar_start, 7); const rawEnd = readHour(settings.calendar_end, 21); const H_END = Math.max(H_START + 1, Math.min(24, rawEnd)); const DEFAULT_APPT_START = `${String(Math.min(Math.max(9, H_START), H_END - 1)).padStart(2, '0')}:00`; const PX = 64; // px/h const svc = (id) => services.find(s => s.id === id); const cli = (id) => clients.find(c => c.id === id); const pro = (id) => team.find(p => p.id === id); const personColor = (proId) => DB.colorValue((pro(proId) || {}).color); const personColorBg = (proId) => DB.colorBg((pro(proId) || {}).color); const eventColorStyle = (a, done = false) => ({ '--c': done ? 'var(--text-3)' : personColor(a._displayProId || a.proId), '--cbg': done ? 'var(--surface-2)' : personColorBg(a._displayProId || a.proId), }); const iso = DB.iso; // ISO local (sem deslocamento de fuso) const sameMonth = (d) => d.getMonth() === anchor.getMonth(); const apptHasPro = (a, proId) => Number(a.proId) === Number(proId) || (a.participants || []).some((p) => Number(p.proId) === Number(proId)); const proAvailabilityForDate = (p, date) => { if (!p || !date || !p.availability) return null; const d = new Date(date + 'T00:00'); return p.availability[String(d.getDay())] || null; }; const proWorksAt = (p, date, start, end) => { const row = proAvailabilityForDate(p, date); if (row) { if (!row.enabled) return false; const ps = window.timeToMin(row.start || p.start || '00:00'); const pe = window.timeToMin(row.end || p.end || '23:59'); const ls = row.lunchStart ? window.timeToMin(row.lunchStart) : -1; const le = row.lunchEnd ? window.timeToMin(row.lunchEnd) : -1; return start >= ps && end <= pe && !(le > ls && start < le && end > ls); } const ps = window.timeToMin(p.start || '00:00'); const pe = window.timeToMin(p.end || '23:59'); const hasLunch = !!(p.lunchStart && p.lunchEnd); const ls = hasLunch ? window.timeToMin(p.lunchStart) : -1; const le = hasLunch ? window.timeToMin(p.lunchEnd) : -1; return start >= ps && end <= pe && !(hasLunch && le > ls && start < le && end > ls); }; const apptIsMine = (a) => Number(a.createdByUserId) === Number(user.id) || (userProId > 0 && apptHasPro(a, userProId)); const filteredByPro = (list) => (isPlainUser || proFilter === 'mine') ? list.filter(apptIsMine) : (proFilter === 'all' ? list : list.filter(a => apptHasPro(a, proFilter))); const filtered = (list) => { const byPro = filteredByPro(list); return clientFilter === 'all' ? byPro : byPro.filter((a) => Number(a.clientId) === Number(clientFilter)); }; const newApptProId = () => { if (isPlainUser) return userProId || undefined; if (proFilter === 'mine') return userProId || undefined; return proFilter !== 'all' ? proFilter : undefined; }; const filterLabel = () => { if (isPlainUser || proFilter === 'mine') return 'Minha Agenda'; if (proFilter === 'all') return terms.pros; const selected = pro(proFilter); return selected ? selected.name.replace('Dra. ', '') : terms.pros; }; const clientFilterLabel = () => { if (clientFilter === 'all') return `${terms.clients} (todos)`; const selected = cli(Number(clientFilter)); return selected ? selected.name : `${terms.clients} (todos)`; }; const computeOverlaps = (list) => { const sorted = [...list].sort((a, b) => window.timeToMin(a.start) - window.timeToMin(b.start) || window.apptDur(b) - window.apptDur(a)); const cols = []; sorted.forEach(a => { const aStart = window.timeToMin(a.start); const aDur = a._groupDur || window.apptDur(a); const aEnd = aStart + aDur; let placed = false; for (let i = 0; i < cols.length; i++) { const last = cols[i][cols[i].length - 1]; const lastDur = last._groupDur || window.apptDur(last); const lastEnd = window.timeToMin(last.start) + lastDur; if (aStart >= lastEnd) { cols[i].push(a); a._col = i; placed = true; break; } } if (!placed) { a._col = cols.length; cols.push([a]); } }); // Agrupa por grupos de sobreposição let currentGroup = []; let groupEnd = 0; let maxColsInGroup = 0; sorted.forEach((a, index) => { const aStart = window.timeToMin(a.start); const aDur = a._groupDur || window.apptDur(a); const aEnd = aStart + aDur; if (aStart >= groupEnd && currentGroup.length > 0) { currentGroup.forEach(ev => ev._maxCol = maxColsInGroup); currentGroup = []; groupEnd = 0; maxColsInGroup = 0; } currentGroup.push(a); groupEnd = Math.max(groupEnd, aEnd); maxColsInGroup = Math.max(maxColsInGroup, a._col + 1); if (index === sorted.length - 1) { currentGroup.forEach(ev => ev._maxCol = maxColsInGroup); } }); return sorted; }; const buildWeekItems = (list) => { const makeGroupItem = (items) => { const group = [...items]; const sortedGroup = [...group].sort((a, b) => String(a.clientLabel || a.clientName || '').localeCompare(String(b.clientLabel || b.clientName || ''), 'pt-BR')); const minS = Math.min(...group.map(x => window.timeToMin(x.start))); const maxE = Math.max(...group.map(x => window.timeToMin(x.start) + window.apptDur(x))); return { ...sortedGroup[0], id: `group-${sortedGroup.map((item) => item.id).join('-')}`, _group: sortedGroup, start: window.minToTime(minS), _groupEnd: window.minToTime(maxE), _groupDur: maxE - minS }; }; const teamKey = (a) => { const ids = [a.proId, ...(a.participants || []).map((part) => part.proId)] .map(Number) .filter((id) => id > 0); return [...new Set(ids)].sort((x, y) => x - y).join(',') || `none-${a.id}`; }; const byTeam = new Map(); list.forEach((a) => { const key = teamKey(a); byTeam.set(key, [...(byTeam.get(key) || []), a]); }); const condensed = []; byTeam.forEach((teamItems) => { const sorted = [...teamItems].sort((a, b) => window.timeToMin(a.start) - window.timeToMin(b.start) || window.apptDur(b) - window.apptDur(a)); let cluster = []; let clusterEnd = -1; const flush = () => { if (!cluster.length) return; condensed.push(cluster.length >= 2 ? makeGroupItem(cluster) : cluster[0]); cluster = []; clusterEnd = -1; }; sorted.forEach((a) => { const start = window.timeToMin(a.start); const end = start + window.apptDur(a); if (cluster.length && start >= clusterEnd) flush(); cluster.push(a); clusterEnd = Math.max(clusterEnd, end); }); flush(); }); return computeOverlaps(condensed); }; // week days (domingo a sábado) const weekStart = (() => { const d = new Date(anchor); d.setDate(d.getDate() - d.getDay()); d.setHours(0, 0, 0, 0); return d; })(); const weekDays = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(d.getDate() + i); return d; }); const move = (dir) => { const d = new Date(anchor); if (view === 'dia') d.setDate(d.getDate() + dir); else if (view === 'semana') d.setDate(d.getDate() + dir * 7); else d.setMonth(d.getMonth() + dir); setAnchor(d); }; const nowMin = now.getHours() * 60 + now.getMinutes(); const isToday = (d) => iso(d) === iso(now); const title = view === 'mes' ? anchor.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }) : view === 'semana' ? `${weekDays[0].toLocaleDateString('pt-BR', { day: 'numeric', month: 'short' })} – ${weekDays[6].toLocaleDateString('pt-BR', { day: 'numeric', month: 'short' })}` : anchor.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }); const hours = Array.from({ length: H_END - H_START }, (_, i) => H_START + i); const apptById = (id) => appts.find(a => a.id === Number(id)); const canMoveAppt = (a) => !!(ctx.user && Number(a.createdByUserId) === Number(ctx.user.id) && !['cancelado', 'concluido', 'faltou'].includes(a.status)); const markDragHandled = () => { dragMovedRef.current = true; window.setTimeout(() => { dragMovedRef.current = false; }, 250); }; const onDragStart = (e, a) => { if (!canMoveAppt(a)) { e.preventDefault(); return; } e.stopPropagation(); setDraggingId(a.id); dragMovedRef.current = false; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(a.id)); const rect = e.currentTarget.getBoundingClientRect(); dragOffsetYRef.current = e.clientY - rect.top; }; const onDragEnd = () => setDraggingId(null); const moveAppt = async (a, nextDate, nextStart, nextProId) => { if (!a || !nextDate || !nextStart) return; if (!canMoveAppt(a)) { toast(`Somente quem criou este ${terms.appt.toLowerCase()} pode movê-lo.`, 'x'); return; } const dur = window.apptDur(a); const nextEnd = window.addMinutes(nextStart, dur); const nextPrimaryProId = nextProId || a.proId; if (a.date === nextDate && a.start === nextStart && window.apptEnd(a) === nextEnd && Number(a.proId) === Number(nextPrimaryProId)) return; try { await addAppt({ editId: a.id, clientId: a.clientId, serviceId: a.serviceId, proId: nextPrimaryProId, date: nextDate, start: nextStart, end: nextEnd, status: a.status, notes: a.notes, showNotification: a.showNotification, launchFinance: a.launchFinance, }); toast(`${terms.appt} movido!`, 'calendar'); } catch (e) { toast(e.message || `Não foi possível mover o ${terms.appt.toLowerCase()}.`, 'x'); } }; const startFromDrop = (e, a) => { const rect = e.currentTarget.getBoundingClientRect(); const dur = Math.max(15, window.apptDur(a)); const minStart = H_START * 60; const maxStart = Math.max(minStart, H_END * 60 - dur); const raw = minStart + Math.round(((e.clientY - dragOffsetYRef.current - rect.top) / PX) * 4) * 15; return window.minToTime(Math.max(minStart, Math.min(maxStart, raw))); }; const dropOnTimedGrid = async (e, d, targetProId) => { e.preventDefault(); e.stopPropagation(); const a = apptById(e.dataTransfer.getData('text/plain')); if (!a) return; markDragHandled(); setDraggingId(null); await moveAppt(a, iso(d), startFromDrop(e, a), targetProId); }; const dropOnMonthDay = async (e, d) => { e.preventDefault(); e.stopPropagation(); const a = apptById(e.dataTransfer.getData('text/plain')); if (!a) return; markDragHandled(); setDraggingId(null); await moveAppt(a, iso(d), a.start || DEFAULT_APPT_START); }; function EventBlock({ a, compact }) { const displayProId = a._displayProId || a.proId; const s = a.manualServiceName ? { name: a.manualServiceName, price: a.manualServicePrice || 0, color: 'violet' } : svc(a.serviceId), c = cli(a.clientId), p = pro(displayProId); const partCount = (a.participants || []).length; const partConfirmed = (a.participants || []).filter((x) => x.response === 'confirmado').length; if (!s) return null; const startMin = window.timeToMin(a.start); if (startMin < H_START * 60 || startMin >= H_END * 60) return null; const top = (startMin - H_START * 60) / 60 * PX; const durMin = a._groupDur || window.apptDur(a); const height = Math.max(26, durMin / 60 * PX - 4); const done = a.status === 'concluido' || a.status === 'faltou'; const group = Array.isArray(a._group) ? a._group : null; const maxCol = a._maxCol || 1; const col = a._col || 0; const width = `calc(${100 / maxCol}% - 2px)`; const left = `${(100 / maxCol) * col}%`; const shortName = (name) => String(name || '').replace('Dra. ', '').trim(); const teamNamesFor = (item) => { const ids = [item.proId, item._displayProId, ...(item.participants || []).map((part) => part.proId)] .map(Number) .filter((id) => id > 0); return [...new Set(ids)] .map((id) => pro(id)) .filter(Boolean) .map((itemPro) => shortName(itemPro.name)) .filter(Boolean) .join(', '); }; const clientNameFor = (item) => { const itemClient = cli(item.clientId); return shortName(item.clientLabel || item.clientName || (itemClient ? itemClient.name : '')); }; const proNameFor = (item, fallbackProId) => { const itemPro = pro(fallbackProId || item._displayProId || item.proId); return shortName(itemPro ? itemPro.name : ''); }; const peopleLabelFor = (item, fallbackProId) => [proNameFor(item, fallbackProId), clientNameFor(item)].filter(Boolean).join(' · '); const peopleLabel = peopleLabelFor(a, displayProId); const teamLabel = teamNamesFor(a) || proNameFor(a, displayProId) || terms.pro; const apptStatus = statusMap[a.status] || { label: a.status || 'Pendente', cls: 'amber' }; const eventTitle = [s.name, peopleLabel].filter(Boolean).join(' - '); if (group) { const sameService = group.every((item) => Number(item.serviceId) === Number(group[0].serviceId)); const groupTitle = `${group.length} ${terms.appt.toLowerCase()}s${sameService ? ` · ${s.name}` : ''}`; const groupPros = []; const addGroupPro = (proId) => { const pid = Number(proId); if (!pid || groupPros.some((item) => Number(item.id) === pid)) return; const found = pro(pid); if (found) groupPros.push(found); }; group.forEach((item) => { addGroupPro(item.proId); (item.participants || []).forEach((part) => addGroupPro(part.proId)); }); const groupColors = groupPros.map((item) => personColor(item.id)); const groupColorBar = groupColors.length ? `linear-gradient(90deg, ${groupColors.map((color, i) => `${color} ${Math.round(i / groupColors.length * 100)}% ${Math.round((i + 1) / groupColors.length * 100)}%`).join(', ')})` : personColor(displayProId); const groupProNames = groupPros.map((item) => String(item.name || '').replace('Dra. ', '')).filter(Boolean); const visibleProNames = groupProNames.slice(0, 3).join(', ') + (groupProNames.length > 3 ? ` +${groupProNames.length - 3}` : ''); const mixedPros = groupPros.length > 1; const sameStatus = group.every((item) => item.status === group[0].status); const statusCounts = group.reduce((counts, item) => { const status = item.status || 'pendente'; counts[status] = (counts[status] || 0) + 1; return counts; }, {}); const statusOrder = ['confirmado', 'pendente', 'concluido', 'faltou', 'cancelado']; const countStatusLabel = (status, count) => { const labels = { confirmado: ['confirmado', 'confirmados'], pendente: ['pendente', 'pendentes'], concluido: ['concluído', 'concluídos'], faltou: ['falta', 'faltas'], cancelado: ['cancelado', 'cancelados'], }; const pair = labels[status]; if (pair) return `${count} ${pair[count === 1 ? 0 : 1]}`; const fallback = (statusMap[status] || {}).label || status; return `${count} ${String(fallback).toLowerCase()}`; }; const mixedStatusLabel = [...statusOrder, ...Object.keys(statusCounts).filter((status) => !statusOrder.includes(status))] .filter((status) => statusCounts[status]) .map((status) => countStatusLabel(status, statusCounts[status])) .join(' · '); const mixedStatusClass = statusCounts.cancelado || statusCounts.faltou ? 'red' : (statusCounts.pendente ? 'amber' : (statusCounts.concluido ? 'blue' : 'green')); const groupedStatus = sameStatus ? (statusMap[group[0].status] || { label: group[0].status || 'Pendente', cls: 'amber' }) : { label: mixedStatusLabel, cls: mixedStatusClass }; const groupDone = group.every((item) => item.status === 'concluido' || item.status === 'faltou'); const groupHeight = Math.max(58, height); const groupStyle = groupDone ? { ...eventColorStyle(a, groupDone), '--group-colors': groupColorBar } : (mixedPros ? { '--c': 'var(--text-2)', '--cbg': 'var(--surface-2)', '--group-colors': groupColorBar } : { ...eventColorStyle(a, groupDone), '--group-colors': groupColorBar }); const addAtSameTime = (e) => { e.stopPropagation(); openNewAppt({ date: a.date, start: a.start, end: window.apptEnd(a), proId: newApptProId() || displayProId, serviceId: sameService ? a.serviceId : undefined }); }; const names = group.map((item) => { return peopleLabelFor(item) || 'Sem participante informado'; }).filter(Boolean); const visibleNames = names.slice(0, 4).join(', ') + (names.length > 4 ? ` +${names.length - 4}` : ''); return (
{ e.stopPropagation(); setOpenGroup(group); }}>
{compact ? `${visibleProNames || terms.pros} (${group.length})` : groupTitle} {groupedStatus.label}
{!compact &&
{visibleNames}
} {!compact && mixedPros && groupHeight > 72 &&
{visibleProNames}
} {!compact &&
{a.start}–{a._groupEnd || window.apptEnd(a)}
}
); } return (
onDragStart(e, a)} onDragEnd={onDragEnd} onClick={(e) => { e.stopPropagation(); if (dragMovedRef.current) return; openAppt(a); }}>
{compact ? teamLabel : s.name} {apptStatus.label}
{!compact && height > 40 && peopleLabel &&
{peopleLabel} {partCount > 1 ? ` · ${partConfirmed}/${partCount} confirmados` : ''}
} {!compact && height > 58 &&
{a.start}–{window.apptEnd(a)}
}
); } // === WEEK / DAY grid === const gridDays = view === 'dia' ? [anchor] : weekDays; const visibleDayPros = (() => { const activeTeam = team.filter((p) => !p.archived); if (isPlainUser || proFilter === 'mine') return activeTeam.filter((p) => Number(p.id) === Number(userProId)); if (proFilter === 'all') return activeTeam; const selected = pro(proFilter); return selected && !selected.archived ? [selected] : []; })(); const dayPros = visibleDayPros.length ? visibleDayPros : [{ id: newApptProId() || 0, name: filterLabel(), initials: '?', color: 'gray' }]; const dayISO = iso(anchor); const baseDayAppts = filtered(appts.filter((a) => a.date === dayISO)); const weekDayItems = view === 'semana' ? weekDays.map((d) => buildWeekItems(filtered(appts.filter((a) => a.date === iso(d))))) : []; const weekColumnWidths = weekDayItems.map((items) => { const concurrentColumns = Math.max(1, ...items.map((item) => Number(item._maxCol) || 1)); return Math.max(170, concurrentColumns * 150); }); const weekColumnTracks = weekColumnWidths.map((width) => { const proportionalShare = Math.max(1, width / 170).toFixed(3); return `minmax(${width}px, ${proportionalShare}fr)`; }); const calendarToolbar = (

{title}

{/* agendar para / filtro de pessoa */} {isPlainUser ? (
Minha Agenda
) : ( {filterLabel()} }>
{terms.pros}
{(isAdmin || isPro) && } {team.filter(p => !p.archived).map(p => ( ))}
)} {clientFilterLabel()} }>
{terms.clients}
{[...clients].sort((a, b) => String(a.name).localeCompare(String(b.name), 'pt-BR')).map((c) => ( ))}
setPrintOpen(true)}>Imprimir {ctx.businessType === 'pilates' && setFreeSlotsOpen(true)}>Horários livres}
); return (
{ctx.calendarToolbarHost && ReactDOM.createPortal(calendarToolbar, ctx.calendarToolbarHost)} {ctx.calendarNewActionHost && ReactDOM.createPortal( openNewAppt({ proId: newApptProId() })}>Novo, ctx.calendarNewActionHost )} {view === 'mes' ? ( { setAnchor(d); setView('dia'); } }} /> ) : (
{hours.map(h => (
{String(h).padStart(2, '0')}:00
))}
sum + width, 0), }}> {view === 'dia' ? dayPros.map((p) => { const dayAppts = computeOverlaps(baseDayAppts .filter((a) => p.id ? apptHasPro(a, p.id) : true) .map((a) => ({ ...a, _displayProId: p.id || a.proId }))); return (
{terms.pro}
{String(p.name || filterLabel()).replace('Dra. ', '')}
e.preventDefault()} onDrop={(e) => dropOnTimedGrid(e, anchor, p.id || undefined)}> {hours.map(h => (
openNewAppt({ date: dayISO, start: `${String(h).padStart(2, '0')}:00`, proId: p.id || newApptProId() })} /> ))} {isToday(anchor) && nowMin >= H_START * 60 && nowMin <= H_END * 60 && (
)} {dayAppts.map(a => )}
); }) : gridDays.map((d, di) => { const dayAppts = weekDayItems[di] || []; return (
{d.toLocaleDateString('pt-BR', { weekday: 'short' }).replace('.', '')} {d.getDate()}
e.preventDefault()} onDrop={(e) => dropOnTimedGrid(e, d)}> {hours.map(h => (
openNewAppt({ date: iso(d), start: `${String(h).padStart(2, '0')}:00`, proId: newApptProId() })} /> ))} {isToday(d) && nowMin >= H_START * 60 && nowMin <= H_END * 60 && (
)} {dayAppts.map(a => )}
); })}
)} {openGroup && ( Number(item.serviceId) === Number(openGroup[0].serviceId)) ? (openGroup[0].manualServiceName || svc(openGroup[0].serviceId)?.name || terms.appt) : `${terms.services.toLowerCase()} diferentes` }`} onClose={() => setOpenGroup(null)}>
{openGroup.map((a) => { const s = a.manualServiceName ? { name: a.manualServiceName, price: a.manualServicePrice || 0, color: 'violet' } : svc(a.serviceId), c = cli(a.clientId), p = pro(a.proId), st = statusMap[a.status]; const clientLabel = a.clientLabel || a.clientName || (c ? c.name : ''); const proLabel = p ? String(p.name || '').replace('Dra. ', '') : ''; const name = [proLabel, clientLabel].filter(Boolean).join(' · ') || 'Sem participante informado'; return ( ); })}
)} {freeSlotsOpen && ( { setFreeSlotsOpen(false); openNewAppt(prefill); }} onClose={() => setFreeSlotsOpen(false)} /> )} {printOpen && setPrintOpen(false)} />}
); } function CalendarPrintModal({ anchor, initialPeriod, appts, clients, services, team, terms, statusMap, user, initialClientId, initialProId, apptHasPro, onClose }) { const DB = window.DB; const [period, setPeriod] = React.useState(['dia', 'semana', 'mes'].includes(initialPeriod) ? initialPeriod : 'semana'); const [referenceDate, setReferenceDate] = React.useState(DB.iso(anchor)); const [scope, setScope] = React.useState(initialClientId ? 'client' : (initialProId ? 'pro' : 'all')); const [clientId, setClientId] = React.useState(initialClientId || (clients[0] ? clients[0].id : 0)); const [proId, setProId] = React.useState(initialProId || (team.find((p) => !p.archived) || {}).id || 0); const [includeCancelled, setIncludeCancelled] = React.useState(false); const activeTeam = team.filter((p) => !p.archived); const dateRange = () => { const target = new Date(referenceDate + 'T12:00'); let start = new Date(target), end = new Date(target); if (period === 'semana') { start.setDate(start.getDate() - start.getDay()); end = new Date(start); end.setDate(end.getDate() + 6); } else if (period === 'mes') { start = new Date(target.getFullYear(), target.getMonth(), 1, 12); end = new Date(target.getFullYear(), target.getMonth() + 1, 0, 12); } return { startISO: DB.iso(start), endISO: DB.iso(end) }; }; const range = dateRange(); const rows = (appts || []).filter((a) => { if (a.date < range.startISO || a.date > range.endISO || (!includeCancelled && a.status === 'cancelado')) return false; if (scope === 'client') return Number(a.clientId) === Number(clientId); if (scope === 'pro') return apptHasPro(a, proId); return true; }).sort((a, b) => String(a.date).localeCompare(String(b.date)) || window.timeToMin(a.start) - window.timeToMin(b.start)); const findClient = (id) => clients.find((c) => Number(c.id) === Number(id)); const findService = (id) => services.find((s) => Number(s.id) === Number(id)); const findPro = (id) => team.find((p) => Number(p.id) === Number(id)); const proNames = (a) => [...new Set([a.proId, ...(a.participants || []).map((p) => p.proId)].map((id) => { const p = findPro(id); return p ? p.name : ''; }).filter(Boolean))].join(', '); const clientName = (a) => (findClient(a.clientId) || {}).name || a.clientLabel || a.clientName || '—'; const serviceName = (a) => a.manualServiceName || (findService(a.serviceId) || {}).name || terms.service; const periodLabel = `${new Date(range.startISO + 'T12:00').toLocaleDateString('pt-BR')} a ${new Date(range.endISO + 'T12:00').toLocaleDateString('pt-BR')}`; const scopeLabel = scope === 'client' ? `${terms.client}: ${(findClient(clientId) || {}).name || 'não selecionado'}` : scope === 'pro' ? `${terms.pro}: ${(findPro(proId) || {}).name || 'não selecionado'}` : 'Todos os clientes e profissionais'; const printReport = () => { const esc = (value) => String(value == null ? '' : value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); const uniqueClients = new Set(rows.map((a) => Number(a.clientId) || clientName(a))).size; const bodyRows = rows.map((a) => { const status = statusMap[a.status] ? statusMap[a.status].label : a.status; return `□${esc(new Date(a.date + 'T12:00').toLocaleDateString('pt-BR'))}${esc(a.start)}–${esc(window.apptEnd(a))}${esc(clientName(a))}${esc(proNames(a) || '—')}${esc(serviceName(a))}${esc(status)}`; }).join(''); const popup = window.open('', '_blank', 'width=1100,height=800'); if (!popup) return; popup.document.write(`Agenda - ${esc(periodLabel)}

${esc(user.tenantName || 'Agenda')}

${esc(periodLabel)} · ${esc(scopeLabel)}${includeCancelled ? ' · incluindo cancelados' : ''}
${rows.length} agendamento${rows.length === 1 ? '' : 's'}${uniqueClients} ${esc(terms.clients.toLowerCase())}
${bodyRows || ``}
DataHorário${esc(terms.client)}${esc(terms.pro)}${esc(terms.service)}Status
Nenhum agendamento encontrado neste filtro.