feature/pacientes-consulta #41

Merged
Jonasbomfim merged 9 commits from feature/pacientes-consulta into develop 2025-10-09 17:48:20 +00:00
10 changed files with 3149 additions and 240 deletions

View File

@ -1,86 +1,263 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FileDown } from "lucide-react"; import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
// Dados fictícios para demonstração
const metricas = [
{ label: "Atendimentos", value: 1240, icon: <CalendarCheck className="w-6 h-6 text-blue-500" /> },
{ label: "Absenteísmo", value: "7,2%", icon: <UserCheck className="w-6 h-6 text-red-500" /> },
{ label: "Satisfação", value: "92%", icon: <ThumbsUp className="w-6 h-6 text-green-500" /> },
{ label: "Faturamento (Mês)", value: "R$ 45.000", icon: <DollarSign className="w-6 h-6 text-emerald-500" /> },
{ label: "No-show", value: "5,1%", icon: <User className="w-6 h-6 text-yellow-500" /> },
];
const consultasPorPeriodo = [
{ periodo: "Jan", consultas: 210 },
{ periodo: "Fev", consultas: 180 },
{ periodo: "Mar", consultas: 250 },
{ periodo: "Abr", consultas: 230 },
{ periodo: "Mai", consultas: 270 },
{ periodo: "Jun", consultas: 220 },
];
const faturamentoMensal = [
{ mes: "Jan", valor: 35000 },
{ mes: "Fev", valor: 29000 },
{ mes: "Mar", valor: 42000 },
{ mes: "Abr", valor: 38000 },
{ mes: "Mai", valor: 45000 },
{ mes: "Jun", valor: 41000 },
];
const taxaNoShow = [
{ mes: "Jan", noShow: 6.2 },
{ mes: "Fev", noShow: 5.8 },
{ mes: "Mar", noShow: 4.9 },
{ mes: "Abr", noShow: 5.5 },
{ mes: "Mai", noShow: 5.1 },
{ mes: "Jun", noShow: 4.7 },
];
const pacientesMaisAtendidos = [
{ nome: "Ana Souza", consultas: 18 },
{ nome: "Bruno Lima", consultas: 15 },
{ nome: "Carla Menezes", consultas: 13 },
{ nome: "Diego Alves", consultas: 12 },
{ nome: "Fernanda Dias", consultas: 11 },
];
const medicosMaisProdutivos = [
{ nome: "Dr. Carlos Andrade", consultas: 62 },
{ nome: "Dra. Paula Silva", consultas: 58 },
{ nome: "Dr. João Pedro", consultas: 54 },
{ nome: "Dra. Marina Costa", consultas: 51 },
];
const convenios = [
{ nome: "Unimed", valor: 18000 },
{ nome: "Bradesco", valor: 12000 },
{ nome: "SulAmérica", valor: 9000 },
{ nome: "Particular", valor: 15000 },
];
const performancePorMedico = [
{ nome: "Dr. Carlos Andrade", consultas: 62, absenteismo: 4.8 },
{ nome: "Dra. Paula Silva", consultas: 58, absenteismo: 6.1 },
{ nome: "Dr. João Pedro", consultas: 54, absenteismo: 7.5 },
{ nome: "Dra. Marina Costa", consultas: 51, absenteismo: 5.2 },
];
const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"];
function exportPDF(title: string, content: string) {
const doc = new jsPDF();
doc.text(title, 10, 10);
doc.text(content, 10, 20);
doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`);
}
export default function RelatoriosPage() { export default function RelatoriosPage() {
// Dados fictícios para o gráfico financeiro
const financeiro = [
{ mes: "Jan", faturamento: 35000, despesas: 12000 },
{ mes: "Fev", faturamento: 29000, despesas: 15000 },
{ mes: "Mar", faturamento: 42000, despesas: 18000 },
{ mes: "Abr", faturamento: 38000, despesas: 14000 },
{ mes: "Mai", faturamento: 45000, despesas: 20000 },
{ mes: "Jun", faturamento: 41000, despesas: 17000 },
];
// ============================
// PASSO 3 - Funções de exportar
// ============================
const exportConsultasPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Consultas", 10, 10);
doc.text("Resumo das consultas realizadas.", 10, 20);
doc.save("relatorio-consultas.pdf");
};
const exportPacientesPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Pacientes", 10, 10);
doc.text("Informações gerais dos pacientes cadastrados.", 10, 20);
doc.save("relatorio-pacientes.pdf");
};
const exportFinanceiroPDF = () => {
const doc = new jsPDF();
doc.text("Relatório Financeiro", 10, 10);
doc.text("Receitas e despesas da clínica.", 10, 20);
doc.save("relatorio-financeiro.pdf");
};
return ( return (
<div className="p-6 bg-background min-h-screen"> <div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-foreground">Relatórios</h1> <h1 className="text-2xl font-bold mb-6 text-foreground">Dashboard Executivo de Relatórios</h1>
<div className="grid grid-cols-3 gap-6"> {/* Métricas principais */}
{/* Card Consultas */} <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
<div className="p-4 border border-border rounded-lg shadow bg-card"> {metricas.map((m) => (
<h2 className="font-semibold text-lg text-foreground">Relatório de Consultas</h2> <div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
<p className="text-sm text-muted-foreground">Resumo das consultas realizadas.</p> {m.icon}
{/* PASSO 4 - Botão chama a função */} <span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
<Button onClick={exportConsultasPDF} className="mt-4"> <span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF </div>
</Button> ))}
</div> </div>
{/* Card Pacientes */} {/* Gráficos e Relatórios */}
<div className="p-4 border border-border rounded-lg shadow bg-card"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<h2 className="font-semibold text-lg text-foreground">Relatório de Pacientes</h2> {/* Consultas realizadas por período */}
<p className="text-sm text-muted-foreground">Informações gerais dos pacientes cadastrados.</p> <div className="bg-card border border-border rounded-lg shadow p-6">
<Button onClick={exportPacientesPDF} className="mt-4"> <div className="flex items-center justify-between mb-2">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF <h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
</Button> <Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={consultasPorPeriodo}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="periodo" />
<YAxis />
<Tooltip />
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
</BarChart>
</ResponsiveContainer>
</div> </div>
{/* Card Financeiro com gráfico */} {/* Faturamento mensal/anual */}
<div className="p-4 border border-border rounded-lg shadow col-span-3 md:col-span-3 bg-card"> <div className="bg-card border border-border rounded-lg shadow p-6">
<h2 className="font-semibold text-lg mb-2 text-foreground">Relatório Financeiro</h2> <div className="flex items-center justify-between mb-2">
<ResponsiveContainer width="100%" height={300}> <h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
<BarChart data={financeiro} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}> <Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={faturamentoMensal}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" /> <XAxis dataKey="mes" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Legend /> <Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
<Bar dataKey="faturamento" fill="#10b981" name="Faturamento" /> </LineChart>
<Bar dataKey="despesas" fill="#ef4444" name="Despesas" />
</BarChart>
</ResponsiveContainer> </ResponsiveContainer>
<Button onClick={exportFinanceiroPDF} className="mt-4"> </div>
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF </div>
</Button>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Taxa de no-show */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={taxaNoShow}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis unit="%" />
<Tooltip />
<Line type="monotone" dataKey="noShow" stroke="#ef4444" name="No-show (%)" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
{/* Indicadores de satisfação */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<div className="flex flex-col items-center justify-center h-[220px]">
<span className="text-5xl font-bold text-green-500">92%</span>
<span className="text-muted-foreground mt-2">Índice de satisfação geral</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Pacientes mais atendidos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Paciente</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{pacientesMaisAtendidos.map((p) => (
<tr key={p.nome}>
<td className="py-1">{p.nome}</td>
<td className="py-1">{p.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Médicos mais produtivos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{medicosMaisProdutivos.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Análise de convênios */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={convenios} dataKey="valor" nameKey="nome" cx="50%" cy="50%" outerRadius={80} label>
{convenios.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
{/* Performance por médico */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
<th className="text-left font-medium">Absenteísmo (%)</th>
</tr>
</thead>
<tbody>
{performancePorMedico.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
<td className="py-1">{m.absenteismo}</td>
</tr>
))}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,94 +1,750 @@
'use client' 'use client'
import { useAuth } from '@/hooks/useAuth' // import { useAuth } from '@/hooks/useAuth' // removido duplicado
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button' 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { User, LogOut, Home } from 'lucide-react' 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 Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute' 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',
consultas: 'Consultas',
exames: 'Exames & Laudos',
mensagens: 'Mensagens',
perfil: 'Perfil',
sair: 'Sair',
proximaConsulta: 'Próxima Consulta',
ultimosExames: 'Últimos Exames',
mensagensNaoLidas: 'Mensagens Não Lidas',
agendar: 'Agendar',
reagendar: 'Reagendar',
cancelar: 'Cancelar',
detalhes: 'Detalhes',
adicionarCalendario: 'Adicionar ao calendário',
visualizarLaudo: 'Visualizar Laudo',
download: 'Download',
compartilhar: 'Compartilhar',
inbox: 'Caixa de Entrada',
enviarMensagem: 'Enviar Mensagem',
salvar: 'Salvar',
editarPerfil: 'Editar Perfil',
consentimentos: 'Consentimentos',
notificacoes: 'Preferências de Notificação',
vazio: 'Nenhum dado encontrado.',
erro: 'Ocorreu um erro. Tente novamente.',
carregando: 'Carregando...',
sucesso: 'Salvo com sucesso!',
erroSalvar: 'Erro ao salvar.',
}
export default function PacientePage() { export default function PacientePage() {
const { logout, user } = useAuth() const { logout, user } = useAuth()
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
// Simulação de loaders, empty states e erro
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null)
// Acessibilidade: foco visível e ordem de tabulação garantidos por padrão nos botões e inputs
const handleLogout = async () => { const handleLogout = async () => {
console.log('[PACIENTE] Iniciando logout...') setLoading(true)
setError('')
try {
await logout() await logout()
} catch {
setError(strings.erro)
} finally {
setLoading(false)
}
}
// Estado para edição do perfil
const [isEditingProfile, setIsEditingProfile] = useState(false)
const [profileData, setProfileData] = useState({
nome: "Maria Silva Santos",
email: user?.email || "paciente@example.com",
telefone: "(11) 99999-9999",
endereco: "Rua das Flores, 123",
cidade: "São Paulo",
cep: "01234-567",
biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.",
})
const handleProfileChange = (field: string, value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }))
}
const handleSaveProfile = () => {
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
}
const handleCancelEdit = () => {
setIsEditingProfile(false)
}
function DashboardCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="flex flex-col items-center justify-center p-4">
<Calendar className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.proximaConsulta}</span>
<span className="text-2xl">12/10/2025</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<FileText className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.ultimosExames}</span>
<span className="text-2xl">2</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<MessageCircle className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
<span className="text-2xl">1</span>
</Card>
</div>
)
}
// Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
const consultasFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
especialidade: "Cardiologia",
local: "Clínica Coração Feliz",
data: new Date().toISOString().split('T')[0],
hora: "09:00",
status: "Confirmada"
},
{
id: 2,
medico: "Dra. Fernanda Lima",
especialidade: "Dermatologia",
local: "Clínica Pele Viva",
data: new Date().toISOString().split('T')[0],
hora: "14:30",
status: "Pendente"
},
{
id: 3,
medico: "Dr. João Silva",
especialidade: "Ortopedia",
local: "Hospital Ortopédico",
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
hora: "11:00",
status: "Cancelada"
},
];
function formatDatePt(date: Date) {
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
function navigateDate(direction: 'prev' | 'next') {
const newDate = new Date(currentDate);
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
setCurrentDate(newDate);
}
function goToToday() {
setCurrentDate(new Date());
}
const todayStr = currentDate.toISOString().split('T')[0];
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
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 ( return (
<ProtectedRoute requiredUserType={["paciente"]}> <section className="bg-card shadow-md rounded-lg border border-border p-6">
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> <div className="max-w-3xl mx-auto space-y-8">
<Card className="w-full max-w-md shadow-lg"> <header className="text-center space-y-2">
<CardHeader className="text-center"> <h2 className="text-3xl font-semibold text-foreground">Agende sua próxima consulta</h2>
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4"> <p className="text-muted-foreground">Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.</p>
<User className="h-8 w-8 text-primary" /> </header>
</div>
<CardTitle className="text-2xl font-bold text-gray-900">
Portal do Paciente
</CardTitle>
<p className="text-sm text-gray-600">
Bem-vindo ao seu espaço pessoal
</p>
</CardHeader>
<CardContent className="space-y-6"> <div className="space-y-6 rounded-lg border border-border bg-muted/40 p-6">
{/* Informações do Paciente */} <div className="space-y-3">
<div className="text-center"> <Label>Tipo de consulta</Label>
<h2 className="text-xl font-semibold text-gray-800 mb-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
Maria Silva Santos
</h2>
<p className="text-sm text-gray-600">
CPF: 123.456.789-00
</p>
<p className="text-sm text-gray-600">
Idade: 35 anos
</p>
</div>
{/* Informações do Login */}
<div className="bg-gray-100 rounded-lg p-4">
<div className="text-center">
<p className="text-sm text-gray-600 mb-1">
Conectado como:
</p>
<p className="font-medium text-gray-800">
{user?.email || 'paciente@example.com'}
</p>
<p className="text-xs text-gray-500 mt-1">
Tipo de usuário: Paciente
</p>
</div>
</div>
{/* Botão Voltar ao Início */}
<Button <Button
asChild type="button"
variant="outline" className={tipoConsulta === 'teleconsulta' ? activeToggleClass : inactiveToggleClass}
className="w-full flex items-center justify-center gap-2 cursor-pointer" 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>
<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>
)
}
// Exames e laudos fictícios
const examesFicticios = [
{
id: 1,
nome: "Hemograma Completo",
data: "2025-09-20",
status: "Disponível",
prontuario: "Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
},
{
id: 2,
nome: "Raio-X de Tórax",
data: "2025-08-10",
status: "Disponível",
prontuario: "Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
},
{
id: 3,
nome: "Eletrocardiograma",
data: "2025-07-05",
status: "Disponível",
prontuario: "Ritmo sinusal, sem arritmias. Exame dentro da normalidade.",
},
];
const laudosFicticios = [
{
id: 1,
nome: "Laudo Hemograma Completo",
data: "2025-09-21",
status: "Assinado",
laudo: "Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
},
{
id: 2,
nome: "Laudo Raio-X de Tórax",
data: "2025-08-11",
status: "Assinado",
laudo: "Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
},
{
id: 3,
nome: "Laudo Eletrocardiograma",
data: "2025-07-06",
status: "Assinado",
laudo: "ECG normal. Não há sinais de isquemia ou sobrecarga.",
},
];
const [exameSelecionado, setExameSelecionado] = useState<null | typeof examesFicticios[0]>(null)
const [laudoSelecionado, setLaudoSelecionado] = useState<null | typeof laudosFicticios[0]>(null)
function ExamesLaudos() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Exames</h2>
<div className="mb-8">
<h3 className="text-lg font-semibold mb-2">Meus Exames</h3>
<div className="space-y-3">
{examesFicticios.map(exame => (
<div key={exame.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{exame.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(exame.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setExameSelecionado(exame)}>Ver Prontuário</Button>
<Button variant="secondary">Download</Button>
</div>
</div>
))}
</div>
</div>
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
<div>
<h3 className="text-lg font-semibold mb-2">Meus Laudos</h3>
<div className="space-y-3">
{laudosFicticios.map(laudo => (
<div key={laudo.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{laudo.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(laudo.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setLaudoSelecionado(laudo)}>Visualizar</Button>
<Button variant="secondary">Compartilhar</Button>
</div>
</div>
))}
</div>
</div>
{/* Modal Prontuário Exame */}
<Dialog open={!!exameSelecionado} onOpenChange={open => !open && setExameSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Prontuário do Exame</DialogTitle>
<DialogDescription>
{exameSelecionado && (
<>
<div className="font-semibold mb-2">{exameSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(exameSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{exameSelecionado.prontuario}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setExameSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal Visualizar Laudo */}
<Dialog open={!!laudoSelecionado} onOpenChange={open => !open && setLaudoSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{laudoSelecionado && (
<>
<div className="font-semibold mb-2">{laudoSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(laudoSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{laudoSelecionado.laudo}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setLaudoSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
)
}
// Mensagens fictícias recebidas do médico
const mensagensFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
data: "2025-10-06T15:30:00",
conteudo: "Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
lida: false
},
{
id: 2,
medico: "Dra. Fernanda Lima",
data: "2025-09-21T10:15:00",
conteudo: "Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
lida: true
},
{
id: 3,
medico: "Dr. João Silva",
data: "2025-08-12T09:00:00",
conteudo: "Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
lida: true
},
];
function Mensagens() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Mensagens Recebidas</h2>
<div className="space-y-3">
{mensagensFicticias.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<MessageCircle className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma mensagem recebida</p>
<p className="text-sm">Você ainda não recebeu mensagens dos seus médicos.</p>
</div>
) : (
mensagensFicticias.map(msg => (
<div key={msg.id} className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!msg.lida ? 'border-primary' : 'border-transparent'}`}>
<div>
<div className="font-medium text-foreground flex items-center gap-2">
<User className="h-4 w-4 text-primary" />
{msg.medico}
{!msg.lida && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
</div>
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.data).toLocaleString('pt-BR')}</div>
<div className="text-foreground whitespace-pre-line">{msg.conteudo}</div>
</div>
</div>
))
)}
</div>
</section>
)
}
function Perfil() {
return (
<div className="space-y-6 max-w-2xl mx-auto">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-foreground">Meu Perfil</h2>
{!isEditingProfile ? (
<Button onClick={() => setIsEditingProfile(true)} className="flex items-center gap-2">
Editar Perfil
</Button>
) : (
<div className="flex gap-2">
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Informações Pessoais */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Informações Pessoais</h3>
<div className="space-y-2">
<Label htmlFor="nome">Nome Completo</Label>
<p className="p-2 bg-muted rounded text-muted-foreground">{profileData.nome}</p>
<span className="text-xs text-muted-foreground">Este campo não pode ser alterado</span>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
{isEditingProfile ? (
<Input id="email" type="email" value={profileData.email} onChange={e => handleProfileChange('email', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
{isEditingProfile ? (
<Input id="telefone" value={profileData.telefone} onChange={e => handleProfileChange('telefone', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.telefone}</p>
)}
</div>
</div>
{/* Endereço e Contato */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Endereço</h3>
<div className="space-y-2">
<Label htmlFor="endereco">Endereço</Label>
{isEditingProfile ? (
<Input id="endereco" value={profileData.endereco} onChange={e => handleProfileChange('endereco', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.endereco}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label>
{isEditingProfile ? (
<Input id="cidade" value={profileData.cidade} onChange={e => handleProfileChange('cidade', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cidade}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cep">CEP</Label>
{isEditingProfile ? (
<Input id="cep" value={profileData.cep} onChange={e => handleProfileChange('cep', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="biografia">Biografia</Label>
{isEditingProfile ? (
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
) : (
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
)}
</div>
</div>
</div>
{/* Foto do Perfil */}
<div className="border-t border-border pt-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarFallback className="text-lg">
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
</AvatarFallback>
</Avatar>
{isEditingProfile && (
<div className="space-y-2">
<Button variant="outline" size="sm">Alterar Foto</Button>
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
</div>
)}
</div>
</div>
</div>
)
}
// Renderização principal
return (
<ProtectedRoute requiredUserType={["paciente"]}>
<div className="min-h-screen bg-background text-foreground flex flex-col">
{/* Header só com título e botão de sair */}
<header className="flex items-center justify-between px-4 py-2 border-b bg-card">
<div className="flex items-center gap-2">
<User className="h-6 w-6 text-primary" aria-hidden />
<span className="font-bold">Portal do Paciente</span>
<Button asChild variant="outline" className="ml-4">
<Link href="/"> <Link href="/">
<Home className="h-4 w-4" /> <Home className="h-4 w-4 mr-1" /> Início
Voltar ao Início
</Link> </Link>
</Button> </Button>
{/* Botão de Logout */}
<Button
onClick={handleLogout}
variant="destructive"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
<LogOut className="h-4 w-4" />
Sair
</Button>
{/* Informação adicional */}
<div className="text-center">
<p className="text-xs text-gray-500">
Em breve, mais funcionalidades estarão disponíveis
</p>
</div> </div>
</CardContent> <div className="flex items-center gap-2">
</Card> <SimpleThemeToggle />
<Button onClick={handleLogout} variant="destructive" aria-label={strings.sair} disabled={loading} className="ml-2"><LogOut className="h-4 w-4" /> {strings.sair}</Button>
</div>
</header>
<div className="flex flex-1 min-h-0">
{/* Sidebar vertical */}
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
</nav>
{/* Conteúdo principal */}
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
{/* Toasts de feedback */}
{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>
)}
{/* Loader global */}
{loading && <div className="flex-1 flex items-center justify-center"><span>{strings.carregando}</span></div>}
{error && <div className="flex-1 flex items-center justify-center text-red-600"><span>{error}</span></div>}
{/* Conteúdo principal */}
{!loading && !error && (
<main className="flex-1">
{tab==='dashboard' && <DashboardCards />}
{tab==='consultas' && <Consultas />}
{tab==='exames' && <ExamesLaudos />}
{tab==='mensagens' && <Mensagens />}
{tab==='perfil' && <Perfil />}
</main>
)}
</div>
</div>
</div> </div>
</ProtectedRoute> </ProtectedRoute>
) )

View File

@ -5,11 +5,14 @@ import SignatureCanvas from "react-signature-canvas";
import Link from "next/link"; import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute"; import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { buscarPacientes } from "@/lib/api"; import { buscarPacientes, listarPacientes, buscarPacientePorId, type Paciente } from "@/lib/api";
import { useReports } from "@/hooks/useReports";
import { CreateReportData, ReportFormData } from "@/types/report";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import { import {
Table, Table,
TableBody, TableBody,
@ -107,11 +110,20 @@ const ProfissionalPage = () => {
prognostico: "", prognostico: "",
tratamentosRealizados: "", tratamentosRealizados: "",
recomendacoes: "", recomendacoes: "",
cid: "",
dataRelatorio: new Date().toISOString().split('T')[0] dataRelatorio: new Date().toISOString().split('T')[0]
}); });
const [relatoriosMedicos, setRelatoriosMedicos] = useState<any[]>([]); const [relatoriosMedicos, setRelatoriosMedicos] = useState<any[]>([]);
const [editandoRelatorio, setEditandoRelatorio] = useState<any>(null); const [editandoRelatorio, setEditandoRelatorio] = useState<any>(null);
// Estados para integração com API de Relatórios
const [pacientesReais, setPacientesReais] = useState<Paciente[]>([]);
const [carregandoPacientes, setCarregandoPacientes] = useState(false);
const [pacienteSelecionadoReport, setPacienteSelecionadoReport] = useState<Paciente | null>(null);
// Hook personalizado para relatórios
const reportsApi = useReports();
// Estados para funcionalidades do prontuário // Estados para funcionalidades do prontuário
const [consultasRegistradas, setConsultasRegistradas] = useState<any[]>([]); const [consultasRegistradas, setConsultasRegistradas] = useState<any[]>([]);
const [historicoMedico, setHistoricoMedico] = useState<any[]>([]); const [historicoMedico, setHistoricoMedico] = useState<any[]>([]);
@ -305,6 +317,7 @@ const ProfissionalPage = () => {
prognostico: "", prognostico: "",
tratamentosRealizados: "", tratamentosRealizados: "",
recomendacoes: "", recomendacoes: "",
cid: "",
dataRelatorio: new Date().toISOString().split('T')[0] dataRelatorio: new Date().toISOString().split('T')[0]
}); });
}; };
@ -338,10 +351,271 @@ const ProfissionalPage = () => {
prognostico: "", prognostico: "",
tratamentosRealizados: "", tratamentosRealizados: "",
recomendacoes: "", recomendacoes: "",
cid: "",
dataRelatorio: new Date().toISOString().split('T')[0] dataRelatorio: new Date().toISOString().split('T')[0]
}); });
}; };
// ===== FUNÇÕES PARA INTEGRAÇÃO COM API DE RELATÓRIOS =====
// Carregar pacientes reais do Supabase
const carregarPacientesReais = async () => {
setCarregandoPacientes(true);
try {
console.log('📋 [REPORTS] Carregando pacientes do Supabase...');
// Tentar primeiro usando a função da API que já existe
try {
console.log('📋 [REPORTS] Tentando função listarPacientes...');
const pacientes = await listarPacientes({ limit: 50 });
console.log('✅ [REPORTS] Pacientes do Supabase via API:', pacientes);
if (pacientes && pacientes.length > 0) {
setPacientesReais(pacientes);
console.log('✅ [REPORTS] Usando pacientes do Supabase:', pacientes.length);
return;
}
} catch (apiError) {
console.warn('⚠️ [REPORTS] Erro na função listarPacientes:', apiError);
}
// Se a função da API falhar, tentar diretamente
console.log('📋 [REPORTS] Tentando buscar diretamente do Supabase...');
const supabaseUrl = 'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients';
console.log('📋 [REPORTS] URL do Supabase:', supabaseUrl);
// Verificar se há token de autenticação
const token = localStorage.getItem("auth_token") || localStorage.getItem("token") ||
sessionStorage.getItem("auth_token") || sessionStorage.getItem("token");
console.log('🔑 [REPORTS] Token encontrado:', token ? 'SIM' : 'NÃO');
const headers: Record<string, string> = {
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(supabaseUrl, {
method: 'GET',
headers
});
console.log('📡 [REPORTS] Status da resposta do Supabase:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('❌ [REPORTS] Erro detalhado do Supabase:', errorText);
throw new Error(`Supabase HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
const data = await response.json();
console.log('✅ [REPORTS] Resposta completa do Supabase:', data);
console.log('✅ [REPORTS] Tipo da resposta:', Array.isArray(data) ? 'Array' : typeof data);
let pacientes: Paciente[] = [];
if (Array.isArray(data)) {
pacientes = data;
} else if (data.data && Array.isArray(data.data)) {
pacientes = data.data;
} else {
console.warn('⚠️ [REPORTS] Formato de resposta inesperado do Supabase:', data);
pacientes = [];
}
console.log('✅ [REPORTS] Pacientes encontrados no Supabase:', pacientes.length);
if (pacientes.length > 0) {
console.log('✅ [REPORTS] Primeiro paciente:', pacientes[0]);
console.log('✅ [REPORTS] Últimos 3 pacientes:', pacientes.slice(-3));
}
setPacientesReais(pacientes);
if (pacientes.length === 0) {
console.warn('⚠️ [REPORTS] Nenhum paciente encontrado no Supabase - verifique se há dados na tabela patients');
}
} catch (error) {
console.error('❌ [REPORTS] Erro detalhado ao carregar pacientes:', {
error,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
setPacientesReais([]);
alert('Erro ao carregar pacientes do Supabase: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setCarregandoPacientes(false);
}
};
// Calcular idade do paciente baseado na data de nascimento
const calcularIdade = (birthDate: string | null | undefined): string => {
if (!birthDate) return '';
const hoje = new Date();
const nascimento = new Date(birthDate);
let idade = hoje.getFullYear() - nascimento.getFullYear();
const mesAtual = hoje.getMonth();
const mesNascimento = nascimento.getMonth();
if (mesAtual < mesNascimento || (mesAtual === mesNascimento && hoje.getDate() < nascimento.getDate())) {
idade--;
}
return idade.toString();
};
// Selecionar paciente para o relatório
const selecionarPacienteParaRelatorio = (paciente: Paciente) => {
setPacienteSelecionadoReport(paciente);
// Atualizar o formulário de relatório com dados do paciente
setRelatorioMedico(prev => ({
...prev,
pacienteNome: paciente.full_name,
pacienteCpf: paciente.cpf || '',
pacienteIdade: calcularIdade(paciente.birth_date),
}));
console.log('👤 [REPORTS] Paciente selecionado:', paciente);
};
// Salvar relatório usando a API
const salvarRelatorioAPI = async () => {
if (!pacienteSelecionadoReport) {
alert('Por favor, selecione um paciente.');
return;
}
if (!relatorioMedico.motivoRelatorio.trim()) {
alert('Por favor, preencha o motivo do relatório.');
return;
}
try {
console.log('💾 [REPORTS] Salvando relatório...');
// Dados para enviar à API
const reportData: CreateReportData = {
patient_id: pacienteSelecionadoReport.id,
doctor_id: user?.id || 'temp-doctor-id', // Usar ID do usuário logado
report_type: 'Relatório Médico',
chief_complaint: relatorioMedico.motivoRelatorio,
clinical_history: relatorioMedico.historicoClinico,
symptoms_and_signs: relatorioMedico.sinaisSintomas,
physical_examination: '', // Pode adicionar campo no formulário se necessário
complementary_exams: relatorioMedico.examesRealizados,
exam_results: relatorioMedico.resultadosExames,
diagnosis: relatorioMedico.diagnosticos,
prognosis: relatorioMedico.prognostico,
treatment_performed: relatorioMedico.tratamentosRealizados,
objective_recommendations: relatorioMedico.recomendacoes || '',
icd_code: relatorioMedico.cid,
report_date: relatorioMedico.dataRelatorio,
};
const novoRelatorio = await reportsApi.createNewReport(reportData);
console.log('✅ [REPORTS] Relatório salvo com sucesso:', novoRelatorio);
// Recarregar a lista de relatórios para garantir que está sincronizada
await reportsApi.loadReports();
alert('Relatório médico salvo com sucesso!');
// Limpar formulário
limparFormularioRelatorio();
} catch (error) {
console.error('❌ [REPORTS] Erro ao salvar relatório:', error);
alert('Erro ao salvar relatório: ' + error);
}
};
// Limpar formulário de relatório
const limparFormularioRelatorio = () => {
setRelatorioMedico({
pacienteNome: "",
pacienteCpf: "",
pacienteIdade: "",
profissionalNome: medico.nome,
profissionalCrm: medico.identificacao,
motivoRelatorio: "",
historicoClinico: "",
sinaisSintomas: "",
examesRealizados: "",
resultadosExames: "",
diagnosticos: "",
prognostico: "",
tratamentosRealizados: "",
recomendacoes: "",
cid: "",
dataRelatorio: new Date().toISOString().split('T')[0]
});
setPacienteSelecionadoReport(null);
};
// Carregar relatórios existentes
const carregarRelatorios = async () => {
try {
await reportsApi.loadReports();
console.log('✅ [REPORTS] Relatórios carregados:', reportsApi.reports.length);
} catch (error) {
console.error('❌ [REPORTS] Erro ao carregar relatórios:', error);
}
};
// useEffect para carregar dados iniciais
useEffect(() => {
if (activeSection === 'relatorios-medicos') {
console.log('🔄 [REPORTS] Seção de relatórios ativada - carregando dados...');
carregarPacientesReais();
carregarRelatorios();
}
}, [activeSection]);
// Buscar pacientes faltantes por patient_id após carregar relatórios e pacientes
useEffect(() => {
if (activeSection !== 'relatorios-medicos') return;
if (!reportsApi.reports || reportsApi.reports.length === 0) return;
// IDs de pacientes já carregados
const idsPacientesReais = new Set(pacientesReais.map(p => String(p.id)));
// IDs de pacientes presentes nos relatórios
const idsRelatorios = Array.from(new Set(reportsApi.reports.map(r => String(r.patient_id)).filter(Boolean)));
// IDs que faltam
const idsFaltantes = idsRelatorios.filter(id => !idsPacientesReais.has(id));
if (idsFaltantes.length === 0) return;
// Buscar pacientes faltantes individualmente, apenas se o ID for string/UUID
(async () => {
const novosPacientes: Paciente[] = [];
for (const id of idsFaltantes) {
// Só busca se for string e não for número
if (typeof id === 'string' && isNaN(Number(id))) {
try {
const paciente = await buscarPacientePorId(id);
if (paciente) novosPacientes.push(paciente);
} catch (e) {
console.warn('⚠️ [REPORTS] Paciente não encontrado para o relatório:', id);
}
} else {
console.warn('⚠️ [REPORTS] Ignorando busca de paciente por ID não-string/UUID:', id);
}
}
if (novosPacientes.length > 0) {
setPacientesReais(prev => ([...prev, ...novosPacientes]));
}
})();
}, [activeSection, reportsApi.reports, pacientesReais]);
const handleDateClick = (arg: any) => { const handleDateClick = (arg: any) => {
setSelectedDate(arg.dateStr); setSelectedDate(arg.dateStr);
@ -2032,14 +2306,40 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
} }
}, [laudo, isNewLaudo]); }, [laudo, isNewLaudo]);
const formatText = (type: string) => { // Histórico para desfazer/refazer
const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
// Atualiza histórico ao digitar
useEffect(() => {
if (history[historyIndex] !== content) {
const newHistory = history.slice(0, historyIndex + 1);
setHistory([...newHistory, content]);
setHistoryIndex(newHistory.length);
}
// eslint-disable-next-line
}, [content]);
const handleUndo = () => {
if (historyIndex > 0) {
setContent(history[historyIndex - 1]);
setHistoryIndex(historyIndex - 1);
}
};
const handleRedo = () => {
if (historyIndex < history.length - 1) {
setContent(history[historyIndex + 1]);
setHistoryIndex(historyIndex + 1);
}
};
// Formatação avançada
const formatText = (type: string, value?: any) => {
const textarea = document.querySelector('textarea') as HTMLTextAreaElement; const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
if (!textarea) return; if (!textarea) return;
const start = textarea.selectionStart; const start = textarea.selectionStart;
const end = textarea.selectionEnd; const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end); const selectedText = textarea.value.substring(start, end);
let formattedText = ""; let formattedText = "";
switch(type) { switch(type) {
case "bold": case "bold":
@ -2049,13 +2349,44 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*"; formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*";
break; break;
case "underline": case "underline":
formattedText = selectedText ? `<u>${selectedText}</u>` : "<u>texto sublinhado</u>"; formattedText = selectedText ? `__${selectedText}__` : "__texto sublinhado__";
break; break;
case "list": case "list-ul":
formattedText = selectedText ? `${selectedText}` : "• item da lista"; formattedText = selectedText ? selectedText.split('\n').map(l => `${l}`).join('\n') : "• item da lista";
break; break;
case "list-ol":
formattedText = selectedText ? selectedText.split('\n').map((l,i) => `${i+1}. ${l}`).join('\n') : "1. item da lista";
break;
case "indent":
formattedText = selectedText ? selectedText.split('\n').map(l => ` ${l}`).join('\n') : " ";
break;
case "outdent":
formattedText = selectedText ? selectedText.split('\n').map(l => l.replace(/^\s{1,4}/, "")).join('\n') : "";
break;
case "align-left":
formattedText = selectedText ? `[left]${selectedText}[/left]` : "[left]Texto à esquerda[/left]";
break;
case "align-center":
formattedText = selectedText ? `[center]${selectedText}[/center]` : "[center]Texto centralizado[/center]";
break;
case "align-right":
formattedText = selectedText ? `[right]${selectedText}[/right]` : "[right]Texto à direita[/right]";
break;
case "align-justify":
formattedText = selectedText ? `[justify]${selectedText}[/justify]` : "[justify]Texto justificado[/justify]";
break;
case "font-size":
formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`;
break;
case "font-family":
formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`;
break;
case "font-color":
formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`;
break;
default:
return;
} }
const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end); const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
setContent(newText); setContent(newText);
}; };
@ -2084,7 +2415,14 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
return content return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/<u>(.*?)<\/u>/g, '<u>$1</u>') .replace(/__(.*?)__/g, '<u>$1</u>')
.replace(/\[left\](.*?)\[\/left\]/gs, '<div style="text-align:left">$1</div>')
.replace(/\[center\](.*?)\[\/center\]/gs, '<div style="text-align:center">$1</div>')
.replace(/\[right\](.*?)\[\/right\]/gs, '<div style="text-align:right">$1</div>')
.replace(/\[justify\](.*?)\[\/justify\]/gs, '<div style="text-align:justify">$1</div>')
.replace(/\[size=(\d+)\](.*?)\[\/size\]/gs, '<span style="font-size:$1px">$2</span>')
.replace(/\[font=([^\]]+)\](.*?)\[\/font\]/gs, '<span style="font-family:$1">$2</span>')
.replace(/\[color=([^\]]+)\](.*?)\[\/color\]/gs, '<span style="color:$1">$2</span>')
.replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]') .replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]')
.replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]') .replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')
.replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]') .replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]')
@ -2383,44 +2721,60 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Toolbar */} {/* Toolbar */}
<div className="p-3 border-b border-border"> <div className="p-3 border-b border-border">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2 items-center">
<Button {/* Tamanho da fonte */}
variant="outline" <label className="text-xs mr-1">Tamanho</label>
size="sm" <input
onClick={() => formatText("bold")} type="number"
title="Negrito" min={8}
className="hover:bg-blue-50 dark:hover:bg-accent" max={32}
defaultValue={14}
onBlur={e => formatText('font-size', e.target.value)}
className="w-14 border rounded px-1 py-0.5 text-xs mr-2"
title="Tamanho da fonte"
/>
{/* Família da fonte */}
<label className="text-xs mr-1">Fonte</label>
<select
defaultValue={'Arial'}
onBlur={e => formatText('font-family', e.target.value)}
className="border rounded px-1 py-0.5 text-xs mr-2"
title="Família da fonte"
> >
<strong>B</strong> <option value="Arial">Arial</option>
</Button> <option value="Helvetica">Helvetica</option>
<Button <option value="Times New Roman">Times New Roman</option>
variant="outline" <option value="Courier New">Courier New</option>
size="sm" <option value="Verdana">Verdana</option>
onClick={() => formatText("italic")} <option value="Georgia">Georgia</option>
title="Itálico" </select>
className="hover:bg-blue-50 dark:hover:bg-accent" {/* Cor da fonte */}
> <label className="text-xs mr-1">Cor</label>
<em>I</em> <input
</Button> type="color"
<Button defaultValue="#222222"
variant="outline" onBlur={e => formatText('font-color', e.target.value)}
size="sm" className="w-6 h-6 border rounded mr-2"
onClick={() => formatText("underline")} title="Cor da fonte"
title="Sublinhado" />
className="hover:bg-blue-50 dark:hover:bg-accent" {/* Alinhamento */}
> <Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<u>U</u> <Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1"><svg width="16" height="16" fill="none"><rect x="4" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="3" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
</Button> <Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1"><svg width="16" height="16" fill="none"><rect x="6" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="4" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button <Button variant="outline" size="sm" onClick={() => formatText('align-justify')} title="Justificar" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="12" height="2" rx="1" fill="currentColor"/></svg></Button>
variant="outline" {/* Listas */}
size="sm" <Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1">1.</Button>
onClick={() => formatText("list")} <Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1"></Button>
title="Lista" {/* Recuo */}
className="hover:bg-blue-50 dark:hover:bg-accent" <Button variant="outline" size="sm" onClick={() => formatText('indent')} title="Aumentar recuo" className="px-1"></Button>
> <Button variant="outline" size="sm" onClick={() => formatText('outdent')} title="Diminuir recuo" className="px-1"></Button>
{/* Desfazer/Refazer */}
</Button> <Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1"></Button>
<Button variant="outline" size="sm" onClick={handleRedo} title="Refazer" className="px-1"></Button>
{/* Negrito, itálico, sublinhado */}
<Button variant="outline" size="sm" onClick={() => formatText("bold") } title="Negrito" className="hover:bg-blue-50 dark:hover:bg-accent"><strong>B</strong></Button>
<Button variant="outline" size="sm" onClick={() => formatText("italic") } title="Itálico" className="hover:bg-blue-50 dark:hover:bg-accent"><em>I</em></Button>
<Button variant="outline" size="sm" onClick={() => formatText("underline") } title="Sublinhado" className="hover:bg-blue-50 dark:hover:bg-accent"><u>U</u></Button>
</div> </div>
{/* Templates */} {/* Templates */}
@ -2443,12 +2797,13 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div> </div>
{/* Editor */} {/* Editor */}
<div className="flex-1 p-4"> <div className="flex-1 p-4 overflow-auto max-h-[500px]">
<Textarea <Textarea
value={content} value={content}
onChange={(e) => setContent(e.target.value)} onChange={(e) => setContent(e.target.value)}
placeholder="Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico, <u></u> para sublinhado." placeholder="Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico, <u></u> para sublinhado."
className="h-full min-h-[400px] resize-none" className="h-full min-h-[400px] resize-none scrollbar-thin scrollbar-thumb-blue-400 scrollbar-track-blue-100"
style={{ maxHeight: 400, overflow: 'auto' }}
/> />
</div> </div>
</div> </div>
@ -2801,32 +3156,54 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div> </div>
</div> </div>
{/* Identificação do Paciente */} {/* Identificação do Paciente - USANDO API REAL */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-md font-medium text-primary border-b pb-2">Identificação do Paciente</h4> <div className="flex items-center justify-between border-b pb-2">
<h4 className="text-md font-medium text-primary">Identificação do Paciente</h4>
<Button
variant="outline"
size="sm"
onClick={carregarPacientesReais}
disabled={carregandoPacientes}
className="flex items-center gap-2 text-xs"
>
🔄 {carregandoPacientes ? 'Carregando...' : 'Recarregar Pacientes'}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="pacienteNome">Nome do Paciente *</Label> <Label htmlFor="pacienteNome">Nome do Paciente *</Label>
<Select <Select
value={relatorioMedico.pacienteNome} value={pacienteSelecionadoReport?.id || ''}
onValueChange={(value) => { onValueChange={(value) => {
const pacienteSelecionado = pacientes.find(p => p.nome === value); const paciente = pacientesReais.find(p => p.id === value);
handleRelatorioChange('pacienteNome', value); if (paciente) {
if (pacienteSelecionado) { selecionarPacienteParaRelatorio(paciente);
handleRelatorioChange('pacienteCpf', pacienteSelecionado.cpf); }
handleRelatorioChange('pacienteIdade', pacienteSelecionado.idade.toString()); }}
onOpenChange={(open) => {
// Carregar pacientes quando o dropdown for aberto pela primeira vez
if (open && pacientesReais.length === 0 && !carregandoPacientes) {
console.log('🔄 [REPORTS] Dropdown aberto - carregando pacientes...');
carregarPacientesReais();
} }
}} }}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione o paciente" /> <SelectValue placeholder={carregandoPacientes ? "Carregando..." : "Selecione o paciente"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{pacientes.map((paciente) => ( {carregandoPacientes ? (
<SelectItem key={paciente.cpf} value={paciente.nome}> <SelectItem value="loading" disabled>Carregando pacientes...</SelectItem>
{paciente.nome} ) : pacientesReais.length === 0 ? (
<SelectItem value="empty" disabled>Nenhum paciente encontrado</SelectItem>
) : (
pacientesReais.map((paciente) => (
<SelectItem key={paciente.id} value={paciente.id}>
{paciente.full_name} - {paciente.cpf || 'CPF não informado'}
</SelectItem> </SelectItem>
))} ))
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -2835,27 +3212,50 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<Input <Input
id="pacienteCpf" id="pacienteCpf"
value={relatorioMedico.pacienteCpf} value={relatorioMedico.pacienteCpf}
onChange={(e) => handleRelatorioChange('pacienteCpf', e.target.value)} disabled
placeholder="000.000.000-00" className="bg-muted"
placeholder="CPF será preenchido automaticamente"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="pacienteIdade">Idade</Label> <Label htmlFor="pacienteIdade">Idade</Label>
<Input <Input
id="pacienteIdade" id="pacienteIdade"
type="number" type="text"
value={relatorioMedico.pacienteIdade} value={relatorioMedico.pacienteIdade}
onChange={(e) => handleRelatorioChange('pacienteIdade', e.target.value)} disabled
placeholder="Idade do paciente" className="bg-muted"
placeholder="Idade será calculada automaticamente"
/> />
</div> </div>
</div> </div>
{/* Informações adicionais do paciente selecionado */}
{pacienteSelecionadoReport && (
<div className="bg-muted/50 p-4 rounded-lg">
<h5 className="font-medium text-sm text-muted-foreground mb-2">Informações do Paciente Selecionado:</h5>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium">Nome Completo:</span><br />
<span>{pacienteSelecionadoReport.full_name}</span>
</div>
<div>
<span className="font-medium">Email:</span><br />
<span>{pacienteSelecionadoReport.email || 'Não informado'}</span>
</div>
<div>
<span className="font-medium">Telefone:</span><br />
<span>{pacienteSelecionadoReport.phone_mobile || 'Não informado'}</span>
</div>
</div>
</div>
)}
</div> </div>
{/* Informações do Relatório */} {/* Informações do Relatório */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-md font-medium text-primary border-b pb-2">Informações do Relatório</h4> <h4 className="text-md font-medium text-primary border-b pb-2">Informações do Relatório</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="motivoRelatorio">Motivo do Relatório *</Label> <Label htmlFor="motivoRelatorio">Motivo do Relatório *</Label>
<Textarea <Textarea
@ -2866,6 +3266,15 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
rows={3} rows={3}
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="cid">CID</Label>
<Input
id="cid"
value={relatorioMedico.cid}
onChange={(e) => handleRelatorioChange('cid', e.target.value)}
placeholder="Ex: A00, B20, C34..."
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dataRelatorio">Data do Relatório</Label> <Label htmlFor="dataRelatorio">Data do Relatório</Label>
<Input <Input
@ -2876,7 +3285,6 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="historicoClinico">Histórico Clínico Conciso</Label> <Label htmlFor="historicoClinico">Histórico Clínico Conciso</Label>
<Textarea <Textarea
@ -2986,19 +3394,54 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<Button variant="outline" onClick={handleCancelarEdicaoRelatorio} className="hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground"> <Button variant="outline" onClick={handleCancelarEdicaoRelatorio} className="hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground">
Cancelar Cancelar
</Button> </Button>
<Button onClick={handleSalvarRelatorio} className="flex items-center gap-2"> <Button
onClick={salvarRelatorioAPI}
className="flex items-center gap-2"
disabled={reportsApi.loading || !pacienteSelecionadoReport}
>
<FileCheck className="h-4 w-4" /> <FileCheck className="h-4 w-4" />
{editandoRelatorio ? 'Atualizar Relatório' : 'Salvar Relatório'} {reportsApi.loading ? 'Salvando...' : (editandoRelatorio ? 'Atualizar Relatório' : 'Salvar Relatório')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{/* Lista de Relatórios Existentes */} {/* Lista de Relatórios da API */}
<div className="bg-card shadow-md rounded-lg p-6"> <div className="bg-card shadow-md rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Relatórios Médicos Salvos</h3> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">Relatórios Médicos</h3>
<Button
variant="outline"
size="sm"
onClick={carregarRelatorios}
disabled={reportsApi.loading}
className="flex items-center gap-2"
>
<FileCheck className="h-4 w-4" />
{reportsApi.loading ? 'Carregando...' : 'Atualizar'}
</Button>
</div>
{relatoriosMedicos.length === 0 ? ( {reportsApi.error && (
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 mb-4">
<p className="text-destructive text-sm">{reportsApi.error}</p>
<Button
variant="outline"
size="sm"
onClick={reportsApi.clearError}
className="mt-2"
>
Limpar erro
</Button>
</div>
)}
{reportsApi.loading ? (
<div className="text-center py-8 text-muted-foreground">
<FileCheck className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50 animate-pulse" />
<p className="text-lg mb-2">Carregando relatórios...</p>
</div>
) : reportsApi.reports.length === 0 ? (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
<FileCheck className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" /> <FileCheck className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhum relatório médico encontrado</p> <p className="text-lg mb-2">Nenhum relatório médico encontrado</p>
@ -3006,30 +3449,50 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{relatoriosMedicos.map((relatorio) => ( {reportsApi.reports.filter(relatorio => relatorio != null).map((relatorio, idx) => {
<div key={relatorio.id} className="border rounded-lg p-4 hover:shadow-md transition-shadow"> // Buscar dados do paciente pelos pacientes carregados
const pacienteEncontrado = pacientesReais.find(p => p.id === relatorio?.patient_id);
const nomeExibir = relatorio?.patient?.full_name || pacienteEncontrado?.full_name || 'Paciente não identificado';
const cpfExibir = relatorio?.patient?.cpf || pacienteEncontrado?.cpf || 'Não informado';
return (
<div key={relatorio?.id ? `report-${relatorio.id}-${idx}` : `report-idx-${idx}`} className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<div> <div>
<h4 className="font-semibold text-lg">{relatorio.pacienteNome}</h4> <h4 className="font-semibold text-lg">
<p className="text-sm text-muted-foreground">CPF: {relatorio.pacienteCpf} Idade: {relatorio.pacienteIdade} anos</p> {nomeExibir}
<p className="text-sm text-muted-foreground">Data do relatório: {new Date(relatorio.dataRelatorio).toLocaleDateString('pt-BR')}</p> </h4>
<p className="text-xs text-muted-foreground/70">Gerado em: {relatorio.dataGeracao}</p> <p className="text-sm text-muted-foreground">
CPF: {cpfExibir}
Tipo: {relatorio?.report_type || 'Relatório Médico'}
</p>
<p className="text-sm text-muted-foreground">
Data do relatório: {relatorio?.report_date ? new Date(relatorio.report_date).toLocaleDateString('pt-BR') : 'Data não informada'}
</p>
<p className="text-xs text-muted-foreground/70">
Criado em: {relatorio?.created_at ? new Date(relatorio.created_at).toLocaleDateString('pt-BR') : 'Data não informada'}
</p>
<p className="text-sm text-foreground/80 mt-2 line-clamp-2">
<strong>Motivo:</strong> {relatorio?.chief_complaint || 'Não informado'}
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handleEditarRelatorio(relatorio)} onClick={() => relatorio?.id && reportsApi.loadReportById(relatorio.id)}
className="flex items-center gap-1" className="flex items-center gap-1"
disabled={!relatorio?.id}
> >
<Edit className="h-3 w-3" /> <Eye className="h-3 w-3" />
Editar Visualizar
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={() => handleExcluirRelatorio(relatorio.id)} onClick={() => relatorio?.id && reportsApi.deleteExistingReport(relatorio.id)}
className="flex items-center gap-1" className="flex items-center gap-1"
disabled={reportsApi.loading || !relatorio?.id}
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
Excluir Excluir
@ -3039,26 +3502,34 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="font-medium text-primary">Motivo:</span> <span className="font-medium text-primary">Queixa Principal:</span>
<p className="text-foreground mt-1">{relatorio.motivoRelatorio}</p> <p className="text-foreground mt-1 line-clamp-3">{relatorio.chief_complaint}</p>
</div> </div>
{relatorio.diagnosticos && ( {relatorio.diagnosis && (
<div> <div>
<span className="font-medium text-primary">Diagnóstico(s):</span> <span className="font-medium text-primary">Diagnóstico(s):</span>
<p className="text-foreground mt-1">{relatorio.diagnosticos}</p> <p className="text-foreground mt-1 line-clamp-3">{relatorio.diagnosis}</p>
</div> </div>
)} )}
{relatorio.recomendacoes && ( {relatorio.objective_recommendations && (
<div className="md:col-span-2"> <div className="md:col-span-2">
<span className="font-medium text-primary">Recomendações:</span> <span className="font-medium text-primary">Recomendações:</span>
<p className="text-foreground mt-1">{relatorio.recomendacoes}</p> <p className="text-foreground mt-1 line-clamp-3">{relatorio.objective_recommendations}</p>
</div>
)}
{relatorio.icd_code && (
<div>
<span className="font-medium text-primary">CID:</span>
<p className="text-foreground mt-1">{relatorio.icd_code}</p>
</div> </div>
)} )}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)} )}
</div> </div>
@ -3270,6 +3741,8 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button <Button
variant="outline" variant="outline"
onClick={logout} onClick={logout}
@ -3277,6 +3750,7 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
> >
Sair Sair
</Button> </Button>
</div>
</header> </header>
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6"> <div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">

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

@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react" import { useState, useEffect, useRef } from "react"
import { SidebarTrigger } from "../ui/sidebar" import { SidebarTrigger } from "../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) { export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth(); const { logout, user } = useAuth();
@ -44,7 +45,12 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
</Button> </Button>
<SimpleThemeToggle />
<Button
variant="outline"
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
asChild
></Button>
{/* Avatar Dropdown Simples */} {/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<Button <Button

View File

@ -0,0 +1,219 @@
// hooks/useReports.ts
import { useState, useEffect, useCallback } from 'react';
import {
Report,
CreateReportData,
UpdateReportData,
ApiError
} from '@/types/report-types';
import {
listarRelatorios,
buscarRelatorioPorId,
criarRelatorio,
atualizarRelatorio,
deletarRelatorio,
listarRelatoriosPorPaciente,
listarRelatoriosPorMedico
} from '@/lib/reports';
interface UseReportsReturn {
// Estados
reports: Report[];
selectedReport: Report | null;
loading: boolean;
error: string | null;
// Ações
loadReports: () => Promise<void>;
loadReportById: (id: string) => Promise<void>;
createNewReport: (data: CreateReportData) => Promise<Report>;
updateExistingReport: (id: string, data: UpdateReportData) => Promise<Report>;
deleteExistingReport: (id: string) => Promise<void>;
loadReportsByPatient: (patientId: string) => Promise<void>;
loadReportsByDoctor: (doctorId: string) => Promise<void>;
clearError: () => void;
clearSelectedReport: () => void;
}
export function useReports(): UseReportsReturn {
// Estados
const [reports, setReports] = useState<Report[]>([]);
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Função para tratar erros
const handleError = useCallback((error: any) => {
console.error('❌ [useReports] Erro:', error);
if (error && typeof error === 'object' && 'message' in error) {
setError(error.message);
} else if (typeof error === 'string') {
setError(error);
} else {
setError('Ocorreu um erro inesperado');
}
}, []);
// Carregar todos os relatórios
const loadReports = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listarRelatorios();
setReports(data);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
}, [handleError]);
// Carregar um relatório específico
const loadReportById = useCallback(async (id: string) => {
setLoading(true);
setError(null);
try {
const report = await buscarRelatorioPorId(id);
setSelectedReport(report);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
}, [handleError]);
// Criar novo relatório
const createNewReport = useCallback(async (data: CreateReportData): Promise<Report> => {
setLoading(true);
setError(null);
try {
const newReport = await criarRelatorio(data);
// Adicionar o novo relatório à lista
setReports(prev => [newReport, ...prev]);
return newReport;
} catch (err) {
handleError(err);
throw err;
} finally {
setLoading(false);
}
}, [handleError]);
// Atualizar relatório existente
const updateExistingReport = useCallback(async (id: string, data: UpdateReportData): Promise<Report> => {
setLoading(true);
setError(null);
try {
const updatedReport = await atualizarRelatorio(id, data);
// Atualizar na lista
setReports(prev =>
prev.map(report =>
report.id === id ? updatedReport : report
)
);
// Atualizar o selecionado se for o mesmo
if (selectedReport?.id === id) {
setSelectedReport(updatedReport);
}
return updatedReport;
} catch (err) {
handleError(err);
throw err;
} finally {
setLoading(false);
}
}, [handleError, selectedReport]);
// Deletar relatório
const deleteExistingReport = useCallback(async (id: string): Promise<void> => {
setLoading(true);
setError(null);
try {
await deletarRelatorio(id);
// Remover da lista
setReports(prev => prev.filter(report => report.id !== id));
// Limpar seleção se for o mesmo
if (selectedReport?.id === id) {
setSelectedReport(null);
}
} catch (err) {
handleError(err);
throw err;
} finally {
setLoading(false);
}
}, [handleError, selectedReport]);
// Carregar relatórios por paciente
const loadReportsByPatient = useCallback(async (patientId: string) => {
setLoading(true);
setError(null);
try {
const data = await listarRelatoriosPorPaciente(patientId);
setReports(data);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
}, [handleError]);
// Carregar relatórios por médico
const loadReportsByDoctor = useCallback(async (doctorId: string) => {
setLoading(true);
setError(null);
try {
const data = await listarRelatoriosPorMedico(doctorId);
setReports(data);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
}, [handleError]);
// Limpar erro
const clearError = useCallback(() => {
setError(null);
}, []);
// Limpar relatório selecionado
const clearSelectedReport = useCallback(() => {
setSelectedReport(null);
}, []);
return {
// Estados
reports,
selectedReport,
loading,
error,
// Ações
loadReports,
loadReportById,
createNewReport,
updateExistingReport,
deleteExistingReport,
loadReportsByPatient,
loadReportsByDoctor,
clearError,
clearSelectedReport,
};
}

View File

@ -320,7 +320,12 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
} }
export async function buscarPacientePorId(id: string | number): Promise<Paciente> { export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
const url = `${REST}/patients?id=eq.${id}`; // Se for string e não for só número, coloca aspas duplas (para UUID/texto)
let idParam: string | number = id;
if (typeof id === 'string' && isNaN(Number(id))) {
idParam = `\"${id}\"`;
}
const url = `${REST}/patients?id=eq.${idParam}`;
const res = await fetch(url, { method: "GET", headers: baseHeaders() }); const res = await fetch(url, { method: "GET", headers: baseHeaders() });
const arr = await parse<Paciente[]>(res); const arr = await parse<Paciente[]>(res);
if (!arr?.length) throw new Error("404: Paciente não encontrado"); if (!arr?.length) throw new Error("404: Paciente não encontrado");

289
susconecta/lib/reports.ts Normal file
View File

@ -0,0 +1,289 @@
/**
* Atualiza um relatório existente (edição)
* @param id ID do relatório a ser atualizado
* @param dados Dados a serem atualizados no relatório
*/
export async function editarRelatorio(id: string, dados: Partial<{
patient_id: string;
order_number: string;
exam: string;
diagnosis: string;
conclusion: string;
cid_code: string;
content_html: string;
content_json: any;
status: string;
requested_by: string;
due_at: string;
hide_date: boolean;
hide_signature: boolean;
}>): Promise<any> {
const url = `${BASE_API_RELATORIOS}/${id}`;
const cabecalhos: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token');
if (token) {
cabecalhos['Authorization'] = `Bearer ${token}`;
}
}
const resposta = await fetch(url, {
method: 'PATCH',
headers: cabecalhos,
body: JSON.stringify(dados),
});
if (!resposta.ok) throw new Error('Erro ao atualizar relatório');
return resposta.json();
}
// services/reports.ts
import {
Report,
CreateReportData,
UpdateReportData,
ReportsResponse,
ReportResponse,
ApiError
} from '@/types/report-types';
// URL base da API Mock
const BASE_API_RELATORIOS = 'https://mock.apidog.com/m1/1053378-0-default/rest/v1/reports';
// Cabeçalhos base para as requisições
function obterCabecalhos(): HeadersInit {
const cabecalhos: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
};
// Adiciona token de autenticação do localStorage se disponível
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token');
if (token) {
cabecalhos['Authorization'] = `Bearer ${token}`;
}
}
return cabecalhos;
}
// Função para tratar erros da API
async function tratarRespostaApi<T>(resposta: Response): Promise<T> {
if (!resposta.ok) {
let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`;
try {
const dadosErro = await resposta.json();
mensagemErro = dadosErro.message || dadosErro.error || mensagemErro;
} catch (e) {
// Se não conseguir parsear como JSON, usa a mensagem de status HTTP
}
const erro: ApiError = {
message: mensagemErro,
code: resposta.status.toString(),
};
throw erro;
}
const dados = await resposta.json();
return dados;
}
// ===== SERVIÇOS DE RELATÓRIOS MÉDICOS =====
/**
* Lista relatórios médicos com filtros opcionais (patient_id, status)
*/
export async function listarRelatorios(filtros?: { patient_id?: string; status?: string }): Promise<Report[]> {
// Monta query string se houver filtros
let url = BASE_API_RELATORIOS;
if (filtros && (filtros.patient_id || filtros.status)) {
const params = new URLSearchParams();
if (filtros.patient_id) params.append('patient_id', filtros.patient_id);
if (filtros.status) params.append('status', filtros.status);
url += `?${params.toString()}`;
}
// Monta cabeçalhos conforme cURL
const cabecalhos: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token');
if (token) {
cabecalhos['Authorization'] = `Bearer ${token}`;
}
}
const resposta = await fetch(url, {
method: 'GET',
headers: cabecalhos,
});
if (!resposta.ok) throw new Error('Erro ao buscar relatórios');
const dados = await resposta.json();
if (Array.isArray(dados)) return dados;
if (dados && Array.isArray(dados.data)) return dados.data;
for (const chave in dados) {
if (Array.isArray(dados[chave])) return dados[chave];
}
return [];
}
/**
* Busca um relatório específico por ID
*/
export async function buscarRelatorioPorId(id: string): Promise<Report> {
try {
console.log('🔍 [API RELATÓRIOS] Buscando relatório ID:', id);
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
method: 'GET',
headers: obterCabecalhos(),
});
const resultado = await tratarRespostaApi<ReportResponse>(resposta);
console.log('✅ [API RELATÓRIOS] Relatório encontrado:', resultado.data);
return resultado.data;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatório:', erro);
throw erro;
}
}
/**
* Cria um novo relatório médico
*/
export async function criarRelatorio(dadosRelatorio: CreateReportData): Promise<Report> {
try {
console.log('📝 [API RELATÓRIOS] Criando novo relatório...');
console.log('📤 [API RELATÓRIOS] Dados enviados:', dadosRelatorio);
const resposta = await fetch(BASE_API_RELATORIOS, {
method: 'POST',
headers: obterCabecalhos(),
body: JSON.stringify(dadosRelatorio),
});
console.log('📝 [API RELATÓRIOS] Status da criação:', resposta.status);
console.log('📝 [API RELATÓRIOS] Response OK:', resposta.ok);
console.log('📝 [API RELATÓRIOS] Response URL:', resposta.url);
if (!resposta.ok) {
let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`;
try {
const dadosErro = await resposta.json();
mensagemErro = dadosErro.message || dadosErro.error || mensagemErro;
console.log('📝 [API RELATÓRIOS] Erro da API:', dadosErro);
} catch (e) {
console.log('📝 [API RELATÓRIOS] Não foi possível parsear erro como JSON');
}
const erro: ApiError = {
message: mensagemErro,
code: resposta.status.toString(),
};
throw erro;
}
const resultadoBruto = await resposta.json();
console.log('📝 [API RELATÓRIOS] Resposta bruta da criação:', resultadoBruto);
console.log('📝 [API RELATÓRIOS] Tipo da resposta:', typeof resultadoBruto);
console.log('📝 [API RELATÓRIOS] Chaves da resposta:', Object.keys(resultadoBruto || {}));
let relatorioCriado: Report;
// Verifica formato da resposta similar ao listarRelatorios
if (resultadoBruto && resultadoBruto.data) {
relatorioCriado = resultadoBruto.data;
} else if (resultadoBruto && resultadoBruto.id) {
relatorioCriado = resultadoBruto;
} else if (Array.isArray(resultadoBruto) && resultadoBruto.length > 0) {
relatorioCriado = resultadoBruto[0];
} else {
console.warn('📝 [API RELATÓRIOS] Formato de resposta inesperado, criando relatório local');
relatorioCriado = {
id: 'local-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...dadosRelatorio
};
}
console.log('✅ [API RELATÓRIOS] Relatório processado:', relatorioCriado);
return relatorioCriado;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao criar relatório:', erro);
throw erro;
}
}
/**
* Atualiza um relatório existente
*/
export async function atualizarRelatorio(id: string, dadosRelatorio: UpdateReportData): Promise<Report> {
try {
console.log('📝 [API RELATÓRIOS] Atualizando relatório ID:', id);
console.log('📤 [API RELATÓRIOS] Dados:', dadosRelatorio);
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
method: 'PATCH',
headers: obterCabecalhos(),
body: JSON.stringify(dadosRelatorio),
});
const resultado = await tratarRespostaApi<ReportResponse>(resposta);
console.log('✅ [API RELATÓRIOS] Relatório atualizado:', resultado.data);
return resultado.data;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao atualizar relatório:', erro);
throw erro;
}
}
/**
* Deleta um relatório
*/
export async function deletarRelatorio(id: string): Promise<void> {
try {
console.log('🗑️ [API RELATÓRIOS] Deletando relatório ID:', id);
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
method: 'DELETE',
headers: obterCabecalhos(),
});
await tratarRespostaApi<void>(resposta);
console.log('✅ [API RELATÓRIOS] Relatório deletado com sucesso');
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao deletar relatório:', erro);
throw erro;
}
}
/**
* Lista relatórios de um paciente específico
*/
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
try {
console.log('👤 [API RELATÓRIOS] Buscando relatórios do paciente:', idPaciente);
const resposta = await fetch(`${BASE_API_RELATORIOS}?patient_id=${idPaciente}`, {
method: 'GET',
headers: obterCabecalhos(),
});
const resultado = await tratarRespostaApi<ReportsResponse>(resposta);
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados:', resultado.data?.length || 0);
return resultado.data || [];
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do paciente:', erro);
throw erro;
}
}
/**
* Lista relatórios de um médico específico
*/
export async function listarRelatoriosPorMedico(idMedico: string): Promise<Report[]> {
try {
console.log('👨‍⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', idMedico);
const resposta = await fetch(`${BASE_API_RELATORIOS}?doctor_id=${idMedico}`, {
method: 'GET',
headers: obterCabecalhos(),
});
const resultado = await tratarRespostaApi<ReportsResponse>(resposta);
console.log('✅ [API RELATÓRIOS] Relatórios do médico encontrados:', resultado.data?.length || 0);
return resultado.data || [];
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
throw erro;
}
}

