backup/agendamento #60
@ -327,13 +327,13 @@ export default function PacientePage() {
|
|||||||
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
|
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
|
||||||
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
|
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
|
||||||
|
|
||||||
const handlePesquisar = () => {
|
// Monta a URL de resultados com os filtros atuais
|
||||||
const params = new URLSearchParams({
|
const buildResultadosHref = () => {
|
||||||
tipo: tipoConsulta,
|
const qs = new URLSearchParams()
|
||||||
especialidade,
|
qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial'
|
||||||
local: localizacao
|
if (especialidade) qs.set('especialidade', especialidade)
|
||||||
})
|
if (localizacao) qs.set('local', localizacao)
|
||||||
router.push(`/resultados?${params.toString()}`)
|
return `/resultados?${qs.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -398,11 +398,11 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{/* Botão agora redireciona direto para /resultados */}
|
||||||
className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}
|
<Button asChild className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}>
|
||||||
onClick={handlePesquisar}
|
<Link href={buildResultadosHref()} prefetch={false}>
|
||||||
>
|
|
||||||
Pesquisar
|
Pesquisar
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -539,7 +539,7 @@ export default function PacientePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
|
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
|
||||||
<Button variant="outline" onClick={() => setMostrarAgendadas(false)} className="w-full sm:w-auto">
|
<Button variant="outline" onClick={() => { /* dialog fechado (controle externo) */ }} className="w-full sm:w-auto">
|
||||||
Fechar
|
Fechar
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { useSearchParams, useRouter } from 'next/navigation'
|
import { useSearchParams, useRouter } from 'next/navigation'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
@ -24,105 +24,199 @@ import {
|
|||||||
UserRound
|
UserRound
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
buscarMedicos,
|
||||||
|
getAvailableSlots,
|
||||||
|
criarAgendamento,
|
||||||
|
getUserInfo,
|
||||||
|
buscarPacientes,
|
||||||
|
type Medico,
|
||||||
|
} from '@/lib/api'
|
||||||
|
|
||||||
|
// ...existing code (tipagens locais de UI)...
|
||||||
type TipoConsulta = 'teleconsulta' | 'local'
|
type TipoConsulta = 'teleconsulta' | 'local'
|
||||||
|
|
||||||
type Medico = {
|
// Utilidades de formatação/agenda
|
||||||
id: number
|
const shortWeek = ['DOM.', 'SEG.', 'TER.', 'QUA.', 'QUI.', 'SEX.', 'SÁB.']
|
||||||
nome: string
|
const monthPt = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']
|
||||||
especialidade: string
|
const fmtDay = (d: Date) => `${d.getDate()} ${monthPt[d.getMonth()]}`
|
||||||
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 }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MedicoBase = Omit<Medico, 'experiencia' | 'planosSaude' | 'consultorios' | 'servicos' | 'opinioes'> &
|
type DayAgenda = { label: string; data: string; dateKey: string; horarios: Array<{ iso: string; label: string }> }
|
||||||
Partial<Pick<Medico, 'experiencia' | 'planosSaude' | 'consultorios' | 'servicos' | 'opinioes'>>;
|
|
||||||
|
|
||||||
const especialidadesHero = ['Psicólogo', 'Médico clínico geral', 'Pediatra', 'Dentista', 'Ginecologista', 'Veja mais']
|
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() {
|
export default function ResultadosClient() {
|
||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Filtros/controles da UI
|
||||||
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>(
|
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>(
|
||||||
params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
|
params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
|
||||||
)
|
)
|
||||||
const [especialidadeHero, setEspecialidadeHero] = useState<string>(params?.get('especialidade') || 'Psicólogo')
|
const [especialidadeHero, setEspecialidadeHero] = useState<string>(params?.get('especialidade') || 'Psicólogo')
|
||||||
const [convenio, setConvenio] = useState<string>('Todos')
|
const [convenio, setConvenio] = useState<string>('Todos')
|
||||||
const [bairro, setBairro] = useState<string>('Todos')
|
const [bairro, setBairro] = useState<string>('Todos')
|
||||||
const [agendasExpandida, setAgendasExpandida] = useState<Record<number, boolean>>({})
|
|
||||||
|
// Estado dinâmico
|
||||||
|
const [patientId, setPatientId] = useState<string | null>(null)
|
||||||
|
const [medicos, setMedicos] = useState<Medico[]>([])
|
||||||
|
const [loadingMedicos, setLoadingMedicos] = useState(false)
|
||||||
|
|
||||||
|
// agenda por médico e loading por médico
|
||||||
|
const [agendaByDoctor, setAgendaByDoctor] = useState<Record<string, DayAgenda[]>>({})
|
||||||
|
const [agendaLoading, setAgendaLoading] = useState<Record<string, boolean>>({})
|
||||||
|
const [agendasExpandida, setAgendasExpandida] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Seleção para o Dialog de perfil completo
|
||||||
const [medicoSelecionado, setMedicoSelecionado] = useState<Medico | null>(null)
|
const [medicoSelecionado, setMedicoSelecionado] = useState<Medico | null>(null)
|
||||||
const [abaDetalhe, setAbaDetalhe] = useState('experiencia')
|
const [abaDetalhe, setAbaDetalhe] = useState('experiencia')
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days }))
|
||||||
|
} 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
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro visual (convenio/bairro são cosméticos; quando sem dado, mantemos tudo)
|
||||||
const profissionais = useMemo(() => {
|
const profissionais = useMemo(() => {
|
||||||
return medicosMock.filter(medico => {
|
return (medicos || []).filter((m: any) => {
|
||||||
if (tipoConsulta === 'local' && !medico.atendeLocal) return false
|
if (convenio !== 'Todos' && m.convenios && !m.convenios.includes(convenio)) return false
|
||||||
if (tipoConsulta === 'teleconsulta' && !medico.atendeTele) return false
|
if (bairro !== 'Todos' && m.neighborhood && String(m.neighborhood).toLowerCase() !== String(bairro).toLowerCase()) 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 true
|
return true
|
||||||
})
|
})
|
||||||
}, [bairro, convenio, especialidadeHero, tipoConsulta])
|
}, [medicos, convenio, bairro])
|
||||||
|
|
||||||
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]'
|
|
||||||
|
|
||||||
|
// Render
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-4 py-10 md:px-8">
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-4 py-10 md:px-8">
|
||||||
|
{/* Toast */}
|
||||||
|
{toast && (
|
||||||
|
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type==='success'?'bg-green-600 text-white':'bg-red-600 text-white'}`} role="alert">
|
||||||
|
{toast.msg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hero de filtros (mantido) */}
|
||||||
<section className="rounded-3xl bg-primary p-6 text-primary-foreground shadow-lg">
|
<section className="rounded-3xl bg-primary p-6 text-primary-foreground shadow-lg">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@ -153,11 +247,13 @@ export default function ResultadosClient() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Barra de filtros secundários (mantida) */}
|
||||||
<section className="sticky top-0 z-30 flex flex-wrap gap-3 rounded-2xl border border-border bg-card/90 p-4 shadow-lg backdrop-blur">
|
<section className="sticky top-0 z-30 flex flex-wrap gap-3 rounded-2xl border border-border bg-card/90 p-4 shadow-lg backdrop-blur">
|
||||||
<Toggle
|
<Toggle
|
||||||
pressed={tipoConsulta === 'teleconsulta'}
|
pressed={tipoConsulta === 'teleconsulta'}
|
||||||
onPressedChange={() => setTipoConsulta('teleconsulta')}
|
onPressedChange={() => 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')}
|
||||||
>
|
>
|
||||||
<Globe className="mr-2 h-4 w-4" />
|
<Globe className="mr-2 h-4 w-4" />
|
||||||
Teleconsulta
|
Teleconsulta
|
||||||
@ -165,7 +261,8 @@ export default function ResultadosClient() {
|
|||||||
<Toggle
|
<Toggle
|
||||||
pressed={tipoConsulta === 'local'}
|
pressed={tipoConsulta === 'local'}
|
||||||
onPressedChange={() => setTipoConsulta('local')}
|
onPressedChange={() => 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')}
|
||||||
>
|
>
|
||||||
<Building2 className="mr-2 h-4 w-4" />
|
<Building2 className="mr-2 h-4 w-4" />
|
||||||
Consulta no local
|
Consulta no local
|
||||||
@ -215,10 +312,32 @@ export default function ResultadosClient() {
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Lista de profissionais */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
{profissionais.map(medico => (
|
{loadingMedicos && (
|
||||||
|
<Card className="flex items-center justify-center border border-dashed border-border bg-card/60 p-12 text-muted-foreground">
|
||||||
|
Buscando profissionais...
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!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 (
|
||||||
<Card
|
<Card
|
||||||
key={medico.id}
|
key={id}
|
||||||
className="flex flex-col gap-4 border border-border bg-card/80 p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
|
className="flex flex-col gap-4 border border-border bg-card/80 p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-start gap-4">
|
<div className="flex flex-wrap items-start gap-4">
|
||||||
@ -229,16 +348,17 @@ export default function ResultadosClient() {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-1 flex-col gap-2">
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h2 className="text-lg font-semibold text-foreground">{medico.nome}</h2>
|
<h2 className="text-lg font-semibold text-foreground">{nome}</h2>
|
||||||
<Badge className="rounded-full bg-primary/10 text-primary">{medico.especialidade}</Badge>
|
<Badge className="rounded-full bg-primary/10 text-primary">{esp}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-primary">
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-primary">
|
||||||
<Star className="h-4 w-4 fill-primary text-primary" />
|
<Star className="h-4 w-4 fill-primary text-primary" />
|
||||||
{medico.avaliacao.toFixed(1)} • {medico.avaliacaoQtd} avaliações
|
{/* sem avaliação → travar layout */}
|
||||||
|
{'4.9'} • {'23'} avaliações
|
||||||
</span>
|
</span>
|
||||||
<span>{medico.crm}</span>
|
<span>{crm || '—'}</span>
|
||||||
<span>{medico.convenios.join(', ')}</span>
|
<span>{convenios}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -247,32 +367,34 @@ export default function ResultadosClient() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMedicoSelecionado(medico)
|
setMedicoSelecionado(medico)
|
||||||
setAbaDetalhe('experiencia')
|
setAbaDetalhe('experiencia')
|
||||||
|
// carregar agenda para o diálogo
|
||||||
|
if (!agendaByDoctor[id]) loadAgenda(id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ver perfil completo
|
Ver perfil completo
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tipoConsulta === 'local' && medico.atendeLocal && (
|
{tipoConsulta === 'local' && atendeLocal && (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border bg-muted/40 p-4 text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border bg-muted/40 p-4 text-sm text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-2 text-foreground">
|
<span className="inline-flex items-center gap-2 text-foreground">
|
||||||
<MapPin className="h-4 w-4 text-primary" />
|
<MapPin className="h-4 w-4 text-primary" />
|
||||||
{medico.endereco}
|
{endereco}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col text-right">
|
<div className="flex flex-col text-right">
|
||||||
<span className="text-xs text-muted-foreground">{medico.cidade}</span>
|
<span className="text-xs text-muted-foreground">{cidade || '—'}</span>
|
||||||
<span className="text-sm font-semibold text-primary">{medico.precoLocal}</span>
|
<span className="text-sm font-semibold text-primary">{precoLocal}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tipoConsulta === 'teleconsulta' && medico.atendeTele && (
|
{tipoConsulta === 'teleconsulta' && atendeTele && (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-primary/30 bg-primary/5 p-4 text-primary">
|
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-primary/30 bg-primary/5 p-4 text-primary">
|
||||||
<span className="inline-flex items-center gap-2 font-medium">
|
<span className="inline-flex items-center gap-2 font-medium">
|
||||||
<Globe className="h-4 w-4" />
|
<Globe className="h-4 w-4" />
|
||||||
Teleconsulta
|
Teleconsulta
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold">{medico.precoTeleconsulta}</span>
|
<span className="text-sm font-semibold">{precoTeleconsulta}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -296,41 +418,55 @@ export default function ResultadosClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3 pt-2">
|
<div className="flex flex-wrap gap-3 pt-2">
|
||||||
<Button className="h-11 rounded-full bg-primary text-primary-foreground hover:bg-primary/90">Agendar consulta</Button>
|
<Button
|
||||||
|
className="h-11 rounded-full bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
onClick={() => { if (!agendaByDoctor[id]) loadAgenda(id) }}
|
||||||
|
>
|
||||||
|
Agendar consulta
|
||||||
|
</Button>
|
||||||
<Button variant="outline" className="h-11 rounded-full border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
|
<Button variant="outline" className="h-11 rounded-full border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
|
||||||
Enviar mensagem
|
Enviar mensagem
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-11 rounded-full text-primary hover:bg-primary/10"
|
className="h-11 rounded-full text-primary hover:bg-primary/10"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setAgendasExpandida(prev => ({
|
setAgendasExpandida(prev => ({ ...prev, [id]: !prev[id] }))
|
||||||
...prev,
|
if (!agendaByDoctor[id]) loadAgenda(id)
|
||||||
[medico.id]: !prev[medico.id]
|
}}
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{agendasExpandida[medico.id] ? 'Ocultar horários' : 'Mostrar mais horários'}
|
{agendasExpandida[id] ? 'Ocultar horários' : 'Mostrar mais horários'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Agenda: 4 colunas como no layout. Se ainda não carregou, mostra placeholders. */}
|
||||||
<div className="mt-4 overflow-x-auto">
|
<div className="mt-4 overflow-x-auto">
|
||||||
<div className="grid min-w-[360px] grid-cols-4 gap-3">
|
<div className="grid min-w-[360px] grid-cols-4 gap-3">
|
||||||
{medico.agenda.map(coluna => {
|
{(agenda || [
|
||||||
const horarios = agendasExpandida[medico.id] ? coluna.horarios : coluna.horarios.slice(0, 3)
|
{ 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 (
|
return (
|
||||||
<div key={`${medico.id}-${coluna.label}`} className="rounded-2xl border border-border p-3 text-center">
|
<div key={`${id}-${col.label}-${idx}`} className="rounded-2xl border border-border p-3 text-center">
|
||||||
<p className="text-xs font-semibold uppercase text-muted-foreground">{coluna.label}</p>
|
<p className="text-xs font-semibold uppercase text-muted-foreground">{col.label}</p>
|
||||||
<p className="text-[10px] text-muted-foreground">{coluna.data}</p>
|
<p className="text-[10px] text-muted-foreground">{col.data}</p>
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{horarios.length ? (
|
{isLoadingAgenda && !agenda ? (
|
||||||
horarios.map(horario => (
|
<span className="rounded-lg border border-dashed border-border px-2 py-3 text-[11px] text-muted-foreground">
|
||||||
|
Carregando...
|
||||||
|
</span>
|
||||||
|
) : horarios.length ? (
|
||||||
|
horarios.map(h => (
|
||||||
<button
|
<button
|
||||||
key={horario}
|
key={h.iso}
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground"
|
className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground"
|
||||||
|
onClick={() => agendar(id, h.iso)}
|
||||||
>
|
>
|
||||||
{horario}
|
{h.label}
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@ -338,8 +474,8 @@ export default function ResultadosClient() {
|
|||||||
Sem horários
|
Sem horários
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!agendasExpandida[medico.id] && coluna.horarios.length > 3 && (
|
{!agendasExpandida[id] && (col.horarios.length > 3) && (
|
||||||
<span className="text-[10px] text-muted-foreground">+{coluna.horarios.length - 3} horários</span>
|
<span className="text-[10px] text-muted-foreground">+{col.horarios.length - 3} horários</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -348,25 +484,29 @@ export default function ResultadosClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{!profissionais.length && (
|
{!loadingMedicos && !profissionais.length && (
|
||||||
<Card className="flex flex-col items-center justify-center gap-3 border border-dashed border-border bg-card/60 p-12 text-center text-muted-foreground">
|
<Card className="flex flex-col items-center justify-center gap-3 border border-dashed border-border bg-card/60 p-12 text-center text-muted-foreground">
|
||||||
Nenhum profissional encontrado. Ajuste os filtros para ver outras opções.
|
Nenhum profissional encontrado. Ajuste os filtros para ver outras opções.
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Dialog de perfil completo (mantido e adaptado) */}
|
||||||
<Dialog open={!!medicoSelecionado} onOpenChange={open => !open && setMedicoSelecionado(null)}>
|
<Dialog open={!!medicoSelecionado} onOpenChange={open => !open && setMedicoSelecionado(null)}>
|
||||||
<DialogContent className="max-h-[90vh] w-full max-w-5xl overflow-y-auto border border-border bg-card p-0">
|
<DialogContent className="max-h[90vh] max-h-[90vh] w-full max-w-5xl overflow-y-auto border border-border bg-card p-0">
|
||||||
{medicoSelecionado && (
|
{medicoSelecionado && (
|
||||||
<>
|
<>
|
||||||
<DialogHeader className="border-b border-border px-6 py-4">
|
<DialogHeader className="border-b border-border px-6 py-4">
|
||||||
<DialogTitle className="text-2xl font-semibold text-foreground">
|
<DialogTitle className="text-2xl font-semibold text-foreground">
|
||||||
{medicoSelecionado.nome}
|
{medicoSelecionado.full_name || 'Profissional'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{medicoSelecionado.especialidade} • {medicoSelecionado.crm}
|
{((medicoSelecionado as any).specialty || medicoSelecionado.especialidade || '—')}
|
||||||
|
{ ' • ' }
|
||||||
|
{[medicoSelecionado.crm, (medicoSelecionado as any).crm_uf].filter(Boolean).join(' / ') || '—'}
|
||||||
</p>
|
</p>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -374,9 +514,9 @@ export default function ResultadosClient() {
|
|||||||
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-primary">
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-primary">
|
||||||
<Star className="h-4 w-4 fill-primary text-primary" />
|
<Star className="h-4 w-4 fill-primary text-primary" />
|
||||||
{medicoSelecionado.avaliacao.toFixed(1)} ({medicoSelecionado.avaliacaoQtd} avaliações)
|
4.9 (23 avaliações)
|
||||||
</span>
|
</span>
|
||||||
<span>{medicoSelecionado.planosSaude.join(' • ')}</span>
|
<span>Planos de saúde: —</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={abaDetalhe} onValueChange={setAbaDetalhe} className="space-y-6">
|
<Tabs value={abaDetalhe} onValueChange={setAbaDetalhe} className="space-y-6">
|
||||||
@ -394,65 +534,38 @@ export default function ResultadosClient() {
|
|||||||
Serviços
|
Serviços
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="opinioes" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
|
<TabsTrigger value="opinioes" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
|
||||||
Opiniões ({medicoSelecionado.opinioes.length})
|
Opiniões (0)
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="agenda" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
|
<TabsTrigger value="agenda" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary" onClick={() => loadAgenda(String(medicoSelecionado.id))}>
|
||||||
Agenda
|
Agenda
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="experiencia" className="space-y-3 text-sm text-muted-foreground">
|
<TabsContent value="experiencia" className="space-y-3 text-sm text-muted-foreground">
|
||||||
{medicoSelecionado.experiencia.map((linha, index) => (
|
<p>Informações fornecidas pelo profissional.</p>
|
||||||
<p key={index}>{linha}</p>
|
|
||||||
))}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="planos" className="flex flex-wrap gap-2">
|
<TabsContent value="planos" className="flex flex-wrap gap-2">
|
||||||
{medicoSelecionado.planosSaude.map(plano => (
|
<span className="rounded-full border border-primary/30 bg-primary/5 px-4 py-1 text-xs font-medium text-primary">—</span>
|
||||||
<span key={plano} className="rounded-full border border-primary/30 bg-primary/5 px-4 py-1 text-xs font-medium text-primary">
|
|
||||||
{plano}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="consultorios" className="space-y-3 text-sm text-muted-foreground">
|
<TabsContent value="consultorios" className="space-y-3 text-sm text-muted-foreground">
|
||||||
{medicoSelecionado.consultorios.length ? (
|
<div className="rounded-xl border border-border bg-muted/40 p-4">
|
||||||
medicoSelecionado.consultorios.map((consultorio, index) => (
|
<p>Atendimento por teleconsulta ou endereço informado no card.</p>
|
||||||
<div key={index} className="rounded-xl border border-border bg-muted/40 p-4">
|
|
||||||
<p className="font-medium text-foreground">{consultorio.nome}</p>
|
|
||||||
<p>{consultorio.endereco}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">Telefone: {consultorio.telefone}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p>Atendimento exclusivamente por teleconsulta.</p>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="servicos" className="space-y-3 text-sm text-muted-foreground">
|
<TabsContent value="servicos" className="space-y-3 text-sm text-muted-foreground">
|
||||||
{medicoSelecionado.servicos.map(servico => (
|
<div className="flex items-center justify-between rounded-xl border border-border bg-card/70 px-4 py-3">
|
||||||
<div key={servico.nome} className="flex items-center justify-between rounded-xl border border-border bg-card/70 px-4 py-3">
|
<span>Consulta</span>
|
||||||
<span>{servico.nome}</span>
|
<span className="font-semibold text-primary">—</span>
|
||||||
<span className="font-semibold text-primary">{servico.preco}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="opinioes" className="space-y-3">
|
<TabsContent value="opinioes" className="space-y-3">
|
||||||
{medicoSelecionado.opinioes.map(opiniao => (
|
<div className="rounded-xl border border-border bg-muted/40 p-4 text-sm text-muted-foreground">
|
||||||
<div key={opiniao.id} className="rounded-xl border border-border bg-muted/40 p-4 text-sm text-muted-foreground">
|
<p>Nenhuma opinião disponível.</p>
|
||||||
<div className="flex items-center justify-between text-foreground">
|
|
||||||
<span className="font-semibold">{opiniao.paciente}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{opiniao.data}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center gap-1 text-primary">
|
|
||||||
{Array.from({ length: opiniao.nota }).map((_, index) => (
|
|
||||||
<Star key={index} className="h-4 w-4 fill-primary text-primary" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-muted-foreground">{opiniao.comentario}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="agenda" className="space-y-4">
|
<TabsContent value="agenda" className="space-y-4">
|
||||||
@ -461,19 +574,20 @@ export default function ResultadosClient() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="grid min-w-[420px] grid-cols-4 gap-3">
|
<div className="grid min-w-[420px] grid-cols-4 gap-3">
|
||||||
{medicoSelecionado.agenda.map(coluna => (
|
{(agendaByDoctor[String(medicoSelecionado.id)] || []).map((col, idx) => (
|
||||||
<div key={coluna.label} className="rounded-2xl border border-border bg-muted/30 p-3 text-center text-sm">
|
<div key={`${medicoSelecionado.id}-${col.label}-${idx}`} className="rounded-2xl border border-border bg-muted/30 p-3 text-center text-sm">
|
||||||
<p className="font-semibold text-foreground">{coluna.label}</p>
|
<p className="font-semibold text-foreground">{col.label}</p>
|
||||||
<p className="text-xs text-muted-foreground">{coluna.data}</p>
|
<p className="text-xs text-muted-foreground">{col.data}</p>
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{coluna.horarios.length ? (
|
{col.horarios.length ? (
|
||||||
coluna.horarios.map(horario => (
|
col.horarios.map(h => (
|
||||||
<button
|
<button
|
||||||
key={horario}
|
key={h.iso}
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground"
|
className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground"
|
||||||
|
onClick={() => agendar(String(medicoSelecionado.id), h.iso)}
|
||||||
>
|
>
|
||||||
{horario}
|
{h.label}
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@ -484,6 +598,9 @@ export default function ResultadosClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{!(agendaByDoctor[String(medicoSelecionado.id)] || []).length && (
|
||||||
|
<div className="col-span-4 text-center text-muted-foreground">Carregando horários...</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
1227
susconecta/package-lock.json
generated
1227
susconecta/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user