'use client' import type { ReactNode } from 'react' import { useState, useEffect, useMemo } from 'react' import { useRouter } from 'next/navigation' import Image from 'next/image' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react' import { SimpleThemeToggle } from '@/components/ui/simple-theme-toggle' import { UploadAvatar } from '@/components/ui/upload-avatar' import { useAvatarUrl } from '@/hooks/useAvatarUrl' 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, addDeletedAppointmentId, listarTodosMedicos } 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' import { listarRelatoriosPorPaciente } from '@/lib/reports' // reports are rendered statically for now // Simulação de internacionalização básica const strings = { dashboard: 'Dashboard', consultas: 'Consultas', exames: 'Exames & Laudos', mensagens: 'Mensagens', perfil: 'Perfil', sair: 'Sair', proximaConsulta: 'Próxima Consulta', ultimosExames: 'Últimos Exames', mensagensNaoLidas: 'Mensagens Não Lidas', agendar: 'Agendar', cancelar: 'Cancelar', detalhes: 'Detalhes', adicionarCalendario: 'Adicionar ao calendário', visualizarLaudo: 'Visualizar Laudo', download: 'Download', compartilhar: 'Compartilhar', inbox: 'Caixa de Entrada', enviarMensagem: 'Enviar Mensagem', salvar: 'Salvar', editarPerfil: 'Editar Perfil', consentimentos: 'Consentimentos', notificacoes: 'Preferências de Notificação', vazio: 'Nenhum dado encontrado.', erro: 'Ocorreu um erro. Tente novamente.', carregando: 'Carregando...', sucesso: 'Salvo com sucesso!', erroSalvar: 'Erro ao salvar.', } export default function PacientePage() { const { logout, user } = useAuth() const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'perfil'>('dashboard') // Simulação de loaders, empty states e erro const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null) const handleLogout = async () => { setLoading(true) setError('') try { await logout() } catch { setError(strings.erro) } finally { setLoading(false) } } // Estado para edição do perfil const [isEditingProfile, setIsEditingProfile] = useState(false) const [profileData, setProfileData] = useState({ nome: '', email: user?.email || '', telefone: '', endereco: '', cidade: '', cep: '', biografia: '', id: undefined, foto_url: undefined, }) const [patientId, setPatientId] = useState(null) // Hook para carregar automaticamente o avatar do paciente const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(patientId) // Load authoritative patient row for the logged-in user (prefer user_id lookup) useEffect(() => { let mounted = true const uid = user?.id ?? null const uemail = user?.email ?? null if (!uid && !uemail) return async function loadProfile() { try { setLoading(true) setError('') // 1) exact lookup by user_id on patients table let paciente: any = null if (uid) paciente = await buscarPacientePorUserId(uid) // 2) fallback: search patients by email and prefer a row that has user_id equal to auth id if (!paciente && uemail) { try { const results = await buscarPacientes(uemail) if (results && results.length) { paciente = results.find((r: any) => String(r.user_id) === String(uid)) || results[0] } } catch (e) { console.warn('[PacientePage] buscarPacientes falhou', e) } } // 3) fallback: use getUserInfo() (auth profile) if available if (!paciente) { try { const info = await getUserInfo().catch(() => null) const p = info?.profile ?? null if (p) { // map auth profile to our local shape (best-effort) paciente = { full_name: p.full_name ?? undefined, email: p.email ?? undefined, phone_mobile: p.phone ?? undefined, } } } catch (e) { // ignore } } if (paciente && mounted) { try { if ((paciente as any).id) setPatientId(String((paciente as any).id)) } catch {} const getFirst = (obj: any, keys: string[]) => { if (!obj) return undefined for (const k of keys) { const v = obj[k] if (v !== undefined && v !== null && String(v).trim() !== '') return String(v) } return undefined } const nome = getFirst(paciente, ['full_name','fullName','name','nome','social_name']) || '' const telefone = getFirst(paciente, ['phone_mobile','phone','telefone','mobile']) || '' const rua = getFirst(paciente, ['street','logradouro','endereco','address']) const numero = getFirst(paciente, ['number','numero']) const bairro = getFirst(paciente, ['neighborhood','bairro']) const endereco = rua ? (numero ? `${rua}, ${numero}` : rua) + (bairro ? ` - ${bairro}` : '') : '' const cidade = getFirst(paciente, ['city','cidade','localidade']) || '' const cep = getFirst(paciente, ['cep','postal_code','zip']) || '' const biografia = getFirst(paciente, ['biography','bio','notes']) || '' const emailFromRow = getFirst(paciente, ['email']) || uemail || '' if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente) setProfileData({ nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia }) } } catch (err) { console.warn('[PacientePage] erro ao carregar paciente', err) } finally { if (mounted) setLoading(false) } } loadProfile() return () => { mounted = false } }, [user?.id, user?.email]) // Load authoritative patient row for the logged-in user (prefer user_id lookup) useEffect(() => { let mounted = true const uid = user?.id ?? null const uemail = user?.email ?? null if (!uid && !uemail) return async function loadProfile() { try { setLoading(true) setError('') let paciente: any = null if (uid) paciente = await buscarPacientePorUserId(uid) if (!paciente && uemail) { try { const res = await buscarPacientes(uemail) if (res && res.length) paciente = res.find((r:any) => String((r as any).user_id) === String(uid)) || res[0] } catch (e) { console.warn('[PacientePage] busca por email falhou', e) } } if (paciente && mounted) { try { if ((paciente as any).id) setPatientId(String((paciente as any).id)) } catch {} const getFirst = (obj: any, keys: string[]) => { if (!obj) return undefined for (const k of keys) { const v = obj[k] if (v !== undefined && v !== null && String(v).trim() !== '') return String(v) } return undefined } const nome = getFirst(paciente, ['full_name','fullName','name','nome','social_name']) || profileData.nome const telefone = getFirst(paciente, ['phone_mobile','phone','telefone','mobile']) || profileData.telefone const rua = getFirst(paciente, ['street','logradouro','endereco','address']) const numero = getFirst(paciente, ['number','numero']) const bairro = getFirst(paciente, ['neighborhood','bairro']) const endereco = rua ? (numero ? `${rua}, ${numero}` : rua) + (bairro ? ` - ${bairro}` : '') : profileData.endereco const cidade = getFirst(paciente, ['city','cidade','localidade']) || profileData.cidade const cep = getFirst(paciente, ['cep','postal_code','zip']) || profileData.cep const biografia = getFirst(paciente, ['biography','bio','notes']) || profileData.biografia || '' const emailFromRow = getFirst(paciente, ['email']) || user?.email || profileData.email if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente) setProfileData((prev: any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia })) } } catch (err) { console.warn('[PacientePage] erro ao carregar paciente', err) } finally { if (mounted) setLoading(false) } } loadProfile() return () => { mounted = false } // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id, user?.email]) // Sincroniza a URL do avatar recuperada com o profileData useEffect(() => { if (retrievedAvatarUrl) { setProfileData((prev: any) => ({ ...prev, foto_url: retrievedAvatarUrl })) } }, [retrievedAvatarUrl]) const handleProfileChange = (field: string, value: string) => { setProfileData((prev: any) => ({ ...prev, [field]: value })) } const handleSaveProfile = async () => { if (!patientId) { setToast({ type: 'error', msg: 'Paciente não identificado. Não foi possível salvar.' }) setIsEditingProfile(false) return } setLoading(true) try { const payload: any = {} if (profileData.email) payload.email = profileData.email if (profileData.telefone) payload.phone_mobile = profileData.telefone if (profileData.endereco) payload.street = profileData.endereco if (profileData.cidade) payload.city = profileData.cidade if (profileData.cep) payload.cep = profileData.cep if (profileData.biografia) payload.notes = profileData.biografia await atualizarPaciente(String(patientId), payload) // refresh patient row const refreshed = await buscarPacientePorId(String(patientId)).catch(() => null) if (refreshed) { const getFirst = (obj: any, keys: string[]) => { if (!obj) return undefined for (const k of keys) { const v = obj[k] if (v !== undefined && v !== null && String(v).trim() !== '') return String(v) } return undefined } const nome = getFirst(refreshed, ['full_name','fullName','name','nome','social_name']) || profileData.nome const telefone = getFirst(refreshed, ['phone_mobile','phone','telefone','mobile']) || profileData.telefone const rua = getFirst(refreshed, ['street','logradouro','endereco','address']) const numero = getFirst(refreshed, ['number','numero']) const bairro = getFirst(refreshed, ['neighborhood','bairro']) const endereco = rua ? (numero ? `${rua}, ${numero}` : rua) + (bairro ? ` - ${bairro}` : '') : profileData.endereco const cidade = getFirst(refreshed, ['city','cidade','localidade']) || profileData.cidade const cep = getFirst(refreshed, ['cep','postal_code','zip']) || profileData.cep const biografia = getFirst(refreshed, ['biography','bio','notes']) || profileData.biografia || '' const emailFromRow = getFirst(refreshed, ['email']) || profileData.email const foto = getFirst(refreshed, ['foto_url','avatar_url','fotoUrl']) || profileData.foto_url setProfileData((prev:any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia, foto_url: foto })) } setIsEditingProfile(false) setToast({ type: 'success', msg: strings.sucesso }) } catch (err: any) { console.warn('[PacientePage] erro ao atualizar paciente', err) setToast({ type: 'error', msg: err?.message || strings.erroSalvar }) } finally { setLoading(false) } } const handleCancelEdit = () => { setIsEditingProfile(false) } function DashboardCards() { const router = useRouter() const [nextAppt, setNextAppt] = useState(null) const [examsCount, setExamsCount] = useState(null) const [loading, setLoading] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [medicos, setMedicos] = useState([]) const [searchLoading, setSearchLoading] = useState(false) const [especialidades, setEspecialidades] = useState([]) const [especialidadesLoading, setEspecialidadesLoading] = useState(true) useEffect(() => { let mounted = true async function load() { if (!patientId) { setNextAppt(null) setExamsCount(null) return } setLoading(true) try { // Load appointments for this patient (upcoming) const q = `patient_id=eq.${encodeURIComponent(String(patientId))}&order=scheduled_at.asc&limit=200` const ags = await listarAgendamentos(q).catch(() => []) if (!mounted) return const now = Date.now() // find the first appointment with scheduled_at >= now const upcoming = (ags || []).map((a: any) => ({ ...a, _sched: a.scheduled_at ? new Date(a.scheduled_at).getTime() : null })) .filter((a: any) => a._sched && a._sched >= now) .sort((x: any, y: any) => Number(x._sched) - Number(y._sched)) if (upcoming && upcoming.length) { setNextAppt(new Date(upcoming[0]._sched).toLocaleDateString('pt-BR')) } else { setNextAppt(null) } // Load reports/laudos and compute count matching the Laudos session rules const reports = await listarRelatoriosPorPaciente(String(patientId)).catch(() => []) if (!mounted) return let count = 0 try { if (!Array.isArray(reports) || reports.length === 0) { count = 0 } else { // Use the same robust doctor-resolution strategy as ExamesLaudos so // the card matches the list: try buscarMedicosPorIds, then per-id // getDoctorById and finally a REST fallback by user_id. const ids = Array.from(new Set((reports as any[]).map((r:any) => r.doctor_id || r.created_by || r.doctor).filter(Boolean).map(String))) if (ids.length === 0) { // fallback: count reports that have any direct doctor reference count = (reports as any[]).filter((r:any) => !!(r && (r.doctor_id || r.created_by || r.doctor || r.user_id))).length } else { const docs = await buscarMedicosPorIds(ids).catch(() => []) const map: Record = {} for (const d of docs || []) { if (!d) continue try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = map[String(d.user_id)] || d } catch {} } // Try per-id fallback using getDoctorById for any unresolved ids const unresolved = ids.filter(i => !map[i]) if (unresolved.length) { for (const u of unresolved) { try { const d = await getDoctorById(String(u)).catch(() => null) if (d) { try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } catch {} } } catch (e) { // ignore per-id failure } } } // REST fallback: try lookup by user_id for still unresolved ids const stillUnresolved = ids.filter(i => !map[i]) if (stillUnresolved.length) { for (const u of stillUnresolved) { try { const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null const headers: Record = { apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json' } if (token) headers.Authorization = `Bearer ${token}` const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/doctors?user_id=eq.${encodeURIComponent(String(u))}&limit=1` const res = await fetch(url, { method: 'GET', headers }) if (!res || res.status >= 400) continue const rows = await res.json().catch(() => []) if (rows && Array.isArray(rows) && rows.length) { const d = rows[0] if (d) { try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } catch {} } } } catch (e) { // ignore network errors } } } // Count only reports whose referenced doctor record has user_id count = (reports as any[]).filter((r:any) => { const maybeId = String(r.doctor_id || r.created_by || r.doctor || '') const doc = map[maybeId] return !!(doc && (doc.user_id || (doc as any).user_id)) }).length } } } catch (e) { count = Array.isArray(reports) ? reports.length : 0 } if (!mounted) return setExamsCount(count) } catch (e) { console.warn('[DashboardCards] erro ao carregar dados', e) if (!mounted) return setNextAppt(null) setExamsCount(null) } finally { if (mounted) setLoading(false) } } load() return () => { mounted = false } }, []) // Carregar especialidades únicas dos médicos ao montar useEffect(() => { let mounted = true setEspecialidadesLoading(true) ;(async () => { try { console.log('[DashboardCards] Carregando especialidades...') const todos = await listarTodosMedicos().catch((err) => { console.error('[DashboardCards] Erro ao buscar médicos:', err) return [] }) console.log('[DashboardCards] Médicos carregados:', todos?.length || 0, todos) if (!mounted) return // Mapeamento de correções para especialidades com encoding errado const specialtyFixes: Record = { 'Cl\u00EDnica Geral': 'Clínica Geral', 'Cl\u00E3nica Geral': 'Clínica Geral', 'Cl?nica Geral': 'Clínica Geral', 'Cl©nica Geral': 'Clínica Geral', 'Cl\uFFFDnica Geral': 'Clínica Geral', }; let specs: string[] = [] if (Array.isArray(todos) && todos.length > 0) { // Extrai TODAS as especialidades únicas do campo specialty specs = Array.from(new Set( todos .map((m: any) => { let spec = m.specialty || m.speciality || '' // Aplica correções conhecidas for (const [wrong, correct] of Object.entries(specialtyFixes)) { spec = String(spec).replace(new RegExp(wrong, 'g'), correct) } // Normaliza caracteres UTF-8 e limpa try { const normalized = String(spec || '').normalize('NFC').trim() return normalized } catch (e) { return String(spec || '').trim() } }) .filter((s: string) => s && s.length > 0) )) } console.log('[DashboardCards] Especialidades encontradas:', specs) // Ordenação alfabética usando localeCompare para suportar acentuação (português) setEspecialidades(specs.length > 0 ? specs.sort((a, b) => a.localeCompare(b, 'pt', { sensitivity: 'base' })) : []) } catch (e) { console.error('[DashboardCards] erro ao carregar especialidades', e) if (mounted) setEspecialidades([]) } finally { if (mounted) setEspecialidadesLoading(false) } })() return () => { mounted = false } }, []) // Debounced search por médico useEffect(() => { let mounted = true const term = String(searchQuery || '').trim() const handle = setTimeout(async () => { if (!mounted) return if (!term || term.length < 2) { setMedicos([]) return } try { setSearchLoading(true) const results = await buscarMedicos(term).catch(() => []) if (!mounted) return setMedicos(Array.isArray(results) ? results : []) } catch (e) { if (mounted) setMedicos([]) } finally { if (mounted) setSearchLoading(false) } }, 300) return () => { mounted = false; clearTimeout(handle) } }, [searchQuery]) const handleSearchMedico = (medico: any) => { const qs = new URLSearchParams() qs.set('tipo', 'teleconsulta') if (medico?.full_name) qs.set('medico', medico.full_name) if (medico?.specialty) qs.set('especialidade', medico.specialty || medico.especialidade || '') qs.set('origin', 'paciente') router.push(`/paciente/resultados?${qs.toString()}`) setSearchQuery('') setMedicos([]) } const handleEspecialidadeClick = (especialidade: string) => { const qs = new URLSearchParams() qs.set('tipo', 'teleconsulta') qs.set('especialidade', especialidade) qs.set('origin', 'paciente') router.push(`/paciente/resultados?${qs.toString()}`) } return (
{/* Hero Section com Busca */}

