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)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx index 0ce1acc..8629de1 100644 --- a/susconecta/app/(main-routes)/consultas/page.tsx +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -55,7 +55,7 @@ import { } from "@/components/ui/select"; import { mockProfessionals } from "@/lib/mocks/appointment-mocks"; -import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento, buscarAgendamentoPorId, deletarAgendamento } from "@/lib/api"; +import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento, buscarAgendamentoPorId, deletarAgendamento, addDeletedAppointmentId } from "@/lib/api"; import { CalendarRegistrationForm } from "@/components/features/forms/calendar-registration-form"; const formatDate = (date: string | Date) => { @@ -140,6 +140,8 @@ export default function ConsultasPage() { try { // call server DELETE await deletarAgendamento(appointmentId); + // Mark as deleted in cache so it won't appear again + addDeletedAppointmentId(appointmentId); // remove from UI setAppointments((prev) => prev.filter((a) => a.id !== appointmentId)); // also update originalAppointments cache 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}
-
-
-
); } diff --git a/susconecta/app/laudos/[id]/page.tsx b/susconecta/app/laudos/[id]/page.tsx index 4677027..2d62bb4 100644 --- a/susconecta/app/laudos/[id]/page.tsx +++ b/susconecta/app/laudos/[id]/page.tsx @@ -96,6 +96,136 @@ export default function LaudoPage() { window.print() } + const handleDownloadPDF = async () => { + if (!report) return + + try { + // Para simplificar, vamos usar jsPDF com html2canvas para capturar o conteúdo + const { jsPDF } = await import('jspdf') + const html2canvas = await import('html2canvas').then((m) => m.default) + + // Criar um elemento temporário com o conteúdo + const element = document.createElement('div') + element.style.position = 'absolute' + element.style.left = '-9999px' + element.style.width = '210mm' // A4 width + element.style.padding = '20mm' + element.style.backgroundColor = 'white' + element.style.fontFamily = 'Arial, sans-serif' + + // Extrair informações + const reportDate = new Date(report.report_date || report.created_at || Date.now()).toLocaleDateString('pt-BR') + const cid = report.cid ?? report.cid_code ?? report.cidCode ?? report.cie ?? '' + const exam = report.exam ?? report.exame ?? report.especialidade ?? report.report_type ?? '' + const diagnosis = report.diagnosis ?? report.diagnostico ?? report.diagnosis_text ?? report.diagnostico_text ?? '' + const conclusion = report.conclusion ?? report.conclusao ?? report.conclusion_text ?? report.conclusao_text ?? '' + const notesText = report.content ?? report.body ?? report.conteudo ?? report.notes ?? report.observacoes ?? '' + + // Extrair nome do médico + let doctorName = '' + if (doctor) { + doctorName = doctor.full_name || doctor.name || doctor.fullName || doctor.doctor_name || '' + } + if (!doctorName) { + const rd = report as any + const tryKeys = [ + 'doctor_name', 'doctor_full_name', 'doctorFullName', 'doctorName', + 'requested_by_name', 'requested_by', 'requester_name', 'requester', + 'created_by_name', 'created_by', 'executante', 'executante_name', + ] + for (const k of tryKeys) { + const v = rd[k] + if (v !== undefined && v !== null && String(v).trim() !== '') { + doctorName = String(v) + break + } + } + } + + // Montar HTML do documento + element.innerHTML = ` +
+

RELATÓRIO MÉDICO

+

Data: ${reportDate}

+ ${doctorName ? `

Profissional: ${doctorName}

` : ''} +
+ +
+
+ ${cid ? `

CID

${cid}

` : ''} + ${exam ? `

EXAME / TIPO

${exam}

` : ''} +
+
+ + ${diagnosis ? ` +
+

DIAGNÓSTICO

+

${diagnosis}

+
+ ` : ''} + + ${conclusion ? ` +
+

CONCLUSÃO

+

${conclusion}

+
+ ` : ''} + + ${notesText ? ` +
+

NOTAS DO PROFISSIONAL

+

${notesText}

+
+ ` : ''} + +
+ Documento gerado em ${new Date().toLocaleString('pt-BR')} +
+ ` + + document.body.appendChild(element) + + // Capturar como canvas + const canvas = await html2canvas(element, { + scale: 2, + useCORS: true, + backgroundColor: '#ffffff', + }) + + document.body.removeChild(element) + + // Converter para PDF + const imgData = canvas.toDataURL('image/png') + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + }) + + const imgWidth = 210 // A4 width in mm + const pageHeight = 297 // A4 height in mm + const imgHeight = (canvas.height * imgWidth) / canvas.width + let heightLeft = imgHeight + + let position = 0 + + while (heightLeft >= 0) { + pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight) + heightLeft -= pageHeight + position -= pageHeight + if (heightLeft > 0) { + pdf.addPage() + } + } + + // Download + pdf.save(`laudo-${reportDate}-${doctorName || 'profissional'}.pdf`) + } catch (error) { + console.error('Erro ao gerar PDF:', error) + alert('Erro ao gerar PDF. Tente novamente.') + } + } + if (loading) { return ( @@ -158,7 +288,7 @@ export default function LaudoPage() { : 'bg-gradient-to-br from-slate-50 to-slate-100' }`}> {/* Header Toolbar */} -
{/* Main Content Area */} -
+
{/* Document Container */} -
{/* Document Content */} -
+
{/* Title */}
- -
-
- +
+ +
+
+
{/* rótulo e número com mesma fonte e mesmo tamanho (harmônico) */} - + {strings.proximaConsulta} - + {loading ? strings.carregando : (nextAppt ?? '-')}
- -
-
- + +
+
+
- + {strings.ultimosExames} - + {loading ? strings.carregando : (examsCount !== null ? String(examsCount) : '-')}
@@ -610,6 +610,19 @@ export default function PacientePage() { hora: sched ? sched.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '', status: a.status ? String(a.status) : 'Pendente', } + }).filter((consulta: any) => { + // Filter out cancelled appointments (those with cancelled_at set OR status='cancelled') + const raw = rows.find((r: any) => String(r.id) === String(consulta.id)); + if (!raw) return false; + + // Check cancelled_at field + const cancelled = raw.cancelled_at; + if (cancelled && cancelled !== '' && cancelled !== 'null') return false; + + // Check status field + if (raw.status && String(raw.status).toLowerCase() === 'cancelled') return false; + + return true; }) setDoctorsMap(doctorsMap) @@ -700,16 +713,16 @@ export default function PacientePage() { return (
{/* Hero Section */} -
-
-
-

Agende sua próxima consulta

-

Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.

+
+
+
+

Agende sua próxima consulta

+

Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.

-
+
-
{/* Consultas Agendadas Section */} -
-
+
+
-

Suas Consultas Agendadas

-

Gerencie suas consultas confirmadas, pendentes ou canceladas.

+

Suas Consultas Agendadas

+

Gerencie suas consultas confirmadas, pendentes ou canceladas.

{/* Date Navigation */} -
+
- {formatDatePt(currentDate)} + {formatDatePt(currentDate)} {isSelectedDateToday && ( )}
-
+
+ {_todaysAppointments.length} consulta{_todaysAppointments.length !== 1 ? 's' : ''} agendada{_todaysAppointments.length !== 1 ? 's' : ''}
@@ -780,50 +794,50 @@ export default function PacientePage() { const todays = _todaysAppointments if (!todays || todays.length === 0) { return ( -
-
- +
+
+
-

Nenhuma consulta agendada para este dia

-

Use a busca acima para marcar uma nova consulta ou navegue entre os dias.

+

Nenhuma consulta agendada para este dia

+

Use a busca acima para marcar uma nova consulta ou navegue entre os dias.

) } return todays.map((consulta: any) => (
-
+
{/* Doctor Info */} -
+
-
-
- +
+
+ {consulta.medico}
-

+

{consulta.especialidade} - + {consulta.local}

{/* Time */} -
- - {consulta.hora} +
+ + {consulta.hora}
{/* Status Badge */}
- setSelectedAppointment(consulta)} > Detalhes @@ -849,13 +863,15 @@ export default function PacientePage() { + )}
+ + {/* Date filter and sort controls */} +
+ {/* Sort buttons */} +
+ + +
+ + {/* Date picker */} +
+ { setFilterDate(e.target.value); setReportsPage(1) }} + className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 border border-border rounded bg-background" + /> + {filterDate && ( + + )} +
+
+ {loadingReports ? ( -
{strings.carregando}
+
{strings.carregando}
) : reportsError ? ( -
{reportsError}
+
{reportsError}
) : (!reports || reports.length === 0) ? ( -
Nenhum laudo encontrado para este paciente.
+
Nenhum laudo encontrado para este paciente.
) : (filteredReports.length === 0) ? ( searchingRemote ? ( -
Buscando laudo...
+
Buscando laudo...
) : ( -
Nenhum laudo corresponde à pesquisa.
+
Nenhum laudo corresponde à pesquisa.
) ) : ( (() => { @@ -1415,31 +1507,31 @@ export default function PacientePage() { return ( <> {pageItems.map((r) => ( -
-
+
+
{(() => { const maybeId = r?.doctor_id || r?.created_by || r?.doctor || null if (resolvingDoctors && maybeId && !doctorsMap[String(maybeId)]) { - return
{strings.carregando}
+ return
{strings.carregando}
} - return
{reportTitle(r)}
+ return
{reportTitle(r)}
})()} -
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
+
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
-
- - +
+ +
))} {/* Pagination controls */} -
-
Mostrando {Math.min(start+1, total)}–{Math.min(end, total)} de {total}
-
- -
{page} / {totalPages}
- +
+
Mostrando {Math.min(start+1, total)}–{Math.min(end, total)} de {total}
+
+ +
{page} / {totalPages}
+
@@ -1459,30 +1551,31 @@ export default function PacientePage() { function Perfil() { return ( -
+
{/* Header com Título e Botão */} -
-
-

Meu Perfil

-

Bem-vindo à sua área exclusiva.

+
+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

{!isEditingProfile ? ( ) : ( -
+
{/* Grid de 3 colunas (2 + 1) */} -
+
{/* Coluna Esquerda - Informações Pessoais */} -
+
{/* Informações Pessoais */} -
-

Informações Pessoais

+
+

Informações Pessoais

-
+
{/* Nome Completo */}
-