+
-
-
+
-
-
-
-
-
-
- Lembrete de Consulta
- Resultado de Exame
- Instruções Pós-Consulta
- Outro
-
-
+
+
-
-
-
-
-
03/09/2025
+
+
+
+
-
-
-
Pendente
+
+
+
-
-
-
-
"Ok, obrigado pelo lembrete!"
-
03/09/2025 14:30
-
-
-
-
-
);
diff --git a/susconecta/app/resultados/ResultadosClient.tsx b/susconecta/app/resultados/ResultadosClient.tsx
index be6e4e0..e9ef340 100644
--- a/susconecta/app/resultados/ResultadosClient.tsx
+++ b/susconecta/app/resultados/ResultadosClient.tsx
@@ -1,6 +1,8 @@
"use client"
-import React, { useMemo, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
+import { Input } from '@/components/ui/input'
+import { Textarea } from '@/components/ui/textarea'
import { useSearchParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
@@ -24,105 +26,545 @@ import {
UserRound
} from 'lucide-react'
import { cn } from '@/lib/utils'
+import {
+ buscarMedicos,
+ getAvailableSlots,
+ criarAgendamento,
+ criarAgendamentoDireto,
+ getUserInfo,
+ buscarPacientes,
+ listarDisponibilidades,
+ listarExcecoes,
+ type Medico,
+} from '@/lib/api'
+// ...existing code (tipagens locais de UI)...
type TipoConsulta = 'teleconsulta' | 'local'
-type Medico = {
- id: number
- nome: string
- especialidade: string
- crm: string
- categoriaHero: string
- avaliacao: number
- avaliacaoQtd: number
- convenios: string[]
- endereco?: string
- bairro?: string
- cidade?: string
- precoLocal?: string
- precoTeleconsulta?: string
- atendeLocal: boolean
- atendeTele: boolean
- agenda: {
- label: string
- data: string
- horarios: string[]
- }[]
- experiencia: string[]
- planosSaude: string[]
- consultorios: { nome: string; endereco: string; telefone: string }[]
- servicos: { nome: string; preco: string }[]
- opinioes: { id: number; paciente: string; data: string; nota: number; comentario: string }[]
-}
+// Utilidades de formatação/agenda
+const shortWeek = ['DOM.', 'SEG.', 'TER.', 'QUA.', 'QUI.', 'SEX.', 'SÁB.']
+const monthPt = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']
+const fmtDay = (d: Date) => `${d.getDate()} ${monthPt[d.getMonth()]}`
-type MedicoBase = Omit
&
- Partial>;
+type DayAgenda = { label: string; data: string; dateKey: string; horarios: Array<{ iso: string; label: string }> }
const especialidadesHero = ['Psicólogo', 'Médico clínico geral', 'Pediatra', 'Dentista', 'Ginecologista', 'Veja mais']
-// NOTE: keep this mock local to component to avoid cross-file references
-const medicosMock: Medico[] = [
- {
- id: 1,
- nome: 'Paula Pontes',
- especialidade: 'Psicóloga clínica',
- crm: 'CRP SE 19/4244',
- categoriaHero: 'Psicólogo',
- avaliacao: 4.9,
- avaliacaoQtd: 23,
- convenios: ['Amil', 'Unimed'],
- endereco: 'Av. Doutor José Machado de Souza, 200 - Jardins',
- bairro: 'Jardins',
- cidade: 'Aracaju • SE',
- precoLocal: 'R$ 180',
- precoTeleconsulta: 'R$ 160',
- atendeLocal: true,
- atendeTele: true,
- agenda: [
- { label: 'Hoje', data: '9 Out', horarios: [] },
- { label: 'Amanhã', data: '10 Out', horarios: ['09:00', '10:00', '11:00', '12:00', '13:00'] },
- { label: 'Sáb.', data: '11 Out', horarios: ['11:00', '12:00', '13:00', '14:00'] },
- { label: 'Dom.', data: '12 Out', horarios: [] }
- ],
- experiencia: ['Atendimento clínico há 8 anos'],
- planosSaude: ['Amil'],
- consultorios: [],
- servicos: [],
- opinioes: []
- }
-]
-
export default function ResultadosClient() {
const params = useSearchParams()
const router = useRouter()
+
+ // Filtros/controles da UI
const [tipoConsulta, setTipoConsulta] = useState(
params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
)
const [especialidadeHero, setEspecialidadeHero] = useState(params?.get('especialidade') || 'Psicólogo')
const [convenio, setConvenio] = useState('Todos')
const [bairro, setBairro] = useState('Todos')
- const [agendasExpandida, setAgendasExpandida] = useState>({})
+
+ // Estado dinâmico
+ const [patientId, setPatientId] = useState(null)
+ const [medicos, setMedicos] = useState([])
+ const [loadingMedicos, setLoadingMedicos] = useState(false)
+
+ // agenda por médico e loading por médico
+ const [agendaByDoctor, setAgendaByDoctor] = useState>({})
+ const [agendaLoading, setAgendaLoading] = useState>({})
+ const [agendasExpandida, setAgendasExpandida] = useState>({})
+ const [nearestSlotByDoctor, setNearestSlotByDoctor] = useState>({})
+
+ // "Mostrar mais horários" modal state
+ const [moreTimesForDoctor, setMoreTimesForDoctor] = useState(null)
+ const [moreTimesDate, setMoreTimesDate] = useState(() => new Date().toISOString().slice(0,10))
+ const [moreTimesLoading, setMoreTimesLoading] = useState(false)
+ const [moreTimesSlots, setMoreTimesSlots] = useState>([])
+ const [moreTimesException, setMoreTimesException] = useState(null)
+
+ // Seleção para o Dialog de perfil completo
const [medicoSelecionado, setMedicoSelecionado] = useState(null)
const [abaDetalhe, setAbaDetalhe] = useState('experiencia')
+ // Confirmation dialog for booking: hold pending selection until user confirms
+ const [confirmOpen, setConfirmOpen] = useState(false)
+ const [pendingAppointment, setPendingAppointment] = useState<{ doctorId: string; iso: string } | null>(null)
+ const [confirmLoading, setConfirmLoading] = useState(false)
+ // Fields editable in the confirmation dialog to be sent to the create endpoint
+ const [confirmDuration, setConfirmDuration] = useState(30)
+ const [confirmInsurance, setConfirmInsurance] = useState('')
+ const [confirmChiefComplaint, setConfirmChiefComplaint] = useState('')
+ const [confirmPatientNotes, setConfirmPatientNotes] = useState('')
+
+ // Toast simples
+ const [toast, setToast] = useState<{ type: 'success' | 'error', msg: string } | null>(null)
+ const showToast = (type: 'success' | 'error', msg: string) => {
+ setToast({ type, msg })
+ setTimeout(() => setToast(null), 3000)
+ }
+ // booking success modal (used when origin=paciente)
+ const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false)
+ const [bookedWhenLabel, setBookedWhenLabel] = useState(null)
+
+ // 1) Obter patientId a partir do usuário autenticado (email -> patients)
+ useEffect(() => {
+ let mounted = true
+ ;(async () => {
+ try {
+ const info = await getUserInfo().catch(() => null)
+ const uid = info?.user?.id ?? null
+ const email = info?.user?.email ?? null
+ if (!email) return
+ const results = await buscarPacientes(email).catch(() => [])
+ // preferir linha com user_id igual ao auth id
+ const row = (results || []).find((p: any) => String(p.user_id) === String(uid)) || results?.[0]
+ if (row && mounted) setPatientId(String(row.id))
+ } catch {
+ // silencioso
+ }
+ })()
+ return () => { mounted = false }
+ }, [])
+
+ // 2) Buscar médicos conforme especialidade selecionada
+ useEffect(() => {
+ let mounted = true
+ ;(async () => {
+ try {
+ setLoadingMedicos(true)
+ setMedicos([])
+ setAgendaByDoctor({})
+ setAgendasExpandida({})
+ // termo de busca: usar a especialidade escolhida (fallback para string genérica)
+ const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : (params?.get('q') || 'medico')
+ const list = await buscarMedicos(termo).catch(() => [])
+ if (!mounted) return
+ setMedicos(Array.isArray(list) ? list : [])
+ } catch (e: any) {
+ showToast('error', e?.message || 'Falha ao buscar profissionais')
+ } finally {
+ if (mounted) setLoadingMedicos(false)
+ }
+ })()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [especialidadeHero])
+
+ // 3) Carregar horários disponíveis para um médico (próximos 7 dias) e agrupar por dia
+ async function loadAgenda(doctorId: string) {
+ if (!doctorId) return
+ if (agendaLoading[doctorId]) return
+ setAgendaLoading((s) => ({ ...s, [doctorId]: true }))
+ try {
+ // janela de 7 dias
+ const start = new Date(); start.setHours(0,0,0,0)
+ const end = new Date(); end.setDate(end.getDate() + 7); end.setHours(23,59,59,999)
+ const res = await getAvailableSlots({
+ doctor_id: doctorId,
+ start_date: start.toISOString(),
+ end_date: end.toISOString(),
+ appointment_type: tipoConsulta === 'local' ? 'presencial' : 'telemedicina',
+ })
+
+ // construir colunas: hoje, amanhã, +2 dias (4 colunas visíveis)
+ const days: DayAgenda[] = []
+ for (let i = 0; i < 4; i++) {
+ const d = new Date(start); d.setDate(start.getDate() + i)
+ const dateKey = d.toISOString().split('T')[0]
+ const label = i === 0 ? 'HOJE' : i === 1 ? 'AMANHÃ' : shortWeek[d.getDay()]
+ days.push({ label, data: fmtDay(d), dateKey, horarios: [] })
+ }
+
+ const onlyAvail = (res?.slots || []).filter(s => s.available)
+ for (const s of onlyAvail) {
+ const dt = new Date(s.datetime)
+ const key = dt.toISOString().split('T')[0]
+ const bucket = days.find(d => d.dateKey === key)
+ if (!bucket) continue
+ const label = dt.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
+ bucket.horarios.push({ iso: s.datetime, label })
+ }
+
+ // ordenar horários em cada dia
+ for (const d of days) {
+ d.horarios.sort((a, b) => new Date(a.iso).getTime() - new Date(b.iso).getTime())
+ }
+
+ // compute nearest slot (earliest available in the returned window, but after now)
+ let nearest: { iso: string; label: string } | null = null
+ const nowMs = Date.now()
+ const allSlots = days.flatMap(d => d.horarios || [])
+ const futureSorted = allSlots
+ .map(s => ({ ...s, ms: new Date(s.iso).getTime() }))
+ .filter(s => s.ms >= nowMs)
+ .sort((a,b) => a.ms - b.ms)
+ if (futureSorted.length) {
+ const s = futureSorted[0]
+ nearest = { iso: s.iso, label: s.label }
+ }
+
+ setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days }))
+ setNearestSlotByDoctor((prev) => ({ ...prev, [doctorId]: nearest }))
+ } catch (e: any) {
+ showToast('error', e?.message || 'Falha ao buscar horários')
+ } finally {
+ setAgendaLoading((s) => ({ ...s, [doctorId]: false }))
+ }
+ }
+
+ // 4) Agendar ao clicar em um horário (performs the actual create call)
+ async function agendar(doctorId: string, iso: string) {
+ if (!patientId) {
+ showToast('error', 'Paciente não identificado. Faça login novamente.')
+ return
+ }
+ try {
+ await criarAgendamento({
+ patient_id: String(patientId),
+ doctor_id: String(doctorId),
+ scheduled_at: String(iso),
+ duration_minutes: 30,
+ appointment_type: (tipoConsulta === 'local' ? 'presencial' : 'telemedicina'),
+ })
+ showToast('success', 'Consulta agendada com sucesso!')
+ // remover horário da lista local
+ setAgendaByDoctor((prev) => {
+ const days = prev[doctorId]
+ if (!days) return prev
+ const updated = days.map(d => ({ ...d, horarios: d.horarios.filter(h => h.iso !== iso) }))
+ return { ...prev, [doctorId]: updated }
+ })
+ } catch (e: any) {
+ showToast('error', e?.message || 'Falha ao agendar')
+ }
+ }
+
+ // Open confirmation dialog for a selected slot instead of immediately booking
+ function openConfirmDialog(doctorId: string, iso: string) {
+ setPendingAppointment({ doctorId, iso })
+ setConfirmOpen(true)
+ }
+
+ // Called when the user confirms the booking in the dialog
+ async function confirmAndBook() {
+ if (!pendingAppointment) return
+ const { doctorId, iso } = pendingAppointment
+ if (!patientId) {
+ showToast('error', 'Paciente não identificado. Faça login novamente.')
+ return
+ }
+ // Debug: indicate the handler was invoked
+ console.debug('[ResultadosClient] confirmAndBook invoked', { doctorId, iso, patientId, confirmDuration, confirmInsurance })
+ showToast('success', 'Iniciando agendamento...')
+ setConfirmLoading(true)
+ try {
+ // Use direct POST to ensure creation even if availability checks would block
+ await criarAgendamentoDireto({
+ patient_id: String(patientId),
+ doctor_id: String(doctorId),
+ scheduled_at: String(iso),
+ duration_minutes: Number(confirmDuration) || 30,
+ appointment_type: (tipoConsulta === 'local' ? 'presencial' : 'telemedicina'),
+ chief_complaint: confirmChiefComplaint || null,
+ patient_notes: confirmPatientNotes || null,
+ insurance_provider: confirmInsurance || null,
+ })
+ showToast('success', 'Consulta agendada com sucesso!')
+ // remover horário da lista local
+ setAgendaByDoctor((prev) => {
+ const days = prev[doctorId]
+ if (!days) return prev
+ const updated = days.map(d => ({ ...d, horarios: d.horarios.filter(h => h.iso !== iso) }))
+ return { ...prev, [doctorId]: updated }
+ })
+ setConfirmOpen(false)
+ setPendingAppointment(null)
+ // If the user came from the paciente area, keep them here and show a success modal
+ const origin = params?.get('origin')
+ if (origin === 'paciente') {
+ try {
+ const when = new Date(iso).toLocaleString('pt-BR', { dateStyle: 'long', timeStyle: 'short' })
+ setBookedWhenLabel(when)
+ } catch {
+ setBookedWhenLabel(iso)
+ }
+ setBookingSuccessOpen(true)
+ } else {
+ // Navigate to agenda after a short delay so user sees the toast
+ setTimeout(() => router.push('/agenda'), 500)
+ }
+ } catch (e: any) {
+ showToast('error', e?.message || 'Falha ao agendar')
+ } finally {
+ setConfirmLoading(false)
+ }
+ }
+
+ // Fetch slots for an arbitrary date using the same logic as CalendarRegistrationForm
+ async function fetchSlotsForDate(doctorId: string, dateOnly: string) {
+ if (!doctorId || !dateOnly) return []
+ setMoreTimesLoading(true)
+ setMoreTimesException(null)
+ try {
+ // Check for blocking exceptions (listarExcecoes can filter by date)
+ const exceptions = await listarExcecoes({ doctorId: String(doctorId), date: String(dateOnly) }).catch(() => [])
+ if (exceptions && exceptions.length) {
+ const blocking = (exceptions || []).find((e: any) => e && e.kind === 'bloqueio')
+ if (blocking) {
+ const reason = blocking.reason ? ` Motivo: ${blocking.reason}` : ''
+ setMoreTimesException(`Não é possível agendar nesta data.${reason}`)
+ setMoreTimesSlots([])
+ return []
+ }
+ }
+
+ // Build local start/end for the day
+ let start: Date
+ let end: Date
+ try {
+ const parts = String(dateOnly).split('-').map((p) => Number(p))
+ if (parts.length === 3 && parts.every((n) => !Number.isNaN(n))) {
+ const [y, m, d] = parts
+ start = new Date(y, m - 1, d, 0, 0, 0, 0)
+ end = new Date(y, m - 1, d, 23, 59, 59, 999)
+ } else {
+ start = new Date(dateOnly)
+ start.setHours(0,0,0,0)
+ end = new Date(dateOnly)
+ end.setHours(23,59,59,999)
+ }
+ } catch (err) {
+ start = new Date(dateOnly)
+ start.setHours(0,0,0,0)
+ end = new Date(dateOnly)
+ end.setHours(23,59,59,999)
+ }
+
+ const av = await getAvailableSlots({
+ doctor_id: String(doctorId),
+ start_date: start.toISOString(),
+ end_date: end.toISOString(),
+ appointment_type: tipoConsulta === 'local' ? 'presencial' : 'telemedicina',
+ })
+
+ // Try to restrict to public availability windows and synthesize missing slots
+ try {
+ const disponibilidades = await listarDisponibilidades({ doctorId: String(doctorId) }).catch(() => [])
+ const weekdayNumber = start.getDay()
+ const weekdayNames: Record = {
+ 0: ['0','sun','sunday','domingo'],
+ 1: ['1','mon','monday','segunda','segunda-feira'],
+ 2: ['2','tue','tuesday','terca','terça','terça-feira'],
+ 3: ['3','wed','wednesday','quarta','quarta-feira'],
+ 4: ['4','thu','thursday','quinta','quinta-feira'],
+ 5: ['5','fri','friday','sexta','sexta-feira'],
+ 6: ['6','sat','saturday','sabado','sábado']
+ }
+ const allowed = (weekdayNames[weekdayNumber] || []).map(s => String(s).toLowerCase())
+ const matched = (disponibilidades || []).filter((d: any) => {
+ try {
+ const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase()
+ if (!raw) return false
+ if (allowed.includes(raw)) return true
+ if (typeof d.weekday === 'number' && d.weekday === weekdayNumber) return true
+ if (typeof d.day_of_week === 'number' && d.day_of_week === weekdayNumber) return true
+ return false
+ } catch (e) { return false }
+ })
+
+ if (matched && matched.length) {
+ const windows = matched.map((d: any) => {
+ const parseTime = (t?: string) => {
+ if (!t) return { hh: 0, mm: 0, ss: 0 }
+ const parts = String(t).split(':').map((p) => Number(p))
+ return { hh: parts[0] || 0, mm: parts[1] || 0, ss: parts[2] || 0 }
+ }
+ const s = parseTime(d.start_time)
+ const e2 = parseTime(d.end_time)
+ const winStart = new Date(start.getFullYear(), start.getMonth(), start.getDate(), s.hh, s.mm, s.ss || 0, 0)
+ const winEnd = new Date(start.getFullYear(), start.getMonth(), start.getDate(), e2.hh, e2.mm, e2.ss || 0, 999)
+ const slotMinutes = (() => { const n = Number(d.slot_minutes ?? d.slot_minutes_minutes ?? NaN); return Number.isFinite(n) ? n : undefined })()
+ return { winStart, winEnd, slotMinutes }
+ })
+
+ // compute step based on backend slot diffs
+ let stepMinutes = 30
+ try {
+ const times = (av.slots || []).map((s: any) => new Date(s.datetime).getTime()).sort((a:number,b:number)=>a-b)
+ const diffs: number[] = []
+ for (let i = 1; i < times.length; i++) {
+ const d = Math.round((times[i] - times[i-1]) / 60000)
+ if (d > 0) diffs.push(d)
+ }
+ if (diffs.length) stepMinutes = Math.min(...diffs)
+ } catch(e) {}
+
+ const generatedSet = new Set()
+ windows.forEach((w:any) => {
+ try {
+ const perWindowStep = Number(w.slotMinutes) || stepMinutes
+ const startMs = w.winStart.getTime()
+ const endMs = w.winEnd.getTime()
+ const lastStartMs = endMs - perWindowStep * 60000
+ const backendSlotsInWindow = (av.slots || []).filter((s:any) => {
+ try {
+ const sd = new Date(s.datetime)
+ const sm = sd.getHours() * 60 + sd.getMinutes()
+ const wmStart = w.winStart.getHours() * 60 + w.winStart.getMinutes()
+ const wmEnd = w.winEnd.getHours() * 60 + w.winEnd.getMinutes()
+ return sm >= wmStart && sm <= wmEnd
+ } catch(e) { return false }
+ }).map((s:any) => new Date(s.datetime).getTime()).sort((a:number,b:number)=>a-b)
+
+ if (!backendSlotsInWindow.length) {
+ let cursorMs = startMs
+ while (cursorMs <= lastStartMs) {
+ generatedSet.add(new Date(cursorMs).toISOString())
+ cursorMs += perWindowStep * 60000
+ }
+ } else {
+ const lastBackendMs = backendSlotsInWindow[backendSlotsInWindow.length - 1]
+ let cursorMs = lastBackendMs + perWindowStep * 60000
+ while (cursorMs <= lastStartMs) {
+ generatedSet.add(new Date(cursorMs).toISOString())
+ cursorMs += perWindowStep * 60000
+ }
+ }
+ } catch(e) {}
+ })
+
+ const mergedMap = new Map()
+ const findWindowSlotMinutes = (isoDt: string) => {
+ try {
+ const sd = new Date(isoDt)
+ const sm = sd.getHours() * 60 + sd.getMinutes()
+ const w = windows.find((win:any) => {
+ const ws = win.winStart
+ const we = win.winEnd
+ const winStartMinutes = ws.getHours() * 60 + ws.getMinutes()
+ const winEndMinutes = we.getHours() * 60 + we.getMinutes()
+ return sm >= winStartMinutes && sm <= winEndMinutes
+ })
+ return w && w.slotMinutes ? Number(w.slotMinutes) : null
+ } catch(e) { return null }
+ }
+
+ const existingInWindow: any[] = (av.slots || []).filter((s:any) => {
+ try {
+ const sd = new Date(s.datetime)
+ const slotMinutes = sd.getHours() * 60 + sd.getMinutes()
+ return windows.some((w:any) => {
+ const ws = w.winStart
+ const we = w.winEnd
+ const winStartMinutes = ws.getHours() * 60 + ws.getMinutes()
+ const winEndMinutes = we.getHours() * 60 + we.getMinutes()
+ return slotMinutes >= winStartMinutes && slotMinutes <= winEndMinutes
+ })
+ } catch(e) { return false }
+ })
+
+ for (const s of (existingInWindow || [])) {
+ const sm = findWindowSlotMinutes(s.datetime)
+ mergedMap.set(s.datetime, sm ? { ...s, slot_minutes: sm } : { ...s })
+ }
+ Array.from(generatedSet).forEach((dt) => {
+ if (!mergedMap.has(dt)) {
+ const sm = findWindowSlotMinutes(dt) || stepMinutes
+ mergedMap.set(dt, { datetime: dt, available: true, slot_minutes: sm })
+ }
+ })
+
+ const merged = Array.from(mergedMap.values()).sort((a:any,b:any) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
+ const formatted = (merged || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
+ setMoreTimesSlots(formatted)
+ return formatted
+ } else {
+ const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
+ setMoreTimesSlots(slots)
+ return slots
+ }
+ } catch (e) {
+ console.warn('[ResultadosClient] erro ao filtrar por disponibilidades', e)
+ const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }))
+ setMoreTimesSlots(slots)
+ return slots
+ }
+ } catch (e) {
+ console.warn('[ResultadosClient] falha ao carregar horários para data', e)
+ setMoreTimesSlots([])
+ setMoreTimesException('Falha ao buscar horários para a data selecionada')
+ return []
+ } finally {
+ setMoreTimesLoading(false)
+ }
+ }
+
+ // Filtro visual (convenio/bairro são cosméticos; quando sem dado, mantemos tudo)
const profissionais = useMemo(() => {
- return medicosMock.filter(medico => {
- if (tipoConsulta === 'local' && !medico.atendeLocal) return false
- if (tipoConsulta === 'teleconsulta' && !medico.atendeTele) return false
- if (convenio !== 'Todos' && !medico.convenios.includes(convenio)) return false
- if (bairro !== 'Todos' && medico.bairro !== bairro) return false
- if (especialidadeHero !== 'Veja mais' && medico.categoriaHero !== especialidadeHero) return false
- if (especialidadeHero === 'Veja mais' && medico.categoriaHero !== 'Veja mais') return false
+ return (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
})
- }, [bairro, convenio, especialidadeHero, tipoConsulta])
-
- const toggleBase =
- 'rounded-full px-4 py-[10px] text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]'
+ }, [medicos, convenio, bairro])
+ // Render
return (
+ {/* Toast */}
+ {toast && (
+
+ {toast.msg}
+
+ )}
+
+ {/* Confirmation dialog shown when a user selects a slot */}
+
+
+ {/* Booking success modal shown when origin=paciente */}
+
+
+ {/* Hero de filtros (mantido) */}
@@ -153,11 +595,13 @@ export default function ResultadosClient() {
+ {/* Barra de filtros secundários (mantida) */}
setTipoConsulta('teleconsulta')}
- className={cn(toggleBase, tipoConsulta === 'teleconsulta' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
+ className={cn('rounded-full px-4 py-[10px] text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
+ tipoConsulta === 'teleconsulta' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
Teleconsulta
@@ -165,7 +609,8 @@ export default function ResultadosClient() {
setTipoConsulta('local')}
- className={cn(toggleBase, tipoConsulta === 'local' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
+ className={cn('rounded-full px-4 py-[10px] text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
+ tipoConsulta === 'local' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
Consulta no local
@@ -215,158 +660,220 @@ export default function ResultadosClient() {
+ {/* Lista de profissionais */}
- {profissionais.map(medico => (
-
-
-
-
-
-
-
-
-
-
{medico.nome}
- {medico.especialidade}
-
-
-
-
- {medico.avaliacao.toFixed(1)} • {medico.avaliacaoQtd} avaliações
-
- {medico.crm}
- {medico.convenios.join(', ')}
-
-
-
-
-
- {tipoConsulta === 'local' && medico.atendeLocal && (
-
-
-
- {medico.endereco}
-
-
- {medico.cidade}
- {medico.precoLocal}
-
-
- )}
-
- {tipoConsulta === 'teleconsulta' && medico.atendeTele && (
-
-
-
- Teleconsulta
-
- {medico.precoTeleconsulta}
-
- )}
-
-
-
-
- Idiomas: Português, Inglês
-
-
-
- Acolhimento em cada consulta
-
-
-
- Pagamento seguro
-
-
-
- Especialista recomendado
-
-
-
-
-
-
-
-
-
-
-
- {medico.agenda.map(coluna => {
- const horarios = agendasExpandida[medico.id] ? coluna.horarios : coluna.horarios.slice(0, 3)
- return (
-
-
{coluna.label}
-
{coluna.data}
-
- {horarios.length ? (
- horarios.map(horario => (
-
- ))
- ) : (
-
- Sem horários
-
- )}
- {!agendasExpandida[medico.id] && coluna.horarios.length > 3 && (
- +{coluna.horarios.length - 3} horários
- )}
-
-
- )
- })}
-
-
+ {loadingMedicos && (
+
+ Buscando profissionais...
- ))}
+ )}
- {!profissionais.length && (
+ {!loadingMedicos && profissionais.map((medico) => {
+ const id = String(medico.id)
+ const agenda = agendaByDoctor[id]
+ const isLoadingAgenda = !!agendaLoading[id]
+ const atendeLocal = true // dados ausentes → manter visual
+ const atendeTele = true
+ const nome = medico.full_name || 'Profissional'
+ const esp = (medico as any).specialty || medico.especialidade || '—'
+ const crm = [medico.crm, (medico as any).crm_uf].filter(Boolean).join(' / ')
+ const convenios = '—'
+ const endereco = [medico.street, medico.number].filter(Boolean).join(', ') || medico.street || '—'
+ const cidade = [medico.city, medico.state].filter(Boolean).join(' • ')
+ const precoLocal = '—'
+ const precoTeleconsulta = '—'
+
+ return (
+
+
+
+
+
+
+
+
+
+
{nome}
+ {esp}
+
+
+
+
+ {/* sem avaliação → travar layout */}
+ {'4.9'} • {'23'} avaliações
+
+ {crm || '—'}
+ {convenios}
+
+
+
+
+
+ {tipoConsulta === 'local' && atendeLocal && (
+
+
+
+ {endereco}
+
+
+ {cidade || '—'}
+ {precoLocal}
+
+
+ )}
+
+ {tipoConsulta === 'teleconsulta' && atendeTele && (
+
+
+
+ Teleconsulta
+
+ {precoTeleconsulta}
+
+ )}
+
+
+
+
+ Idiomas: Português, Inglês
+
+
+
+ Acolhimento em cada consulta
+
+
+
+ Pagamento seguro
+
+
+
+ Especialista recomendado
+
+
+
+ {/* Quick action: nearest available slot */}
+ {nearestSlotByDoctor[id] && (
+
+ Próximo horário:
+
+
+ )}
+
+
+
+
+
+
+
+ {/* Agenda: 4 colunas como no layout. Se ainda não carregou, mostra placeholders. */}
+
+
+ {(agenda || [
+ { label: 'HOJE', data: fmtDay(new Date()), horarios: [] },
+ { label: 'AMANHÃ', data: fmtDay(new Date(Date.now()+86400000)), horarios: [] },
+ { label: shortWeek[new Date(Date.now()+2*86400000).getDay()], data: fmtDay(new Date(Date.now()+2*86400000)), horarios: [] },
+ { label: shortWeek[new Date(Date.now()+3*86400000).getDay()], data: fmtDay(new Date(Date.now()+3*86400000)), horarios: [] },
+ ]).map((col, idx) => {
+ const horarios = agendasExpandida[id] ? col.horarios : col.horarios.slice(0, 3)
+ return (
+
+
{col.label}
+
{col.data}
+
+ {isLoadingAgenda && !agenda ? (
+
+ Carregando...
+
+ ) : horarios.length ? (
+ horarios.map(h => (
+
+ ))
+ ) : (
+
+ Sem horários
+
+ )}
+ {!agendasExpandida[id] && (col.horarios.length > 3) && (
+ +{col.horarios.length - 3} horários
+ )}
+
+
+ )
+ })}
+
+
+
+ )
+ })}
+
+ {!loadingMedicos && !profissionais.length && (
Nenhum profissional encontrado. Ajuste os filtros para ver outras opções.
)}
+ {/* Dialog de perfil completo (mantido e adaptado) */}
)
diff --git a/susconecta/components/dashboard/header.tsx b/susconecta/components/dashboard/header.tsx
index 0872b66..002d16e 100644
--- a/susconecta/components/dashboard/header.tsx
+++ b/susconecta/components/dashboard/header.tsx
@@ -6,11 +6,13 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
+import { useRouter } from "next/navigation"
import { SidebarTrigger } from "../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth();
+ const router = useRouter();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
@@ -84,7 +86,14 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
-