// ============================================================ // JuFlow — Relatórios // ============================================================ const { useState: useStRL, useMemo: useMemoRL } = React; const URL2 = window.JFUtils; const _RCRL = window.Recharts || {}; const BChart = _RCRL.BarChart || (() => null); const BBar = _RCRL.Bar || (() => null); const BX = _RCRL.XAxis || (() => null); const BY = _RCRL.YAxis || (() => null); const BCG = _RCRL.CartesianGrid || (() => null); const BT = _RCRL.Tooltip || (() => null); const BRC = _RCRL.ResponsiveContainer || (({ children, width, height }) => React.createElement('div', { style:{ width, height: height||260 } }, children)); const BPie = _RCRL.PieChart || (() => null); const BPiePie= _RCRL.Pie || (() => null); const BCell = _RCRL.Cell || (() => null); const BLeg = _RCRL.Legend || (() => null); const Relatorios = ({ appointments }) => { const [activeReport, setActiveReport] = useStRL(null); const [period, setPeriod] = useStRL('30'); const cutoff = URL2.addDays(window.JF.TODAY, -Number(period)); const periodAppts = appointments.filter(a => new Date(a.datetime) >= cutoff && a.status === 'completed'); // Report data computations const revenueByDay = useMemoRL(() => { const map = {}; periodAppts.forEach(a => { const d = URL2.formatDate(a.datetime,'dd/MM'); map[d] = (map[d]||0) + a.price; }); return Object.entries(map).slice(-14).map(([day,value]) => ({ day, value })); }, [periodAppts]); const serviceFreq = useMemoRL(() => { const map = {}; periodAppts.forEach(a => { const s = URL2.getService(a.serviceId); if (s) map[s.name] = (map[s.name]||0) + 1; }); return Object.entries(map).sort((a,b)=>b[1]-a[1]).slice(0,8).map(([name,count])=>({ name, count })); }, [periodAppts]); const profPerf = useMemoRL(() => window.JF.PROFESSIONALS.map(p => { const pa = periodAppts.filter(a => a.professionalId === p.id); return { name:p.name.split(' ')[0], sessions:pa.length, revenue:pa.reduce((s,a)=>s+a.price,0), color:p.color }; }).filter(p=>p.sessions>0) , [periodAppts]); const hourlyHeatmap = useMemoRL(() => { const map = {}; periodAppts.forEach(a => { const h = new Date(a.datetime).getHours(); map[h] = (map[h]||0) + 1; }); return Array.from({length:14},(_,i)=>i+7).map(h => ({ hour:`${String(h).padStart(2,'0')}h`, count:map[h]||0 })); }, [periodAppts]); const REPORT_CARDS = [ { id:'faturamento', title:'Faturamento por Período', icon:'DollarSign', color:'#059669', desc:'Receita diária no período selecionado' }, { id:'servicos', title:'Serviços Mais Procurados', icon:'Scissors', color:'#7C3AED', desc:'Ranking de serviços por quantidade de atendimentos' }, { id:'profissionais', title:'Performance por Profissional', icon:'UserCheck', color:'#0891B2', desc:'Sessões e receita por profissional' }, { id:'horarios', title:'Horários Mais Movimentados', icon:'Clock', color:'#FF6B35', desc:'Distribuição de atendimentos por hora do dia' }, { id:'cancelamentos', title:'Taxa de Cancelamento', icon:'XCircle', color:'#EF4444', desc:'Análise de faltas e cancelamentos' }, { id:'retorno', title:'Taxa de Retorno', icon:'RotateCcw', color:'#D97706', desc:'Frequência de retorno dos clientes' }, ]; const RTooltip = ({ active, payload, label }) => { if (!active || !payload?.length) return null; return (

{label}

{URL2.formatCurrency ? URL2.formatCurrency(payload[0]?.value) : payload[0]?.value}

); }; const cancelRate = Math.round((appointments.filter(a=>['cancelled','no_show'].includes(a.status)&&new Date(a.datetime)>=cutoff).length / Math.max(1,appointments.filter(a=>new Date(a.datetime)>=cutoff).length)) * 100); const returnRate = 78; // mock const renderChart = (id) => { switch(id) { case 'faturamento': return ( 'R$'+v} /> } /> ); case 'servicos': return ( [v+' sessões','']} /> ); case 'profissionais': return ( [n==='sessions'?v+' sessões':URL2.formatCurrency(v),n==='sessions'?'Sessões':'Receita']} /> ); case 'horarios': return ( [v+' atendimentos','']} /> ); default: return null; } }; return (
{/* Period selector */}
Período:
{[['7','7 dias'],['30','30 dias'],['60','60 dias'],['90','90 dias']].map(([v,l]) => ( ))}
{periodAppts.length} atendimentos realizados no período
{/* Quick stats */}
s+a.price,0))} icon="TrendingUp" color="#059669" trend={8} trendLabel="vs. período anterior" />
{/* Report cards grid */}
{REPORT_CARDS.map(report => (
setActiveReport(activeReport===report.id ? null : report.id)} >
{report.title}
{report.desc}
{activeReport === report.id ? (
{renderChart(report.id) || (

Gráfico em desenvolvimento

)}
) : (
Ver relatório
)}
))}
); }; Object.assign(window, { Relatorios });