- {filteredAppointments.map(app => {
- const [date, timeStr] = app.time.split('T');
- const [hours, minutes] = timeStr.split(':');
+ {filteredAppointments.map(app => {
+ const d = new Date(app.time);
+ const parts = new Intl.DateTimeFormat('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'America/Sao_Paulo' }).formatToParts(d);
+ const hourPart = parts.find(p => p.type === 'hour')?.value ?? '00';
+ const minutePart = parts.find(p => p.type === 'minute')?.value ?? '00';
+ const hours = String(hourPart).padStart(2,'0');
+ const minutes = String(minutePart).padStart(2,'0');
return (
diff --git a/susconecta/components/ui/calendar.tsx b/susconecta/components/ui/calendar.tsx
index 4d7c46a..5479c11 100644
--- a/susconecta/components/ui/calendar.tsx
+++ b/susconecta/components/ui/calendar.tsx
@@ -35,9 +35,9 @@ function Calendar({
className
)}
captionLayout={captionLayout}
- formatters={{
+ formatters={{
formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
+ date.toLocaleString("default", { month: "short", timeZone: 'America/Sao_Paulo' }),
...formatters,
}}
classNames={{
@@ -155,7 +155,7 @@ function Calendar({
)
},
- DayButton: CalendarDayButton,
+ DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
@@ -190,7 +190,7 @@ function CalendarDayButton({
ref={ref}
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(undefined, { timeZone: 'America/Sao_Paulo' })}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
--
2.47.2
From a2d90bf68f6c4f0c51d280d24ef03e115e23cb67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?=
<166467972+JoaoGustavo-dev@users.noreply.github.com>
Date: Thu, 30 Oct 2025 18:21:30 -0300
Subject: [PATCH 2/8] fix-report-page
---
susconecta/app/paciente/page.tsx | 60 ++++++++++++++++++++++++++++++--
1 file changed, 57 insertions(+), 3 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index eb631df..6822431 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -730,6 +730,60 @@ export default function PacientePage() {
const [loadingReports, setLoadingReports] = useState(false)
const [reportsError, setReportsError] = useState(null)
const [reportDoctorName, setReportDoctorName] = useState(null)
+ const [doctorsMap, setDoctorsMap] = useState>({})
+
+ // 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 {
+ 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 = {}
+ for (const d of docs || []) {
+ if (d && d.id !== undefined && d.id !== null) map[String(d.id)] = d
+ }
+ setDoctorsMap(map)
+ } catch (e) {
+ // ignore resolution errors
+ }
+ })()
+ return () => { mounted = false }
+ }, [reports])
useEffect(() => {
let mounted = true
@@ -789,11 +843,11 @@ export default function PacientePage() {
{reportsError}
) : (!reports || reports.length === 0) ? (
Nenhum laudo encontrado para este paciente.
- ) : (
+ ) : (
reports.map((r) => (
-
{r.title || r.name || r.report_name || 'Laudo'}
+
{reportTitle(r)}
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
@@ -813,7 +867,7 @@ export default function PacientePage() {
{selectedReport && (
<>
-
{selectedReport.title || selectedReport.name || 'Laudo'}
+
{reportTitle(selectedReport, reportDoctorName)}
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
{reportDoctorName &&
Profissional: {reportDoctorName}
}
--
2.47.2
From c1471ea4faa899645f1aaa543d62161cbb7e5510 Mon Sep 17 00:00:00 2001
From: M-Gabrielly
Date: Thu, 30 Oct 2025 18:26:01 -0300
Subject: [PATCH 3/8] fix: fix of hovers
---
.../app/(main-routes)/dashboard/page.tsx | 6 +--
susconecta/app/paciente/page.tsx | 20 +++++++---
susconecta/app/profissional/page.tsx | 38 +++++++++----------
susconecta/components/dashboard/header.tsx | 2 +-
susconecta/components/simple-theme-toggle.tsx | 2 +-
susconecta/components/ui/sidebar.tsx | 2 +-
6 files changed, 39 insertions(+), 31 deletions(-)
diff --git a/susconecta/app/(main-routes)/dashboard/page.tsx b/susconecta/app/(main-routes)/dashboard/page.tsx
index 1ec2c13..5d6bf2d 100644
--- a/susconecta/app/(main-routes)/dashboard/page.tsx
+++ b/susconecta/app/(main-routes)/dashboard/page.tsx
@@ -283,15 +283,15 @@ export default function DashboardPage() {
Novo Paciente
- router.push('/agenda')} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
+ router.push('/agenda')} variant="outline" className="gap-2 hover:!bg-primary hover:!text-white transition-colors">
Novo Agendamento
- setShowDoctorForm(true)} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
+ setShowDoctorForm(true)} variant="outline" className="gap-2 hover:!bg-primary hover:!text-white transition-colors">
Novo Médico
- router.push('/dashboard/relatorios')} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
+ router.push('/dashboard/relatorios')} variant="outline" className="gap-2 hover:!bg-primary hover:!text-white transition-colors">
Ver Relatórios
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index eb631df..18ca84e 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -982,7 +982,7 @@ export default function PacientePage() {
Portal do Paciente
-
+
Início
@@ -990,7 +990,15 @@ export default function PacientePage() {
- {strings.sair}
+
+ {strings.sair}
+
@@ -1001,7 +1009,7 @@ export default function PacientePage() {
variant={tab==='dashboard'?'secondary':'ghost'}
aria-current={tab==='dashboard'}
onClick={()=>setTab('dashboard')}
- className={`justify-start ${tab==='dashboard' ? 'bg-primary/10 text-primary' : ''}`}
+ className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='dashboard' ? 'bg-primary/10 text-primary' : ''}`}
>
{strings.dashboard}
@@ -1009,7 +1017,7 @@ export default function PacientePage() {
variant={tab==='consultas'?'secondary':'ghost'}
aria-current={tab==='consultas'}
onClick={()=>setTab('consultas')}
- className={`justify-start ${tab==='consultas' ? 'bg-primary/10 text-primary' : ''}`}
+ className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='consultas' ? 'bg-primary/10 text-primary' : ''}`}
>
{strings.consultas}
@@ -1017,7 +1025,7 @@ export default function PacientePage() {
variant={tab==='exames'?'secondary':'ghost'}
aria-current={tab==='exames'}
onClick={()=>setTab('exames')}
- className={`justify-start ${tab==='exames' ? 'bg-primary/10 text-primary' : ''}`}
+ className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='exames' ? 'bg-primary/10 text-primary' : ''}`}
>
{strings.exames}
@@ -1026,7 +1034,7 @@ export default function PacientePage() {
variant={tab==='perfil'?'secondary':'ghost'}
aria-current={tab==='perfil'}
onClick={()=>setTab('perfil')}
- className={`justify-start ${tab==='perfil' ? 'bg-primary/10 text-primary' : ''}`}
+ className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='perfil' ? 'bg-primary/10 text-primary' : ''}`}
>
{strings.perfil}
diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx
index 5efa5be..74794b5 100644
--- a/susconecta/app/profissional/page.tsx
+++ b/susconecta/app/profissional/page.tsx
@@ -690,9 +690,9 @@ const ProfissionalPage = () => {
variant="outline"
size="sm"
onClick={() => navigateDate('prev')}
- className="p-2 hover:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground"
+ className="p-2 hover:!bg-primary hover:!text-white cursor-pointer transition-colors"
>
-
+
{formatDate(currentCalendarDate)}
@@ -701,9 +701,9 @@ const ProfissionalPage = () => {
variant="outline"
size="sm"
onClick={() => navigateDate('next')}
- className="p-2 hover:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground"
+ className="p-2 hover:!bg-primary hover:!text-white cursor-pointer transition-colors"
>
-
+
@@ -900,7 +900,7 @@ const ProfissionalPage = () => {
variant={selectedRange === 'todos' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedRange('todos')}
- className="hover:bg-primary/10 hover:text-primary"
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Todos
@@ -908,7 +908,7 @@ const ProfissionalPage = () => {
variant={selectedRange === 'semana' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedRange('semana')}
- className="hover:bg-primary/10 hover:text-primary"
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Semana
@@ -916,7 +916,7 @@ const ProfissionalPage = () => {
variant={selectedRange === 'mes' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedRange('mes')}
- className="hover:bg-primary/10 hover:text-primary"
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Mês
@@ -1077,7 +1077,7 @@ const ProfissionalPage = () => {
Buscar
-
+
Limpar
@@ -1383,7 +1383,7 @@ const ProfissionalPage = () => {
setIsViewing(true);
}
}}
- className="flex items-center gap-1 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground"
+ className="flex items-center gap-1 hover:!bg-primary hover:!text-white transition-colors"
>
Ver Laudo
@@ -2457,7 +2457,7 @@ const ProfissionalPage = () => {
Este editor permite escrever relatórios de forma livre, com formatação de texto rica.
-
+
Cancelar
{/* botão 'Salvar Rascunho' removido por não ser utilizado */}
@@ -2656,7 +2656,7 @@ const ProfissionalPage = () => {
Salvar
-
+
Cancelar
@@ -2779,7 +2779,7 @@ const ProfissionalPage = () => {
{isEditingProfile && (
-
+
Alterar Foto
@@ -2875,7 +2875,7 @@ const ProfissionalPage = () => {
setActiveSection('calendario')}
>
@@ -2883,7 +2883,7 @@ const ProfissionalPage = () => {
setActiveSection('pacientes')}
>
@@ -2891,7 +2891,7 @@ const ProfissionalPage = () => {
setActiveSection('laudos')}
>
@@ -2899,7 +2899,7 @@ const ProfissionalPage = () => {
setActiveSection('comunicacao')}
>
@@ -2907,7 +2907,7 @@ const ProfissionalPage = () => {
setActiveSection('perfil')}
>
@@ -2957,7 +2957,7 @@ const ProfissionalPage = () => {
setShowPopup(false)}
variant="outline"
- className="flex-1 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground"
+ className="flex-1 hover:!bg-primary hover:!text-white transition-colors"
>
Cancelar
@@ -3072,7 +3072,7 @@ const ProfissionalPage = () => {
setShowActionModal(false)}
variant="outline"
- className="w-full mt-2 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground"
+ className="w-full mt-2 hover:!bg-primary hover:!text-white transition-colors"
>
Cancelar
diff --git a/susconecta/components/dashboard/header.tsx b/susconecta/components/dashboard/header.tsx
index 002d16e..9963776 100644
--- a/susconecta/components/dashboard/header.tsx
+++ b/susconecta/components/dashboard/header.tsx
@@ -43,7 +43,7 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
-
+
diff --git a/susconecta/components/simple-theme-toggle.tsx b/susconecta/components/simple-theme-toggle.tsx
index c322131..3f96dcd 100644
--- a/susconecta/components/simple-theme-toggle.tsx
+++ b/susconecta/components/simple-theme-toggle.tsx
@@ -17,7 +17,7 @@ export function SimpleThemeToggle() {
variant="outline"
size="icon"
onClick={toggleTheme}
- className="hover:text-muted-foreground cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border"
+ className="hover:!bg-primary hover:!text-white hover:!border-primary cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border transition-colors"
>
diff --git a/susconecta/components/ui/sidebar.tsx b/susconecta/components/ui/sidebar.tsx
index 116d4d3..4409ae3 100644
--- a/susconecta/components/ui/sidebar.tsx
+++ b/susconecta/components/ui/sidebar.tsx
@@ -266,7 +266,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
- className={cn("size-7", className)}
+ className={cn("size-7 hover:!bg-primary hover:!text-white transition-colors", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
--
2.47.2
From d2bb921b69d589cca9fd0535e37235e4b2a42233 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Thu, 30 Oct 2025 18:47:41 -0300
Subject: [PATCH 4/8] feat(paciente): padronizar tipografia e ajustar estilo
dos cards do dashboard
---
susconecta/app/paciente/page.tsx | 37 ++++++++++++++++++++++++--------
1 file changed, 28 insertions(+), 9 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index eb631df..75211ca 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -341,16 +341,35 @@ export default function PacientePage() {
}, [patientId])
return (
-
-
-
- {strings.proximaConsulta}
- {loading ? '...' : (nextAppt ?? '-')}
+
+
+
+
+
+
+
+ {strings.proximaConsulta}
+
+ {/* mesmo tamanho e fonte do rótulo */}
+
+ {loading ? '—' : (nextAppt ?? '-')}
+
+
-
-
- {strings.ultimosExames}
- {loading ? '...' : (examsCount !== null ? String(examsCount) : '-')}
+
+
+
+
+
+
+
+ {strings.ultimosExames}
+
+ {/* mesmo tamanho e fonte do rótulo */}
+
+ {loading ? '—' : (examsCount !== null ? String(examsCount) : '-')}
+
+
)
--
2.47.2
From 3ff349905d119ebf31149d3d0fa65d43300f843e Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Thu, 30 Oct 2025 18:59:24 -0300
Subject: [PATCH 5/8] =?UTF-8?q?style(paciente):=20padronizar=20fonte=20e?=
=?UTF-8?q?=20tamanho=20dos=20r=C3=B3tulos=20e=20n=C3=BAmeros=20nos=20card?=
=?UTF-8?q?s=20do=20dashboard?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
susconecta/app/paciente/page.tsx | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index a1b52ee..0b79647 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -347,11 +347,11 @@ export default function PacientePage() {
-
+ {/* rótulo e número com mesma fonte e mesmo tamanho (harmônico) */}
+
{strings.proximaConsulta}
- {/* mesmo tamanho e fonte do rótulo */}
-
+
{loading ? '—' : (nextAppt ?? '-')}
@@ -362,11 +362,10 @@ export default function PacientePage() {
-
+
{strings.ultimosExames}
- {/* mesmo tamanho e fonte do rótulo */}
-
+
{loading ? '—' : (examsCount !== null ? String(examsCount) : '-')}
@@ -864,20 +863,20 @@ export default function PacientePage() {
Nenhum laudo encontrado para este paciente.
) : (
reports.map((r) => (
-
+
-
{reportTitle(r)}
-
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
+
{reportTitle(r)}
+
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
-
-
{ setSelectedReport(r); }}>{strings.visualizarLaudo}
-
{ try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}
+
+ { setSelectedReport(r); }}>{strings.visualizarLaudo}
+ { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}
))
)}
-
+
!open && setSelectedReport(null)}>
@@ -886,7 +885,7 @@ export default function PacientePage() {
{selectedReport && (
<>
-
{reportTitle(selectedReport, reportDoctorName)}
+
{reportTitle(selectedReport, reportDoctorName)}
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
{reportDoctorName &&
Profissional: {reportDoctorName}
}
--
2.47.2
From d3c897d95ad3fa802382f7aefdfaf30854db7566 Mon Sep 17 00:00:00 2001
From: M-Gabrielly
Date: Thu, 30 Oct 2025 22:57:26 -0300
Subject: [PATCH 6/8] fix: Redesign of the patient portal and add hovers
- placed hovers on the medical and administrator/manager/secretary pages
- Transformed "Scheduled Appointments" modal into a normal page section
- Reorganized flex layout for responsive grid [220px_1fr]
- Redesigned header with avatar, patient info, and rounded edges, now sticky
- Side menu revamped with rounded edges, sticky and responsive
- Appointment cards with gradients, smooth hovers, and responsive grid layout
- Unified visual standard with professional/doctor page
- Improvements: reduced icons, consistent spacing, smooth transitions
- Content remains centered without forced margins
- Responsiveness guaranteed for mobile, tablet, and desktop
---
.../app/(main-routes)/calendar/page.tsx | 4 +-
.../app/(main-routes)/consultas/page.tsx | 20 +-
.../app/(main-routes)/dashboard/page.tsx | 2 +-
.../dashboard/relatorios/page.tsx | 17 +-
.../app/(main-routes)/doutores/page.tsx | 20 +-
.../app/(main-routes)/pacientes/page.tsx | 22 +-
susconecta/app/paciente/page.tsx | 303 +++++++++---------
.../app/resultados/ResultadosClient.tsx | 35 +-
susconecta/components/ui/dropdown-menu.tsx | 2 +-
9 files changed, 223 insertions(+), 202 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index f1ce1b3..148bf33 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -147,7 +147,7 @@ export default function AgendamentoPage() {
setActiveTab("calendar")}
>
Calendário
@@ -155,7 +155,7 @@ export default function AgendamentoPage() {
setActiveTab("espera")}
>
Lista de espera
diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx
index f351ff5..889b622 100644
--- a/susconecta/app/(main-routes)/consultas/page.tsx
+++ b/susconecta/app/(main-routes)/consultas/page.tsx
@@ -526,12 +526,12 @@ export default function ConsultasPage() {
) : (
-
- Paciente
- Médico
- Status
- Data e Hora
- Ações
+
+ Paciente
+ Médico
+ Status
+ Data e Hora
+ Ações
@@ -564,7 +564,7 @@ export default function ConsultasPage() {
-
+
Abrir menu
@@ -601,7 +601,7 @@ export default function ConsultasPage() {
setItemsPerPage(Number(e.target.value))}
- className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
+ className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
10
15
@@ -619,6 +619,7 @@ export default function ConsultasPage() {
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Primeira
@@ -627,6 +628,7 @@ export default function ConsultasPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Anterior
@@ -638,6 +640,7 @@ export default function ConsultasPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Próxima
@@ -646,6 +649,7 @@ export default function ConsultasPage() {
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Última
diff --git a/susconecta/app/(main-routes)/dashboard/page.tsx b/susconecta/app/(main-routes)/dashboard/page.tsx
index 5d6bf2d..1bd16a2 100644
--- a/susconecta/app/(main-routes)/dashboard/page.tsx
+++ b/susconecta/app/(main-routes)/dashboard/page.tsx
@@ -340,7 +340,7 @@ export default function DashboardPage() {
{report.exam || 'Sem descrição'}
))}
- router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2" size="sm">
+ router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2 hover:!bg-primary hover:!text-white transition-colors" size="sm">
Ver Todos
diff --git a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx
index 65bdcaf..126fc8e 100644
--- a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx
+++ b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx
@@ -102,7 +102,7 @@ export default function RelatoriosPage() {
Consultas por Período
- exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> Exportar PDF
+ exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> Exportar PDF
@@ -119,7 +119,7 @@ export default function RelatoriosPage() {
Faturamento Mensal
- exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> Exportar PDF
+ exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> Exportar PDF
@@ -138,7 +138,7 @@ export default function RelatoriosPage() {
Taxa de No-show
- exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> Exportar PDF
+ exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> Exportar PDF
@@ -155,7 +155,7 @@ export default function RelatoriosPage() {
Satisfação dos Pacientes
- exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> Exportar PDF
+ exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> Exportar PDF
92%
@@ -169,7 +169,7 @@ export default function RelatoriosPage() {
Pacientes Mais Atendidos
- exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> Exportar PDF
+ exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> Exportar PDF
@@ -193,7 +193,7 @@ export default function RelatoriosPage() {
Médicos Mais Produtivos
- exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> Exportar PDF
+ exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> Exportar PDF
@@ -219,7 +219,7 @@ export default function RelatoriosPage() {
Análise de Convênios
- exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> Exportar PDF
+ exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> Exportar PDF
@@ -238,7 +238,7 @@ export default function RelatoriosPage() {
Performance por Médico
- exportPDF("Performance por Médico", "Resumo da performance por médico.")}> Exportar PDF
+ exportPDF("Performance por Médico", "Resumo da performance por médico.")}> Exportar PDF
@@ -263,3 +263,4 @@ export default function RelatoriosPage() {
);
}
+
diff --git a/susconecta/app/(main-routes)/doutores/page.tsx b/susconecta/app/(main-routes)/doutores/page.tsx
index 85a795e..3e958f7 100644
--- a/susconecta/app/(main-routes)/doutores/page.tsx
+++ b/susconecta/app/(main-routes)/doutores/page.tsx
@@ -483,12 +483,12 @@ export default function DoutoresPage() {
-
- Nome
- Especialidade
- CRM
- Contato
- Ações
+
+ Nome
+ Especialidade
+ CRM
+ Contato
+ Ações
@@ -515,7 +515,7 @@ export default function DoutoresPage() {
-
+
Abrir menu
@@ -605,7 +605,7 @@ export default function DoutoresPage() {
setItemsPerPage(Number(e.target.value))}
- className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
+ className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
10
15
@@ -623,6 +623,7 @@ export default function DoutoresPage() {
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Primeira
@@ -631,6 +632,7 @@ export default function DoutoresPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Anterior
@@ -642,6 +644,7 @@ export default function DoutoresPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Próxima
@@ -650,6 +653,7 @@ export default function DoutoresPage() {
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Última
diff --git a/susconecta/app/(main-routes)/pacientes/page.tsx b/susconecta/app/(main-routes)/pacientes/page.tsx
index fe592e9..df77a10 100644
--- a/susconecta/app/(main-routes)/pacientes/page.tsx
+++ b/susconecta/app/(main-routes)/pacientes/page.tsx
@@ -236,13 +236,13 @@ export default function PacientesPage() {
-
- Nome
- CPF
- Telefone
- Cidade
- Estado
- Ações
+
+ Nome
+ CPF
+ Telefone
+ Cidade
+ Estado
+ Ações
@@ -257,7 +257,7 @@ export default function PacientesPage() {
-
+
Abrir menu
@@ -302,7 +302,7 @@ export default function PacientesPage() {
setItemsPerPage(Number(e.target.value))}
- className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
+ className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
10
15
@@ -320,6 +320,7 @@ export default function PacientesPage() {
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Primeira
@@ -328,6 +329,7 @@ export default function PacientesPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Anterior
@@ -339,6 +341,7 @@ export default function PacientesPage() {
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Próxima
@@ -347,6 +350,7 @@ export default function PacientesPage() {
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
+ className="hover:!bg-primary hover:!text-white transition-colors"
>
Última
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 18ca84e..0b98fa6 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -418,25 +418,21 @@ export default function PacientePage() {
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 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 "Ver consultas agendadas" is opened)
+ // Appointments state (loaded when component mounts)
const [appointments, setAppointments] = useState(null)
const [loadingAppointments, setLoadingAppointments] = useState(false)
const [appointmentsError, setAppointmentsError] = useState(null)
useEffect(() => {
let mounted = true
- if (!mostrarAgendadas) return
if (!patientId) {
setAppointmentsError('Paciente não identificado. Faça login novamente.')
return
@@ -532,7 +528,7 @@ export default function PacientePage() {
loadAppointments()
return () => { mounted = false }
- }, [mostrarAgendadas, patientId])
+ }, [patientId])
// Monta a URL de resultados com os filtros atuais
const buildResultadosHref = () => {
@@ -545,70 +541,63 @@ export default function PacientePage() {
return `/resultados?${qs.toString()}`
}
- // derived lists for the "Ver consultas agendadas" dialog (computed after appointments state is declared)
+ // 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)
return (
-
-
-
+
+ {/* Hero Section */}
+
+
+
-
- {/* Remover campos de especialidade e localização, deixar só o botão centralizado */}
-
-
-
- Pesquisar
-
-
+
+
+
+
+ Pesquisar Médicos
+
+
+
+
-
- setMostrarAgendadas(true)}
- >
- Ver consultas agendadas
-
-
-
+ {/* Consultas Agendadas Section */}
+
+
+
-
setMostrarAgendadas(open)}>
-
-
- Consultas agendadas
- Gerencie suas consultas confirmadas, pendentes ou canceladas.
-
-
-
-
+ {/* Date Navigation */}
+
+
{ e.stopPropagation(); e.preventDefault(); navigateDate('prev') }}
aria-label="Dia anterior"
- className={`group shadow-sm ${hoverPrimaryIconClass}`}
+ className={`group shadow-sm hover:!bg-primary hover:!text-white hover:!border-primary transition-all ${hoverPrimaryIconClass}`}
>
-
+
- {formatDatePt(currentDate)}
+ {formatDatePt(currentDate)}
{ e.stopPropagation(); e.preventDefault(); navigateDate('next') }}
aria-label="Próximo dia"
- className={`group shadow-sm ${hoverPrimaryIconClass}`}
+ className={`group shadow-sm hover:!bg-primary hover:!text-white hover:!border-primary transition-all ${hoverPrimaryIconClass}`}
>
-
+
{isSelectedDateToday && (
Hoje
)}
-
- {`${_todaysAppointments.length} consulta${_todaysAppointments.length !== 1 ? 's' : ''} agendada${_todaysAppointments.length !== 1 ? 's' : ''}`}
+
+ {_todaysAppointments.length} consulta{_todaysAppointments.length !== 1 ? 's' : ''} agendada{_todaysAppointments.length !== 1 ? 's' : ''}
-
- {loadingAppointments && mostrarAgendadas ? (
+ {/* Appointments List */}
+
+ {loadingAppointments ? (
Carregando consultas...
) : appointmentsError ? (
{appointmentsError}
) : (
- // prefer appointments (client-loaded) when present; fallback to fictitious list
(() => {
const todays = _todaysAppointments
if (!todays || todays.length === 0) {
return (
-
-
-
Nenhuma consulta agendada para este dia
-
Use a busca para marcar uma nova consulta.
+
+
+
+
+
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.medico}
-
- {consulta.especialidade} • {consulta.local}
+
+ {consulta.especialidade}
+ •
+ {consulta.local}
-
-
-
{consulta.hora}
+ {/* Time */}
+
+
+ {consulta.hora}
-
-
+ {/* Status Badge */}
+
+
{consulta.status}
-
+ {/* Action Buttons */}
+
Detalhes
{consulta.status !== 'Cancelada' && (
-
+
Reagendar
)}
{consulta.status !== 'Cancelada' && (
Cancelar
@@ -710,15 +716,9 @@ export default function PacientePage() {
})()
)}
-
-
- { setMostrarAgendadas(false) }} className="w-full sm:w-auto">
- Fechar
-
-
-
-
-
+
+
+
)
}
@@ -797,8 +797,8 @@ export default function PacientePage() {
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
- { setSelectedReport(r); }}>{strings.visualizarLaudo}
- { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}
+ { setSelectedReport(r); }}>{strings.visualizarLaudo}
+ { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}
))
@@ -976,74 +976,84 @@ export default function PacientePage() {
// Renderização principal
return (
-
- {/* Header só com título e botão de sair */}
-
-
-
-
Portal do Paciente
-
+
+ {/* Header com informações do paciente */}
+
-
- {/* Sidebar vertical */}
-
- setTab('dashboard')}
- className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='dashboard' ? 'bg-primary/10 text-primary' : ''}`}
- >
- {strings.dashboard}
-
- setTab('consultas')}
- className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='consultas' ? 'bg-primary/10 text-primary' : ''}`}
- >
- {strings.consultas}
-
- setTab('exames')}
- className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='exames' ? 'bg-primary/10 text-primary' : ''}`}
- >
- {strings.exames}
-
-
- setTab('perfil')}
- className={`justify-start hover:!bg-primary hover:!text-white transition-colors ${tab==='perfil' ? 'bg-primary/10 text-primary' : ''}`}
- >
- {strings.perfil}
-
-
+ {/* Layout com sidebar e conteúdo */}
+
+ {/* Sidebar vertical - sticky */}
+
+
+ setTab('dashboard')}
+ className={`w-full justify-start transition-colors hover:!bg-primary hover:!text-white cursor-pointer`}
+ >
+ {strings.dashboard}
+
+ setTab('consultas')}
+ className={`w-full justify-start transition-colors hover:!bg-primary hover:!text-white cursor-pointer`}
+ >
+ {strings.consultas}
+
+ setTab('exames')}
+ className={`w-full justify-start transition-colors hover:!bg-primary hover:!text-white cursor-pointer`}
+ >
+ {strings.exames}
+
+
+ setTab('perfil')}
+ className={`w-full justify-start transition-colors hover:!bg-primary hover:!text-white cursor-pointer`}
+ >
+ {strings.perfil}
+
+
+
+
{/* Conteúdo principal */}
-
+
{/* Toasts de feedback */}
{toast && (
- {toast.msg}
+ {toast.msg}
)}
{/* Loader global */}
@@ -1052,15 +1062,14 @@ export default function PacientePage() {
{/* Conteúdo principal */}
{!loading && !error && (
-
+ <>
{tab==='dashboard' && }
{tab==='consultas' && }
{tab==='exames' && }
-
{tab==='perfil' && }
-
+ >
)}
-
+
diff --git a/susconecta/app/resultados/ResultadosClient.tsx b/susconecta/app/resultados/ResultadosClient.tsx
index 485a7e1..8c6391d 100644
--- a/susconecta/app/resultados/ResultadosClient.tsx
+++ b/susconecta/app/resultados/ResultadosClient.tsx
@@ -573,7 +573,7 @@ export default function ResultadosClient() {
Ajustar filtros
@@ -617,7 +617,7 @@ export default function ResultadosClient() {
-
+
@@ -631,7 +631,7 @@ export default function ResultadosClient() {
-
+
@@ -644,7 +644,7 @@ export default function ResultadosClient() {
Mais filtros
@@ -652,7 +652,7 @@ export default function ResultadosClient() {
router.back()}
>
Voltar
@@ -711,7 +711,7 @@ export default function ResultadosClient() {
{
setMedicoSelecionado(medico)
setAbaDetalhe('experiencia')
@@ -782,12 +782,12 @@ export default function ResultadosClient() {
>
Agendar consulta
-
+
Enviar mensagem
{
const willOpen = !agendasExpandida[id]
setAgendasExpandida(prev => ({ ...prev, [id]: !prev[id] }))
@@ -979,18 +979,17 @@ export default function ResultadosClient() {
{/* Dialog: Mostrar mais horários (escolher data arbitrária) */}
{ if (!open) { setMoreTimesForDoctor(null); setMoreTimesSlots([]); setMoreTimesException(null); } }}>
-
-
-
- Mais horários
-
-
- setMoreTimesDate(e.target.value)} />
- { if (moreTimesForDoctor) await fetchSlotsForDate(moreTimesForDoctor, moreTimesDate) }}>Buscar horários
-
+
+
+ Mais horários
+
+
+
+ setMoreTimesDate(e.target.value)} />
+ { if (moreTimesForDoctor) await fetchSlotsForDate(moreTimesForDoctor, moreTimesDate) }}>Buscar horários
-
+
{moreTimesLoading ? (
Carregando horários...
) : moreTimesException ? (
diff --git a/susconecta/components/ui/dropdown-menu.tsx b/susconecta/components/ui/dropdown-menu.tsx
index ec51e9c..b4aeee9 100644
--- a/susconecta/components/ui/dropdown-menu.tsx
+++ b/susconecta/components/ui/dropdown-menu.tsx
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
- "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "focus:bg-primary focus:text-primary-foreground hover:bg-primary hover:text-primary-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
className
)}
{...props}
--
2.47.2
From 94a4176bc8ba4c9995a34dfecd14b8b5c660f84e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?=
<166467972+JoaoGustavo-dev@users.noreply.github.com>
Date: Thu, 30 Oct 2025 23:01:37 -0300
Subject: [PATCH 7/8] fix-report-page
---
susconecta/app/paciente/page.tsx | 287 ++++++++++++++++++++++++++++++-
1 file changed, 280 insertions(+), 7 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 0b79647..cd5cf40 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -1,7 +1,7 @@
'use client'
import type { ReactNode } from 'react'
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
@@ -17,7 +17,8 @@ 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, listarAgendamentos, buscarMedicosPorIds, atualizarPaciente, buscarPacientePorId } from '@/lib/api'
+import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById } from '@/lib/api'
+import { buscarRelatorioPorId, listarRelatoriosPorMedico } from '@/lib/reports'
import { ENV_CONFIG } from '@/lib/env-config'
import { listarRelatoriosPorPaciente } from '@/lib/reports'
// reports are rendered statically for now
@@ -749,6 +750,140 @@ export default function PacientePage() {
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)
+
+ // derived filtered list based on search term
+ 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]
+ }
+
+ if (!q) return reports
+ return 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
+ }
+ })
+ }, [reports, searchTerm, doctorsMap, remoteMatch])
+
+ // 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) {
+ 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
+ const promises = docs.map(d => listarRelatoriosPorMedico(String(d.id)).catch(() => []))
+ 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) => {
@@ -787,17 +922,69 @@ export default function PacientePage() {
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 && d.id !== undefined && d.id !== null) map[String(d.id)] = d
+ 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)
+ setResolvingDoctors(false)
} catch (e) {
// ignore resolution errors
+ setResolvingDoctors(false)
}
})()
return () => { mounted = false }
@@ -842,6 +1029,36 @@ export default function PacientePage() {
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
@@ -850,22 +1067,57 @@ export default function PacientePage() {
return () => { mounted = false }
}, [selectedReport])
+ // 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) }} />
+ {searchTerm && (
+ { setSearchTerm(''); setReportsPage(1) }}>Limpar
+ )}
+
{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.
+ )
) : (
- reports.map((r) => (
+ (() => {
+ const total = Array.isArray(filteredReports) ? filteredReports.length : 0
+ const totalPages = Math.max(1, Math.ceil(total / reportsPerPage))
+ // keep page inside bounds
+ const page = Math.min(Math.max(1, reportsPage), totalPages)
+ const start = (page - 1) * reportsPerPage
+ const end = start + reportsPerPage
+ const pageItems = (filteredReports || []).slice(start, end)
+
+ return (
+ <>
+ {pageItems.map((r) => (
-
{reportTitle(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')}
@@ -873,7 +1125,20 @@ export default function PacientePage() {
{ try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}
- ))
+ ))}
+
+ {/* Pagination controls */}
+
+
Mostrando {Math.min(start+1, total)}–{Math.min(end, total)} de {total}
+
+
setReportsPage(p => Math.max(1, p-1))} disabled={page <= 1} className="px-3">Anterior
+
{page} / {totalPages}
+
setReportsPage(p => Math.min(totalPages, p+1))} disabled={page >= totalPages} className="px-3">Próxima
+
+
+ >
+ )
+ })()
)}
@@ -885,7 +1150,15 @@ export default function PacientePage() {
{selectedReport && (
<>
-
{reportTitle(selectedReport, reportDoctorName)}
+ {
+ // prefer the resolved doctor name; while resolving, show a loading indicator instead of raw IDs
+ (() => {
+ const maybeId = selectedReport?.doctor_id || selectedReport?.created_by || selectedReport?.doctor || null
+ if (reportDoctorName) return
{reportTitle(selectedReport, reportDoctorName)}
+ if (resolvingDoctors && maybeId && !doctorsMap[String(maybeId)]) return
{strings.carregando}
+ return
{reportTitle(selectedReport)}
+ })()
+ }
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
{reportDoctorName &&
Profissional: {reportDoctorName}
}
--
2.47.2
From 82d5644c2839f395be010ead6c3ea85ea741353f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?=
<166467972+JoaoGustavo-dev@users.noreply.github.com>
Date: Thu, 30 Oct 2025 23:20:49 -0300
Subject: [PATCH 8/8] fix-doctor-ID
---
susconecta/app/paciente/page.tsx | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 4058a86..5e427d1 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -1153,10 +1153,20 @@ export default function PacientePage() {
{
// prefer the resolved doctor name; while resolving, show a loading indicator instead of raw IDs
(() => {
+ const looksLikeIdStr = (s: any) => {
+ try {
+ const hexOnly = String(s || '').replace(/[^0-9a-fA-F]/g, '')
+ const len = (typeof hexOnly === 'string') ? hexOnly.length : (Number(hexOnly) || 0)
+ return len >= 8
+ } catch { return false }
+ }
const maybeId = selectedReport?.doctor_id || selectedReport?.created_by || selectedReport?.doctor || null
- if (reportDoctorName) return {reportTitle(selectedReport, reportDoctorName)}
+ // derive the title text
+ const derived = reportDoctorName ? reportTitle(selectedReport, reportDoctorName) : reportTitle(selectedReport)
+ // if the derived title looks like an id (UUID/hex) avoid showing it — show loading instead
+ if (looksLikeIdStr(derived)) return {strings.carregando}
if (resolvingDoctors && maybeId && !doctorsMap[String(maybeId)]) return {strings.carregando}
- return {reportTitle(selectedReport)}
+ return {derived}
})()
}
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
--
2.47.2