Compare commits
2 Commits
79eb63ad96
...
a37dbb4c75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37dbb4c75 | ||
|
|
bb6e3b0d25 |
@ -17,7 +17,7 @@ import Link from 'next/link'
|
|||||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente, listarAgendamentos, buscarMedicosPorIds } from '@/lib/api'
|
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, atualizarPaciente, buscarPacientePorId } from '@/lib/api'
|
||||||
import { ENV_CONFIG } from '@/lib/env-config'
|
import { ENV_CONFIG } from '@/lib/env-config'
|
||||||
import { listarRelatoriosPorPaciente } from '@/lib/reports'
|
import { listarRelatoriosPorPaciente } from '@/lib/reports'
|
||||||
// reports are rendered statically for now
|
// reports are rendered statically for now
|
||||||
@ -55,7 +55,7 @@ const strings = {
|
|||||||
|
|
||||||
export default function PacientePage() {
|
export default function PacientePage() {
|
||||||
const { logout, user } = useAuth()
|
const { logout, user } = useAuth()
|
||||||
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
|
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'perfil'>('dashboard')
|
||||||
|
|
||||||
// Simulação de loaders, empty states e erro
|
// Simulação de loaders, empty states e erro
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@ -238,44 +238,142 @@ export default function PacientePage() {
|
|||||||
const handleProfileChange = (field: string, value: string) => {
|
const handleProfileChange = (field: string, value: string) => {
|
||||||
setProfileData((prev: any) => ({ ...prev, [field]: value }))
|
setProfileData((prev: any) => ({ ...prev, [field]: value }))
|
||||||
}
|
}
|
||||||
const handleSaveProfile = () => {
|
const handleSaveProfile = async () => {
|
||||||
setIsEditingProfile(false)
|
if (!patientId) {
|
||||||
setToast({ type: 'success', msg: strings.sucesso })
|
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 = () => {
|
const handleCancelEdit = () => {
|
||||||
setIsEditingProfile(false)
|
setIsEditingProfile(false)
|
||||||
}
|
}
|
||||||
function DashboardCards() {
|
function DashboardCards() {
|
||||||
|
const [nextAppt, setNextAppt] = useState<string | null>(null)
|
||||||
|
const [examsCount, setExamsCount] = useState<number | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
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 count
|
||||||
|
const reports = await listarRelatoriosPorPaciente(String(patientId)).catch(() => [])
|
||||||
|
if (!mounted) return
|
||||||
|
setExamsCount(Array.isArray(reports) ? reports.length : 0)
|
||||||
|
} 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 }
|
||||||
|
}, [patientId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<Card className="flex flex-col items-center justify-center p-4">
|
<Card className="flex flex-col items-center justify-center p-4">
|
||||||
<Calendar className="mb-2 text-primary" aria-hidden />
|
<Calendar className="mb-2 text-primary" aria-hidden />
|
||||||
<span className="font-semibold">{strings.proximaConsulta}</span>
|
<span className="font-semibold">{strings.proximaConsulta}</span>
|
||||||
<span className="text-2xl">12/10/2025</span>
|
<span className="text-2xl">{loading ? '...' : (nextAppt ?? '-')}</span>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="flex flex-col items-center justify-center p-4">
|
<Card className="flex flex-col items-center justify-center p-4">
|
||||||
<FileText className="mb-2 text-primary" aria-hidden />
|
<FileText className="mb-2 text-primary" aria-hidden />
|
||||||
<span className="font-semibold">{strings.ultimosExames}</span>
|
<span className="font-semibold">{strings.ultimosExames}</span>
|
||||||
<span className="text-2xl">2</span>
|
<span className="text-2xl">{loading ? '...' : (examsCount !== null ? String(examsCount) : '-')}</span>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="flex flex-col items-center justify-center p-4">
|
</div>
|
||||||
<MessageCircle className="mb-2 text-primary" aria-hidden />
|
|
||||||
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
|
|
||||||
<span className="text-2xl">1</span>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consultas fictícias
|
// Consultas fictícias
|
||||||
const [currentDate, setCurrentDate] = useState(new Date())
|
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 = [
|
const consultasFicticias = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
medico: "Dr. Carlos Andrade",
|
medico: "Dr. Carlos Andrade",
|
||||||
especialidade: "Cardiologia",
|
especialidade: "Cardiologia",
|
||||||
local: "Clínica Coração Feliz",
|
local: "Clínica Coração Feliz",
|
||||||
data: new Date().toISOString().split('T')[0],
|
data: localDateKey(new Date()),
|
||||||
hora: "09:00",
|
hora: "09:00",
|
||||||
status: "Confirmada"
|
status: "Confirmada"
|
||||||
},
|
},
|
||||||
@ -284,7 +382,7 @@ export default function PacientePage() {
|
|||||||
medico: "Dra. Fernanda Lima",
|
medico: "Dra. Fernanda Lima",
|
||||||
especialidade: "Dermatologia",
|
especialidade: "Dermatologia",
|
||||||
local: "Clínica Pele Viva",
|
local: "Clínica Pele Viva",
|
||||||
data: new Date().toISOString().split('T')[0],
|
data: localDateKey(new Date()),
|
||||||
hora: "14:30",
|
hora: "14:30",
|
||||||
status: "Pendente"
|
status: "Pendente"
|
||||||
},
|
},
|
||||||
@ -293,7 +391,7 @@ export default function PacientePage() {
|
|||||||
medico: "Dr. João Silva",
|
medico: "Dr. João Silva",
|
||||||
especialidade: "Ortopedia",
|
especialidade: "Ortopedia",
|
||||||
local: "Hospital Ortopédico",
|
local: "Hospital Ortopédico",
|
||||||
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
|
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return localDateKey(d) })(),
|
||||||
hora: "11:00",
|
hora: "11:00",
|
||||||
status: "Cancelada"
|
status: "Cancelada"
|
||||||
},
|
},
|
||||||
@ -312,7 +410,7 @@ export default function PacientePage() {
|
|||||||
setCurrentDate(new Date());
|
setCurrentDate(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayStr = currentDate.toISOString().split('T')[0];
|
const todayStr = localDateKey(currentDate)
|
||||||
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
|
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
|
||||||
|
|
||||||
function Consultas() {
|
function Consultas() {
|
||||||
@ -415,7 +513,7 @@ export default function PacientePage() {
|
|||||||
medico: doc?.full_name || a.doctor_id || '---',
|
medico: doc?.full_name || a.doctor_id || '---',
|
||||||
especialidade: doc?.specialty || '',
|
especialidade: doc?.specialty || '',
|
||||||
local: a.location || a.place || '',
|
local: a.location || a.place || '',
|
||||||
data: sched ? sched.toISOString().split('T')[0] : '',
|
data: sched ? localDateKey(sched) : '',
|
||||||
hora: sched ? sched.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '',
|
hora: sched ? sched.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '',
|
||||||
status: a.status ? String(a.status) : 'Pendente',
|
status: a.status ? String(a.status) : 'Pendente',
|
||||||
}
|
}
|
||||||
@ -442,6 +540,8 @@ export default function PacientePage() {
|
|||||||
qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial'
|
qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial'
|
||||||
if (especialidade) qs.set('especialidade', especialidade)
|
if (especialidade) qs.set('especialidade', especialidade)
|
||||||
if (localizacao) qs.set('local', localizacao)
|
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 `/resultados?${qs.toString()}`
|
return `/resultados?${qs.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,7 +632,7 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
|
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
|
||||||
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden">
|
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
|
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
|
||||||
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
|
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
|
||||||
@ -544,7 +644,7 @@ export default function PacientePage() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => navigateDate('prev')}
|
onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('prev') }}
|
||||||
aria-label="Dia anterior"
|
aria-label="Dia anterior"
|
||||||
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
||||||
>
|
>
|
||||||
@ -555,7 +655,7 @@ export default function PacientePage() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => navigateDate('next')}
|
onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('next') }}
|
||||||
aria-label="Próximo dia"
|
aria-label="Próximo dia"
|
||||||
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
className={`group shadow-sm ${hoverPrimaryIconClass}`}
|
||||||
>
|
>
|
||||||
@ -579,7 +679,7 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 overflow-y-auto max-h-[70vh] pr-1 sm:pr-2">
|
<div className="flex-1 flex flex-col gap-4 overflow-y-auto pr-1 sm:pr-2 pb-6">
|
||||||
{loadingAppointments && mostrarAgendadas ? (
|
{loadingAppointments && mostrarAgendadas ? (
|
||||||
<div className="text-center py-10 text-muted-foreground">Carregando consultas...</div>
|
<div className="text-center py-10 text-muted-foreground">Carregando consultas...</div>
|
||||||
) : appointmentsError ? (
|
) : appointmentsError ? (
|
||||||
@ -663,7 +763,7 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
|
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
|
||||||
<Button variant="outline" onClick={() => { /* dialog fechado (controle externo) */ }} className="w-full sm:w-auto">
|
<Button variant="outline" onClick={() => { setMostrarAgendadas(false) }} className="w-full sm:w-auto">
|
||||||
Fechar
|
Fechar
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -680,6 +780,7 @@ export default function PacientePage() {
|
|||||||
const [reports, setReports] = useState<any[] | null>(null)
|
const [reports, setReports] = useState<any[] | null>(null)
|
||||||
const [loadingReports, setLoadingReports] = useState(false)
|
const [loadingReports, setLoadingReports] = useState(false)
|
||||||
const [reportsError, setReportsError] = useState<string | null>(null)
|
const [reportsError, setReportsError] = useState<string | null>(null)
|
||||||
|
const [reportDoctorName, setReportDoctorName] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
@ -701,6 +802,33 @@ export default function PacientePage() {
|
|||||||
return () => { mounted = false }
|
return () => { mounted = false }
|
||||||
}, [patientId])
|
}, [patientId])
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => { mounted = false }
|
||||||
|
}, [selectedReport])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-card shadow-md rounded-lg border border-border p-6">
|
<section className="bg-card shadow-md rounded-lg border border-border p-6">
|
||||||
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
|
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
|
||||||
@ -730,79 +858,76 @@ export default function PacientePage() {
|
|||||||
|
|
||||||
<Dialog open={!!selectedReport} onOpenChange={open => !open && setSelectedReport(null)}>
|
<Dialog open={!!selectedReport} onOpenChange={open => !open && setSelectedReport(null)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Laudo Médico</DialogTitle>
|
<DialogTitle>Laudo Médico</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{selectedReport && (
|
{selectedReport && (
|
||||||
<>
|
<>
|
||||||
<div className="font-semibold mb-2">{selectedReport.title || selectedReport.name || 'Laudo'}</div>
|
<div className="mb-2">
|
||||||
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
|
<div className="font-semibold text-lg">{selectedReport.title || selectedReport.name || 'Laudo'}</div>
|
||||||
<div className="mb-4 whitespace-pre-line">{selectedReport.content || selectedReport.body || JSON.stringify(selectedReport, null, 2)}</div>
|
<div className="text-sm text-muted-foreground">Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
|
||||||
</>
|
{reportDoctorName && <div className="text-sm text-muted-foreground">Profissional: <strong className="text-foreground">{reportDoctorName}</strong></div>}
|
||||||
)}
|
</div>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
{/* Standardized laudo sections: CID, Exame, Diagnóstico, Conclusão, Notas (prefer HTML when available) */}
|
||||||
<DialogFooter>
|
{(() => {
|
||||||
<Button variant="outline" onClick={() => setSelectedReport(null)}>Fechar</Button>
|
const cid = selectedReport.cid ?? selectedReport.cid_code ?? selectedReport.cidCode ?? selectedReport.cie ?? '-'
|
||||||
</DialogFooter>
|
const exam = selectedReport.exam ?? selectedReport.exame ?? selectedReport.especialidade ?? selectedReport.report_type ?? '-'
|
||||||
</DialogContent>
|
const diagnosis = selectedReport.diagnosis ?? selectedReport.diagnostico ?? selectedReport.diagnosis_text ?? selectedReport.diagnostico_text ?? ''
|
||||||
|
const conclusion = selectedReport.conclusion ?? selectedReport.conclusao ?? selectedReport.conclusion_text ?? selectedReport.conclusao_text ?? ''
|
||||||
|
const notesHtml = selectedReport.content_html ?? selectedReport.conteudo_html ?? selectedReport.contentHtml ?? null
|
||||||
|
const notesText = selectedReport.content ?? selectedReport.body ?? selectedReport.conteudo ?? selectedReport.notes ?? selectedReport.observacoes ?? ''
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground">CID</div>
|
||||||
|
<div className="text-foreground">{cid || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground">Exame</div>
|
||||||
|
<div className="text-foreground">{exam || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground">Diagnóstico</div>
|
||||||
|
<div className="whitespace-pre-line text-foreground">{diagnosis || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground">Conclusão</div>
|
||||||
|
<div className="whitespace-pre-line text-foreground">{conclusion || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground">Notas do Profissional</div>
|
||||||
|
{notesHtml ? (
|
||||||
|
<div className="prose max-w-none p-2 bg-muted rounded" dangerouslySetInnerHTML={{ __html: String(notesHtml) }} />
|
||||||
|
) : (
|
||||||
|
<div className="whitespace-pre-line text-foreground p-2 bg-muted rounded">{notesText || '-'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
{/* Optional: doctor signature or footer */}
|
||||||
|
{selectedReport.doctor_signature && (
|
||||||
|
<div className="mt-4 text-sm text-muted-foreground">Assinatura: <img src={selectedReport.doctor_signature} alt="assinatura" className="inline-block h-10" /></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setSelectedReport(null)}>Fechar</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Mensagens() {
|
|
||||||
const [msgs, setMsgs] = useState<any[]>([])
|
|
||||||
const [loadingMsgs, setLoadingMsgs] = useState(false)
|
|
||||||
const [msgsError, setMsgsError] = useState<string | null>(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 (
|
|
||||||
<section className="bg-card shadow-md rounded-lg border border-border p-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-6">Mensagens Recebidas</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{loadingMsgs ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">Carregando mensagens...</div>
|
|
||||||
) : msgsError ? (
|
|
||||||
<div className="text-center py-8 text-red-600">{msgsError}</div>
|
|
||||||
) : (!msgs || msgs.length === 0) ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">Nenhuma mensagem encontrada.</div>
|
|
||||||
) : (
|
|
||||||
msgs.map((msg: any) => (
|
|
||||||
<div key={msg.id || JSON.stringify(msg)} className="bg-muted rounded p-4">
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
<User className="h-4 w-4 text-primary" />
|
|
||||||
{msg.sender_name || msg.from || msg.doctor_name || 'Remetente'}
|
|
||||||
{!msg.read && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.created_at || msg.data || Date.now()).toLocaleString('pt-BR')}</div>
|
|
||||||
<div className="text-foreground whitespace-pre-line">{msg.body || msg.content || msg.text || JSON.stringify(msg)}</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Perfil() {
|
function Perfil() {
|
||||||
const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep)
|
const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep)
|
||||||
@ -920,7 +1045,7 @@ export default function PacientePage() {
|
|||||||
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
|
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
|
||||||
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
|
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
|
||||||
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
|
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
|
||||||
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
|
|
||||||
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
||||||
</nav>
|
</nav>
|
||||||
{/* Conteúdo principal */}
|
{/* Conteúdo principal */}
|
||||||
@ -940,7 +1065,7 @@ export default function PacientePage() {
|
|||||||
{tab==='dashboard' && <DashboardCards />}
|
{tab==='dashboard' && <DashboardCards />}
|
||||||
{tab==='consultas' && <Consultas />}
|
{tab==='consultas' && <Consultas />}
|
||||||
{tab==='exames' && <ExamesLaudos />}
|
{tab==='exames' && <ExamesLaudos />}
|
||||||
{tab==='mensagens' && <Mensagens />}
|
|
||||||
{tab==='perfil' && <Perfil />}
|
{tab==='perfil' && <Perfil />}
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -100,6 +100,9 @@ export default function ResultadosClient() {
|
|||||||
setToast({ type, msg })
|
setToast({ type, msg })
|
||||||
setTimeout(() => setToast(null), 3000)
|
setTimeout(() => setToast(null), 3000)
|
||||||
}
|
}
|
||||||
|
// booking success modal (used when origin=paciente)
|
||||||
|
const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false)
|
||||||
|
const [bookedWhenLabel, setBookedWhenLabel] = useState<string | null>(null)
|
||||||
|
|
||||||
// 1) Obter patientId a partir do usuário autenticado (email -> patients)
|
// 1) Obter patientId a partir do usuário autenticado (email -> patients)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -273,8 +276,20 @@ export default function ResultadosClient() {
|
|||||||
})
|
})
|
||||||
setConfirmOpen(false)
|
setConfirmOpen(false)
|
||||||
setPendingAppointment(null)
|
setPendingAppointment(null)
|
||||||
// Navigate to agenda after a short delay so user sees the toast
|
// If the user came from the paciente area, keep them here and show a success modal
|
||||||
setTimeout(() => router.push('/agenda'), 500)
|
const origin = params?.get('origin')
|
||||||
|
if (origin === 'paciente') {
|
||||||
|
try {
|
||||||
|
const when = new Date(iso).toLocaleString('pt-BR', { dateStyle: 'long', timeStyle: 'short' })
|
||||||
|
setBookedWhenLabel(when)
|
||||||
|
} catch {
|
||||||
|
setBookedWhenLabel(iso)
|
||||||
|
}
|
||||||
|
setBookingSuccessOpen(true)
|
||||||
|
} else {
|
||||||
|
// Navigate to agenda after a short delay so user sees the toast
|
||||||
|
setTimeout(() => router.push('/agenda'), 500)
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast('error', e?.message || 'Falha ao agendar')
|
showToast('error', e?.message || 'Falha ao agendar')
|
||||||
} finally {
|
} finally {
|
||||||
@ -534,6 +549,21 @@ export default function ResultadosClient() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Booking success modal shown when origin=paciente */}
|
||||||
|
<Dialog open={bookingSuccessOpen} onOpenChange={(open) => setBookingSuccessOpen(open)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Consulta agendada</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm">Sua consulta foi agendada com sucesso{bookedWhenLabel ? ` para ${bookedWhenLabel}` : ''}.</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<Button variant="outline" onClick={() => setBookingSuccessOpen(false)}>Fechar</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Hero de filtros (mantido) */}
|
{/* Hero de filtros (mantido) */}
|
||||||
<section className="rounded-3xl bg-primary p-6 text-primary-foreground shadow-lg">
|
<section className="rounded-3xl bg-primary p-6 text-primary-foreground shadow-lg">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user