"use client"; import React, { useEffect, useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { FileDown, BarChart2, Users, CalendarCheck } from "lucide-react"; import jsPDF from "jspdf"; import html2canvas from "html2canvas"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import { countAppointmentsToday, getAppointmentsByDateRange, listarAgendamentos, buscarMedicosPorIds, buscarPacientesPorIds, } from "@/lib/api"; // ============================================================================ // Constants // ============================================================================ const FALLBACK_MEDICOS = [ { 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 }, ]; // ============================================================================ // Helper Functions // ============================================================================ async function exportPDF(title: string, content: string, chartElementId?: string) { const doc = new jsPDF(); let yPosition = 15; // Add title doc.setFontSize(16); doc.setFont(undefined, "bold"); doc.text(title, 15, yPosition); yPosition += 10; // Add description/content doc.setFontSize(11); doc.setFont(undefined, "normal"); const contentLines = doc.splitTextToSize(content, 180); doc.text(contentLines, 15, yPosition); yPosition += contentLines.length * 5 + 15; // Capture chart if chartElementId is provided if (chartElementId) { try { const chartElement = document.getElementById(chartElementId); if (chartElement) { // Create a canvas from the chart element const canvas = await html2canvas(chartElement, { backgroundColor: "#ffffff", scale: 2, logging: false, }); // Convert canvas to image const imgData = canvas.toDataURL("image/png"); const imgWidth = 180; const imgHeight = (canvas.height * imgWidth) / canvas.width; // Add image to PDF doc.addImage(imgData, "PNG", 15, yPosition, imgWidth, imgHeight); yPosition += imgHeight + 10; } } catch (error) { console.error("Error capturing chart:", error); doc.text("(Erro ao capturar gráfico)", 15, yPosition); yPosition += 10; } } doc.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`); } // ============================================================================ // Main Component // ============================================================================ export default function RelatoriosPage() { // State const [metricsState, setMetricsState] = useState>([]); const [consultasData, setConsultasData] = useState>([]); const [pacientesTop, setPacientesTop] = useState>([]); const [medicosTop, setMedicosTop] = useState(FALLBACK_MEDICOS); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Data Loading useEffect(() => { let mounted = true; async function load() { setLoading(true); try { // Fetch appointments let appointments: any[] = []; try { appointments = await listarAgendamentos( "select=patient_id,doctor_id,scheduled_at,status&order=scheduled_at.desc&limit=1000" ); } catch (e) { console.warn("[relatorios] listarAgendamentos failed, using fallback", e); appointments = await getAppointmentsByDateRange(30).catch(() => []); } // Fetch today's appointments count let appointmentsToday = 0; try { appointmentsToday = await countAppointmentsToday().catch(() => 0); } catch (e) { appointmentsToday = 0; } if (!mounted) return; // ===== Build Consultas Chart (last 30 days) ===== const daysCount = 30; const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const startTs = start.getTime() - (daysCount - 1) * 86400000; 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 }; } 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 } } setConsultasData(Object.values(dayBuckets)); // ===== Aggregate Counts ===== const patientCounts: Record = {}; const doctorCounts: Record = {}; const doctorNoShowCounts: Record = {}; for (const a of appts) { 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; if (String(a.status || "").toLowerCase() === "no_show" || String(a.status || "").toLowerCase() === "no-show") { doctorNoShowCounts[did] = (doctorNoShowCounts[did] || 0) + 1; } } } // ===== Top 5 Patients & Doctors ===== 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([]), ]); // ===== Build Patient List ===== 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 }; }); // ===== Build Doctor List ===== 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 }; }); // ===== Update State ===== setPacientesTop(pacientesList); setMedicosTop(medicosList.length ? medicosList : FALLBACK_MEDICOS); setMetricsState([ { label: "Atendimentos", value: appointmentsToday ?? 0, icon: }, ] as any); } catch (err: any) { console.error("[relatorios] error loading data:", 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: 1 }).map((_, i) => (
)) ) : ( metricsState.map((m) => (
{m.icon} {m.value} {m.label}
)) )}
{/* Consultas Chart */}

Consultas por Período

{loading ? (
Carregando dados...
) : (
)}
{/* 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
); }