From dc83db3e7cf606dc8b52aac6f7d05b29c74f3dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?= <166467972+JoaoGustavo-dev@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:44:51 -0300 Subject: [PATCH] fix-report-page --- susconecta/app/(auth)/login-paciente/page.tsx | 127 +----- .../dashboard/relatorios/page.tsx | 419 ++++-------------- 2 files changed, 85 insertions(+), 461 deletions(-) diff --git a/susconecta/app/(auth)/login-paciente/page.tsx b/susconecta/app/(auth)/login-paciente/page.tsx index 67b2329..0927014 100644 --- a/susconecta/app/(auth)/login-paciente/page.tsx +++ b/susconecta/app/(auth)/login-paciente/page.tsx @@ -3,7 +3,6 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { useAuth } from '@/hooks/useAuth' -import { ENV_CONFIG } from '@/lib/env-config' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -55,83 +54,7 @@ export default function LoginPacientePage() { - // --- Auto-cadastro (client-side) --- - const [showRegister, setShowRegister] = useState(false) - const [reg, setReg] = useState({ email: '', full_name: '', phone_mobile: '', cpf: '', birth_date: '' }) - const [regLoading, setRegLoading] = useState(false) - const [regError, setRegError] = useState('') - const [regSuccess, setRegSuccess] = useState('') - - function cleanCpf(cpf: string) { - return String(cpf || '').replace(/\D/g, '') - } - - function validateCPF(cpfRaw: string) { - const cpf = cleanCpf(cpfRaw) - if (!/^\d{11}$/.test(cpf)) return false - if (/^([0-9])\1+$/.test(cpf)) return false - const digits = cpf.split('').map((d) => Number(d)) - const calc = (len: number) => { - let sum = 0 - for (let i = 0; i < len; i++) sum += digits[i] * (len + 1 - i) - const v = (sum * 10) % 11 - return v === 10 ? 0 : v - } - return calc(9) === digits[9] && calc(10) === digits[10] - } - - const handleRegister = async (e?: React.FormEvent) => { - if (e) e.preventDefault() - setRegError('') - setRegSuccess('') - - // client-side validation - if (!reg.email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(reg.email)) return setRegError('Email inválido') - if (!reg.full_name || reg.full_name.trim().length < 3) return setRegError('Nome deve ter ao menos 3 caracteres') - if (!reg.phone_mobile || !/^\d{10,11}$/.test(reg.phone_mobile)) return setRegError('Telefone inválido (10-11 dígitos)') - if (!reg.cpf || !/^\d{11}$/.test(cleanCpf(reg.cpf))) return setRegError('CPF deve conter 11 dígitos') - if (!validateCPF(reg.cpf)) return setRegError('CPF inválido') - - setRegLoading(true) - try { - const url = `${ENV_CONFIG.SUPABASE_URL}/functions/v1/register-patient` - const body = { - email: reg.email, - full_name: reg.full_name, - phone_mobile: reg.phone_mobile, - cpf: cleanCpf(reg.cpf), - // always include redirect to patient landing as requested - redirect_url: 'https://mediconecta-app-liart.vercel.app/' - } as any - if (reg.birth_date) body.birth_date = reg.birth_date - - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json', apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json' }, - body: JSON.stringify(body), - }) - - const json = await res.json().catch(() => null) - if (res.ok) { - setRegSuccess(json?.message ?? 'Cadastro realizado com sucesso! Verifique seu email para acessar a plataforma.') - // clear form but keep email for convenience - setReg({ ...reg, full_name: '', phone_mobile: '', cpf: '', birth_date: '' }) - } else if (res.status === 400) { - setRegError(json?.error ?? json?.message ?? 'Dados inválidos') - } else if (res.status === 409) { - setRegError(json?.error ?? 'CPF ou email já cadastrado') - } else if (res.status === 429) { - setRegError(json?.error ?? 'Rate limit excedido. Tente novamente mais tarde.') - } else { - setRegError(json?.error ?? json?.message ?? `Erro (${res.status})`) - } - } catch (err: any) { - console.error('[REGISTER PACIENTE] erro', err) - setRegError(err?.message ?? String(err)) - } finally { - setRegLoading(false) - } - } + // Auto-cadastro foi removido (UI + client-side endpoint call) return (
@@ -206,53 +129,7 @@ export default function LoginPacientePage() {
-
-
Ainda não tem conta?
- - {showRegister && ( - - - Auto-cadastro de Paciente - - -
-
- - setReg({...reg, full_name: e.target.value})} required /> -
-
- - setReg({...reg, email: e.target.value})} required /> -
-
- - setReg({...reg, phone_mobile: e.target.value.replace(/\D/g,'')})} placeholder="11999998888" required /> -
-
- - setReg({...reg, cpf: e.target.value.replace(/\D/g,'')})} placeholder="12345678901" required /> -
-
- - setReg({...reg, birth_date: e.target.value})} /> -
- - {regError && ( - {regError} - )} - {regSuccess && ( - {regSuccess} - )} - -
- - -
-
-
-
- )} -
+ {/* Auto-cadastro UI removed */} diff --git a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx index a15da37..af905b2 100644 --- a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx +++ b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx @@ -1,332 +1,180 @@ + "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 { FileDown, BarChart2, Users, CalendarCheck } from "lucide-react"; import jsPDF from "jspdf"; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts"; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } 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: }, -]; +// ============================================================================ +// Constants +// ============================================================================ -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 = [ +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 }, ]; -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"]; +// ============================================================================ +// Helper Functions +// ============================================================================ 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`); + doc.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`); } +// ============================================================================ +// Main Component +// ============================================================================ + 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 + // State 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 [medicosTop, setMedicosTop] = useState(FALLBACK_MEDICOS); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [conveniosData, setConveniosData] = useState>(convenios); + // Data Loading 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), - ]); - + // Fetch appointments 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'); + appointments = await listarAgendamentos( + "select=patient_id,doctor_id,scheduled_at,status&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); + 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; - // 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 + // ===== 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; // include today + 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')}`; + 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]; + 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); + setConsultasData(Object.values(dayBuckets)); - // 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) + // ===== Aggregate Counts ===== 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; + + 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; - const status = String(a.status || '').toLowerCase(); - if (status === 'no_show' || status === 'no-show') doctorNoShowCounts[did] = (doctorNoShowCounts[did] || 0) + 1; + if (String(a.status || "").toLowerCase() === "no_show" || String(a.status || "").toLowerCase() === "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]); + // ===== 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 }; }); - // 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 + // ===== Update State ===== 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 + setMedicosTop(medicosList.length ? medicosList : FALLBACK_MEDICOS); 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.at(-1)?.valor ?? 0}`, icon: }, - { label: "No-show", value: `${taxaArr.at(-1)?.noShow ?? 0}%`, icon: }, ] as any); - } catch (err: any) { - console.error('[relatorios] erro ao carregar dados', err); + console.error("[relatorios] error loading data:", err); if (mounted) setError(err?.message ?? String(err)); } finally { if (mounted) setLoading(false); } } - load(); - return () => { mounted = false; }; - }, []); - return ( + 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) => ( + Array.from({ length: 1 }).map((_, i) => (
@@ -344,13 +192,21 @@ export default function RelatoriosPage() { )}
- {/* Gráficos e Relatórios */} -
- {/* Consultas realizadas por período */} + {/* Consultas Chart */} +
-
-

Consultas por Período

- +
+

+ Consultas por Período +

+
{loading ? (
Carregando dados...
@@ -366,62 +222,6 @@ export default function RelatoriosPage() { )}
- - {/* 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 -
-
@@ -462,7 +262,7 @@ export default function RelatoriosPage() { {/* Médicos mais produtivos */}
-

Médicos Mais Produtivos

+

Médicos Mais Produtivos

@@ -493,59 +293,6 @@ export default function RelatoriosPage() {
- -
- {/* 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édicoConsultasAbsenteísmo (%)
{m.nome}{m.consultas}{m.absenteismo}
-
-
-
); }