feature/pacientes-consulta #41
@ -1,86 +1,263 @@
|
||||
|
||||
"use client";
|
||||
|
||||
|
||||
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 { 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() {
|
||||
// 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 (
|
||||
<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">
|
||||
{/* Card Consultas */}
|
||||
<div className="p-4 border border-border rounded-lg shadow bg-card">
|
||||
<h2 className="font-semibold text-lg text-foreground">Relatório de Consultas</h2>
|
||||
<p className="text-sm text-muted-foreground">Resumo das consultas realizadas.</p>
|
||||
{/* PASSO 4 - Botão chama a função */}
|
||||
<Button onClick={exportConsultasPDF} className="mt-4">
|
||||
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
|
||||
</Button>
|
||||
{/* Métricas principais */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
|
||||
{metricas.map((m) => (
|
||||
<div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
|
||||
{m.icon}
|
||||
<span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
|
||||
<span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Gráficos e Relatórios */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Consultas realizadas por período */}
|
||||
<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"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
|
||||
<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>
|
||||
|
||||
{/* Card Pacientes */}
|
||||
<div className="p-4 border border-border rounded-lg shadow bg-card">
|
||||
<h2 className="font-semibold text-lg text-foreground">Relatório de Pacientes</h2>
|
||||
<p className="text-sm text-muted-foreground">Informações gerais dos pacientes cadastrados.</p>
|
||||
<Button onClick={exportPacientesPDF} className="mt-4">
|
||||
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Card Financeiro com gráfico */}
|
||||
<div className="p-4 border border-border rounded-lg shadow col-span-3 md:col-span-3 bg-card">
|
||||
<h2 className="font-semibold text-lg mb-2 text-foreground">Relatório Financeiro</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={financeiro} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
{/* Faturamento mensal/anual */}
|
||||
<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" /> Faturamento Mensal</h2>
|
||||
<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" />
|
||||
<XAxis dataKey="mes" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="faturamento" fill="#10b981" name="Faturamento" />
|
||||
<Bar dataKey="despesas" fill="#ef4444" name="Despesas" />
|
||||
</BarChart>
|
||||
<Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<Button onClick={exportFinanceiroPDF} className="mt-4">
|
||||
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
@ -1,94 +1,750 @@
|
||||
'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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
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 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() {
|
||||
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 () => {
|
||||
console.log('[PACIENTE] Iniciando logout...')
|
||||
await logout()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
await logout()
|
||||
} catch {
|
||||
setError(strings.erro)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredUserType={["paciente"]}>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md shadow-lg">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||
<User className="h-8 w-8 text-primary" />
|
||||
</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">
|
||||
{/* Informações do Paciente */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
||||
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>
|
||||
// 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.",
|
||||
})
|
||||
|
||||
{/* 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>
|
||||
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 (
|
||||
<section className="bg-card shadow-md rounded-lg border border-border p-6">
|
||||
<div className="max-w-3xl mx-auto space-y-8">
|
||||
<header className="text-center space-y-2">
|
||||
<h2 className="text-3xl font-semibold text-foreground">Agende sua próxima consulta</h2>
|
||||
<p className="text-muted-foreground">Escolha o formato ideal, selecione a especialidade e encontre o profissional perfeito para você.</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6 rounded-lg border border-border bg-muted/40 p-6">
|
||||
<div className="space-y-3">
|
||||
<Label>Tipo de consulta</Label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
className={tipoConsulta === 'teleconsulta' ? activeToggleClass : inactiveToggleClass}
|
||||
aria-pressed={tipoConsulta === 'teleconsulta'}
|
||||
onClick={() => setTipoConsulta('teleconsulta')}
|
||||
>
|
||||
Teleconsulta
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className={tipoConsulta === 'presencial' ? activeToggleClass : inactiveToggleClass}
|
||||
aria-pressed={tipoConsulta === 'presencial'}
|
||||
onClick={() => setTipoConsulta('presencial')}
|
||||
>
|
||||
Consulta no local
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão Voltar ao Início */}
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||
<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="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Voltar ao Início
|
||||
<Home className="h-4 w-4 mr-1" /> Início
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Informação adicional */}
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-gray-500">
|
||||
Em breve, mais funcionalidades estarão disponíveis
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 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>
|
||||
</ProtectedRoute>
|
||||
)
|
||||
|
||||
@ -5,11 +5,14 @@ import SignatureCanvas from "react-signature-canvas";
|
||||
import Link from "next/link";
|
||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -107,11 +110,20 @@ const ProfissionalPage = () => {
|
||||
prognostico: "",
|
||||
tratamentosRealizados: "",
|
||||
recomendacoes: "",
|
||||
cid: "",
|
||||
dataRelatorio: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
const [relatoriosMedicos, setRelatoriosMedicos] = useState<any[]>([]);
|
||||
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
|
||||
const [consultasRegistradas, setConsultasRegistradas] = useState<any[]>([]);
|
||||
const [historicoMedico, setHistoricoMedico] = useState<any[]>([]);
|
||||
@ -305,6 +317,7 @@ const ProfissionalPage = () => {
|
||||
prognostico: "",
|
||||
tratamentosRealizados: "",
|
||||
recomendacoes: "",
|
||||
cid: "",
|
||||
dataRelatorio: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
};
|
||||
@ -338,10 +351,271 @@ const ProfissionalPage = () => {
|
||||
prognostico: "",
|
||||
tratamentosRealizados: "",
|
||||
recomendacoes: "",
|
||||
cid: "",
|
||||
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) => {
|
||||
setSelectedDate(arg.dateStr);
|
||||
@ -2032,14 +2306,40 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
}
|
||||
}, [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;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
|
||||
let formattedText = "";
|
||||
switch(type) {
|
||||
case "bold":
|
||||
@ -2049,13 +2349,44 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*";
|
||||
break;
|
||||
case "underline":
|
||||
formattedText = selectedText ? `<u>${selectedText}</u>` : "<u>texto sublinhado</u>";
|
||||
formattedText = selectedText ? `__${selectedText}__` : "__texto sublinhado__";
|
||||
break;
|
||||
case "list":
|
||||
formattedText = selectedText ? `• ${selectedText}` : "• item da lista";
|
||||
case "list-ul":
|
||||
formattedText = selectedText ? selectedText.split('\n').map(l => `• ${l}`).join('\n') : "• item da lista";
|
||||
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);
|
||||
setContent(newText);
|
||||
};
|
||||
@ -2084,7 +2415,14 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
return content
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.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(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')
|
||||
.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">
|
||||
{/* Toolbar */}
|
||||
<div className="p-3 border-b border-border">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => formatText("bold")}
|
||||
title="Negrito"
|
||||
className="hover:bg-blue-50 dark:hover:bg-accent"
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{/* Tamanho da fonte */}
|
||||
<label className="text-xs mr-1">Tamanho</label>
|
||||
<input
|
||||
type="number"
|
||||
min={8}
|
||||
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>
|
||||
</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>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => formatText("list")}
|
||||
title="Lista"
|
||||
className="hover:bg-blue-50 dark:hover:bg-accent"
|
||||
>
|
||||
•
|
||||
</Button>
|
||||
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
</select>
|
||||
{/* Cor da fonte */}
|
||||
<label className="text-xs mr-1">Cor</label>
|
||||
<input
|
||||
type="color"
|
||||
defaultValue="#222222"
|
||||
onBlur={e => formatText('font-color', e.target.value)}
|
||||
className="w-6 h-6 border rounded mr-2"
|
||||
title="Cor da fonte"
|
||||
/>
|
||||
{/* 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>
|
||||
<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 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 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>
|
||||
{/* Listas */}
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1">1.</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1">•</Button>
|
||||
{/* Recuo */}
|
||||
<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 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>
|
||||
|
||||
{/* Templates */}
|
||||
@ -2443,12 +2797,13 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="flex-1 p-4 overflow-auto max-h-[500px]">
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
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>
|
||||
@ -2801,32 +3156,54 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Identificação do Paciente */}
|
||||
{/* Identificação do Paciente - USANDO API REAL */}
|
||||
<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="space-y-2">
|
||||
<Label htmlFor="pacienteNome">Nome do Paciente *</Label>
|
||||
<Select
|
||||
value={relatorioMedico.pacienteNome}
|
||||
value={pacienteSelecionadoReport?.id || ''}
|
||||
onValueChange={(value) => {
|
||||
const pacienteSelecionado = pacientes.find(p => p.nome === value);
|
||||
handleRelatorioChange('pacienteNome', value);
|
||||
if (pacienteSelecionado) {
|
||||
handleRelatorioChange('pacienteCpf', pacienteSelecionado.cpf);
|
||||
handleRelatorioChange('pacienteIdade', pacienteSelecionado.idade.toString());
|
||||
const paciente = pacientesReais.find(p => p.id === value);
|
||||
if (paciente) {
|
||||
selecionarPacienteParaRelatorio(paciente);
|
||||
}
|
||||
}}
|
||||
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>
|
||||
<SelectValue placeholder="Selecione o paciente" />
|
||||
<SelectValue placeholder={carregandoPacientes ? "Carregando..." : "Selecione o paciente"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pacientes.map((paciente) => (
|
||||
<SelectItem key={paciente.cpf} value={paciente.nome}>
|
||||
{paciente.nome}
|
||||
</SelectItem>
|
||||
))}
|
||||
{carregandoPacientes ? (
|
||||
<SelectItem value="loading" disabled>Carregando pacientes...</SelectItem>
|
||||
) : 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>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -2835,27 +3212,50 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
<Input
|
||||
id="pacienteCpf"
|
||||
value={relatorioMedico.pacienteCpf}
|
||||
onChange={(e) => handleRelatorioChange('pacienteCpf', e.target.value)}
|
||||
placeholder="000.000.000-00"
|
||||
disabled
|
||||
className="bg-muted"
|
||||
placeholder="CPF será preenchido automaticamente"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pacienteIdade">Idade</Label>
|
||||
<Input
|
||||
id="pacienteIdade"
|
||||
type="number"
|
||||
type="text"
|
||||
value={relatorioMedico.pacienteIdade}
|
||||
onChange={(e) => handleRelatorioChange('pacienteIdade', e.target.value)}
|
||||
placeholder="Idade do paciente"
|
||||
disabled
|
||||
className="bg-muted"
|
||||
placeholder="Idade será calculada automaticamente"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Informações do Relatório */}
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<Label htmlFor="motivoRelatorio">Motivo do Relatório *</Label>
|
||||
<Textarea
|
||||
@ -2866,6 +3266,15 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
rows={3}
|
||||
/>
|
||||
</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">
|
||||
<Label htmlFor="dataRelatorio">Data do Relatório</Label>
|
||||
<Input
|
||||
@ -2876,7 +3285,6 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="historicoClinico">Histórico Clínico Conciso</Label>
|
||||
<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">
|
||||
Cancelar
|
||||
</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" />
|
||||
{editandoRelatorio ? 'Atualizar Relatório' : 'Salvar Relatório'}
|
||||
{reportsApi.loading ? 'Salvando...' : (editandoRelatorio ? 'Atualizar Relatório' : 'Salvar Relatório')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Relatórios Existentes */}
|
||||
{/* Lista de Relatórios da API */}
|
||||
<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">
|
||||
<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>
|
||||
@ -3006,30 +3449,50 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{relatoriosMedicos.map((relatorio) => (
|
||||
<div key={relatorio.id} className="border rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg">{relatorio.pacienteNome}</h4>
|
||||
<p className="text-sm text-muted-foreground">CPF: {relatorio.pacienteCpf} • Idade: {relatorio.pacienteIdade} anos</p>
|
||||
<p className="text-sm text-muted-foreground">Data do relatório: {new Date(relatorio.dataRelatorio).toLocaleDateString('pt-BR')}</p>
|
||||
<p className="text-xs text-muted-foreground/70">Gerado em: {relatorio.dataGeracao}</p>
|
||||
{reportsApi.reports.filter(relatorio => relatorio != null).map((relatorio, idx) => {
|
||||
// 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>
|
||||
<h4 className="font-semibold text-lg">
|
||||
{nomeExibir}
|
||||
</h4>
|
||||
<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 className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditarRelatorio(relatorio)}
|
||||
onClick={() => relatorio?.id && reportsApi.loadReportById(relatorio.id)}
|
||||
className="flex items-center gap-1"
|
||||
disabled={!relatorio?.id}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
Editar
|
||||
<Eye className="h-3 w-3" />
|
||||
Visualizar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleExcluirRelatorio(relatorio.id)}
|
||||
onClick={() => relatorio?.id && reportsApi.deleteExistingReport(relatorio.id)}
|
||||
className="flex items-center gap-1"
|
||||
disabled={reportsApi.loading || !relatorio?.id}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
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>
|
||||
<span className="font-medium text-primary">Motivo:</span>
|
||||
<p className="text-foreground mt-1">{relatorio.motivoRelatorio}</p>
|
||||
<span className="font-medium text-primary">Queixa Principal:</span>
|
||||
<p className="text-foreground mt-1 line-clamp-3">{relatorio.chief_complaint}</p>
|
||||
</div>
|
||||
|
||||
{relatorio.diagnosticos && (
|
||||
{relatorio.diagnosis && (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{relatorio.recomendacoes && (
|
||||
{relatorio.objective_recommendations && (
|
||||
<div className="md:col-span-2">
|
||||
<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>
|
||||
@ -3270,13 +3741,16 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={logout}
|
||||
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
|
||||
>
|
||||
Sair
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleThemeToggle />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={logout}
|
||||
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
|
||||
>
|
||||
Sair
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">
|
||||
|
||||
977
susconecta/app/resultados/page.tsx
Normal file
977
susconecta/app/resultados/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { SidebarTrigger } from "../ui/sidebar"
|
||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
||||
|
||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||
const { logout, user } = useAuth();
|
||||
@ -44,7 +45,12 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
<Bell className="h-4 w-4" />
|
||||
</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 */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Button
|
||||
|
||||
219
susconecta/hooks/useReports.ts
Normal file
219
susconecta/hooks/useReports.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -320,7 +320,12 @@ export async function buscarPacientes(termo: string): 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 arr = await parse<Paciente[]>(res);
|
||||
if (!arr?.length) throw new Error("404: Paciente não encontrado");
|
||||
|
||||
289
susconecta/lib/reports.ts
Normal file
289
susconecta/lib/reports.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
3
susconecta/next-env.d.ts
vendored
3
susconecta/next-env.d.ts
vendored
@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
107
susconecta/types/report-types.ts
Normal file
107
susconecta/types/report-types.ts
Normal 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...
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user