Encontre especialistas e clínicas

Busque por médico, especialidade ou localização

{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full px-4 sm:px-5 md:px-6 py-3 sm:py-3.5 md:py-4 rounded-xl bg-white text-foreground placeholder:text-muted-foreground text-sm sm:text-base border-0 shadow-md" /> {searchQuery && medicos.length > 0 && (
{medicos.map((medico) => ( ))}
)}
{/* Especialidades */}

Especialidades populares

{especialidadesLoading ? (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
) : especialidades && especialidades.length > 0 ? ( // Grid responsivo com botões arredondados e tamanho uniforme
{especialidades.map((esp) => ( ))}
) : (

Nenhuma especialidade disponível

)}
{/* Cards com Informações */}
{strings.proximaConsulta} {loading ? strings.carregando : (nextAppt ?? '-')}
{strings.ultimosExames} {loading ? strings.carregando : (examsCount !== null ? String(examsCount) : '-')}
) } const [currentDate, setCurrentDate] = useState(new Date()) // helper: produce a local YYYY-MM-DD key (uses local timezone, not toISOString UTC) const localDateKey = (d: Date) => { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } const consultasFicticias = [ { id: 1, medico: "Dr. Carlos Andrade", especialidade: "Cardiologia", local: "Clínica Coração Feliz", data: localDateKey(new Date()), hora: "09:00", status: "Confirmada" }, { id: 2, medico: "Dra. Fernanda Lima", especialidade: "Dermatologia", local: "Clínica Pele Viva", data: localDateKey(new Date()), hora: "14:30", status: "Pendente" }, { id: 3, medico: "Dr. João Silva", especialidade: "Ortopedia", local: "Hospital Ortopédico", data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return localDateKey(d) })(), hora: "11:00", status: "Cancelada" }, ]; function formatDatePt(date: Date) { return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } function navigateDate(direction: 'prev' | 'next') { const newDate = new Date(currentDate); newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1)); setCurrentDate(newDate); } function goToToday() { setCurrentDate(new Date()); } const todayStr = localDateKey(currentDate) const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr); function Consultas() { const router = useRouter() const [tipoConsulta, setTipoConsulta] = useState<'teleconsulta' | 'presencial'>('teleconsulta') const [especialidade, setEspecialidade] = useState('cardiologia') const [localizacao, setLocalizacao] = useState('') const hoverPrimaryClass = "hover-primary-blue focus-visible:ring-2 focus-visible:ring-blue-500/60 active:scale-[0.97]" const activeToggleClass = "w-full transition duration-200 focus-visible:ring-2 focus-visible:ring-blue-500/60 active:scale-[0.97] bg-blue-500 text-white hover:bg-blue-500 hover:text-white" const inactiveToggleClass = "w-full transition duration-200 bg-slate-50 text-blue-500 border border-blue-500/30 hover:bg-blue-50 hover:text-blue-500 dark:bg-white/5 dark:text-white dark:hover:bg-blue-500/20 dark:border-white/20" const hoverPrimaryIconClass = "rounded-xl bg-white text-slate-900 border border-black/10 shadow-[0_2px_8px_rgba(0,0,0,0.03)] hover-primary-blue focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:shadow-none" const today = new Date(); today.setHours(0, 0, 0, 0); const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0); const isSelectedDateToday = selectedDate.getTime() === today.getTime() // Appointments state (loaded when component mounts) const [appointments, setAppointments] = useState(null) const [doctorsMap, setDoctorsMap] = useState>({}) // Store doctor info by ID const [loadingAppointments, setLoadingAppointments] = useState(false) const [appointmentsError, setAppointmentsError] = useState(null) // expanded appointment id for inline details (kept for possible fallback) const [expandedId, setExpandedId] = useState(null) // selected appointment for modal details const [selectedAppointment, setSelectedAppointment] = useState(null) useEffect(() => { let mounted = true if (!patientId) { setAppointmentsError('Paciente não identificado. Faça login novamente.') return } async function loadAppointments() { try { setLoadingAppointments(true) setAppointmentsError(null) setAppointments(null) // Try `eq.` first, then fallback to `in.(id)` which some views expect const baseEncoded = encodeURIComponent(String(patientId)) const queriesToTry = [ `patient_id=eq.${baseEncoded}&order=scheduled_at.asc&limit=200`, `patient_id=in.(${baseEncoded})&order=scheduled_at.asc&limit=200`, ]; let rows: any[] = [] for (const q of queriesToTry) { try { // Debug: also fetch raw response to inspect headers/response body in the browser try { const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null const headers: Record = { apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json', } if (token) headers.Authorization = `Bearer ${token}` const rawUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/appointments?${q}` console.debug('[Consultas][debug] GET', rawUrl, 'Headers(masked):', { ...headers, Authorization: headers.Authorization ? `${String(headers.Authorization).slice(0,6)}...${String(headers.Authorization).slice(-6)}` : undefined }) const rawRes = await fetch(rawUrl, { method: 'GET', headers }) const rawText = await rawRes.clone().text().catch(() => '') console.debug('[Consultas][debug] raw response', { url: rawUrl, status: rawRes.status, bodyPreview: (typeof rawText === 'string' && rawText.length > 0) ? rawText.slice(0, 200) : rawText }) } catch (dbgErr) { console.debug('[Consultas][debug] não foi possível capturar raw response', dbgErr) } const r = await listarAgendamentos(q) if (r && Array.isArray(r) && r.length) { rows = r break } // if r is empty array, continue to next query format } catch (e) { // keep trying next format console.debug('[Consultas] tentativa listarAgendamentos falhou para query', q, e) } } if (!mounted) return if (!rows || rows.length === 0) { // no appointments found for this patient using either filter setAppointments([]) return } const doctorIds = Array.from(new Set(rows.map((r: any) => r.doctor_id).filter(Boolean))) const doctorsMap: Record = {} if (doctorIds.length) { try { const docs = await buscarMedicosPorIds(doctorIds).catch(() => []) for (const d of docs || []) doctorsMap[d.id] = d } catch (e) { // ignore } } const mapped = (rows || []).map((a: any) => { const sched = a.scheduled_at ? new Date(a.scheduled_at) : null const doc = a.doctor_id ? doctorsMap[String(a.doctor_id)] : null return { id: a.id, medico: doc?.full_name || a.doctor_id || '---', especialidade: doc?.specialty || '', local: a.location || a.place || '', data: sched ? localDateKey(sched) : '', 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) setAppointments(mapped) } catch (err: any) { console.warn('[Consultas] falha ao carregar agendamentos', err) if (!mounted) return setAppointmentsError(err?.message ?? 'Falha ao carregar agendamentos.') setAppointments([]) } finally { if (mounted) setLoadingAppointments(false) } } loadAppointments() return () => { mounted = false } }, []) // Monta a URL de resultados com os filtros atuais const buildResultadosHref = () => { const qs = new URLSearchParams() qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial' if (especialidade) qs.set('especialidade', especialidade) if (localizacao) qs.set('local', localizacao) // indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect) qs.set('origin', 'paciente') return `/paciente/resultados?${qs.toString()}` } // derived lists for the page (computed after appointments state is declared) const _dialogSource = (appointments !== null ? appointments : consultasFicticias) const _todaysAppointments = (_dialogSource || []).filter((c: any) => c.data === todayStr) // helper: present a localized label for appointment status const statusLabel = (s: any) => { const raw = (s === null || s === undefined) ? '' : String(s) const key = raw.toLowerCase() const map: Record = { 'requested': 'Solicitado', 'request': 'Solicitado', 'confirmed': 'Confirmado', 'confirmada': 'Confirmada', 'confirmado': 'Confirmado', 'completed': 'Concluído', 'concluído': 'Concluído', 'cancelled': 'Cancelado', 'cancelada': 'Cancelada', 'cancelado': 'Cancelado', 'pending': 'Pendente', 'pendente': 'Pendente', 'checked_in': 'Registrado', 'in_progress': 'Em andamento', 'no_show': 'Não compareceu' } return map[key] || raw } // map an appointment (row) to the CalendarRegistrationForm's formData shape const mapAppointmentToFormData = (appointment: any) => { // Use the raw appointment with all fields: doctor_id, scheduled_at, appointment_type, etc. const schedIso = appointment.scheduled_at || (appointment.data && appointment.hora ? `${appointment.data}T${appointment.hora}` : null) || null const baseDate = schedIso ? new Date(schedIso) : new Date() const appointmentDate = schedIso ? baseDate.toISOString().split('T')[0] : '' const startTime = schedIso ? baseDate.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : (appointment.hora || '') const duration = appointment.duration_minutes ?? appointment.duration ?? 30 // Get doctor name from doctorsMap if available const docName = appointment.medico || (appointment.doctor_id ? doctorsMap[String(appointment.doctor_id)]?.full_name : null) || appointment.doctor_name || appointment.professional_name || '---' return { id: appointment.id, patientName: docName, patientId: null, doctorId: appointment.doctor_id ?? null, professionalName: docName, appointmentDate, startTime, endTime: '', status: appointment.status || undefined, appointmentType: appointment.appointment_type || appointment.type || (appointment.local ? 'presencial' : 'teleconsulta'), duration_minutes: duration, notes: appointment.notes || '', } } return (
{/* Consultas Agendadas Section */}

