diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 1dfee8f..c7313a9 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -90,6 +90,24 @@ const colorsByType = { return p?.idade ?? p?.age ?? ''; }; + // Helpers para normalizar campos do laudo/relatório + const getReportPatientName = (r: any) => r?.paciente?.full_name ?? r?.paciente?.nome ?? r?.patient?.full_name ?? r?.patient?.nome ?? r?.patient_name ?? r?.patient_full_name ?? ''; + const getReportPatientId = (r: any) => r?.paciente?.id ?? r?.patient?.id ?? r?.patient_id ?? r?.patientId ?? r?.patient_id_raw ?? r?.patient_id ?? r?.id ?? ''; + const getReportPatientCpf = (r: any) => r?.paciente?.cpf ?? r?.patient?.cpf ?? r?.patient_cpf ?? ''; + const getReportExecutor = (r: any) => r?.executante ?? r?.requested_by ?? r?.requestedBy ?? r?.created_by ?? r?.createdBy ?? r?.requested_by_name ?? r?.executor ?? ''; + const getReportExam = (r: any) => r?.exame ?? r?.exam ?? r?.especialidade ?? r?.cid_code ?? r?.report_type ?? '-'; + const getReportDate = (r: any) => r?.data ?? r?.created_at ?? r?.due_at ?? r?.report_date ?? ''; + const formatReportDate = (raw?: string) => { + if (!raw) return '-'; + try { + const d = new Date(raw); + if (isNaN(d.getTime())) return raw; + return d.toLocaleDateString('pt-BR'); + } catch (e) { + return raw; + } + }; + const ProfissionalPage = () => { const { logout, user } = useAuth(); const [activeSection, setActiveSection] = useState('calendario'); @@ -484,127 +502,142 @@ const ProfissionalPage = () => { { id: "92953542", nome: "Carla Menezes", cpf: "111.222.333-44", idade: 67, sexo: "Feminino" }, ]); - const [laudos] = useState([ - { - id: "306494942", - data: "29/07/2025", - prazo: "29/07/2025", - paciente: { id: "95170038", nome: "Ana Souza", cpf: "123.456.789-00", idade: 42, sexo: "Feminino" }, - executante: "Carlos Andrade", - exame: "Ecocardiograma", - status: "Entregue", - urgente: true, - especialidade: "Cardiologia", - conteudo: `**ECOCARDIOGRAMA TRANSTORÁCICO** + const { reports, loadReports, loading: reportsLoading, createNewReport, updateExistingReport } = useReports(); + const [laudos, setLaudos] = useState([]); + const [selectedRange, setSelectedRange] = useState<'todos'|'hoje'|'semana'|'mes'|'custom'>('mes'); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); -**Dados do Paciente:** -Nome: Ana Souza -Idade: 42 anos -Sexo: Feminino + // helper to check if a date string is in range + const isInRange = (dateStr: string | undefined, range: 'todos'|'hoje'|'semana'|'mes'|'custom') => { + if (range === 'todos') return true; + if (!dateStr) return false; + const d = new Date(dateStr); + if (isNaN(d.getTime())) return false; + const now = new Date(); + if (range === 'hoje') { + return d.toDateString() === now.toDateString(); + } + if (range === 'semana') { + const start = new Date(now); + start.setDate(now.getDate() - now.getDay()); // sunday start + const end = new Date(start); + end.setDate(start.getDate() + 6); + return d >= start && d <= end; + } + // mes + return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth(); + }; -**Indicação Clínica:** -Investigação de sopro cardíaco + // When selectedRange changes (and isn't custom), compute start/end dates + useEffect(() => { + const now = new Date(); + if (selectedRange === 'todos') { + setStartDate(null); + setEndDate(null); + return; + } + if (selectedRange === 'hoje') { + const iso = now.toISOString().slice(0,10); + setStartDate(iso); + setEndDate(iso); + return; + } + if (selectedRange === 'semana') { + const start = new Date(now); + start.setDate(now.getDate() - now.getDay()); // sunday + const end = new Date(start); + end.setDate(start.getDate() + 6); + setStartDate(start.toISOString().slice(0,10)); + setEndDate(end.toISOString().slice(0,10)); + return; + } + if (selectedRange === 'mes') { + const start = new Date(now.getFullYear(), now.getMonth(), 1); + const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); + setStartDate(start.toISOString().slice(0,10)); + setEndDate(end.toISOString().slice(0,10)); + return; + } + // custom: leave startDate/endDate as-is + }, [selectedRange]); -**Técnica:** -Ecocardiograma transtorácico bidimensional com Doppler colorido e espectral. + const filteredLaudos = (laudos || []).filter(l => { + // If a specific start/end date is set, use that range + if (startDate && endDate) { + const ds = getReportDate(l); + if (!ds) return false; + const d = new Date(ds); + if (isNaN(d.getTime())) return false; + const start = new Date(startDate + 'T00:00:00'); + const end = new Date(endDate + 'T23:59:59'); + return d >= start && d <= end; + } + // Fallback to selectedRange heuristics + if (!selectedRange) return true; + const ds = getReportDate(l); + return isInRange(ds, selectedRange); + }); -**Resultados:** -- Átrio esquerdo: dimensões normais -- Ventrículo esquerdo: função sistólica preservada, FEVE = 65% -- Valvas cardíacas: sem alterações significativas -- Pericárdio: sem derrame + function DateRangeButtons() { + return ( + <> + + + + + + ); + } -**Conclusão:** -Exame ecocardiográfico dentro dos limites da normalidade. + // carregar laudos ao montar + useEffect(() => { + let mounted = true; + (async () => { + try { + await loadReports(); + } catch (e) { + // erro tratado no hook + } + if (mounted) setLaudos(reports || []); + })(); + return () => { mounted = false; }; + }, [loadReports]); -**CID:** I25.9`, - cid: "I25.9", - diagnostico: "Exame ecocardiográfico normal", - conclusao: "Função cardíaca preservada, sem alterações estruturais significativas." - }, - { - id: "306463987", - data: "29/07/2025", - prazo: "29/07/2025", - paciente: { id: "93203056", nome: "Bruno Lima", cpf: "987.654.321-00", idade: 33, sexo: "Masculino" }, - executante: "Carlos Andrade", - exame: "Eletrocardiograma", - status: "Entregue", - urgente: true, - especialidade: "Cardiologia", - conteudo: `**ELETROCARDIOGRAMA DE REPOUSO** + // sincroniza quando reports mudarem no hook + useEffect(() => { + setLaudos(reports || []); + }, [reports]); -**Dados do Paciente:** -Nome: Bruno Lima -Idade: 33 anos -Sexo: Masculino - -**Indicação Clínica:** -Dor precordial atípica - -**Técnica:** -Eletrocardiograma de 12 derivações em repouso. - -**Resultados:** -- Ritmo: sinusal regular -- Frequência cardíaca: 72 bpm -- Eixo elétrico: normal -- Intervalos PR, QRS e QT: dentro dos limites normais -- Ondas Q patológicas: ausentes -- Alterações de ST-T: não observadas - -**Conclusão:** -Eletrocardiograma normal. - -**CID:** Z01.8`, - cid: "Z01.8", - diagnostico: "ECG normal", - conclusao: "Traçado eletrocardiográfico dentro dos parâmetros de normalidade." - }, - { - id: "306452545", - data: "29/07/2025", - prazo: "29/07/2025", - paciente: { id: "92953542", nome: "Carla Menezes", cpf: "111.222.333-44", idade: 67, sexo: "Feminino" }, - executante: "Carlos Andrade", - exame: "Dermatoscopia", - status: "Entregue", - urgente: true, - especialidade: "Dermatologia", - conteudo: `**DERMATOSCOPIA DIGITAL** - -**Dados do Paciente:** -Nome: Carla Menezes -Idade: 67 anos -Sexo: Feminino - -**Indicação Clínica:** -Avaliação de lesão pigmentada em dorso - -**Técnica:** -Dermatoscopia digital com magnificação de 10x e 20x. - -**Localização:** -Região dorsal, região escapular direita - -**Achados Dermatoscópicos:** -- Lesão melanocítica benigna -- Padrão reticular típico -- Bordas regulares e simétricas -- Pigmentação homogênea -- Ausência de estruturas atípicas - -**Conclusão:** -Nevo melanocítico benigno. Seguimento clínico recomendado. - -**CID:** D22.5`, - cid: "D22.5", - diagnostico: "Nevo melanocítico benigno", - conclusao: "Lesão benigna, recomenda-se acompanhamento dermatológico de rotina." - }, - ]); - - const [activeTab, setActiveTab] = useState("entregue"); + const [activeTab, setActiveTab] = useState("descobrir"); const [laudoSelecionado, setLaudoSelecionado] = useState(null); const [isViewing, setIsViewing] = useState(false); const [isCreatingNew, setIsCreatingNew] = useState(false); @@ -644,26 +677,6 @@ Nevo melanocítico benigno. Seguimento clínico recomendado. > A descobrir - - {/* Filtros */} @@ -682,29 +695,18 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
- 01/07/2025 + { setStartDate(e.target.value); setSelectedRange('custom'); }} className="p-1 text-sm" /> - - 31/07/2025 + { setEndDate(e.target.value); setSelectedRange('custom'); }} className="p-1 text-sm" />
- - - + {/* date range buttons: Hoje / Semana / Mês */} +
- - - + {/* Filtros e pesquisa removidos por solicitação */} - + {/* botão 'Salvar Rascunho' removido por não ser utilizado */} -

