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 && ( + + )} +
{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() {
- )) + ))} + + {/* Pagination controls */} +
+
Mostrando {Math.min(start+1, total)}–{Math.min(end, total)} de {total}
+
+ +
{page} / {totalPages}
+ +
+
+ + ) + })() )}
@@ -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}
}