Merge branch 'develop' into feature/add-api-assignment

This commit is contained in:
M-Gabrielly 2025-10-09 15:50:20 -03:00
commit a319fab2f0
6 changed files with 1338 additions and 220 deletions

View File

@ -16,6 +16,8 @@ export default function FinanceiroPage() {
const pathname = usePathname();
const router = useRouter();
const [bloqueio, setBloqueio] = useState(false);
const [formaTipo, setFormaTipo] = useState("");
const [parcelas, setParcelas] = useState("1");
const isAg = pathname?.startsWith("/agendamento");
const isPr = pathname?.startsWith("/procedimento");
@ -94,7 +96,7 @@ export default function FinanceiroPage() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Tipo</Label>
<select className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
<select value={formaTipo} onChange={(e) => setFormaTipo(e.target.value)} className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
<option value="">Selecionar</option>
<option value="dinheiro">Dinheiro</option>
<option value="cartao">Cartão</option>

View File

@ -1,40 +1,21 @@
"use client";
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
User,
LogOut,
Calendar,
FileText,
MessageCircle,
UserCog,
Home,
Clock,
FolderOpen,
ChevronLeft,
ChevronRight,
MapPin,
Stethoscope,
} from "lucide-react";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth";
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
// Simulação de internacionalização básica
const strings = {
dashboard: "Dashboard",
@ -176,14 +157,8 @@ export default function PacientePage() {
},
];
function formatDatePt(dateString: string) {
const date = new Date(dateString);
return date.toLocaleDateString("pt-BR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
function formatDatePt(date: Date) {
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
function navigateDate(direction: "prev" | "next") {
@ -201,121 +176,237 @@ export default function PacientePage() {
);
function Consultas() {
const router = useRouter()
const [tipoConsulta, setTipoConsulta] = useState<'teleconsulta' | 'presencial'>('teleconsulta')
const [especialidade, setEspecialidade] = useState('cardiologia')
const [localizacao, setLocalizacao] = useState('')
const [mostrarAgendadas, setMostrarAgendadas] = useState(false)
const hoverPrimaryClass = "transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97]"
const activeToggleClass = "w-full transition duration-200 focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] bg-[#2563eb] text-white hover:bg-[#2563eb] hover:text-white"
const inactiveToggleClass = "w-full transition duration-200 bg-slate-50 text-[#2563eb] border border-[#2563eb]/30 hover:bg-slate-100 hover:text-[#2563eb] dark:bg-white/5 dark:text-white dark:hover:bg-white/10 dark:border-white/20"
const hoverPrimaryIconClass = "rounded-xl bg-white text-[#1e293b] border border-black/10 shadow-[0_2px_8px_rgba(0,0,0,0.03)] transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:shadow-none dark:hover:bg-[#2563eb] dark:hover:text-white"
const today = new Date(); today.setHours(0, 0, 0, 0);
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
const handlePesquisar = () => {
const params = new URLSearchParams({
tipo: tipoConsulta,
especialidade,
local: localizacao
})
router.push(`/resultados?${params.toString()}`)
}
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Minhas Consultas</h2>
</div>
{/* Navegação de Data */}
<div className="flex items-center justify-between mb-6 p-4 bg-blue-50 rounded-lg dark:bg-muted">
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => navigateDate("prev")}
className="p-2"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<h3 className="text-lg font-medium text-foreground">
{formatDatePt(todayString)}
</h3>
<Button
variant="outline"
size="sm"
onClick={() => navigateDate("next")}
className="p-2"
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={goToToday}
className="ml-4 px-3 py-1 text-sm"
>
Hoje
</Button>
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consultasDoDia.length} consulta
{consultasDoDia.length !== 1 ? "s" : ""} agendada
{consultasDoDia.length !== 1 ? "s" : ""}
</div>
</div>
{/* Lista de Consultas do Dia */}
<div className="space-y-4">
{consultasDoDia.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">
Nenhuma consulta agendada para este dia
</p>
<p className="text-sm">Você pode agendar uma nova consulta</p>
<Button variant="default" className="mt-4">
Agendar Consulta
</Button>
<div className="max-w-3xl mx-auto space-y-8">
<header className="text-center space-y-2">
<h2 className="text-3xl font-semibold text-foreground">Agende sua próxima consulta</h2>
<p className="text-muted-foreground">Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.</p>
</header>
<div className="space-y-6 rounded-lg border border-border bg-muted/40 p-6">
<div className="space-y-3">
<Label>Tipo de consulta</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Button
type="button"
className={tipoConsulta === 'teleconsulta' ? activeToggleClass : inactiveToggleClass}
aria-pressed={tipoConsulta === 'teleconsulta'}
onClick={() => setTipoConsulta('teleconsulta')}
>
Teleconsulta
</Button>
<Button
type="button"
className={tipoConsulta === 'presencial' ? activeToggleClass : inactiveToggleClass}
aria-pressed={tipoConsulta === 'presencial'}
onClick={() => setTipoConsulta('presencial')}
>
Consulta no local
</Button>
</div>
</div>
) : (
consultasDoDia.map((consulta) => (
<div
key={consulta.id}
className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border"
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-3"
style={{
backgroundColor:
consulta.status === "Confirmada"
? "#22c55e"
: consulta.status === "Pendente"
? "#fbbf24"
: "#ef4444",
}}
></div>
<div>
<div className="font-medium flex items-center">
<Stethoscope className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
{consulta.medico}
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consulta.especialidade} {consulta.local}
</div>
</div>
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<div
className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === "Confirmada" ? "bg-green-600" : consulta.status === "Pendente" ? "bg-yellow-500" : "bg-red-600"}`}
>
{consulta.status}
</div>
</div>
<div className="flex items-center justify-end space-x-2">
<Button variant="outline" size="sm">
Detalhes
</Button>
{consulta.status !== "Cancelada" && (
<Button variant="secondary" size="sm">
Reagendar
</Button>
)}
{consulta.status !== "Cancelada" && (
<Button variant="destructive" size="sm">
Cancelar
</Button>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label>Especialidade</Label>
<Select value={especialidade} onValueChange={setEspecialidade}>
<SelectTrigger>
<SelectValue placeholder="Selecione a especialidade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cardiologia">Cardiologia</SelectItem>
<SelectItem value="pediatria">Pediatria</SelectItem>
<SelectItem value="dermatologia">Dermatologia</SelectItem>
<SelectItem value="ortopedia">Ortopedia</SelectItem>
<SelectItem value="ginecologia">Ginecologia</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Localização (opcional)</Label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
value={localizacao}
onChange={event => setLocalizacao(event.target.value)}
placeholder="Cidade ou estado"
className="pl-9"
/>
</div>
</div>
))
)}
</div>
<Button
className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}
onClick={handlePesquisar}
>
Pesquisar
</Button>
</div>
<div className="text-center">
<Button
variant="ghost"
size="sm"
className="transition duration-200 bg-white text-[#1e293b] border border-black/10 rounded-md shadow-[0_2px_6px_rgba(0,0,0,0.03)] hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:hover:bg-[#2563eb] dark:hover:text-white"
onClick={() => setMostrarAgendadas(true)}
>
Ver consultas agendadas
</Button>
</div>
</div>
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 rounded-lg border border-border bg-muted/40 p-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<Button
type="button"
variant="outline"
size="icon"
onClick={() => navigateDate('prev')}
aria-label="Dia anterior"
className={`group shadow-sm ${hoverPrimaryIconClass}`}
>
<ChevronLeft className="h-4 w-4 transition group-hover:text-white" />
</Button>
<span className="text-lg font-medium text-foreground">{formatDatePt(currentDate)}</span>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => navigateDate('next')}
aria-label="Próximo dia"
className={`group shadow-sm ${hoverPrimaryIconClass}`}
>
<ChevronRight className="h-4 w-4 transition group-hover:text-white" />
</Button>
{isSelectedDateToday && (
<Button
type="button"
variant="outline"
size="sm"
onClick={goToToday}
disabled
className="border border-border text-foreground focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] hover:bg-transparent hover:text-foreground disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-foreground"
>
Hoje
</Button>
)}
</div>
<div className="text-sm text-muted-foreground">
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
</div>
</div>
<div className="flex flex-col gap-4 overflow-y-auto max-h-[70vh] pr-1 sm:pr-2">
{consultasDoDia.length === 0 ? (
<div className="text-center py-10 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-60" />
<p className="text-lg font-medium">Nenhuma consulta agendada para este dia</p>
<p className="text-sm">Use a busca para marcar uma nova consulta.</p>
</div>
) : (
consultasDoDia.map(consulta => (
<div
key={consulta.id}
className="rounded-xl border border-black/5 dark:border-white/10 bg-card shadow-[0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-none p-5"
>
<div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.4fr)] items-start">
<div className="flex items-start gap-3">
<span
className="mt-1 h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}
/>
<div className="space-y-1">
<div className="font-medium flex items-center gap-2 text-foreground">
<Stethoscope className="h-4 w-4 text-muted-foreground" />
{consulta.medico}
</div>
<p className="text-sm text-muted-foreground break-words">
{consulta.especialidade} {consulta.local}
</p>
</div>
</div>
<div className="flex items-center gap-2 text-foreground">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>
{consulta.status}
</span>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="border border-[#2563eb]/40 text-[#2563eb] hover:bg-transparent hover:text-[#2563eb] focus-visible:ring-2 focus-visible:ring-[#2563eb]/40 active:scale-[0.97]"
>
Detalhes
</Button>
{consulta.status !== 'Cancelada' && (
<Button type="button" variant="secondary" size="sm" className={hoverPrimaryClass}>
Reagendar
</Button>
)}
{consulta.status !== 'Cancelada' && (
<Button
type="button"
variant="destructive"
size="sm"
className="transition duration-200 hover:bg-[#dc2626] focus-visible:ring-2 focus-visible:ring-[#dc2626]/60 active:scale-[0.97]"
>
Cancelar
</Button>
)}
</div>
</div>
</div>
))
)}
</div>
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
<Button variant="outline" onClick={() => setMostrarAgendadas(false)} className="w-full sm:w-auto">
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
);
}

View File

@ -0,0 +1,977 @@
'use client'
import { useMemo, useState } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Toggle } from '@/components/ui/toggle'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Building2,
Filter,
Globe,
HeartPulse,
Languages,
MapPin,
ShieldCheck,
Star,
Stethoscope,
ChevronRight,
UserRound
} from 'lucide-react'
import { cn } from '@/lib/utils'
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 }[]
}
const especialidadesHero = ['Psicólogo', 'Médico clínico geral', 'Pediatra', 'Dentista', 'Ginecologista', 'Veja mais']
const medicosBase: 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: [] }
]
},
{
id: 2,
nome: 'Marcos Vieira',
especialidade: 'Psicólogo comportamental',
crm: 'CRP SE 24/1198',
categoriaHero: 'Psicólogo',
avaliacao: 4.7,
avaliacaoQtd: 31,
convenios: ['SulAmérica', 'Bradesco Saúde'],
endereco: 'Rua Juarez Távora, 155 - São José',
bairro: 'São José',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 190',
precoTeleconsulta: 'R$ 150',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['14:00', '16:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['10:00', '11:00', '12:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:00', '10:30'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 3,
nome: 'Julia Azevedo',
especialidade: 'Psicóloga infantil',
crm: 'CRP SE 23/4476',
categoriaHero: 'Psicólogo',
avaliacao: 4.95,
avaliacaoQtd: 45,
convenios: ['NotreDame Intermédica', 'Particular'],
precoTeleconsulta: 'R$ 140',
atendeLocal: false,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:30', '11:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['08:30', '10:00', '11:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 4,
nome: 'Rafael Sousa',
especialidade: 'Neuropsicólogo',
crm: 'CRP BA 03/8874',
categoriaHero: 'Psicólogo',
avaliacao: 4.82,
avaliacaoQtd: 52,
convenios: ['Amil', 'Particular'],
endereco: 'Rua Riachão, 77 - Centro',
bairro: 'Centro',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 210',
atendeLocal: true,
atendeTele: false,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: [] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '13:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:00', '12:00'] },
{ label: 'Dom.', data: '12 Out', horarios: ['09:30'] }
]
},
{
id: 5,
nome: 'Lucas Amorim',
especialidade: 'Clínico geral',
crm: 'CRM SE 5122',
categoriaHero: 'Médico clínico geral',
avaliacao: 4.88,
avaliacaoQtd: 98,
convenios: ['Amil', 'Bradesco Saúde'],
endereco: 'Av. Beira Mar, 402 - Coroa do Meio',
bairro: 'Coroa do Meio',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 220',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00', '11:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:30', '14:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:30', '12:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 6,
nome: 'Dr. João Silva',
especialidade: 'Ortopedista',
crm: 'CRM RJ 90876',
categoriaHero: 'Veja mais',
avaliacao: 4.7,
avaliacaoQtd: 96,
convenios: ['Unimed', 'Bradesco Saúde'],
endereco: 'Av. Beira Mar, 1450 - Farolândia',
bairro: 'Farolândia',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 310',
atendeLocal: true,
atendeTele: false,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: [] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 7,
nome: 'Dra. Beatriz Moura',
especialidade: 'Ginecologista',
crm: 'CRM BA 52110',
categoriaHero: 'Veja mais',
avaliacao: 4.95,
avaliacaoQtd: 186,
convenios: ['NotreDame Intermédica', 'Particular', 'Amil'],
endereco: 'Rua Tobias Barreto, 512 - Bairro São José',
bairro: 'São José',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 280',
precoTeleconsulta: 'R$ 240',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['14:00', '15:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '11:00', '16:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:30', '12:30'] },
{ label: 'Dom.', data: '12 Out', horarios: ['11:30'] }
]
},
{
id: 8,
nome: 'Dr. André Lemos',
especialidade: 'Gastroenterologista',
crm: 'CRM SE 9033',
categoriaHero: 'Veja mais',
avaliacao: 4.75,
avaliacaoQtd: 105,
convenios: ['SulAmérica', 'Unimed'],
endereco: 'Rua Arauá, 22 - Centro',
bairro: 'Centro',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 340',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['13:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:00', '11:00', '15:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:30', '10:15'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 9,
nome: 'Dra. Fernanda Lima',
especialidade: 'Médico clínico geral',
crm: 'CRM SE 7890',
categoriaHero: 'Médico clínico geral',
avaliacao: 4.9,
avaliacaoQtd: 110,
convenios: ['Amil', 'Unimed', 'Bradesco Saúde'],
endereco: 'Av. Rio de Janeiro, 300 - São José',
bairro: 'São José',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 250',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00', '11:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:30', '14:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:30', '12:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 10,
nome: 'Dra. Helena Castro',
especialidade: 'Pediatra geral',
crm: 'CRM SE 7812',
categoriaHero: 'Pediatra',
avaliacao: 4.92,
avaliacaoQtd: 134,
convenios: ['Amil', 'Unimed', 'SulAmérica'],
endereco: 'Rua José Hipólito, 98 - Suíssa',
bairro: 'Suíssa',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 260',
precoTeleconsulta: 'R$ 220',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00', '11:30'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:30', '10:00', '14:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:30', '11:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 11,
nome: 'Dr. Vinícius Prado',
especialidade: 'Pediatra neonatologista',
crm: 'CRM SE 6331',
categoriaHero: 'Pediatra',
avaliacao: 4.85,
avaliacaoQtd: 89,
convenios: ['Bradesco Saúde', 'NotreDame Intermédica'],
endereco: 'Av. Augusto Franco, 2220 - Siqueira Campos',
bairro: 'Siqueira Campos',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 280',
atendeLocal: true,
atendeTele: false,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: [] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:00', '11:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:00'] },
{ label: 'Dom.', data: '12 Out', horarios: ['09:30'] }
]
},
{
id: 12,
nome: 'Dra. Marina Salles',
especialidade: 'Pediatra emergencista',
crm: 'CRM BA 85660',
categoriaHero: 'Pediatra',
avaliacao: 4.78,
avaliacaoQtd: 57,
convenios: ['Particular', 'Amil'],
precoTeleconsulta: 'R$ 210',
atendeLocal: false,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['13:00', '15:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:30', '12:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 13,
nome: 'Dr. Caio Moura',
especialidade: 'Pediatra pneumologista',
crm: 'CRM SE 7345',
categoriaHero: 'Pediatra',
avaliacao: 4.91,
avaliacaoQtd: 102,
convenios: ['SulAmérica', 'Unimed'],
endereco: 'Av. Hermes Fontes, 445 - Salgado Filho',
bairro: 'Salgado Filho',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 270',
precoTeleconsulta: 'R$ 230',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['10:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '11:00', '16:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:30', '11:30'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 14,
nome: 'Dra. Patrícia Freire',
especialidade: 'Cirurgiã-dentista',
crm: 'CRO SE 2133',
categoriaHero: 'Dentista',
avaliacao: 4.9,
avaliacaoQtd: 176,
convenios: ['OdontoPrev', 'Amil Dental'],
endereco: 'Rua Itabaiana, 410 - Centro',
bairro: 'Centro',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 200',
precoTeleconsulta: 'R$ 160',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00', '13:30'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:30', '10:00', '14:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:30', '11:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 15,
nome: 'Dr. Henrique Assis',
especialidade: 'Implantodontista',
crm: 'CRO SE 1450',
categoriaHero: 'Dentista',
avaliacao: 4.83,
avaliacaoQtd: 94,
convenios: ['SulAmérica Odonto', 'Particular'],
endereco: 'Av. Jorge Amado, 321 - Atalaia',
bairro: 'Atalaia',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 350',
atendeLocal: true,
atendeTele: false,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: [] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '11:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:30'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 16,
nome: 'Dra. Lívia Teles',
especialidade: 'Ortodontista',
crm: 'CRO BA 11567',
categoriaHero: 'Dentista',
avaliacao: 4.88,
avaliacaoQtd: 140,
convenios: ['Uniodonto', 'Amil Dental', 'Particular'],
precoTeleconsulta: 'R$ 120',
atendeLocal: false,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['17:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '10:30', '15:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['08:30', '09:30'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 17,
nome: 'Dr. Pablo Menezes',
especialidade: 'Endodontista',
crm: 'CRO SE 2099',
categoriaHero: 'Dentista',
avaliacao: 4.76,
avaliacaoQtd: 83,
convenios: ['OdontoPrev', 'SulAmérica Odonto'],
endereco: 'Rua Cedro, 70 - Grageru',
bairro: 'Grageru',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 230',
precoTeleconsulta: 'R$ 190',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:00', '13:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:30'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 18,
nome: 'Dra. Beatriz Moura',
especialidade: 'Ginecologista obstetra',
crm: 'CRM BA 52110',
categoriaHero: 'Ginecologista',
avaliacao: 4.95,
avaliacaoQtd: 186,
convenios: ['NotreDame Intermédica', 'Particular', 'Amil'],
endereco: 'Rua Tobias Barreto, 512 - São José',
bairro: 'São José',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 280',
precoTeleconsulta: 'R$ 240',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['14:00', '15:00'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '11:00', '16:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:30', '12:30'] },
{ label: 'Dom.', data: '12 Out', horarios: ['11:30'] }
]
},
{
id: 19,
nome: 'Dra. Camila Albuquerque',
especialidade: 'Ginecologista endocrinologista',
crm: 'CRM SE 6774',
categoriaHero: 'Ginecologista',
avaliacao: 4.89,
avaliacaoQtd: 122,
convenios: ['SulAmérica', 'Unimed'],
endereco: 'Av. Gonçalo Prado Rollemberg, 167 - São José',
bairro: 'São José',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 300',
atendeLocal: true,
atendeTele: false,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: [] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:00', '09:30', '15:00'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 20,
nome: 'Dra. Renata Figueiredo',
especialidade: 'Ginecologista minimamente invasiva',
crm: 'CRM PE 112233',
categoriaHero: 'Ginecologista',
avaliacao: 4.94,
avaliacaoQtd: 208,
convenios: ['Amil', 'Bradesco Saúde', 'Particular'],
precoTeleconsulta: 'R$ 260',
atendeLocal: false,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['09:00', '10:30'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['08:30', '11:00', '14:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['09:45'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
},
{
id: 21,
nome: 'Dr. Eduardo Fontes',
especialidade: 'Ginecologista mastologista',
crm: 'CRM SE 7012',
categoriaHero: 'Ginecologista',
avaliacao: 4.8,
avaliacaoQtd: 95,
convenios: ['NotreDame Intermédica', 'SulAmérica'],
endereco: 'Rua Teófilo Dantas, 55 - Centro',
bairro: 'Centro',
cidade: 'Aracaju • SE',
precoLocal: 'R$ 310',
atendeLocal: true,
atendeTele: true,
agenda: [
{ label: 'Hoje', data: '9 Out', horarios: ['08:30'] },
{ label: 'Amanhã', data: '10 Out', horarios: ['09:00', '11:00', '16:30'] },
{ label: 'Sáb.', data: '11 Out', horarios: ['10:00', '12:00'] },
{ label: 'Dom.', data: '12 Out', horarios: [] }
]
}
]
const medicosMock: Medico[] = medicosBase.map((medico, index) => ({
...medico,
experiencia:
medico.experiencia ??
[
'Especialista com atuação reconhecida pelo respectivo conselho profissional.',
'Formação continuada em instituições nacionais e internacionais.',
'Atendimento humanizado com foco em resultados sustentáveis.'
],
planosSaude:
medico.planosSaude ?? medico.convenios ?? ['Amil', 'Unimed', 'SulAmérica'],
consultorios:
medico.consultorios ??
(medico.endereco
? [
{
nome: 'Clínica principal',
endereco: `${medico.endereco}${medico.cidade ? `${medico.cidade}` : ''}`,
telefone: '(79) 4002-8922'
}
]
: []),
servicos:
medico.servicos ??
[
{
nome: 'Consulta inicial',
preco: medico.precoLocal ?? medico.precoTeleconsulta ?? 'Sob consulta'
},
{ nome: 'Retorno em até 30 dias', preco: 'R$ 150' }
],
opinioes:
medico.opinioes ??
[
{
id: index * 2 + 1,
paciente: 'Ana P.',
data: '01/09/2025',
nota: 5,
comentario: 'Profissional muito atencioso e detalhista.'
},
{
id: index * 2 + 2,
paciente: 'Marcos L.',
data: '18/08/2025',
nota: 4,
comentario: 'Explicações claras e ambiente acolhedor.'
}
]
}))
export default function ResultadosPage() {
const params = useSearchParams()
const router = useRouter()
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>(
params.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
)
const [especialidadeHero, setEspecialidadeHero] = useState<string>(params.get('especialidade') || 'Psicólogo')
const [convenio, setConvenio] = useState<string>('Todos')
const [bairro, setBairro] = useState<string>('Todos')
const [modalFiltrosAberto, setModalFiltrosAberto] = useState(false)
const [agendasExpandida, setAgendasExpandida] = useState<Record<number, boolean>>({})
const [medicoSelecionado, setMedicoSelecionado] = useState<Medico | null>(null)
const [abaDetalhe, setAbaDetalhe] = useState('experiencia')
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 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]'
return (
<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">
<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>
<h1 className="text-2xl font-semibold md:text-3xl">Resultados da procura</h1>
<p className="text-sm text-primary-foreground/80">Qual especialização você deseja?</p>
</div>
<Button
variant="outline"
className="rounded-full border-primary-foreground/30 bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground hover:text-primary"
>
Ajustar filtros
</Button>
</div>
<div className="mt-6 flex flex-wrap gap-3">
{especialidadesHero.map(item => (
<button
key={item}
type="button"
onClick={() => setEspecialidadeHero(item)}
className={cn(
'rounded-full px-5 py-2 text-sm font-medium transition focus-visible:ring-2 focus-visible:ring-primary-foreground/80',
especialidadeHero === item ? 'bg-primary-foreground text-primary' : 'bg-primary-foreground/10'
)}
>
{item}
</button>
))}
</div>
</section>
<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
pressed={tipoConsulta === 'teleconsulta'}
onPressedChange={() => setTipoConsulta('teleconsulta')}
className={cn(toggleBase, tipoConsulta === 'teleconsulta' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
<Globe className="mr-2 h-4 w-4" />
Teleconsulta
</Toggle>
<Toggle
pressed={tipoConsulta === 'local'}
onPressedChange={() => setTipoConsulta('local')}
className={cn(toggleBase, tipoConsulta === 'local' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
<Building2 className="mr-2 h-4 w-4" />
Consulta no local
</Toggle>
<Select value={convenio} onValueChange={setConvenio}>
<SelectTrigger className="h-10 min-w-[180px] rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
<SelectValue placeholder="Convênio" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos os convênios</SelectItem>
<SelectItem value="Amil">Amil</SelectItem>
<SelectItem value="Unimed">Unimed</SelectItem>
<SelectItem value="SulAmérica">SulAmérica</SelectItem>
<SelectItem value="Bradesco Saúde">Bradesco Saúde</SelectItem>
<SelectItem value="Particular">Particular</SelectItem>
</SelectContent>
</Select>
<Select value={bairro} onValueChange={setBairro}>
<SelectTrigger className="h-10 min-w-[160px] rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
<SelectValue placeholder="Bairro" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos os bairros</SelectItem>
<SelectItem value="Centro">Centro</SelectItem>
<SelectItem value="Jardins">Jardins</SelectItem>
<SelectItem value="Farolândia">Farolândia</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
className="rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground"
>
<Filter className="mr-2 h-4 w-4" />
Mais filtros
</Button>
<Button
variant="ghost"
className="ml-auto rounded-full text-primary hover:bg-primary/10"
onClick={() => router.back()}
>
Voltar
<ChevronRight className="ml-1 h-4 w-4 rotate-180" />
</Button>
</section>
<section className="space-y-4">
{profissionais.map(medico => (
<Card
key={medico.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"
>
<div className="flex flex-wrap items-start gap-4">
<Avatar className="h-14 w-14 border border-primary/20 bg-primary/5">
<AvatarFallback className="bg-primary/10 text-primary">
<UserRound className="h-6 w-6" />
</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold text-foreground">{medico.nome}</h2>
<Badge className="rounded-full bg-primary/10 text-primary">{medico.especialidade}</Badge>
</div>
<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">
<Star className="h-4 w-4 fill-primary text-primary" />
{medico.avaliacao.toFixed(1)} {medico.avaliacaoQtd} avaliações
</span>
<span>{medico.crm}</span>
<span>{medico.convenios.join(', ')}</span>
</div>
</div>
<Button
variant="ghost"
className="ml-auto h-fit rounded-full text-primary hover:bg-primary/10"
onClick={() => {
setMedicoSelecionado(medico)
setAbaDetalhe('experiencia')
}}
>
Ver perfil completo
</Button>
</div>
{tipoConsulta === 'local' && medico.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">
<span className="inline-flex items-center gap-2 text-foreground">
<MapPin className="h-4 w-4 text-primary" />
{medico.endereco}
</span>
<div className="flex flex-col text-right">
<span className="text-xs text-muted-foreground">{medico.cidade}</span>
<span className="text-sm font-semibold text-primary">{medico.precoLocal}</span>
</div>
</div>
)}
{tipoConsulta === 'teleconsulta' && medico.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">
<span className="inline-flex items-center gap-2 font-medium">
<Globe className="h-4 w-4" />
Teleconsulta
</span>
<span className="text-sm font-semibold">{medico.precoTeleconsulta}</span>
</div>
)}
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-3 py-1">
<Languages className="h-3.5 w-3.5 text-primary" />
Idiomas: Português, Inglês
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-3 py-1">
<HeartPulse className="h-3.5 w-3.5 text-primary" />
Acolhimento em cada consulta
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-3 py-1">
<ShieldCheck className="h-3.5 w-3.5 text-primary" />
Pagamento seguro
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-3 py-1">
<Stethoscope className="h-3.5 w-3.5 text-primary" />
Especialista recomendado
</span>
</div>
<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 variant="outline" className="h-11 rounded-full border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
Enviar mensagem
</Button>
<Button
variant="ghost"
className="h-11 rounded-full text-primary hover:bg-primary/10"
onClick={() =>
setAgendasExpandida(prev => ({
...prev,
[medico.id]: !prev[medico.id]
}))
}
>
{agendasExpandida[medico.id] ? 'Ocultar horários' : 'Mostrar mais horários'}
</Button>
</div>
<div className="mt-4 overflow-x-auto">
<div className="grid min-w-[360px] grid-cols-4 gap-3">
{medico.agenda.map(coluna => {
const horarios = agendasExpandida[medico.id] ? coluna.horarios : coluna.horarios.slice(0, 3)
return (
<div key={`${medico.id}-${coluna.label}`} 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-[10px] text-muted-foreground">{coluna.data}</p>
<div className="mt-3 flex flex-col gap-2">
{horarios.length ? (
horarios.map(horario => (
<button
key={horario}
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"
>
{horario}
</button>
))
) : (
<span className="rounded-lg border border-dashed border-border px-2 py-3 text-[11px] text-muted-foreground">
Sem horários
</span>
)}
{!agendasExpandida[medico.id] && coluna.horarios.length > 3 && (
<span className="text-[10px] text-muted-foreground">+{coluna.horarios.length - 3} horários</span>
)}
</div>
</div>
)
})}
</div>
</div>
</Card>
))}
{!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">
Nenhum profissional encontrado. Ajuste os filtros para ver outras opções.
</Card>
)}
</section>
<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">
{medicoSelecionado && (
<>
<DialogHeader className="border-b border-border px-6 py-4">
<DialogTitle className="text-2xl font-semibold text-foreground">
{medicoSelecionado.nome}
</DialogTitle>
<p className="text-sm text-muted-foreground">
{medicoSelecionado.especialidade} {medicoSelecionado.crm}
</p>
</DialogHeader>
<div className="flex flex-col gap-6 px-6 py-5">
<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">
<Star className="h-4 w-4 fill-primary text-primary" />
{medicoSelecionado.avaliacao.toFixed(1)} ({medicoSelecionado.avaliacaoQtd} avaliações)
</span>
<span>{medicoSelecionado.planosSaude.join(' • ')}</span>
</div>
<Tabs value={abaDetalhe} onValueChange={setAbaDetalhe} className="space-y-6">
<TabsList className="w-full justify-start rounded-full bg-muted/50 p-1 text-sm">
<TabsTrigger value="experiencia" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
Experiência
</TabsTrigger>
<TabsTrigger value="planos" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
Planos de saúde
</TabsTrigger>
<TabsTrigger value="consultorios" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
Consultórios
</TabsTrigger>
<TabsTrigger value="servicos" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
Serviços
</TabsTrigger>
<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})
</TabsTrigger>
<TabsTrigger value="agenda" className="rounded-full px-4 py-2 data-[state=active]:bg-card data-[state=active]:text-primary">
Agenda
</TabsTrigger>
</TabsList>
<TabsContent value="experiencia" className="space-y-3 text-sm text-muted-foreground">
{medicoSelecionado.experiencia.map((linha, index) => (
<p key={index}>{linha}</p>
))}
</TabsContent>
<TabsContent value="planos" className="flex flex-wrap gap-2">
{medicoSelecionado.planosSaude.map(plano => (
<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 value="consultorios" className="space-y-3 text-sm text-muted-foreground">
{medicoSelecionado.consultorios.length ? (
medicoSelecionado.consultorios.map((consultorio, index) => (
<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>
))
) : (
<p>Atendimento exclusivamente por teleconsulta.</p>
)}
</TabsContent>
<TabsContent value="servicos" className="space-y-3 text-sm text-muted-foreground">
{medicoSelecionado.servicos.map(servico => (
<div key={servico.nome} className="flex items-center justify-between rounded-xl border border-border bg-card/70 px-4 py-3">
<span>{servico.nome}</span>
<span className="font-semibold text-primary">{servico.preco}</span>
</div>
))}
</TabsContent>
<TabsContent value="opinioes" className="space-y-3">
{medicoSelecionado.opinioes.map(opiniao => (
<div key={opiniao.id} className="rounded-xl border border-border bg-muted/40 p-4 text-sm text-muted-foreground">
<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 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 value="agenda" className="space-y-4">
<p className="text-sm text-muted-foreground">
Escolha o melhor horário disponível para sua consulta.
</p>
<div className="overflow-x-auto">
<div className="grid min-w-[420px] grid-cols-4 gap-3">
{medicoSelecionado.agenda.map(coluna => (
<div key={coluna.label} 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="text-xs text-muted-foreground">{coluna.data}</p>
<div className="mt-3 flex flex-col gap-2">
{coluna.horarios.length ? (
coluna.horarios.map(horario => (
<button
key={horario}
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"
>
{horario}
</button>
))
) : (
<span className="rounded-lg border border-dashed border-border px-2 py-3 text-[11px] text-muted-foreground">
Sem horários
</span>
)}
</div>
</div>
))}
</div>
</div>
</TabsContent>
</Tabs>
</div>
</>
)}
</DialogContent>
</Dialog>
</div>
</div>
)
}

View File

@ -401,29 +401,40 @@ export function DoctorRegistrationForm({
setSubmitting(true);
setErrors((e) => ({ ...e, submit: "" }));
const payload: MedicoInput = {
user_id: null,
crm: form.crm || "",
crm_uf: form.estado_crm || "",
specialty: form.especialidade || "",
full_name: form.full_name || "",
cpf: form.cpf || "",
email: form.email || "",
phone_mobile: form.celular || "",
phone2: form.telefone || null,
cep: form.cep || "",
street: form.logradouro || "",
number: form.numero || "",
complement: form.complemento || undefined,
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
birth_date: form.data_nascimento || null,
rg: form.rg || null,
active: true,
created_by: null,
updated_by: null,
};
const payload: MedicoInput = {
user_id: null,
crm: form.crm || "",
crm_uf: form.estado_crm || "",
specialty: form.especialidade || "",
full_name: form.full_name || "",
cpf: form.cpf || "",
email: form.email || "",
phone_mobile: form.celular || "",
phone2: form.telefone || null,
cep: form.cep || "",
street: form.logradouro || "",
number: form.numero || "",
complement: form.complemento || undefined,
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
// converte dd/MM/yyyy para ISO
birth_date: (() => {
try {
const parts = String(form.data_nascimento).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) return date.toISOString().slice(0, 10);
}
} catch {}
return null;
})(),
rg: form.rg || null,
active: true,
created_by: null,
updated_by: null,
};
// Validação dos campos obrigatórios
const requiredFields = [
@ -790,11 +801,20 @@ export function DoctorRegistrationForm({
<div className="space-y-2">
<Label>Data de Nascimento</Label>
<Input
type="date"
placeholder="dd/mm/aaaa"
value={form.data_nascimento}
onChange={(e) =>
setField("data_nascimento", e.target.value)
}
onChange={(e) => {
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
setField("data_nascimento", v);
}}
onBlur={() => {
const raw = form.data_nascimento;
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const d = `${parts[0].padStart(2,'0')}/${parts[1].padStart(2,'0')}/${parts[2].padStart(4,'0')}`;
setField("data_nascimento", d);
}
}}
/>
</div>
</div>

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { format, parse, isValid, parseISO } from "date-fns";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -154,24 +155,26 @@ export function PatientRegistrationForm({
const p = await buscarPacientePorId(String(patientId));
console.log("[PatientForm] Dados recebidos:", p);
setForm((s) => ({
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date ? (() => {
try { return format(parseISO(String(p.birth_date)), 'dd/MM/yyyy'); } catch { return String(p.birth_date); }
})() : "",
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
const ax = await listarAnexos(String(patientId)).catch(() => []);
setServerAnexos(Array.isArray(ax) ? ax : []);
@ -235,25 +238,38 @@ export function PatientRegistrationForm({
}
function toPayload(): PacienteInput {
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
// converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
let isoDate: string | null = null;
try {
const parts = String(form.birth_date).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) {
isoDate = date.toISOString().slice(0, 10);
}
}
} catch {}
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: isoDate, // enviar ISO ou null
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
async function handleSubmit(event_: React.FormEvent) {
event_.preventDefault();
@ -603,9 +619,22 @@ export function PatientRegistrationForm({
<div className="space-y-2">
<Label>Data de Nascimento</Label>
<Input
type="date"
placeholder="dd/mm/aaaa"
value={form.birth_date}
onChange={(e) => setField("birth_date", e.target.value)}
onChange={(e) => {
// permita apenas números e '/'
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
setField("birth_date", v);
}}
onBlur={() => {
// tenta formatar automaticamente se for uma data válida
const raw = form.birth_date;
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const d = `${parts[0].padStart(2,'0')}/${parts[1].padStart(2,'0')}/${parts[2].padStart(4,'0')}`;
setField("birth_date", d);
}
}}
/>
</div>
</div>

View File

@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.