"use client"; import React, { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react"; import jsPDF from "jspdf"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts"; import { countTotalPatients, countTotalDoctors, countAppointmentsToday, getAppointmentsByDateRange, listarAgendamentos, getUpcomingAppointments, getNewUsersLastDays, getPendingReports, buscarMedicosPorIds, buscarPacientesPorIds, } from "@/lib/api"; // Dados fictícios para demonstração const metricas = [ { label: "Atendimentos", value: 1240, icon: }, { label: "Absenteísmo", value: "7,2%", icon: }, { label: "Satisfação", value: "Dados não foram disponibilizados", icon: }, { label: "Faturamento (Mês)", value: "R$ 45.000", icon: }, { label: "No-show", value: "5,1%", icon: }, ]; const consultasPorPeriodo = [ { periodo: "Jan", consultas: 210 }, { periodo: "Fev", consultas: 180 }, { periodo: "Mar", consultas: 250 }, { periodo: "Abr", consultas: 230 }, { periodo: "Mai", consultas: 270 }, { periodo: "Jun", consultas: 220 }, ]; const faturamentoMensal = [ { mes: "Jan", valor: 35000 }, { mes: "Fev", valor: 29000 }, { mes: "Mar", valor: 42000 }, { mes: "Abr", valor: 38000 }, { mes: "Mai", valor: 45000 }, { mes: "Jun", valor: 41000 }, ]; const taxaNoShow = [ { mes: "Jan", noShow: 6.2 }, { mes: "Fev", noShow: 5.8 }, { mes: "Mar", noShow: 4.9 }, { mes: "Abr", noShow: 5.5 }, { mes: "Mai", noShow: 5.1 }, { mes: "Jun", noShow: 4.7 }, ]; // pacientesMaisAtendidos static list removed — data will be fetched from the API const medicosMaisProdutivos = [ { nome: "Dr. Carlos Andrade", consultas: 62 }, { nome: "Dra. Paula Silva", consultas: 58 }, { nome: "Dr. João Pedro", consultas: 54 }, { nome: "Dra. Marina Costa", consultas: 51 }, ]; const convenios = [ { nome: "Unimed", valor: 18000 }, { nome: "Bradesco", valor: 12000 }, { nome: "SulAmérica", valor: 9000 }, { nome: "Particular", valor: 15000 }, ]; const performancePorMedico = [ { nome: "Dr. Carlos Andrade", consultas: 62, absenteismo: 4.8 }, { nome: "Dra. Paula Silva", consultas: 58, absenteismo: 6.1 }, { nome: "Dr. João Pedro", consultas: 54, absenteismo: 7.5 }, { nome: "Dra. Marina Costa", consultas: 51, absenteismo: 5.2 }, ]; const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"]; function exportPDF(title: string, content: string) { const doc = new jsPDF(); doc.text(title, 10, 10); doc.text(content, 10, 20); doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`); } export default function RelatoriosPage() { // Local state that will be replaced by API data when available // Start with empty data to avoid showing fictitious frontend data while loading const [metricsState, setMetricsState] = useState>([]); const [consultasData, setConsultasData] = useState>([]); const [faturamentoData, setFaturamentoData] = useState>([]); const [taxaNoShowState, setTaxaNoShowState] = useState>([]); const [pacientesTop, setPacientesTop] = useState>([]); const [medicosTop, setMedicosTop] = useState(medicosMaisProdutivos); const [medicosPerformance, setMedicosPerformance] = useState>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [conveniosData, setConveniosData] = useState>(convenios); useEffect(() => { let mounted = true; async function load() { setLoading(true); try { // Fetch counts in parallel, then try to fetch a larger appointments list via listarAgendamentos. // If listarAgendamentos fails (for example: unauthenticated), fall back to getAppointmentsByDateRange(30). const [patientsCount, doctorsCount, appointmentsToday] = await Promise.all([ countTotalPatients().catch(() => 0), countTotalDoctors().catch(() => 0), countAppointmentsToday().catch(() => 0), ]); let appointments: any[] = []; try { // Try to get a larger set of appointments (up to 1000) to compute top patients // select=patient_id,doctor_id,scheduled_at,status to reduce payload // include insurance_provider so we can aggregate convênios client-side appointments = await listarAgendamentos('select=patient_id,doctor_id,scheduled_at,status,insurance_provider&order=scheduled_at.desc&limit=1000'); } catch (e) { // Fallback to the smaller helper if listarAgendamentos cannot be used (e.g., no auth token) console.warn('[relatorios] listarAgendamentos falhou, usando getAppointmentsByDateRange fallback', e); appointments = await getAppointmentsByDateRange(30).catch(() => []); } if (!mounted) return; // Update top metrics card setMetricsState([ { label: "Atendimentos", value: appointmentsToday ?? 0, icon: }, { label: "Absenteísmo", value: "—", icon: }, { label: "Satisfação", value: "Dados não foram disponibilizados", icon: }, { label: "Faturamento (Mês)", value: "—", icon: }, { label: "No-show", value: "—", icon: }, ]); // Build last 30 days series for consultas const daysCount = 30; const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const startTs = start.getTime() - (daysCount - 1) * 86400000; // include today const dayBuckets: Record = {}; for (let i = 0; i < daysCount; i++) { const d = new Date(startTs + i * 86400000); const iso = d.toISOString().split("T")[0]; const periodo = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`; dayBuckets[iso] = { periodo, consultas: 0 }; } // Count appointments per day const appts = Array.isArray(appointments) ? appointments : []; for (const a of appts) { try { const iso = (a.scheduled_at || '').toString().split('T')[0]; if (iso && dayBuckets[iso]) dayBuckets[iso].consultas += 1; } catch (e) { // ignore malformed } } const consultasArr = Object.values(dayBuckets); setConsultasData(consultasArr); // Estimate monthly faturamento for last 6 months using doctor.valor_consulta when available const monthsBack = 6; const monthMap: Record = {}; const nowMonth = new Date(now.getFullYear(), now.getMonth(), 1); const monthKeys: string[] = []; for (let i = monthsBack - 1; i >= 0; i--) { const m = new Date(nowMonth.getFullYear(), nowMonth.getMonth() - i, 1); const key = `${m.getFullYear()}-${String(m.getMonth() + 1).padStart(2, '0')}`; monthKeys.push(key); monthMap[key] = { mes: m.toLocaleString('pt-BR', { month: 'short' }), valor: 0, totalAppointments: 0, noShowCount: 0 }; } // Filter appointments within monthsBack and group const apptsForMonths = appts.filter((a) => { try { const d = new Date(a.scheduled_at); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; return key in monthMap; } catch (e) { return false; } }); // Collect unique doctor ids to fetch valor_consulta in bulk const doctorIds = Array.from(new Set(apptsForMonths.map((a: any) => String(a.doctor_id).trim()).filter(Boolean))); const doctors = doctorIds.length ? await buscarMedicosPorIds(doctorIds) : []; const doctorMap = new Map(); for (const d of doctors) doctorMap.set(String(d.id), d); for (const a of apptsForMonths) { try { const d = new Date(a.scheduled_at); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; const doc = doctorMap.get(String(a.doctor_id)); const price = doc && doc.valor_consulta ? Number(doc.valor_consulta) : 0; monthMap[key].valor += price; monthMap[key].totalAppointments += 1; if (String(a.status || '').toLowerCase() === 'no_show' || String(a.status || '').toLowerCase() === 'no-show') { monthMap[key].noShowCount += 1; } } catch (e) {} } const faturamentoArr = monthKeys.map((k) => ({ mes: monthMap[k].mes, valor: Math.round(monthMap[k].valor) })); setFaturamentoData(faturamentoArr); // Taxa no-show per month const taxaArr = monthKeys.map((k) => { const total = monthMap[k].totalAppointments || 0; const noShow = monthMap[k].noShowCount || 0; const pct = total ? Number(((noShow / total) * 100).toFixed(1)) : 0; return { mes: monthMap[k].mes, noShow: pct }; }); setTaxaNoShowState(taxaArr); // Top patients and doctors (by number of appointments in the period) const patientCounts: Record = {}; const doctorCounts: Record = {}; const doctorNoShowCounts: Record = {}; for (const a of apptsForMonths) { if (a.patient_id) patientCounts[String(a.patient_id)] = (patientCounts[String(a.patient_id)] || 0) + 1; if (a.doctor_id) { const did = String(a.doctor_id); doctorCounts[did] = (doctorCounts[did] || 0) + 1; const status = String(a.status || '').toLowerCase(); if (status === 'no_show' || status === 'no-show') doctorNoShowCounts[did] = (doctorNoShowCounts[did] || 0) + 1; } } const topPatientIds = Object.entries(patientCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map((x) => x[0]); const topDoctorIds = Object.entries(doctorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map((x) => x[0]); const [patientsFetched, doctorsFetched] = await Promise.all([ topPatientIds.length ? buscarPacientesPorIds(topPatientIds) : Promise.resolve([]), topDoctorIds.length ? buscarMedicosPorIds(topDoctorIds) : Promise.resolve([]), ]); const pacientesList = topPatientIds.map((id) => { const p = (patientsFetched || []).find((x: any) => String(x.id) === String(id)); return { nome: p ? p.full_name : id, consultas: patientCounts[id] || 0 }; }); const medicosList = topDoctorIds.map((id) => { const m = (doctorsFetched || []).find((x: any) => String(x.id) === String(id)); return { nome: m ? m.full_name : id, consultas: doctorCounts[id] || 0 }; }); // Build performance list (consultas + absenteísmo) const perfIds = Object.keys(doctorCounts).sort((a, b) => (doctorCounts[b] || 0) - (doctorCounts[a] || 0)).slice(0, 5); const perfDoctors = (doctorsFetched && doctorsFetched.length) ? doctorsFetched : doctors; const perfList = perfIds.map((id) => { const d = (perfDoctors || []).find((x: any) => String(x.id) === String(id)); const consultas = doctorCounts[id] || 0; const noShow = doctorNoShowCounts[id] || 0; const absenteismo = consultas ? Number(((noShow / consultas) * 100).toFixed(1)) : 0; return { nome: d ? d.full_name : id, consultas, absenteismo }; }); // Use fetched list (may be empty) — do not fall back to static data for patients, but keep fallback for medicosTop setPacientesTop(pacientesList); setMedicosTop(medicosList.length ? medicosList : medicosMaisProdutivos); setMedicosPerformance(perfList.length ? perfList.slice(0,5) : performancePorMedico.map((p) => ({ nome: p.nome, consultas: p.consultas, absenteismo: p.absenteismo })).slice(0,5)); // Aggregate convênios (insurance providers) from appointments in the period try { const providerCounts: Record = {}; for (const a of apptsForMonths) { let prov: any = a?.insurance_provider ?? a?.insuranceProvider ?? a?.insurance ?? ''; // If provider is an object, try to extract a human-friendly name if (prov && typeof prov === 'object') prov = prov.name || prov.full_name || prov.title || ''; prov = String(prov || '').trim(); const key = prov || 'Não disponibilizado'; providerCounts[key] = (providerCounts[key] || 0) + 1; } let conveniosArr = Object.entries(providerCounts).map(([nome, valor]) => ({ nome, valor })); if (!conveniosArr.length) { // No provider info at all — present a single bucket showing the total count as 'Não disponibilizado' conveniosArr = [{ nome: 'Não disponibilizado', valor: apptsForMonths.length }]; } else { // Sort and keep top 5, group the rest into 'Outros' conveniosArr.sort((a, b) => b.valor - a.valor); if (conveniosArr.length > 5) { const top = conveniosArr.slice(0, 5); const others = conveniosArr.slice(5).reduce((s, c) => s + c.valor, 0); top.push({ nome: 'Outros', valor: others }); conveniosArr = top; } } setConveniosData(conveniosArr); } catch (e) { // keep existing static conveniosData if something goes wrong console.warn('[relatorios] erro ao agregar convênios', e); } // Update metrics cards with numbers we fetched setMetricsState([ { label: "Atendimentos", value: appointmentsToday ?? 0, icon: }, { label: "Absenteísmo", value: '—', icon: }, { label: "Satisfação", value: 'Dados não foram disponibilizados', icon: }, { label: "Faturamento (Mês)", value: `R$ ${faturamentoArr[faturamentoArr.length - 1]?.valor ?? 0}`, icon: }, { label: "No-show", value: `${taxaArr[taxaArr.length - 1]?.noShow ?? 0}%`, icon: }, ] as any); } catch (err: any) { console.error('[relatorios] erro ao carregar dados', err); if (mounted) setError(err?.message ?? String(err)); } finally { if (mounted) setLoading(false); } } load(); return () => { mounted = false; }; }, []); return (

Dashboard Executivo de Relatórios

{/* Métricas principais */}
{loading ? ( // simple skeletons while loading to avoid showing fake data Array.from({ length: 5 }).map((_, i) => (
)) ) : ( metricsState.map((m) => (
{m.icon} {m.value} {m.label}
)) )}
{/* Gráficos e Relatórios */}
{/* Consultas realizadas por período */}