View File

@ -1,6 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited // 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.

View File

@ -0,0 +1,107 @@
// Tipo de erro padrão para respostas de API
export interface ApiError {
message: string;
code?: string;
}
// Este arquivo foi renomeado de report.ts para report-types.ts para evitar confusão com outros arquivos de lógica.
// Tipos para o endpoint de Relatórios Médicos
export interface Report {
id: string;
patient_id: string;
doctor_id: string;
report_type: string;
chief_complaint: string;
clinical_history: string;
symptoms_and_signs: string;
physical_examination: string;
complementary_exams: string;
exam_results: string;
diagnosis: string;
prognosis?: string;
treatment_performed: string;
objective_recommendations: string;
icd_code?: string;
report_date: string;
created_at: string;
updated_at: string;
// Dados expandidos (quando incluir dados relacionados)
patient?: {
id: string;
full_name: string;
cpf?: string;
birth_date?: string;
};
doctor?: {
id: string;
full_name: string;
crm?: string;
specialty?: string;
};
}
// Dados para criar um novo relatório
export interface CreateReportData {
patient_id: string;
doctor_id: string;
report_type: string;
chief_complaint: string;
clinical_history: string;
symptoms_and_signs: string;
physical_examination: string;
complementary_exams: string;
exam_results: string;
diagnosis: string;
prognosis?: string;
treatment_performed: string;
objective_recommendations: string;
icd_code?: string;
report_date: string;
}
// Dados para atualizar um relatório existente
export interface UpdateReportData extends Partial<CreateReportData> {
updated_at?: string;
}
// Resposta da API ao listar relatórios
export interface ReportsResponse {
data: Report[];
success: boolean;
message?: string;
}
// Resposta da API ao criar/atualizar um relatório
export interface ReportResponse {
data: Report;
success: boolean;
message?: string;
}
// Dados do formulário (adaptado para a estrutura do front-end existente)
export interface ReportFormData {
// Identificação do Profissional
profissionalNome: string;
profissionalCrm: string;
// Identificação do Paciente
pacienteId: string;
pacienteNome: string;
pacienteCpf: string;
pacienteIdade: string;
// Informações do Relatório
motivoRelatorio: string;
cid?: string;
dataRelatorio: string;
// Histórico Clínico
historicoClinico: string;
// Sinais, Sintomas e Exames
sinaisSintomas: string;
examesRealizados: string;
resultadosExames: string;
// ...restante do código...