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
-
-
-
-
-
- )}
-
+ {/* 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
-
-
-
-
-
-
- | Médico |
- Consultas |
- Absenteísmo (%) |
-
-
-
- {(loading ? performancePorMedico : medicosPerformance).map((m) => (
-
- | {m.nome} |
- {m.consultas} |
- {m.absenteismo} |
-
- ))}
-
-
-
-
-
);
}