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 1/4] 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}
-
-
-
); } -- 2.47.2 From 2cc3687628cd93c73f850271e467cc7e5990cc50 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 22:58:39 -0300 Subject: [PATCH 2/4] fix-delete-appoiment --- .../app/(main-routes)/consultas/page.tsx | 4 +- susconecta/app/paciente/page.tsx | 17 ++- susconecta/lib/api.ts | 126 ++++++++++++++++-- 3 files changed, 135 insertions(+), 12 deletions(-) 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/paciente/page.tsx b/susconecta/app/paciente/page.tsx index ab0e481..2d16f40 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -18,7 +18,7 @@ import Link from 'next/link' import ProtectedRoute from '@/components/shared/ProtectedRoute' import { useAuth } from '@/hooks/useAuth' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento } from '@/lib/api' +import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento, addDeletedAppointmentId } from '@/lib/api' import { CalendarRegistrationForm } from '@/components/features/forms/calendar-registration-form' import { buscarRelatorioPorId, listarRelatoriosPorMedico } from '@/lib/reports' import { ENV_CONFIG } from '@/lib/env-config' @@ -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) @@ -856,6 +869,8 @@ export default function PacientePage() { if (!ok) return // call API to delete await deletarAgendamento(consulta.id) + // Mark as deleted in cache so it won't appear again + addDeletedAppointmentId(consulta.id) // remove from local list setAppointments((prev) => { if (!prev) return prev diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 05e8390..fe7c217 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1293,7 +1293,23 @@ export async function listarAgendamentos(query?: string): Promise if (!res.ok && res.status === 401) { throw new Error('Não autenticado. Token ausente ou expirado. Faça login novamente.'); } - return await parse(res); + const appointments = await parse(res); + // Filter out soft-deleted appointments (those with cancelled_at set OR status='cancelled' OR in deleted cache) + return appointments.filter((a) => { + const id = String(a.id); + + // Check if in deleted cache + if (deletedAppointmentIds.has(id)) return false; + + // Check cancelled_at field + const cancelled = a.cancelled_at; + if (cancelled && cancelled !== '' && cancelled !== 'null') return false; + + // Check status field + if (a.status && String(a.status).toLowerCase() === 'cancelled') return false; + + return true; + }); } /** @@ -1309,13 +1325,68 @@ export async function buscarAgendamentoPorId(id: string | number, select: string const url = `${REST}/appointments?id=eq.${encodeURIComponent(sId)}&${params.toString()}`; const headers = baseHeaders(); const arr = await fetchWithFallback(url, headers); - if (arr && arr.length) return arr[0]; + // Filter out soft-deleted appointments (those with cancelled_at set OR status='cancelled' OR in deleted cache) + const active = arr?.filter((a) => { + const id = String(a.id); + + // Check if in deleted cache + if (deletedAppointmentIds.has(id)) return false; + + // Check cancelled_at field + const cancelled = a.cancelled_at; + if (cancelled && cancelled !== '' && cancelled !== 'null') return false; + + // Check status field + if (a.status && String(a.status).toLowerCase() === 'cancelled') return false; + + return true; + }); + if (active && active.length) return active[0]; throw new Error('404: Agendamento não encontrado'); } /** * Deleta um agendamento por ID (DELETE /rest/v1/appointments?id=eq.) */ +// Track deleted appointment IDs in localStorage to persist across page reloads +const DELETED_APPOINTMENTS_KEY = 'deleted_appointment_ids'; + +function getDeletedAppointmentIds(): Set { + try { + if (typeof window === 'undefined') return new Set(); + const stored = localStorage.getItem(DELETED_APPOINTMENTS_KEY); + if (stored) { + const ids = JSON.parse(stored); + return new Set(Array.isArray(ids) ? ids : []); + } + } catch (e) { + console.warn('[API] Erro ao ler deleted appointments do localStorage', e); + } + return new Set(); +} + +function saveDeletedAppointmentIds(ids: Set) { + try { + if (typeof window === 'undefined') return; + localStorage.setItem(DELETED_APPOINTMENTS_KEY, JSON.stringify(Array.from(ids))); + } catch (e) { + console.warn('[API] Erro ao salvar deleted appointments no localStorage', e); + } +} + +const deletedAppointmentIds = getDeletedAppointmentIds(); + +export function addDeletedAppointmentId(id: string | number) { + const idStr = String(id); + deletedAppointmentIds.add(idStr); + saveDeletedAppointmentIds(deletedAppointmentIds); +} + +export function clearDeletedAppointments() { + deletedAppointmentIds.clear(); + saveDeletedAppointmentIds(deletedAppointmentIds); +} + export async function deletarAgendamento(id: string | number): Promise { if (!id) throw new Error('ID do agendamento é obrigatório'); const url = `${REST}/appointments?id=eq.${encodeURIComponent(String(id))}`; @@ -1325,9 +1396,11 @@ export async function deletarAgendamento(id: string | number): Promise { headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), }); - if (res.status === 204) return; - // Some deployments may return 200 with a representation — accept that too - if (res.status === 200) return; + if (res.status === 204 || res.status === 200) { + // Mark as deleted locally AND persist in localStorage + addDeletedAppointmentId(id); + return; + } // Otherwise surface a friendly error using parse() await parse(res as Response); } @@ -3018,7 +3091,8 @@ export async function countAppointmentsToday(): Promise { const today = new Date().toISOString().split('T')[0]; const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]; - const url = `${REST}/appointments?scheduled_at=gte.${today}T00:00:00&scheduled_at=lt.${tomorrow}T00:00:00&select=id&limit=1`; + // Filter out soft-deleted appointments: cancelled_at is null + const url = `${REST}/appointments?scheduled_at=gte.${today}T00:00:00&scheduled_at=lt.${tomorrow}T00:00:00&cancelled_at=is.null&select=id&limit=1`; const res = await fetch(url, { headers: { ...baseHeaders(), @@ -3045,9 +3119,25 @@ export async function getUpcomingAppointments(limit: number = 10): Promise(res); + const appointments = await parse(res); + // Filter out soft-deleted appointments (those with cancelled_at set OR status='cancelled' OR in deleted cache) + return appointments.filter((a) => { + const id = String(a.id); + + // Check if in deleted cache + if (deletedAppointmentIds.has(id)) return false; + + // Check cancelled_at field + const cancelled = a.cancelled_at; + if (cancelled && cancelled !== '' && cancelled !== 'null') return false; + + // Check status field + if (a.status && String(a.status).toLowerCase() === 'cancelled') return false; + + return true; + }); } catch (err) { console.error('[getUpcomingAppointments] Erro:', err); return []; @@ -3063,9 +3153,25 @@ export async function getAppointmentsByDateRange(days: number = 14): Promise(res); + const appointments = await parse(res); + // Filter out soft-deleted appointments (those with cancelled_at set OR status='cancelled' OR in deleted cache) + return appointments.filter((a) => { + const id = String(a.id); + + // Check if in deleted cache + if (deletedAppointmentIds.has(id)) return false; + + // Check cancelled_at field + const cancelled = a.cancelled_at; + if (cancelled && cancelled !== '' && cancelled !== 'null') return false; + + // Check status field + if (a.status && String(a.status).toLowerCase() === 'cancelled') return false; + + return true; + }); } catch (err) { console.error('[getAppointmentsByDateRange] Erro:', err); return []; -- 2.47.2 From e22ad305c47ee6d5ef6320a14e32de509e7ba6d8 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 23:22:58 -0300 Subject: [PATCH 3/4] fix-response-on-patient-page --- susconecta/app/paciente/page.tsx | 278 ++++++++++++++++--------------- 1 file changed, 140 insertions(+), 138 deletions(-) diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 2d16f40..0b81ac3 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -416,31 +416,31 @@ export default function PacientePage() { }, []) return ( -
- -
-
- +
+ +
+
+
{/* 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) : '-')}
@@ -713,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' : ''}
@@ -793,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 @@ -862,7 +863,7 @@ export default function PacientePage() { + )}
{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.
) ) : ( (() => { @@ -1430,31 +1431,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}
+
@@ -1474,30 +1475,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 */}
-