From 9739fc5687fda871937dec5d54e3145df1fc877e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?= <166467972+JoaoGustavo-dev@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:55:12 -0300 Subject: [PATCH] add-search-in-patient-page --- susconecta/app/layout.tsx | 3 + susconecta/app/paciente/page.tsx | 247 ++++++++++++++---- .../paciente/resultados/ResultadosClient.tsx | 62 ++++- susconecta/lib/api.ts | 42 +++ 4 files changed, 299 insertions(+), 55 deletions(-) diff --git a/susconecta/app/layout.tsx b/susconecta/app/layout.tsx index 4f6ef6b..575356e 100644 --- a/susconecta/app/layout.tsx +++ b/susconecta/app/layout.tsx @@ -19,6 +19,9 @@ export default function RootLayout({ }) { return ( + + + diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index f7ab954..650c525 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -19,7 +19,7 @@ import Link from 'next/link' import ProtectedRoute from '@/components/shared/ProtectedRoute' import { useAuth } from '@/hooks/useAuth' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento, addDeletedAppointmentId } from '@/lib/api' +import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento, addDeletedAppointmentId, listarTodosMedicos } from '@/lib/api' import { CalendarRegistrationForm } from '@/components/features/forms/calendar-registration-form' import { buscarRelatorioPorId, listarRelatoriosPorMedico } from '@/lib/reports' import { ENV_CONFIG } from '@/lib/env-config' @@ -309,9 +309,15 @@ export default function PacientePage() { setIsEditingProfile(false) } function DashboardCards() { + const router = useRouter() const [nextAppt, setNextAppt] = useState(null) const [examsCount, setExamsCount] = useState(null) const [loading, setLoading] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [medicos, setMedicos] = useState([]) + const [searchLoading, setSearchLoading] = useState(false) + const [especialidades, setEspecialidades] = useState([]) + const [especialidadesLoading, setEspecialidadesLoading] = useState(true) useEffect(() => { let mounted = true @@ -429,37 +435,200 @@ export default function PacientePage() { return () => { mounted = false } }, []) - return ( -
- -
-
- -
- {/* rótulo e número com mesma fonte e mesmo tamanho (harmônico) */} - - {strings.proximaConsulta} - - - {loading ? strings.carregando : (nextAppt ?? '-')} - -
-
+ // Carregar especialidades únicas dos médicos ao montar + useEffect(() => { + let mounted = true + setEspecialidadesLoading(true) + ;(async () => { + try { + console.log('[DashboardCards] Carregando especialidades...') + const todos = await listarTodosMedicos().catch((err) => { + console.error('[DashboardCards] Erro ao buscar médicos:', err) + return [] + }) + console.log('[DashboardCards] Médicos carregados:', todos?.length || 0, todos) + if (!mounted) return + + // Mapeamento de correções para especialidades com encoding errado + const specialtyFixes: Record = { + 'Cl\u00EDnica Geral': 'Clínica Geral', + 'Cl\u00E3nica Geral': 'Clínica Geral', + 'Cl?nica Geral': 'Clínica Geral', + 'Cl©nica Geral': 'Clínica Geral', + 'Cl\uFFFDnica Geral': 'Clínica Geral', + }; + + let specs: string[] = [] + if (Array.isArray(todos) && todos.length > 0) { + // Extrai TODAS as especialidades únicas do campo specialty + specs = Array.from(new Set( + todos + .map((m: any) => { + let spec = m.specialty || m.speciality || '' + // Aplica correções conhecidas + for (const [wrong, correct] of Object.entries(specialtyFixes)) { + spec = String(spec).replace(new RegExp(wrong, 'g'), correct) + } + // Normaliza caracteres UTF-8 e limpa + try { + const normalized = String(spec || '').normalize('NFC').trim() + return normalized + } catch (e) { + return String(spec || '').trim() + } + }) + .filter((s: string) => s && s.length > 0) + )) + } + + console.log('[DashboardCards] Especialidades encontradas:', specs) + setEspecialidades(specs.length > 0 ? specs.sort() : []) + } catch (e) { + console.error('[DashboardCards] erro ao carregar especialidades', e) + if (mounted) setEspecialidades([]) + } finally { + if (mounted) setEspecialidadesLoading(false) + } + })() + return () => { mounted = false } + }, []) - -
-
- + // Debounced search por médico + useEffect(() => { + let mounted = true + const term = String(searchQuery || '').trim() + const handle = setTimeout(async () => { + if (!mounted) return + if (!term || term.length < 2) { + setMedicos([]) + return + } + try { + setSearchLoading(true) + const results = await buscarMedicos(term).catch(() => []) + if (!mounted) return + setMedicos(Array.isArray(results) ? results : []) + } catch (e) { + if (mounted) setMedicos([]) + } finally { + if (mounted) setSearchLoading(false) + } + }, 300) + return () => { mounted = false; clearTimeout(handle) } + }, [searchQuery]) + + const handleSearchMedico = (medico: any) => { + const qs = new URLSearchParams() + qs.set('tipo', 'teleconsulta') + if (medico?.full_name) qs.set('medico', medico.full_name) + if (medico?.specialty) qs.set('especialidade', medico.specialty || medico.especialidade || '') + qs.set('origin', 'paciente') + router.push(`/paciente/resultados?${qs.toString()}`) + setSearchQuery('') + setMedicos([]) + } + + const handleEspecialidadeClick = (especialidade: string) => { + const qs = new URLSearchParams() + qs.set('tipo', 'teleconsulta') + qs.set('especialidade', especialidade) + qs.set('origin', 'paciente') + router.push(`/paciente/resultados?${qs.toString()}`) + } + + return ( +
+ {/* Hero Section com Busca */} +
+
+
+

Encontre especialistas e clínicas

+

Busque por médico, especialidade ou localização

+
+ + {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + className="w-full px-4 sm:px-5 md:px-6 py-3 sm:py-3.5 md:py-4 rounded-xl bg-white text-foreground placeholder:text-muted-foreground text-sm sm:text-base border-0 shadow-md" + /> + {searchQuery && medicos.length > 0 && ( +
+ {medicos.map((medico) => ( + + ))} +
+ )} +
+ + {/* Especialidades */} +
+

Especialidades populares

+ {especialidadesLoading ? ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+ ))} +
+ ) : especialidades && especialidades.length > 0 ? ( +
+ {especialidades.map((esp) => ( + + ))} +
+ ) : ( +

Nenhuma especialidade disponível

+ )}
- - {strings.ultimosExames} - - - {loading ? strings.carregando : (examsCount !== null ? String(examsCount) : '-')} -
- -
+ + + {/* Cards com Informações */} +
+ +
+
+ +
+ + {strings.proximaConsulta} + + + {loading ? strings.carregando : (nextAppt ?? '-')} + +
+
+ + +
+
+ +
+ + {strings.ultimosExames} + + + {loading ? strings.carregando : (examsCount !== null ? String(examsCount) : '-')} + +
+
+
+
) } @@ -726,26 +895,6 @@ export default function PacientePage() { return (
- {/* Hero Section */} -
-
-
-

Agende sua próxima consulta

-

Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.

-
- -
-
- -
-
-
-
- {/* Consultas Agendadas Section */}
diff --git a/susconecta/app/paciente/resultados/ResultadosClient.tsx b/susconecta/app/paciente/resultados/ResultadosClient.tsx index b92acb4..f121bbd 100644 --- a/susconecta/app/paciente/resultados/ResultadosClient.tsx +++ b/susconecta/app/paciente/resultados/ResultadosClient.tsx @@ -62,6 +62,8 @@ export default function ResultadosClient() { const [bairro, setBairro] = useState('Todos') // Busca por nome do médico const [searchQuery, setSearchQuery] = useState('') + // Filtro de médico específico vindo da URL (quando clicado no dashboard) + const [medicoFiltro, setMedicoFiltro] = useState(null) // Track if URL params have been synced to avoid race condition const [paramsSync, setParamsSync] = useState(false) @@ -117,6 +119,10 @@ export default function ResultadosClient() { const especialidadeParam = params.get('especialidade') if (especialidadeParam) setEspecialidadeHero(especialidadeParam) + // Ler filtro de médico específico da URL + const medicoParam = params.get('medico') + if (medicoParam) setMedicoFiltro(medicoParam) + // Mark params as synced setParamsSync(true) }, [params]) @@ -163,9 +169,10 @@ export default function ResultadosClient() { }, []) // 4) Re-fetch doctors when especialidade changes (after initial sync) + // SKIP this if medicoFiltro está definido (médico específico selecionado) useEffect(() => { - // Skip if this is the initial render or if user is searching by name - if (!paramsSync || (searchQuery && String(searchQuery).trim().length > 1)) return + // Skip if this is the initial render or if user is searching by name or if a specific doctor is selected + if (!paramsSync || medicoFiltro || (searchQuery && String(searchQuery).trim().length > 1)) return let mounted = true ;(async () => { @@ -191,10 +198,13 @@ export default function ResultadosClient() { } })() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [especialidadeHero, paramsSync]) + }, [especialidadeHero, paramsSync, medicoFiltro]) // 5) Debounced search by doctor name + // SKIP this if medicoFiltro está definido useEffect(() => { + if (medicoFiltro) return // Skip se médico específico foi selecionado + let mounted = true const term = String(searchQuery || '').trim() const handle = setTimeout(async () => { @@ -216,7 +226,35 @@ export default function ResultadosClient() { } }, 350) return () => { mounted = false; clearTimeout(handle) } - }, [searchQuery]) + }, [searchQuery, medicoFiltro]) + + // 5b) Quando um médico específico é selecionado, fazer uma busca por ele (PRIORIDADE MÁXIMA) + useEffect(() => { + if (!medicoFiltro || !paramsSync) return + + let mounted = true + ;(async () => { + try { + setLoadingMedicos(true) + // Resetar agenda e expandidas quando mudar o médico + setAgendaByDoctor({}) + setAgendasExpandida({}) + console.log('[ResultadosClient] Buscando médico específico:', medicoFiltro) + // Tentar buscar pelo nome do médico + const list = await buscarMedicos(medicoFiltro).catch(() => []) + if (!mounted) return + console.log('[ResultadosClient] Médicos encontrados:', list?.length || 0) + setMedicos(Array.isArray(list) ? list : []) + } catch (e: any) { + console.warn('[ResultadosClient] Erro ao buscar médico:', e) + showToast('error', e?.message || 'Falha ao buscar profissional') + } finally { + if (mounted) setLoadingMedicos(false) + } + })() + + return () => { mounted = false } + }, [medicoFiltro, paramsSync]) // 3) Carregar horários disponíveis para um médico (próximos 7 dias) e agrupar por dia async function loadAgenda(doctorId: string): Promise<{ iso: string; label: string } | null> { @@ -618,12 +656,24 @@ export default function ResultadosClient() { // Filtro visual (convenio/bairro são cosméticos; quando sem dado, mantemos tudo) const profissionais = useMemo(() => { - return (medicos || []).filter((m: any) => { + let filtered = (medicos || []).filter((m: any) => { if (convenio !== 'Todos' && m.convenios && !m.convenios.includes(convenio)) return false if (bairro !== 'Todos' && m.neighborhood && String(m.neighborhood).toLowerCase() !== String(bairro).toLowerCase()) return false return true }) - }, [medicos, convenio, bairro]) + + // Se um médico específico foi selecionado no dashboard, filtrar apenas por ele + if (medicoFiltro) { + filtered = filtered.filter((m: any) => { + // Comparar nome completo com flexibilidade + const nomeMedico = String(m.full_name || m.name || '').toLowerCase() + const filtro = String(medicoFiltro).toLowerCase() + return nomeMedico.includes(filtro) || filtro.includes(nomeMedico.split(' ')[0]) // comparar por primeiro nome também + }) + } + + return filtered + }, [medicos, convenio, bairro, medicoFiltro]) // Paginação local para a lista de médicos const [currentPage, setCurrentPage] = useState(1) diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index bbf3c9b..c78eba1 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1847,6 +1847,48 @@ export async function buscarMedicos(termo: string): Promise { return results.slice(0, 20); // Limita a 20 resultados } +export async function listarTodosMedicos(): Promise { + try { + const url = `${REST}/doctors?limit=1000`; + const headers = baseHeaders(); + const res = await fetch(url, { method: 'GET', headers }); + const arr = await parse(res); + + // Mapeamento de correções para especialidades com encoding errado + const specialtyFixes: Record = { + 'Cl\u00EDnica Geral': 'Clínica Geral', + 'Cl\u00E3nica Geral': 'Clínica Geral', + 'Cl?nica Geral': 'Clínica Geral', + 'Cl©nica Geral': 'Clínica Geral', + 'Cl\uFFFDnica Geral': 'Clínica Geral', + }; + + // Sanitiza caracteres UTF-8 nos especialties + if (Array.isArray(arr)) { + return arr.map((medico: any) => { + if (medico.specialty && typeof medico.specialty === 'string') { + try { + // Primeiro tenta aplicar mapeamento + let spec = medico.specialty; + for (const [wrong, correct] of Object.entries(specialtyFixes)) { + spec = spec.replace(new RegExp(wrong, 'g'), correct); + } + // Depois normaliza + medico.specialty = spec.normalize('NFC'); + } catch (e) { + // Se falhar, mantém original + } + } + return medico; + }); + } + return Array.isArray(arr) ? arr : []; + } catch (error) { + console.error('[API] Erro ao listar todos os médicos:', error); + return []; + } +} + 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}$/;