'use client' import type { ReactNode } from 'react' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' 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 } 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/simple-theme-toggle' import { UploadAvatar } from '@/components/ui/upload-avatar' import Link from 'next/link' import ProtectedRoute from '@/components/ProtectedRoute' import { useAuth } from '@/hooks/useAuth' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente, listarAgendamentos, buscarMedicosPorIds } from '@/lib/api' import { useReports } from '@/hooks/useReports' // 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', reagendar: 'Reagendar', 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'|'mensagens'|'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) // 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 } // eslint-disable-next-line react-hooks/exhaustive-deps }, [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]) const handleProfileChange = (field: string, value: string) => { setProfileData((prev: any) => ({ ...prev, [field]: value })) } const handleSaveProfile = () => { setIsEditingProfile(false) setToast({ type: 'success', msg: strings.sucesso }) } const handleCancelEdit = () => { setIsEditingProfile(false) } function DashboardCards() { return (
{strings.proximaConsulta} 12/10/2025 {strings.ultimosExames} 2 {strings.mensagensNaoLidas} 1
) } // Consultas (fetched from server for the logged-in patient) const [currentDate, setCurrentDate] = useState(new Date()) const [consultas, setConsultas] = useState([]) const [consultasLoading, setConsultasLoading] = useState(false) const [consultasError, setConsultasError] = useState(null) 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 = currentDate.toISOString().split('T')[0]; // compute appointments for the selected day (normalize scheduled_at to local YYYY-MM-DD) const consultasDoDia = consultas.filter(c => { try { const scheduled = c.scheduled_at || c.time || c.data || c.date || null if (!scheduled) return false const d = new Date(scheduled) if (isNaN(d.getTime())) return false 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}` === todayStr } catch (e) { return false } }) function Consultas() { const router = useRouter() const [tipoConsulta, setTipoConsulta] = useState<'teleconsulta' | 'presencial'>('teleconsulta') const [especialidade, setEspecialidade] = useState('cardiologia') const [localizacao, setLocalizacao] = useState('') const [mostrarAgendadas, setMostrarAgendadas] = useState(false) const hoverPrimaryClass = "transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97]" const activeToggleClass = "w-full transition duration-200 focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] bg-[#2563eb] text-white hover:bg-[#2563eb] hover:text-white" const inactiveToggleClass = "w-full transition duration-200 bg-slate-50 text-[#2563eb] border border-[#2563eb]/30 hover:bg-slate-100 hover:text-[#2563eb] dark:bg-white/5 dark:text-white dark:hover:bg-white/10 dark:border-white/20" const hoverPrimaryIconClass = "rounded-xl bg-white text-[#1e293b] border border-black/10 shadow-[0_2px_8px_rgba(0,0,0,0.03)] transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:shadow-none dark:hover:bg-[#2563eb] dark:hover:text-white" 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() const handlePesquisar = () => { const params = new URLSearchParams({ tipo: tipoConsulta, especialidade, local: localizacao }) // if we have a linked patient id, include it so the agenda page can prefill try { if (patientId) params.append('patientId', String(patientId)) } catch(e){} router.push(`/agenda?origin=consultas&${params.toString()}`) } // helper: fetch raw appointment data and enrich with doctor objects (returns array) async function fetchConsultasData(): Promise { try { const qs = `?select=*&patient_id=eq.${encodeURIComponent(String(patientId))}&order=scheduled_at.asc&limit=200` const appts = await listarAgendamentos(qs).catch(() => []) const doctorIds = Array.from(new Set((appts || []).map((a:any) => String(a.doctor_id || a.doctor || a.requested_by || '').trim()).filter(Boolean))) let doctorMap = new Map() if (doctorIds.length) { try { const docs = await buscarMedicosPorIds(doctorIds).catch(() => []) for (const d of docs || []) if (d && d.id) doctorMap.set(String(d.id), d) } catch (e) { console.warn('[PacientePage] falha ao buscar medicos para agendamentos', e) } } const normalized = (appts || []).map((a:any) => ({ ...a, doctor: a.doctor || doctorMap.get(String(a.doctor_id || a.doctor || a.requested_by || '')) || undefined })) return normalized } catch (err) { console.warn('[PacientePage] fetchConsultasData error', err) throw err } } // wrapper that updates component state (safe to call from button or useEffect) async function loadAndSetConsultas() { if (!patientId) { setConsultas([]); return } setConsultasLoading(true) setConsultasError(null) try { const data = await fetchConsultasData() setConsultas(data) } catch (err:any) { setConsultasError('Falha ao carregar consultas.') } finally { setConsultasLoading(false) } } useEffect(() => { let mounted = true if (!mostrarAgendadas) return // only load when dialog is requested loadAndSetConsultas() return () => { mounted = false } }, [mostrarAgendadas, patientId]) // click handler for the "Ver consultas agendadas" button const [dbgClicks, setDbgClicks] = useState(0) const [dbgLastAt, setDbgLastAt] = useState(null) async function handleOpenAgendadas() { try { // quick debug log so we can see the click happened // eslint-disable-next-line no-console console.debug('[PacientePage] abrir dialog de consultas') setMostrarAgendadas(true) // record a visible debug pill so it's obvious this ran even if console is closed setDbgClicks(c => c + 1) setDbgLastAt(new Date().toLocaleTimeString()) // load data but don't block UI loadAndSetConsultas().catch(() => {}) } catch (e) { // eslint-disable-next-line no-console console.warn('[PacientePage] handleOpenAgendadas failed', e) } } return (

