fix-endpoints

This commit is contained in:
João Gustavo 2025-10-10 23:27:43 -03:00
parent ab57567f11
commit a1b801b068
2 changed files with 214 additions and 42 deletions

View File

@ -4,7 +4,7 @@ import SignatureCanvas from "react-signature-canvas";
import Link from "next/link"; import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute"; import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { buscarPacientes, listarPacientes, buscarPacientePorId, type Paciente } from "@/lib/api"; import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, type Paciente, buscarRelatorioPorId } from "@/lib/api";
import { useReports } from "@/hooks/useReports"; import { useReports } from "@/hooks/useReports";
import { CreateReportData } from "@/types/report-types"; import { CreateReportData } from "@/types/report-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -150,6 +150,8 @@ const ProfissionalPage = () => {
hide_signature: true hide_signature: true
}); });
const [events, setEvents] = useState<any[]>([ const [events, setEvents] = useState<any[]>([
{ {
@ -398,14 +400,7 @@ const ProfissionalPage = () => {
> >
<ChevronRight className="h-4 w-4 hover:!text-white" /> <ChevronRight className="h-4 w-4 hover:!text-white" />
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={goToToday}
className="ml-4 px-3 py-1 text-sm hover:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground"
>
Hoje
</Button>
</div> </div>
<div className="text-sm text-gray-600 dark:text-muted-foreground"> <div className="text-sm text-gray-600 dark:text-muted-foreground">
{todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''} {todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''}
@ -504,20 +499,18 @@ const ProfissionalPage = () => {
const { reports, loadReports, loading: reportsLoading, createNewReport, updateExistingReport } = useReports(); const { reports, loadReports, loading: reportsLoading, createNewReport, updateExistingReport } = useReports();
const [laudos, setLaudos] = useState<any[]>([]); const [laudos, setLaudos] = useState<any[]>([]);
const [selectedRange, setSelectedRange] = useState<'todos'|'hoje'|'semana'|'mes'|'custom'>('mes'); const [selectedRange, setSelectedRange] = useState<'todos'|'semana'|'mes'|'custom'>('mes');
const [startDate, setStartDate] = useState<string | null>(null); const [startDate, setStartDate] = useState<string | null>(null);
const [endDate, setEndDate] = useState<string | null>(null); const [endDate, setEndDate] = useState<string | null>(null);
// helper to check if a date string is in range // helper to check if a date string is in range
const isInRange = (dateStr: string | undefined, range: 'todos'|'hoje'|'semana'|'mes'|'custom') => { const isInRange = (dateStr: string | undefined, range: 'todos'|'semana'|'mes'|'custom') => {
if (range === 'todos') return true; if (range === 'todos') return true;
if (!dateStr) return false; if (!dateStr) return false;
const d = new Date(dateStr); const d = new Date(dateStr);
if (isNaN(d.getTime())) return false; if (isNaN(d.getTime())) return false;
const now = new Date(); const now = new Date();
if (range === 'hoje') {
return d.toDateString() === now.toDateString();
}
if (range === 'semana') { if (range === 'semana') {
const start = new Date(now); const start = new Date(now);
start.setDate(now.getDate() - now.getDay()); // sunday start start.setDate(now.getDate() - now.getDay()); // sunday start
@ -537,12 +530,7 @@ const ProfissionalPage = () => {
setEndDate(null); setEndDate(null);
return; return;
} }
if (selectedRange === 'hoje') {
const iso = now.toISOString().slice(0,10);
setStartDate(iso);
setEndDate(iso);
return;
}
if (selectedRange === 'semana') { if (selectedRange === 'semana') {
const start = new Date(now); const start = new Date(now);
start.setDate(now.getDate() - now.getDay()); // sunday start.setDate(now.getDate() - now.getDay()); // sunday
@ -590,14 +578,6 @@ const ProfissionalPage = () => {
> >
Todos Todos
</Button> </Button>
<Button
variant={selectedRange === 'hoje' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedRange('hoje')}
className="hover:bg-blue-50"
>
Hoje
</Button>
<Button <Button
variant={selectedRange === 'semana' ? 'default' : 'outline'} variant={selectedRange === 'semana' ? 'default' : 'outline'}
size="sm" size="sm"
@ -618,6 +598,145 @@ const ProfissionalPage = () => {
); );
} }
// SearchBox inserido aqui para acessar reports, setLaudos e loadReports
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const [searching, setSearching] = useState(false);
const { token } = useAuth();
const isMaybeId = (s: string) => {
const t = s.trim();
if (!t) return false;
if (t.includes('-') && t.length > 10) return true;
if (t.toUpperCase().startsWith('REL-')) return true;
const digits = t.replace(/\D/g, '');
if (digits.length >= 8) return true;
return false;
};
const doSearch = async () => {
const term = searchTerm.trim();
if (!term) return;
setSearching(true);
try {
if (isMaybeId(term)) {
try {
const r = await buscarRelatorioPorId(term);
if (r) {
// If token exists, attempt batch enrichment like useReports
const enriched: any = { ...r };
// Collect possible patient/doctor ids from payload
const pidCandidates: string[] = [];
const didCandidates: string[] = [];
const pid = (r as any).patient_id ?? (r as any).patient ?? (r as any).paciente ?? null;
if (pid) pidCandidates.push(String(pid));
const possiblePatientName = (r as any).patient_name ?? (r as any).patient_full_name ?? (r as any).paciente?.full_name ?? (r as any).paciente?.nome ?? null;
if (possiblePatientName) {
enriched.paciente = enriched.paciente ?? {};
enriched.paciente.full_name = possiblePatientName;
}
const did = (r as any).requested_by ?? (r as any).created_by ?? (r as any).executante ?? null;
if (did) didCandidates.push(String(did));
// If token available, perform batch fetch to get full patient/doctor objects
if (token) {
try {
if (pidCandidates.length) {
const patients = await buscarPacientesPorIds(pidCandidates);
if (patients && patients.length) {
const p = patients[0];
enriched.paciente = enriched.paciente ?? {};
enriched.paciente.full_name = enriched.paciente.full_name || p.full_name || (p as any).nome;
enriched.paciente.id = enriched.paciente.id || p.id;
enriched.paciente.cpf = enriched.paciente.cpf || p.cpf;
}
}
if (didCandidates.length) {
const doctors = await buscarMedicosPorIds(didCandidates);
if (doctors && doctors.length) {
const d = doctors[0];
enriched.executante = enriched.executante || d.full_name || (d as any).nome;
}
}
} catch (e) {
// fallback: continue with payload-only enrichment
console.warn('[SearchBox] batch enrichment failed, falling back to payload-only enrichment', e);
}
}
// Final payload-only fallbacks (ensure id/cpf/order_number are populated)
const possiblePatientId = (r as any).paciente?.id ?? (r as any).patient?.id ?? (r as any).patient_id ?? (r as any).patientId ?? (r as any).id ?? undefined;
if (possiblePatientId && !enriched.paciente?.id) {
enriched.paciente = enriched.paciente ?? {};
enriched.paciente.id = possiblePatientId;
}
const possibleCpf = (r as any).patient_cpf ?? (r as any).paciente?.cpf ?? (r as any).patient?.cpf ?? null;
if (possibleCpf) {
enriched.paciente = enriched.paciente ?? {};
enriched.paciente.cpf = possibleCpf;
}
const execName = (r as any).requested_by_name ?? (r as any).requester_name ?? (r as any).requestedByName ?? (r as any).executante_name ?? (r as any).created_by_name ?? (r as any).createdByName ?? (r as any).executante ?? (r as any).requested_by ?? (r as any).created_by ?? '';
if (execName) enriched.executante = enriched.executante || execName;
if ((r as any).order_number) enriched.order_number = (r as any).order_number;
setLaudos([enriched]);
return;
}
} catch (err: any) {
console.warn('Relatório não encontrado por ID:', err);
}
}
const lower = term.toLowerCase();
const filtered = (reports || []).filter((x: any) => {
const name = (x.paciente?.full_name || x.patient_name || x.patient_full_name || x.order_number || x.exame || x.exam || '').toString().toLowerCase();
return name.includes(lower);
});
if (filtered.length) setLaudos(filtered);
else setLaudos([]);
} finally {
setSearching(false);
}
};
const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') doSearch();
};
const handleClear = async () => {
setSearchTerm('');
await loadReports();
setLaudos(reports || []);
};
return (
<div>
<div className="relative">
<Input
placeholder="Buscar paciente / pedido / ID"
className="pl-10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKey}
/>
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="flex items-center gap-2 mt-2">
<Button size="sm" onClick={doSearch} disabled={searching}>
Buscar
</Button>
<Button size="sm" variant="ghost" onClick={handleClear}>
Limpar
</Button>
</div>
</div>
);
}
// carregar laudos ao montar // carregar laudos ao montar
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -683,13 +802,8 @@ const ProfissionalPage = () => {
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<div className="flex flex-wrap items-center gap-4"> <div className="flex flex-wrap items-center gap-4">
<div className="relative flex-1 min-w-[200px]"> <div className="relative flex-1 min-w-[200px]">
<Input {/* Search input integrado com busca por ID */}
placeholder="Buscar paciente/pedido" <SearchBox />
className="pl-10"
/>
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -707,11 +821,6 @@ const ProfissionalPage = () => {
</div> </div>
{/* Filtros e pesquisa removidos por solicitação */} {/* Filtros e pesquisa removidos por solicitação */}
<Button variant="default" size="sm" className="hover:bg-blue-600 dark:hover:bg-primary/90">
<Download className="w-4 h-4 mr-1" />
Exportar
</Button>
</div> </div>
</div> </div>
@ -760,7 +869,6 @@ const ProfissionalPage = () => {
<div className="text-sm"> <div className="text-sm">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User className="w-3 h-3" /> <User className="w-3 h-3" />
<span className="font-mono text-xs">{getReportPatientId(laudo) || '-'}</span>
</div> </div>
<div className="font-medium">{getReportPatientName(laudo) || '—'}</div> <div className="font-medium">{getReportPatientName(laudo) || '—'}</div>
<div className="text-xs text-muted-foreground">{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}</div> <div className="text-xs text-muted-foreground">{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}</div>
@ -818,7 +926,7 @@ const ProfissionalPage = () => {
</div> </div>
<div className="mt-2"> <div className="mt-2">
<div className="font-semibold">{getReportPatientName(laudo) || '—'}</div> <div className="font-semibold">{getReportPatientName(laudo) || '—'}</div>
<div className="text-xs text-muted-foreground">{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : getReportPatientId(laudo) || '-'}</div> <div className="text-xs text-muted-foreground">{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}</div>
</div> </div>
</div> </div>
<div className="flex flex-col items-end ml-4"> <div className="flex flex-col items-end ml-4">

View File

@ -397,6 +397,70 @@ export async function buscarPacientePorId(id: string | number): Promise<Paciente
throw new Error('404: Paciente não encontrado'); throw new Error('404: Paciente não encontrado');
} }
// ===== RELATÓRIOS =====
export type Report = {
id: string;
patient_id?: string;
order_number?: string;
exam?: string;
diagnosis?: string;
conclusion?: string;
cid_code?: string;
content_html?: string;
content_json?: any;
status?: string;
requested_by?: string;
due_at?: string;
hide_date?: boolean;
hide_signature?: boolean;
created_at?: string;
updated_at?: string;
created_by?: string;
};
/**
* Buscar relatório por ID (tenta múltiplas estratégias: id, order_number, patient_id)
* Retorna o primeiro relatório encontrado ou lança erro 404 quando não achar.
*/
export async function buscarRelatorioPorId(id: string | number): Promise<Report> {
const sId = String(id);
const headers = baseHeaders();
// 1) tenta por id (UUID ou campo id)
try {
const urlById = `${REST}/reports?id=eq.${encodeURIComponent(sId)}`;
console.debug('[buscarRelatorioPorId] tentando por id URL:', urlById);
const arr = await fetchWithFallback<Report[]>(urlById, headers);
if (arr && arr.length) return arr[0];
} catch (e) {
console.warn('[buscarRelatorioPorId] falha ao buscar por id:', e);
}
// 2) tenta por order_number (caso o usuário cole um código legível)
try {
const urlByOrder = `${REST}/reports?order_number=eq.${encodeURIComponent(sId)}`;
console.debug('[buscarRelatorioPorId] tentando por order_number URL:', urlByOrder);
const arr2 = await fetchWithFallback<Report[]>(urlByOrder, headers);
if (arr2 && arr2.length) return arr2[0];
} catch (e) {
console.warn('[buscarRelatorioPorId] falha ao buscar por order_number:', e);
}
// 3) tenta por patient_id (caso o usuário passe um patient_id em vez do report id)
try {
const urlByPatient = `${REST}/reports?patient_id=eq.${encodeURIComponent(sId)}`;
console.debug('[buscarRelatorioPorId] tentando por patient_id URL:', urlByPatient);
const arr3 = await fetchWithFallback<Report[]>(urlByPatient, headers);
if (arr3 && arr3.length) return arr3[0];
} catch (e) {
console.warn('[buscarRelatorioPorId] falha ao buscar por patient_id:', e);
}
// Não encontrado
throw new Error('404: Relatório não encontrado');
}
// Buscar vários pacientes por uma lista de IDs (usa query in.(...)) // Buscar vários pacientes por uma lista de IDs (usa query in.(...))
export async function buscarPacientesPorIds(ids: Array<string | number>): Promise<Paciente[]> { export async function buscarPacientesPorIds(ids: Array<string | number>): Promise<Paciente[]> {
if (!ids || !ids.length) return []; if (!ids || !ids.length) return [];