Bem-vindo à sua área exclusiva.

@@ -2225,4 +2388,13 @@ Nevo melanocítico benigno. Seguimento clínico recomendado. ); }; +const getShortId = (id?: string) => { + if (!id) return '-'; + try { + return id.length > 10 ? `${id.slice(0, 8)}...` : id; + } catch (e) { + return id; + } +}; + export default ProfissionalPage; \ No newline at end of file diff --git a/susconecta/hooks/useReports.ts b/susconecta/hooks/useReports.ts index 4a36b59..01c0b4e 100644 --- a/susconecta/hooks/useReports.ts +++ b/susconecta/hooks/useReports.ts @@ -15,6 +15,7 @@ import { listarRelatoriosPorPaciente, listarRelatoriosPorMedico } from '@/lib/reports'; +import { buscarPacientePorId, buscarMedicoPorId, buscarPacientesPorIds, buscarMedicosPorIds } from '@/lib/api'; interface UseReportsReturn { // Estados @@ -41,6 +42,12 @@ export function useReports(): UseReportsReturn { const [selectedReport, setSelectedReport] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // Caches em memória para evitar múltiplas buscas pelo mesmo ID durante a sessão + const patientsCacheRef = (globalThis as any).__reportsPatientsCache__ || new Map(); + const doctorsCacheRef = (globalThis as any).__reportsDoctorsCache__ || new Map(); + // store back to globalThis so cache persiste entre hot reloads + (globalThis as any).__reportsPatientsCache__ = patientsCacheRef; + (globalThis as any).__reportsDoctorsCache__ = doctorsCacheRef; // Função para tratar erros const handleError = useCallback((error: any) => { @@ -62,7 +69,134 @@ export function useReports(): UseReportsReturn { try { const data = await listarRelatorios(); - setReports(data); + + // Enriquecer relatórios: quando o backend retorna apenas IDs para paciente/executante, + // buscamos os detalhes (nome, cpf, etc) em batch e anexamos como `paciente` e `executante`. + const reportsWithRelations = await (async (arr: any[]) => { + if (!arr || !arr.length) return arr; + + const patientIds: string[] = []; + const doctorIds: string[] = []; + + for (const r of arr) { + const pid = r.patient_id ?? r.patient ?? r.paciente; + if (pid && typeof pid === 'string' && !patientIds.includes(pid) && !patientsCacheRef.has(String(pid))) patientIds.push(pid); + + const did = r.requested_by ?? r.created_by ?? r.executante; + if (did && typeof did === 'string' && !doctorIds.includes(did) && !doctorsCacheRef.has(String(did))) doctorIds.push(did); + } + + const patientsById = new Map(); + if (patientIds.length) { + try { + const patients = await buscarPacientesPorIds(patientIds); + for (const p of patients) { + patientsById.set(String(p.id), p); + patientsCacheRef.set(String(p.id), p); + } + } catch (e) { + // ignore batch failure + } + } + + // fallback individual para quaisquer IDs que não foram resolvidos no batch + const unresolvedPatientIds = patientIds.filter(id => !patientsById.has(String(id)) && !patientsCacheRef.has(String(id))); + if (unresolvedPatientIds.length) { + await Promise.all(unresolvedPatientIds.map(async (id) => { + try { + const p = await buscarPacientePorId(id); + if (p) { + patientsById.set(String(id), p); + patientsCacheRef.set(String(id), p); + } + } catch (e) { + // ignore individual failure + } + })); + } + + const doctorsById = new Map(); + if (doctorIds.length) { + try { + const doctors = await buscarMedicosPorIds(doctorIds); + for (const d of doctors) { + doctorsById.set(String(d.id), d); + doctorsCacheRef.set(String(d.id), d); + } + } catch (e) { + // ignore + } + } + + const unresolvedDoctorIds = doctorIds.filter(id => !doctorsById.has(String(id)) && !doctorsCacheRef.has(String(id))); + if (unresolvedDoctorIds.length) { + await Promise.all(unresolvedDoctorIds.map(async (id) => { + try { + const d = await buscarMedicoPorId(id); + if (d) { + doctorsById.set(String(id), d); + doctorsCacheRef.set(String(id), d); + } + } catch (e) { + // ignore + } + })); + } + + const mapped = arr.map((r) => { + const copy = { ...r } as any; + + // Heurísticas: prefira nomes já presentes no payload do relatório + const possiblePatientName = r.patient_name ?? r.patient_full_name ?? r.patientFullName ?? r.paciente?.full_name ?? r.paciente?.nome ?? r.patient?.full_name ?? r.patient?.nome; + if (possiblePatientName) { + copy.paciente = copy.paciente || {}; + copy.paciente.full_name = possiblePatientName; + } + + const pid = r.patient_id ?? r.patient ?? r.paciente; + if (!copy.paciente && pid) { + if (patientsById.has(String(pid))) copy.paciente = patientsById.get(String(pid)); + else if (patientsCacheRef.has(String(pid))) copy.paciente = patientsCacheRef.get(String(pid)); + } + + // Executante: prefira campos de nome já fornecidos + const possibleExecutorName = r.requested_by_name ?? r.requester_name ?? r.requestedByName ?? r.executante_name ?? r.executante?.nome ?? r.executante; + if (possibleExecutorName) { + copy.executante = possibleExecutorName; + } else { + const did = r.requested_by ?? r.created_by ?? r.executante; + if (did) { + if (doctorsById.has(String(did))) copy.executante = doctorsById.get(String(did))?.full_name ?? doctorsById.get(String(did))?.nome ?? copy.executante; + else if (doctorsCacheRef.has(String(did))) copy.executante = doctorsCacheRef.get(String(did))?.full_name ?? doctorsCacheRef.get(String(did))?.nome ?? copy.executante; + } + } + + return copy; + }); + + // Debug: identificar relatórios que ainda não tiveram paciente/doctor resolvido + try { + const unresolvedPatients: string[] = []; + const unresolvedDoctors: string[] = []; + for (const r of mapped) { + const pid = r.patient_id ?? r.patient ?? r.paciente; + if (pid && !r.paciente) unresolvedPatients.push(String(pid)); + const did = r.requested_by ?? r.created_by ?? r.executante; + // note: if executante was resolved to a name, r.executante will be string name; if still ID, it may be ID + if (did && (typeof r.executante === 'undefined' || (typeof r.executante === 'string' && r.executante.length > 30 && r.executante.includes('-')))) { + unresolvedDoctors.push(String(did)); + } + } + if (unresolvedPatients.length) console.warn('[useReports] Pacientes não resolvidos (após batch+fallback):', Array.from(new Set(unresolvedPatients)).slice(0,50)); + if (unresolvedDoctors.length) console.warn('[useReports] Executantes não resolvidos (após batch+fallback):', Array.from(new Set(unresolvedDoctors)).slice(0,50)); + } catch (e) { + // ignore logging errors + } + + return mapped; + })(data as any[]); + + setReports(reportsWithRelations || data); } catch (err) { handleError(err); } finally { diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 1729886..ab1e1a3 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -181,17 +181,62 @@ function withPrefer(h: Record, prefer: string) { return { ...h, Prefer: prefer }; } +// Helper: fetch seguro que tenta urls alternativas caso a requisição primária falhe +async function fetchWithFallback(url: string, headers: Record, altUrls?: string[]): Promise { + try { + console.debug('[fetchWithFallback] tentando URL:', url); + const res = await fetch(url, { method: 'GET', headers }); + if (res.ok) { + return await parse(res); + } + const raw = await res.clone().text().catch(() => ''); + console.warn('[fetchWithFallback] falha na URL primária:', url, 'status:', res.status, 'raw:', raw); + if (!altUrls || !altUrls.length) return null; + for (const alt of altUrls) { + try { + console.debug('[fetchWithFallback] tentando fallback URL:', alt); + const r2 = await fetch(alt, { method: 'GET', headers }); + if (r2.ok) return await parse(r2); + const raw2 = await r2.clone().text().catch(() => ''); + console.warn('[fetchWithFallback] fallback falhou:', alt, 'status:', r2.status, 'raw:', raw2); + } catch (e) { + console.warn('[fetchWithFallback] erro no fallback:', alt, e); + } + } + return null; + } catch (e) { + console.warn('[fetchWithFallback] erro fetch primario:', url, e); + if (!altUrls || !altUrls.length) return null; + for (const alt of altUrls) { + try { + const r2 = await fetch(alt, { method: 'GET', headers }); + if (r2.ok) return await parse(r2); + } catch (_) { + // ignore + } + } + return null; + } +} + // Parse genérico async function parse(res: Response): Promise { let json: any = null; try { json = await res.json(); } catch (err) { - console.error("Erro ao parsear a resposta:", err); + console.error("Erro ao parsear a resposta como JSON:", err); } if (!res.ok) { - console.error("[API ERROR]", res.url, res.status, json); + // Tenta também ler o body como texto cru para obter mensagens detalhadas + let rawText = ''; + try { + rawText = await res.clone().text(); + } catch (tErr) { + // ignore + } + console.error("[API ERROR]", res.url, res.status, json, "raw:", rawText); const code = (json && (json.error?.code || json.code)) ?? res.status; const msg = (json && (json.error?.message || json.message)) ?? res.statusText; @@ -301,8 +346,12 @@ export async function buscarPacientes(termo: string): Promise { // Executa as buscas e combina resultados únicos for (const query of queries) { try { - const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?${query}&limit=10`; - const res = await fetch(url, { method: "GET", headers: baseHeaders() }); + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?${query}&limit=10`; + const headers = baseHeaders(); + const masked = (headers['Authorization'] as string | undefined) ? `${String(headers['Authorization']).slice(0,6)}...${String(headers['Authorization']).slice(-6)}` : null; + console.debug('[buscarPacientes] URL:', url); + console.debug('[buscarPacientes] Headers (masked):', { ...headers, Authorization: masked ? '<>' : undefined }); + const res = await fetch(url, { method: "GET", headers }); const arr = await parse(res); if (arr?.length > 0) { @@ -322,16 +371,91 @@ export async function buscarPacientes(termo: string): Promise { } export async function buscarPacientePorId(id: string | number): Promise { - // Se for string e não for só número, coloca aspas duplas (para UUID/texto) - let idParam: string | number = id; - if (typeof id === 'string' && isNaN(Number(id))) { - idParam = `\"${id}\"`; + const idParam = String(id); + const headers = baseHeaders(); + + // Tenta buscar por id (UUID ou string) primeiro + try { + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?id=eq.${encodeURIComponent(idParam)}`; + console.debug('[buscarPacientePorId] tentando por id URL:', url); + const arr = await fetchWithFallback(url, headers); + if (arr && arr.length) return arr[0]; + } catch (e) { + // continue to next strategies } - const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?id=eq.${idParam}`; - const res = await fetch(url, { method: "GET", headers: baseHeaders() }); - const arr = await parse(res); - if (!arr?.length) throw new Error("404: Paciente não encontrado"); - return arr[0]; + + // Se for string e não numérico, talvez foi passado um nome — tentar por full_name / social_name + if (typeof id === 'string' && isNaN(Number(id))) { + const q = encodeURIComponent(String(id)); + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?full_name=ilike.*${q}*&limit=5`; + const alt = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?social_name=ilike.*${q}*&limit=5`; + console.debug('[buscarPacientePorId] tentando por nome URL:', url); + const arr2 = await fetchWithFallback(url, headers, [alt]); + if (arr2 && arr2.length) return arr2[0]; + } + + throw new Error('404: Paciente não encontrado'); +} + +// Buscar vários pacientes por uma lista de IDs (usa query in.(...)) +export async function buscarPacientesPorIds(ids: Array): Promise { + if (!ids || !ids.length) return []; + // Separe valores que parecem UUIDs daqueles que são nomes/texto + const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + const uuids: string[] = []; + const names: string[] = []; + for (const id of ids) { + const s = String(id).trim(); + if (!s) continue; + if (uuidRegex.test(s)) uuids.push(s); + else names.push(s); + } + + const results: Paciente[] = []; + + // Buscar por UUIDs (coluna id) + if (uuids.length) { + const uuidsParam = uuids.join(','); + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?id=in.(${uuidsParam})&limit=100`; + try { + const res = await fetch(url, { method: 'GET', headers: baseHeaders() }); + const arr = await parse(res); + if (arr && arr.length) results.push(...arr); + } catch (e) { + // ignore + } + } + + // Buscar por nomes (coluna full_name) + if (names.length) { + // Em vez de usar in.(...) (que exige aspas e quebra com encoding), + // fazemos uma requisição por nome usando ilike para cada nome. + for (const name of names) { + try { + const q = encodeURIComponent(name); + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?full_name=ilike.*${q}*&limit=100`; + const alt = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?social_name=ilike.*${q}*&limit=100`; + const headers = baseHeaders(); + console.debug('[buscarPacientesPorIds] URL (patient by name):', url); + const arr = await fetchWithFallback(url, headers, [alt]); + if (arr && arr.length) results.push(...arr); + } catch (e) { + // ignore individual name failures + } + } + } + + // Remover duplicados pelo id + const seen = new Set(); + const unique: Paciente[] = []; + for (const p of results) { + if (!p || !p.id) continue; + if (!seen.has(p.id)) { + seen.add(p.id); + unique.push(p); + } + } + return unique; } export async function criarPaciente(input: PacienteInput): Promise { @@ -441,7 +565,11 @@ export async function buscarMedicos(termo: string): Promise { for (const query of queries) { try { const url = `${REST}/doctors?${query}&limit=10`; - const res = await fetch(url, { method: "GET", headers: baseHeaders() }); + const headers = baseHeaders(); + const masked = (headers['Authorization'] as string | undefined) ? `${String(headers['Authorization']).slice(0,6)}...${String(headers['Authorization']).slice(-6)}` : null; + console.debug('[buscarMedicos] URL:', url); + console.debug('[buscarMedicos] Headers (masked):', { ...headers, Authorization: masked ? '<>' : undefined }); + const res = await fetch(url, { method: 'GET', headers }); const arr = await parse(res); if (arr?.length > 0) { @@ -460,52 +588,134 @@ export async function buscarMedicos(termo: string): Promise { return results.slice(0, 20); // Limita a 20 resultados } -export async function buscarMedicoPorId(id: string | number): Promise { +export async function buscarMedicoPorId(id: string | number): Promise { // Primeiro tenta buscar no Supabase (dados reais) + const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + const isString = typeof id === 'string'; + const sId = String(id); + + // Helper para escape de aspas + const escapeQuotes = (v: string) => v.replace(/"/g, '\\"'); + try { - const url = `${REST}/doctors?id=eq.${id}`; - const res = await fetch(url, { method: "GET", headers: baseHeaders() }); - const arr = await parse(res); - if (arr && arr.length > 0) { - console.log('✅ Médico encontrado no Supabase:', arr[0]); - console.log('🔍 Campo especialidade no médico:', { - especialidade: arr[0].especialidade, - specialty: (arr[0] as any).specialty, - hasEspecialidade: !!arr[0].especialidade, - hasSpecialty: !!((arr[0] as any).specialty) - }); - return arr[0]; + // 1) Se parece UUID, busca por id direto + if (isString && uuidRegex.test(sId)) { + const url = `${REST}/doctors?id=eq.${encodeURIComponent(sId)}`; + console.debug('[buscarMedicoPorId] tentando por id URL:', url); + const arr = await fetchWithFallback(url, baseHeaders()); + if (arr && arr.length > 0) return arr[0]; + } + + // 2) Se for string não numérica (um nome), tente buscar por full_name e nome_social + if (isString && isNaN(Number(sId))) { + const quoted = `"${escapeQuotes(sId)}"`; + // tentar por full_name usando ilike para evitar 400 com espaços/caracteres + try { + const q = encodeURIComponent(sId); + const url = `${REST}/doctors?full_name=ilike.*${q}*&limit=5`; + const alt = `${REST}/doctors?nome_social=ilike.*${q}*&limit=5`; + const arr = await fetchWithFallback(url, baseHeaders(), [alt, `${REST}/doctors?social_name=ilike.*${q}*&limit=5`]); + if (arr && arr.length > 0) return arr[0]; + } catch (e) { + // ignore and try next + } + + // tentar nome_social também com ilike + // (já tratado acima via fetchWithFallback) + } + + // 3) Por fim, tentar buscar por id (como último recurso) + try { + const idParam = encodeURIComponent(sId); + const url3 = `${REST}/doctors?id=eq.${idParam}`; + const arr3 = await fetchWithFallback(url3, baseHeaders()); + if (arr3 && arr3.length > 0) return arr3[0]; + } catch (e) { + // continue to mock fallback } } catch (error) { console.warn('⚠️ Erro ao buscar no Supabase, tentando mock API:', error); } - + // Se não encontrar no Supabase, tenta o mock API try { - const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors/${id}`; - const res = await fetch(url, { - method: "GET", - headers: { - "Accept": "application/json" + const mockUrl = `https://yuanqog.com/m1/1053378-0-default/rest/v1/doctors/${encodeURIComponent(String(id))}`; + console.debug('[buscarMedicoPorId] tentando mock API URL:', mockUrl); + try { + const medico = await fetchWithFallback(mockUrl, { Accept: 'application/json' }); + if (medico) { + console.log('✅ Médico encontrado no Mock API:', medico); + return medico as Medico; } - }); - - if (!res.ok) { - if (res.status === 404) { - throw new Error("404: Médico não encontrado"); - } - throw new Error(`Erro ao buscar médico: ${res.status} ${res.statusText}`); + // fetchWithFallback returned null -> not found + console.warn('[buscarMedicoPorId] mock API returned no result for id:', id); + return null; + } catch (fetchErr) { + console.warn('[buscarMedicoPorId] mock API fetch failed or returned no result:', fetchErr); + return null; } - - const medico = await res.json(); - console.log('✅ Médico encontrado no Mock API:', medico); - return medico as Medico; } catch (error) { console.error('❌ Erro ao buscar médico em ambas as APIs:', error); - throw new Error("404: Médico não encontrado"); + return null; } } +// Buscar vários médicos por IDs +export async function buscarMedicosPorIds(ids: Array): Promise { + if (!ids || !ids.length) return []; + const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + const uuids: string[] = []; + const names: string[] = []; + for (const id of ids) { + const s = String(id).trim(); + if (!s) continue; + if (uuidRegex.test(s)) uuids.push(s); + else names.push(s); + } + + const results: Medico[] = []; + + if (uuids.length) { + const uuidsParam = uuids.join(','); + const url = `${REST}/doctors?id=in.(${uuidsParam})&limit=200`; + try { + const res = await fetch(url, { method: 'GET', headers: baseHeaders() }); + const arr = await parse(res); + if (arr && arr.length) results.push(...arr); + } catch (e) { + // ignore + } + } + + if (names.length) { + // Evitar in.(...) com aspas — fazer uma requisição por nome usando ilike + for (const name of names) { + try { + const q = encodeURIComponent(name); + const url = `${REST}/doctors?full_name=ilike.*${q}*&limit=200`; + const alt = `${REST}/doctors?nome_social=ilike.*${q}*&limit=200`; + const headers = baseHeaders(); + console.debug('[buscarMedicosPorIds] URL (doctor by name):', url); + const arr = await fetchWithFallback(url, headers, [alt, `${REST}/doctors?social_name=ilike.*${q}*&limit=200`]); + if (arr && arr.length) results.push(...arr); + } catch (e) { + // ignore + } + } + } + + const seen = new Set(); + const unique: Medico[] = []; + for (const d of results) { + if (!d || !d.id) continue; + if (!seen.has(d.id)) { + seen.add(d.id); + unique.push(d); + } + } + return unique; +} + // Dentro de lib/api.ts export async function criarMedico(input: MedicoInput): Promise { console.log("Enviando os dados para a API:", input); // Log para depuração @@ -596,7 +806,7 @@ export type UserRole = { }; export async function listarUserRoles(): Promise { - const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/user_roles`; + const url = `https://yuanqog.com/m1/1053378-0-default/rest/v1/user_roles`; const res = await fetch(url, { method: "GET", headers: baseHeaders(), @@ -649,7 +859,7 @@ export type UserInfo = { }; export async function getCurrentUser(): Promise { - const url = `https://mock.apidog.com/m1/1053378-0-default/auth/v1/user`; + const url = `https://yuanqog.com/m1/1053378-0-default/auth/v1/user`; const res = await fetch(url, { method: "GET", headers: baseHeaders(), @@ -658,7 +868,7 @@ export async function getCurrentUser(): Promise { } export async function getUserInfo(): Promise { - const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/user-info`; + const url = `https://yuanqog.com/m1/1053378-0-default/functions/v1/user-info`; const res = await fetch(url, { method: "GET", headers: baseHeaders(), @@ -704,7 +914,7 @@ export function gerarSenhaAleatoria(): string { } export async function criarUsuario(input: CreateUserInput): Promise { - const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/create-user`; + const url = `https://yuanqog.com/m1/1053378-0-default/functions/v1/create-user`; const res = await fetch(url, { method: "POST", headers: { ...baseHeaders(), "Content-Type": "application/json" }, @@ -1052,7 +1262,7 @@ export async function removerFotoMedico(_id: string | number): Promise {} // ===== PERFIS DE USUÁRIOS ===== export async function listarPerfis(): Promise { - const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/profiles`; + const url = `https://yuanq1/1053378-0-default/rest/v1/profiles`; const res = await fetch(url, { method: "GET", headers: baseHeaders(), diff --git a/susconecta/lib/http.ts b/susconecta/lib/http.ts index b0dd5fb..3f714a7 100644 --- a/susconecta/lib/http.ts +++ b/susconecta/lib/http.ts @@ -254,7 +254,7 @@ class HttpClient { } // Instância única do cliente HTTP -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mock.apidog.com/m1/1053378-0-default' +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://yuanqog.com/m1/1053378-0-default' export const httpClient = new HttpClient(API_BASE_URL) export default httpClient \ No newline at end of file diff --git a/susconecta/lib/reports.ts b/susconecta/lib/reports.ts index bd6808c..6aa0b44 100644 --- a/susconecta/lib/reports.ts +++ b/susconecta/lib/reports.ts @@ -59,6 +59,16 @@ const BASE_API_RELATORIOS = 'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/re // Cabeçalhos base para as requisições Supabase function obterCabecalhos(token?: string): HeadersInit { + // If token not passed explicitly, try the same fallbacks as lib/api.ts + if (!token && typeof window !== 'undefined') { + token = + localStorage.getItem('auth_token') || + localStorage.getItem('token') || + sessionStorage.getItem('auth_token') || + sessionStorage.getItem('token') || + undefined; + } + const cabecalhos: HeadersInit = { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -75,12 +85,15 @@ function obterCabecalhos(token?: string): HeadersInit { async function tratarRespostaApi(resposta: Response): Promise { if (!resposta.ok) { let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`; + let rawText = ''; try { - const dadosErro = await resposta.json(); + rawText = await resposta.clone().text(); + const dadosErro = JSON.parse(rawText || '{}'); mensagemErro = dadosErro.message || dadosErro.error || mensagemErro; } catch (e) { - // Se não conseguir parsear como JSON, usa a mensagem de status HTTP + // Se não conseguir parsear como JSON, manter rawText para debug } + console.error('[tratarRespostaApi] response raw:', rawText); const erro: ApiError = { message: mensagemErro, code: resposta.status.toString(), @@ -106,25 +119,44 @@ export async function listarRelatorios(filtros?: { patient_id?: string; status?: url += `?${params.toString()}`; } + // Busca o token do usuário (compatível com lib/api.ts keys) + let token: string | undefined = undefined; + if (typeof window !== 'undefined') { + token = + localStorage.getItem('auth_token') || + localStorage.getItem('token') || + sessionStorage.getItem('auth_token') || + sessionStorage.getItem('token') || + undefined; + } + // Monta cabeçalhos conforme cURL const cabecalhos: HeadersInit = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ', }; - if (typeof window !== 'undefined') { - const token = localStorage.getItem('token'); - if (token) { - cabecalhos['Authorization'] = `Bearer ${token}`; - } + if (token) { + cabecalhos['Authorization'] = `Bearer ${token}`; } + // Logs de depuração (mask token) + const masked = token ? `${token.slice(0, 6)}...${token.slice(-6)}` : null; + console.log('[listarRelatorios] URL:', url); + console.log('[listarRelatorios] Authorization (masked):', masked); + console.log('[listarRelatorios] Headers (masked):', { + ...cabecalhos, + Authorization: cabecalhos['Authorization'] ? '<>' : undefined, + }); + const resposta = await fetch(url, { method: 'GET', headers: cabecalhos, }); + console.log('[listarRelatorios] Status:', resposta.status, resposta.statusText); + const dados = await resposta.json().catch(() => null); + console.log('[listarRelatorios] Payload:', dados); if (!resposta.ok) throw new Error('Erro ao buscar relatórios'); - const dados = await resposta.json(); if (Array.isArray(dados)) return dados; if (dados && Array.isArray(dados.data)) return dados.data; for (const chave in dados) { @@ -158,17 +190,26 @@ export async function buscarRelatorioPorId(id: string): Promise { * Cria um novo relatório médico */ export async function criarRelatorio(dadosRelatorio: CreateReportData, token?: string): Promise { + const headers = obterCabecalhos(token); + const masked = (headers as any)['Authorization'] ? String((headers as any)['Authorization']).replace(/Bearer\s+(.+)/, 'Bearer ') : null; + console.log('[criarRelatorio] POST', BASE_API_RELATORIOS); + console.log('[criarRelatorio] Headers (masked):', { ...headers, Authorization: masked }); + const resposta = await fetch(BASE_API_RELATORIOS, { method: 'POST', - headers: obterCabecalhos(token), + headers, body: JSON.stringify(dadosRelatorio), }); + console.log('[criarRelatorio] Status:', resposta.status, resposta.statusText); if (!resposta.ok) { let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`; try { const dadosErro = await resposta.json(); mensagemErro = dadosErro.message || dadosErro.error || mensagemErro; - } catch (e) {} + console.error('[criarRelatorio] error body:', dadosErro); + } catch (e) { + console.error('[criarRelatorio] erro ao parsear body de erro'); + } const erro: any = { message: mensagemErro, code: resposta.status.toString(), @@ -230,9 +271,14 @@ export async function deletarRelatorio(id: string): Promise { export async function listarRelatoriosPorPaciente(idPaciente: string): Promise { try { console.log('👤 [API RELATÓRIOS] Buscando relatórios do paciente:', idPaciente); - const resposta = await fetch(`${BASE_API_RELATORIOS}?patient_id=eq.${idPaciente}`, { + const url = `${BASE_API_RELATORIOS}?patient_id=eq.${idPaciente}`; + const headers = obterCabecalhos(); + const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null; + console.debug('[listarRelatoriosPorPaciente] URL:', url); + console.debug('[listarRelatoriosPorPaciente] Headers (masked):', { ...headers, Authorization: masked ? '<>' : undefined }); + const resposta = await fetch(url, { method: 'GET', - headers: obterCabecalhos(), + headers, }); const resultado = await tratarRespostaApi(resposta); console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados:', resultado.length); @@ -249,7 +295,12 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise { try { console.log('👨‍⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', idMedico); - const resposta = await fetch(`${BASE_API_RELATORIOS}?requested_by=eq.${idMedico}`, { + const url = `${BASE_API_RELATORIOS}?requested_by=eq.${idMedico}`; + const headers = obterCabecalhos(); + const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null; + console.debug('[listarRelatoriosPorMedico] URL:', url); + console.debug('[listarRelatoriosPorMedico] Headers (masked):', { ...headers, Authorization: masked ? '<>' : undefined }); + const resposta = await fetch(url, { method: 'GET', headers: obterCabecalhos(), }); diff --git a/susconecta/types/report-types.ts b/susconecta/types/report-types.ts index 420abf6..a5cf051 100644 --- a/susconecta/types/report-types.ts +++ b/susconecta/types/report-types.ts @@ -94,12 +94,5 @@ export interface ReportFormData { retornoAgendado: string; // cid10: string; // Removed, not present in schema - // Histórico Clínico - historicoClinico: string; - - // Sinais, Sintomas e Exames - sinaisSintomas: string; - examesRealizados: string; - resultadosExames: string; - // ...restante do código... + } \ No newline at end of file