Agende sua próxima consulta

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

setLocalizacao(event.target.value)} placeholder="Cidade ou estado" className="pl-9" />
setMostrarAgendadas(open)}> Consultas agendadas Gerencie suas consultas confirmadas, pendentes ou canceladas. {/* visible debug pill to show handler ran (appears after first click) */} {dbgClicks > 0 && (
Clicks: {dbgClicks} • último: {dbgLastAt}
)}
{formatDatePt(currentDate)} {isSelectedDateToday && ( )}
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
{consultasLoading ? (
Carregando consultas...
) : consultasError ? (
{consultasError}
) : consultasDoDia.length === 0 ? (

Nenhuma consulta agendada para este dia

Use a busca para marcar uma nova consulta.

) : ( consultasDoDia.map(consulta => { // normalize fields for display const doctorName = consulta.doctor_name || consulta.medico || consulta.medico_name || (consulta.doctor && (consulta.doctor.full_name || consulta.doctor.name)) || 'Médico' const specialty = consulta.specialty || consulta.especialidade || consulta.especialidade || consulta.type || '' const location = consulta.unit || consulta.location || consulta.local || '' const scheduled = consulta.scheduled_at || consulta.time || consulta.date || consulta.data || '' let timeDisplay = '' try { const dt = new Date(scheduled); if (!isNaN(dt.getTime())) timeDisplay = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}` } catch {} const status = (consulta.status || consulta.state || consulta.status_label || '').toString() || 'Confirmada' return (
{doctorName}

{specialty} • {location}

{timeDisplay}
{status}
{status.toLowerCase() !== 'cancelada' && ()} {status.toLowerCase() !== 'cancelada' && ()}
) }) )}
{formatDatePt(currentDate)} {isSelectedDateToday && ( )}
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
{consultasDoDia.length === 0 ? (

Nenhuma consulta agendada para este dia

Use a busca para marcar uma nova consulta.

) : ( consultasDoDia.map(consulta => (
{consulta.medico}

{consulta.especialidade} • {consulta.local}

{consulta.hora}
{consulta.status}
{consulta.status !== 'Cancelada' && ( )} {consulta.status !== 'Cancelada' && ( )}
)) )}
) } // Reports (laudos) hook const { reports, loadReportsByPatient, loading: reportsLoading } = useReports() const [selectedReport, setSelectedReport] = useState(null) function ExamesLaudos() { useEffect(() => { if (!patientId) return // load laudos for this patient loadReportsByPatient(patientId).catch(() => {}) }, [patientId]) return (

Laudos

{reportsLoading ? (
Carregando laudos...
) : (!reports || reports.length === 0) ? (
Nenhum laudo salvo.
) : (
{reports.map((r: any) => (
{r.title || r.report_type || r.exame || r.name || 'Laudo'}
Data: {new Date(r.report_date || r.data || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
))}
)} !open && setSelectedReport(null)}> Laudo Médico {selectedReport && ( <>
{selectedReport.title || selectedReport.report_type || selectedReport.exame || 'Laudo'}
Data: {new Date(selectedReport.report_date || selectedReport.data || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
{selectedReport.content || selectedReport.laudo || selectedReport.body || JSON.stringify(selectedReport, null, 2)}
)}
) } function Mensagens() { const [msgs, setMsgs] = useState([]) const [loadingMsgs, setLoadingMsgs] = useState(false) const [msgsError, setMsgsError] = useState(null) useEffect(() => { let mounted = true if (!patientId) return setLoadingMsgs(true) setMsgsError(null) listarMensagensPorPaciente(String(patientId)) .then(res => { if (!mounted) return setMsgs(Array.isArray(res) ? res : []) }) .catch(err => { console.warn('[Mensagens] erro ao carregar mensagens', err) if (!mounted) return setMsgsError('Falha ao carregar mensagens.') }) .finally(() => { if (mounted) setLoadingMsgs(false) }) return () => { mounted = false } }, [patientId]) return (

Mensagens Recebidas

{loadingMsgs ? (
Carregando mensagens...
) : msgsError ? (
{msgsError}
) : (!msgs || msgs.length === 0) ? (
Nenhuma mensagem encontrada.
) : ( msgs.map((msg: any) => (
{msg.sender_name || msg.from || msg.doctor_name || 'Remetente'} {!msg.read && Nova}
{new Date(msg.created_at || msg.data || Date.now()).toLocaleString('pt-BR')}
{msg.body || msg.content || msg.text || JSON.stringify(msg)}
)) )}
) } function Perfil() { const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep || profileData.biografia) return (

Meu Perfil

{!isEditingProfile ? ( ) : (
)}
{/* Informações Pessoais */}

Informações Pessoais

{profileData.nome}

Este campo não pode ser alterado
{isEditingProfile ? ( handleProfileChange('email', e.target.value)} /> ) : (

{profileData.email}

)}
{isEditingProfile ? ( handleProfileChange('telefone', e.target.value)} /> ) : (

{profileData.telefone}

)}
{/* Endereço e Contato (render apenas se existir algum dado) */} {hasAddress && (

Endereço

{isEditingProfile ? ( handleProfileChange('endereco', e.target.value)} /> ) : (

{profileData.endereco}

)}
{isEditingProfile ? ( handleProfileChange('cidade', e.target.value)} /> ) : (

{profileData.cidade}

)}
{isEditingProfile ? ( handleProfileChange('cep', e.target.value)} /> ) : (

{profileData.cep}

)}
{isEditingProfile ? (