fix-reports

This commit is contained in:
João Gustavo 2025-10-27 23:11:33 -03:00
parent 79eb63ad96
commit bb6e3b0d25
2 changed files with 130 additions and 28 deletions

View File

@ -269,13 +269,22 @@ export default function PacientePage() {
// 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 +293,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 +302,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 +321,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 +424,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 +451,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 +543,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 +555,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 +566,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 +590,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 +674,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 +691,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 +713,30 @@ 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) setReportDoctorName(docs[0].full_name || docs[0].name || 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,22 +766,58 @@ 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> {/* Prefer HTML content when available */}
<DialogFooter> {selectedReport.content_html ? (
<Button variant="outline" onClick={() => setSelectedReport(null)}>Fechar</Button> <div className="prose max-w-none mb-4" dangerouslySetInnerHTML={{ __html: selectedReport.content_html }} />
</DialogFooter> ) : (
</DialogContent> <div className="space-y-3 mb-4">
{selectedReport.exam && (
<div>
<div className="text-xs text-muted-foreground">Exame</div>
<div className="text-foreground">{selectedReport.exam}</div>
</div>
)}
{selectedReport.diagnosis && (
<div>
<div className="text-xs text-muted-foreground">Diagnóstico</div>
<div className="whitespace-pre-line text-foreground">{selectedReport.diagnosis}</div>
</div>
)}
{selectedReport.conclusion && (
<div>
<div className="text-xs text-muted-foreground">Conclusão</div>
<div className="whitespace-pre-line text-foreground">{selectedReport.conclusion}</div>
</div>
)}
{/* fallback to generic content/body */}
{!(selectedReport.content_html || selectedReport.diagnosis || selectedReport.conclusion || selectedReport.content || selectedReport.body) && (
<pre className="text-sm whitespace-pre-wrap bg-muted p-3 rounded">{JSON.stringify(selectedReport, null, 2)}</pre>
)}
</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>
) )

View File

@ -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">