Suas Consultas Agendadas

Gerencie suas consultas confirmadas, pendentes ou canceladas.

{/* Date Navigation */}
{formatDatePt(currentDate)} {isSelectedDateToday && ( )}
{_todaysAppointments.length} consulta{_todaysAppointments.length !== 1 ? 's' : ''} agendada{_todaysAppointments.length !== 1 ? 's' : ''}
{/* Appointments List */}
{loadingAppointments ? (
Carregando consultas...
) : appointmentsError ? (
{appointmentsError}
) : ( (() => { 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.

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

{consulta.especialidade} {consulta.local}

{/* Time */}
{consulta.hora}
{/* Status Badge */}
{statusLabel(consulta.status)}
{/* Action Buttons */}
{/* Reagendar removed by request */} {consulta.status !== 'Cancelada' && consulta.status !== 'cancelled' && ( )}
{/* Inline detalhes removed: modal will show details instead */}
)) })() )}
!open && setSelectedAppointment(null)}> Detalhes da Consulta Detalhes da consulta
{selectedAppointment ? ( <>
Profissional: {selectedAppointment.medico || '-'}
Especialidade: {selectedAppointment.especialidade || '-'}
Data: {(function(d:any,h:any){ try{ const dt = new Date(String(d) + 'T' + String(h||'00:00')); return formatDatePt(dt) }catch(e){ return String(d||'-') } })(selectedAppointment.data, selectedAppointment.hora)}
Hora: {selectedAppointment.hora || '-'}
Status: {statusLabel(selectedAppointment.status) || '-'}
) : (
Carregando...
)}
{/* Reagendar feature removed */}
) } // Selected report state const [selectedReport, setSelectedReport] = useState(null) function ExamesLaudos() { const router = useRouter() const [reports, setReports] = useState(null) const [loadingReports, setLoadingReports] = useState(false) const [reportsError, setReportsError] = useState(null) const [reportDoctorName, setReportDoctorName] = useState(null) const [doctorsMap, setDoctorsMap] = useState>({}) const [resolvingDoctors, setResolvingDoctors] = useState(false) const [reportsPage, setReportsPage] = useState(1) const [reportsPerPage, setReportsPerPage] = useState(5) const [searchTerm, setSearchTerm] = useState('') const [remoteMatch, setRemoteMatch] = useState(null) const [searchingRemote, setSearchingRemote] = useState(false) const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'custom'>('newest') const [filterDate, setFilterDate] = useState('') // derived filtered list based on search term and date filters const filteredReports = useMemo(() => { if (!reports || !Array.isArray(reports)) return [] const qRaw = String(searchTerm || '').trim() const q = qRaw.toLowerCase() // If we have a remote-match result for this query, prefer it. remoteMatch // may be a single report (for id-like queries) or an array (for doctor-name search). const hexOnlyRaw = String(qRaw).replace(/[^0-9a-fA-F]/g, '') // defensive: compute length via explicit number conversion to avoid any // accidental transpilation/patch artifacts that could turn a comparison // into an unexpected call. This avoids runtime "8 is not a function". const hexLenRaw = (typeof hexOnlyRaw === 'string') ? hexOnlyRaw.length : (Number(hexOnlyRaw) || 0) const looksLikeId = hexLenRaw >= 8 if (remoteMatch) { if (Array.isArray(remoteMatch)) return remoteMatch return [remoteMatch] } // Start with all reports or filtered by search let filtered = !q ? reports : reports.filter((r: any) => { try { const id = r.id ? String(r.id).toLowerCase() : '' const title = String(reportTitle(r) || '').toLowerCase() const exam = String(r.exam || r.exame || r.report_type || r.especialidade || '').toLowerCase() const date = String(r.report_date || r.created_at || r.data || '').toLowerCase() const notes = String(r.content || r.body || r.conteudo || r.notes || r.observacoes || '').toLowerCase() const cid = String(r.cid || r.cid_code || r.cidCode || r.cie || '').toLowerCase() const diagnosis = String(r.diagnosis || r.diagnostico || r.diagnosis_text || r.diagnostico_text || '').toLowerCase() const conclusion = String(r.conclusion || r.conclusao || r.conclusion_text || r.conclusao_text || '').toLowerCase() const orderNumber = String(r.order_number || r.orderNumber || r.numero_pedido || '').toLowerCase() // patient fields const patientName = String( r?.paciente?.full_name || r?.paciente?.nome || r?.patient?.full_name || r?.patient?.nome || r?.patient_name || r?.patient_full_name || '' ).toLowerCase() // requester/executor fields const requestedBy = String(r.requested_by_name || r.requested_by || r.requester_name || r.requester || '').toLowerCase() const executor = String(r.executante || r.executante_name || r.executor || r.executor_name || '').toLowerCase() // try to resolve doctor name from map when available const maybeId = r?.doctor_id || r?.created_by || r?.doctor || null const doctorName = maybeId ? String(doctorsMap[String(maybeId)]?.full_name || doctorsMap[String(maybeId)]?.name || '').toLowerCase() : '' // build search corpus const corpus = [id, title, exam, date, notes, cid, diagnosis, conclusion, orderNumber, patientName, requestedBy, executor, doctorName].join(' ') return corpus.includes(q) } catch (e) { return false } }) // Apply date filter if specified if (filterDate) { const filterDateObj = new Date(filterDate) filterDateObj.setHours(0, 0, 0, 0) filtered = filtered.filter((r: any) => { const reportDateObj = new Date(r.report_date || r.created_at || Date.now()) reportDateObj.setHours(0, 0, 0, 0) return reportDateObj.getTime() === filterDateObj.getTime() }) } // Apply sorting const sorted = [...filtered] if (sortOrder === 'newest') { sorted.sort((a: any, b: any) => { const dateA = new Date(a.report_date || a.created_at || 0).getTime() const dateB = new Date(b.report_date || b.created_at || 0).getTime() return dateB - dateA // Newest first }) } else if (sortOrder === 'oldest') { sorted.sort((a: any, b: any) => { const dateA = new Date(a.report_date || a.created_at || 0).getTime() const dateB = new Date(b.report_date || b.created_at || 0).getTime() return dateA - dateB // Oldest first }) } return sorted // eslint-disable-next-line react-hooks/exhaustive-deps }, [reports, searchTerm, doctorsMap, remoteMatch, sortOrder, filterDate]) // When the search term looks like an id, attempt a direct fetch using the reports API useEffect(() => { let mounted = true const q = String(searchTerm || '').trim() if (!q) { setRemoteMatch(null) setSearchingRemote(false) return } // heuristic: id-like strings contain many hex characters (UUID-like) — // avoid calling RegExp.test/match to sidestep any env/type issues here. const hexOnly = String(q).replace(/[^0-9a-fA-F]/g, '') // defensive length computation as above const hexLen = (typeof hexOnly === 'string') ? hexOnly.length : (Number(hexOnly) || 0) const looksLikeId = hexLen >= 8 // If it looks like an id, try the single-report lookup. Otherwise, if it's a // textual query, try searching doctors by full_name and then fetch reports // authored/requested by those doctors. ;(async () => { try { setSearchingRemote(true) setRemoteMatch(null) if (looksLikeId && q) { // Adicionada verificação para q não ser vazio const r = await buscarRelatorioPorId(q).catch(() => null) if (!mounted) return if (r) setRemoteMatch(r) return } // textual search: try to find doctors whose full_name matches the query // and then fetch reports for those doctors. Only run for reasonably // long queries to avoid excessive network calls. if (q.length >= 2) { const docs = await buscarMedicos(q).catch(() => []) if (!mounted) return if (docs && Array.isArray(docs) && docs.length) { // fetch reports for matching doctors in parallel. Some report rows // reference the doctor's account `user_id` in `requested_by` while // others reference the doctor's record `id`. Try both per doctor. const promises = docs.map(async (d: any) => { try { const byId = await listarRelatoriosPorMedico(String(d.id)).catch(() => []) if (Array.isArray(byId) && byId.length) return byId // fallback: if the doctor record has a user_id, try that too if (d && (d.user_id || d.userId)) { const byUser = await listarRelatoriosPorMedico(String(d.user_id || d.userId)).catch(() => []) if (Array.isArray(byUser) && byUser.length) return byUser } return [] } catch (e) { return [] } }) const arrays = await Promise.all(promises) if (!mounted) return const combined = ([] as any[]).concat(...arrays) // dedupe by report id const seen = new Set() const unique: any[] = [] for (const rr of combined) { try { const rid = String(rr.id) if (!seen.has(rid)) { seen.add(rid) unique.push(rr) } } catch (e) { // skip malformed item } } if (unique.length) setRemoteMatch(unique) else setRemoteMatch(null) return } } // nothing useful found if (mounted) setRemoteMatch(null) } catch (e) { if (mounted) setRemoteMatch(null) } finally { if (mounted) setSearchingRemote(false) } })() return () => { mounted = false } }, [searchTerm]) // Helper to derive a human-friendly title for a report/laudo const reportTitle = (rep: any, preferDoctorName?: string | null) => { if (!rep) return 'Laudo' // prefer a resolved doctor name when we have a map try { const maybeId = rep?.doctor_id ?? rep?.created_by ?? rep?.doctor ?? null if (maybeId) { const doc = doctorsMap[String(maybeId)] if (doc) { const name = doc.full_name || doc.name || doc.fullName || doc.doctor_name || null if (name) return String(name) } } } catch (e) { // ignore } // Try common fields that may contain the doctor's/author name first 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', 'title', 'name', 'report_name', 'report_title' ] for (const k of tryKeys) { const v = rep[k] if (v !== undefined && v !== null && String(v).trim() !== '') return String(v) } if (preferDoctorName) return preferDoctorName return 'Laudo' } // When reports are loaded, try to resolve doctor records for display useEffect(() => { let mounted = true if (!reports || !Array.isArray(reports) || reports.length === 0) return ;(async () => { try { setResolvingDoctors(true) const ids = Array.from(new Set(reports.map((r: any) => r.doctor_id || r.created_by || r.doctor).filter(Boolean).map(String))) if (ids.length === 0) return const docs = await buscarMedicosPorIds(ids).catch(() => []) if (!mounted) return const map: Record = {} // index returned docs by both their id and user_id (some reports store user_id) for (const d of docs || []) { if (!d) continue try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } catch {} } // attempt per-id fallback for any unresolved ids (try getDoctorById) const unresolved = ids.filter(i => !map[i]) if (unresolved.length) { for (const u of unresolved) { try { const d = await getDoctorById(String(u)).catch(() => null) if (d) { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } } catch (e) { // ignore per-id failure } } } // final fallback: try lookup by user_id (direct REST using baseHeaders) const stillUnresolved = ids.filter(i => !map[i]) if (stillUnresolved.length) { for (const u of stillUnresolved) { try { const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null const headers: Record = { apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json' } if (token) headers.Authorization = `Bearer ${token}` const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/doctors?user_id=eq.${encodeURIComponent(String(u))}&limit=1` const res = await fetch(url, { method: 'GET', headers }) if (!res || res.status >= 400) continue const rows = await res.json().catch(() => []) if (rows && Array.isArray(rows) && rows.length) { const d = rows[0] if (d) { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } } } catch (e) { // ignore network errors } } } setDoctorsMap(map) // After resolving doctor records, filter out reports whose doctor // record doesn't have a user_id (doctor_userid). If a report's // referenced doctor lacks user_id, we hide that laudo. try { const filtered = (reports || []).filter((r: any) => { const maybeId = String(r?.doctor_id || r?.created_by || r?.doctor || '') const doc = map[maybeId] return !!(doc && (doc.user_id || (doc as any).user_id)) }) // Only update when different to avoid extra cycles if (Array.isArray(filtered) && filtered.length !== (reports || []).length) { setReports(filtered) } } catch (e) { // ignore filtering errors } setResolvingDoctors(false) } catch (e) { // ignore resolution errors setResolvingDoctors(false) } })() return () => { mounted = false } }, [reports]) useEffect(() => { let mounted = true if (!patientId) return setLoadingReports(true) setReportsError(null) ;(async () => { try { const res = await listarRelatoriosPorPaciente(String(patientId)).catch(() => []) if (!mounted) return // If no reports, set empty and return if (!Array.isArray(res) || res.length === 0) { setReports([]) return } // Resolve referenced doctor ids and only keep reports whose // referenced doctor record has a truthy user_id (i.e., created by a doctor) try { setResolvingDoctors(true) const ids = Array.from(new Set((res as any[]).map((r:any) => r.doctor_id || r.created_by || r.doctor).filter(Boolean).map(String))) const map: Record = {} if (ids.length) { const docs = await buscarMedicosPorIds(ids).catch(() => []) for (const d of docs || []) { if (!d) continue try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = map[String(d.user_id)] || d } catch {} } // per-id fallback const unresolved = ids.filter(i => !map[i]) if (unresolved.length) { for (const u of unresolved) { try { const d = await getDoctorById(String(u)).catch(() => null) if (d) { try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } catch {} } } catch (e) { // ignore } } } // REST fallback by user_id const stillUnresolved = ids.filter(i => !map[i]) if (stillUnresolved.length) { for (const u of stillUnresolved) { try { const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null const headers: Record = { apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json' } if (token) headers.Authorization = `Bearer ${token}` const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/doctors?user_id=eq.${encodeURIComponent(String(u))}&limit=1` const r = await fetch(url, { method: 'GET', headers }) if (!r || r.status >= 400) continue const rows = await r.json().catch(() => []) if (rows && Array.isArray(rows) && rows.length) { const d = rows[0] if (d) { try { if (d.id !== undefined && d.id !== null) map[String(d.id)] = d } catch {} try { if (d.user_id !== undefined && d.user_id !== null) map[String(d.user_id)] = d } catch {} } } } catch (e) { // ignore } } } } // Now filter reports to only those whose referenced doctor has user_id const filtered = (res || []).filter((r: any) => { const maybeId = String(r?.doctor_id || r?.created_by || r?.doctor || '') const doc = map[maybeId] return !!(doc && (doc.user_id || (doc as any).user_id)) }) // Update doctorsMap and reports setDoctorsMap(map) setReports(filtered) setResolvingDoctors(false) return } catch (e) { // If resolution fails, fall back to setting raw results console.warn('[ExamesLaudos] falha ao resolver médicos para filtragem', e) setReports(Array.isArray(res) ? res : []) setResolvingDoctors(false) return } } catch (err) { console.warn('[ExamesLaudos] erro ao carregar laudos', err) if (!mounted) return setReportsError('Falha ao carregar laudos.') } finally { if (mounted) setLoadingReports(false) } })() return () => { mounted = false } }, []) // When a report is selected, try to fetch doctor name if we have an id useEffect(() => { let mounted = true if (!selectedReport) { setReportDoctorName(null) return } const maybeDoctorId = selectedReport.doctor_id || selectedReport.created_by || null if (!maybeDoctorId) { setReportDoctorName(null) return } (async () => { try { const docs = await buscarMedicosPorIds([String(maybeDoctorId)]).catch(() => []) if (!mounted) return if (docs && docs.length) { const doc0: any = docs[0] setReportDoctorName(doc0.full_name || doc0.name || doc0.fullName || null) return } // fallback: try single-id lookup try { const d = await getDoctorById(String(maybeDoctorId)).catch(() => null) if (d && mounted) { setReportDoctorName(d.full_name || d.name || d.fullName || null) return } } catch (e) { // ignore } // final fallback: query doctors by user_id try { const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null const headers: Record = { apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Accept: 'application/json' } if (token) headers.Authorization = `Bearer ${token}` const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/doctors?user_id=eq.${encodeURIComponent(String(maybeDoctorId))}&limit=1` const res = await fetch(url, { method: 'GET', headers }) if (res && res.status < 400) { const rows = await res.json().catch(() => []) if (rows && Array.isArray(rows) && rows.length) { const d = rows[0] if (d && mounted) setReportDoctorName(d.full_name || d.name || d.fullName || null) } } } catch (e) { // ignore } } catch (e) { // ignore } })() return () => { mounted = false } }, []) // reset pagination when reports change useEffect(() => { setReportsPage(1) }, [reports]) return (<>