Consultas por Período

{loading ? (
Carregando dados...
) : ( )}
{/* Faturamento mensal/anual */}

Faturamento Mensal

{loading ? (
Carregando dados...
) : ( )}
{/* Taxa de no-show */}

Taxa de No-show

{loading ? (
Carregando dados...
) : ( )}
{/* Indicadores de satisfação */}

Satisfação dos Pacientes

Dados não foram disponibilizados Índice de satisfação geral
{/* Pacientes mais atendidos */}

Pacientes Mais Atendidos

{loading ? ( ) : pacientesTop && pacientesTop.length ? ( pacientesTop.map((p: { nome: string; consultas: number }) => ( )) ) : ( )}
Paciente Consultas
Carregando pacientes...
{p.nome} {p.consultas}
Nenhum paciente encontrado
{/* Médicos mais produtivos */}

Médicos Mais Produtivos

{loading ? ( ) : medicosTop && medicosTop.length ? ( medicosTop.map((m) => ( )) ) : ( )}
Médico Consultas
Carregando médicos...
{m.nome} {m.consultas}
Nenhum médico encontrado
{/* Análise de convênios */}

Análise de Convênios

{loading ? (
Carregando dados...
) : ( {conveniosData.map((entry, index) => ( ))} )}
{/* Performance por médico */}

Performance por Médico

{(loading ? performancePorMedico : medicosPerformance).map((m) => ( ))}
Médico Consultas Absenteísmo (%)
{m.nome} {m.consultas} {m.absenteismo}
); }