Laudos

{/* Search box: allow searching by id, doctor, exam, date or text */}
{ setSearchTerm(e.target.value); setReportsPage(1) }} className="text-xs sm:text-sm" /> {searchTerm && ( )}
{/* 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}
) : reportsError ? (
{reportsError}
) : (!reports || reports.length === 0) ? (
Nenhum laudo encontrado para este paciente.
) : (filteredReports.length === 0) ? ( searchingRemote ? (
Buscando laudo...
) : (
Nenhum laudo corresponde à pesquisa.
) ) : ( (() => { const total = Array.isArray(filteredReports) ? filteredReports.length : 0 // enforce a maximum of 5 laudos per page const perPage = Math.max(1, Math.min(reportsPerPage || 5, 5)) const totalPages = Math.max(1, Math.ceil(total / perPage)) // keep page inside bounds const page = Math.min(Math.max(1, reportsPage), totalPages) const start = (page - 1) * perPage const end = start + perPage const pageItems = (filteredReports || []).slice(start, end) 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
{reportTitle(r)}
})()}
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}
) })() )}
{/* Modal removed - now using dedicated page /app/laudos/[id] */} ) } function Perfil() { return (
{/* Header com Título e Botão */}

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

{/* Nome Completo */}
{profileData.nome || "Não preenchido"}

Este campo não pode ser alterado

{/* Email */}
{profileData.email || "Não preenchido"}

Este campo não pode ser alterado

{/* Telefone */}
{isEditingProfile ? ( handleProfileChange('telefone', e.target.value)} className="mt-2 text-xs sm:text-sm" placeholder="(00) 00000-0000" maxLength={15} /> ) : (
{profileData.telefone || "Não preenchido"}
)}
{/* Endereço e Contato */}

Endereço e Contato

{/* Logradouro */}
{isEditingProfile ? ( handleProfileChange('endereco', e.target.value)} className="mt-2 text-xs sm:text-sm" placeholder="Rua, avenida, etc." /> ) : (
{profileData.endereco || "Não preenchido"}
)}
{/* Cidade */}
{isEditingProfile ? ( handleProfileChange('cidade', e.target.value)} className="mt-2 text-xs sm:text-sm" placeholder="São Paulo" /> ) : (
{profileData.cidade || "Não preenchido"}
)}
{/* CEP */}
{isEditingProfile ? ( handleProfileChange('cep', e.target.value)} className="mt-2 text-xs sm:text-sm" placeholder="00000-000" /> ) : (
{profileData.cep || "Não preenchido"}
)}
{/* Coluna Direita - Foto do Perfil */}

Foto do Perfil

{isEditingProfile ? (
handleProfileChange('foto_url', newUrl)} userName={profileData.nome} />
) : (
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}

{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}

)}
) } // Renderização principal return (
{/* Header com informações do paciente */}
{profileData.nome?.charAt(0) || 'P'}
Conta do paciente {profileData.nome || 'Paciente'} {profileData.email || 'Email não disponível'}
{/* Layout com sidebar e conteúdo */}
{/* Sidebar vertical - sticky */} {/* Conteúdo principal */}
{/* Toasts de feedback */} {toast && (
{toast.msg}
)} {/* Loader global */} {loading &&
{strings.carregando}
} {error &&
{error}
} {/* Conteúdo principal */} {!loading && !error && ( <> {tab==='dashboard' && } {tab==='consultas' && } {tab==='exames' && } {tab==='perfil' && } )}
) }