riseup-squad18/src/pages/AcompanhamentoPaciente.tsx

1416 lines
55 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import {
Calendar,
Clock,
User,
MessageCircle,
HelpCircle,
LogOut,
Home,
Stethoscope,
Video,
MapPin,
CheckCircle,
XCircle,
AlertCircle,
FileText,
Eye,
X,
} from "lucide-react";
import toast from "react-hot-toast";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { appointmentService, doctorService, reportService } from "../services";
import type { Report } from "../services/reports/types";
import AgendamentoConsulta from "../components/AgendamentoConsulta";
import { AvatarUpload } from "../components/ui/AvatarUpload";
import { avatarService } from "../services/avatars/avatarService";
interface Consulta {
_id: string;
pacienteId: string;
medicoId: string;
dataHora: string;
status: "agendada" | "confirmada" | "realizada" | "cancelada" | "faltou";
tipoConsulta: string;
motivoConsulta: string;
observacoes?: string;
resultados?: string;
prescricoes?: string;
proximaConsulta?: string;
medicoNome?: string;
especialidade?: string;
valorConsulta?: number;
}
interface Medico {
id: string;
nome: string;
especialidade: string;
crm: string;
foto?: string;
email?: string;
telefone?: string;
valorConsulta?: number;
}
const AcompanhamentoPaciente: React.FC = () => {
const { user, roles = [], logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
// Helper para formatar nome do médico com Dr.
const formatDoctorName = (fullName: string): string => {
const name = fullName.trim();
// Verifica se já começa com Dr. ou Dr (case insensitive)
if (/^dr\.?\s/i.test(name)) {
return name;
}
return `Dr. ${name}`;
};
// State
const [activeTab, setActiveTab] = useState("dashboard");
const [consultas, setConsultas] = useState<Consulta[]>([]);
const [medicos, setMedicos] = useState<Medico[]>([]);
const [loadingMedicos, setLoadingMedicos] = useState(true);
const [selectedMedicoId, setSelectedMedicoId] = useState<string>("");
const [loading, setLoading] = useState(true);
const [especialidadeFiltro, setEspecialidadeFiltro] = useState<string>("");
const [laudos, setLaudos] = useState<Report[]>([]);
const [loadingLaudos, setLoadingLaudos] = useState(false);
const [paginaProximas, setPaginaProximas] = useState(1);
const [paginaPassadas, setPaginaPassadas] = useState(1);
const consultasPorPagina = 20; // Aumentado de 10 para 20
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null);
const [showLaudoModal, setShowLaudoModal] = useState(false);
const [requestedByNames, setRequestedByNames] = useState<
Record<string, string>
>({});
const pacienteId = user?.id || "";
const pacienteNome = user?.nome || "Paciente";
useEffect(() => {
// Permite acesso se for paciente OU se roles inclui 'paciente'
const isPaciente = user?.role === "paciente" || roles.includes("paciente");
if (!user || !isPaciente) navigate("/paciente");
}, [user, roles, navigate]);
// Detecta se veio de navegação com estado para abrir aba específica
useEffect(() => {
if (
location.state &&
(location.state as { activeTab?: string }).activeTab
) {
const state = location.state as { activeTab: string };
setActiveTab(state.activeTab);
// Limpa o estado após usar
window.history.replaceState({}, document.title);
}
}, [location]);
// Carregar avatar ao montar componente
useEffect(() => {
if (user?.id) {
// Tenta carregar avatar existente (testa png, jpg, webp)
const extensions = ["png", "jpg", "webp"];
const testAvatar = async () => {
for (const ext of extensions) {
try {
const url = avatarService.getPublicUrl({
userId: user.id,
ext: ext as "jpg" | "png" | "webp",
});
const response = await fetch(url, { method: "HEAD" });
if (response.ok) {
setAvatarUrl(url);
console.log(`[AcompanhamentoPaciente] Avatar encontrado: ${url}`);
break;
}
} catch (error) {
// Continua testando próxima extensão
}
}
};
testAvatar();
}
}, [user?.id]);
const fetchConsultas = useCallback(async () => {
if (!pacienteId) return;
setLoading(true);
setLoadingMedicos(true);
try {
// Buscar TODOS os agendamentos da API (sem limite)
const appointments = await appointmentService.list({
patient_id: pacienteId,
limit: 1000, // Aumenta limite para buscar todas
order: "scheduled_at.desc",
});
// Buscar médicos
const medicosData = await doctorService.list();
const medicosFormatted: Medico[] = medicosData.map((d) => ({
id: d.id,
nome: formatDoctorName(d.full_name),
especialidade: d.specialty || "",
crm: d.crm,
email: d.email,
telefone: d.phone_mobile || undefined,
}));
setMedicos(medicosFormatted);
setLoadingMedicos(false);
// Map appointments to old Consulta format
const consultasAPI: Consulta[] = appointments.map((apt) => ({
_id: apt.id,
pacienteId: apt.patient_id,
medicoId: apt.doctor_id,
dataHora: apt.scheduled_at || "",
status:
apt.status === "confirmed"
? "confirmada"
: apt.status === "completed"
? "realizada"
: apt.status === "cancelled"
? "cancelada"
: apt.status === "no_show"
? "faltou"
: "agendada",
tipoConsulta: "presencial",
motivoConsulta: apt.notes || "Consulta médica",
observacoes: apt.notes || undefined,
}));
// Set consultas
setConsultas(consultasAPI);
} catch (error) {
setLoadingMedicos(false);
console.error("Erro ao carregar consultas:", error);
toast.error("Erro ao carregar consultas");
setConsultas([]);
} finally {
setLoading(false);
}
}, [pacienteId]);
useEffect(() => {
fetchConsultas();
}, [fetchConsultas]);
// Função para carregar nomes dos médicos solicitantes
const loadRequestedByNames = useCallback(async (reports: Report[]) => {
const uniqueIds = [
...new Set(
reports.map((r) => r.requested_by).filter((id): id is string => !!id)
),
];
if (uniqueIds.length === 0) return;
try {
const doctors = await doctorService.list();
const nameMap: Record<string, string> = {};
uniqueIds.forEach((id) => {
const doctor = doctors.find((d) => d.id === id);
if (doctor && doctor.full_name) {
nameMap[id] = formatDoctorName(doctor.full_name);
}
});
setRequestedByNames(nameMap);
} catch (error) {
console.error("Erro ao buscar nomes dos médicos:", error);
}
}, []);
// Recarregar consultas quando mudar para a aba de consultas
const fetchLaudos = useCallback(async () => {
if (!pacienteId) return;
setLoadingLaudos(true);
try {
const data = await reportService.list({ patient_id: pacienteId });
setLaudos(data);
// Carregar nomes dos médicos
await loadRequestedByNames(data);
} catch (error) {
console.error("Erro ao buscar laudos:", error);
toast.error("Erro ao carregar laudos");
setLaudos([]);
} finally {
setLoadingLaudos(false);
}
}, [pacienteId, loadRequestedByNames]);
useEffect(() => {
if (activeTab === "appointments") {
fetchConsultas();
}
}, [activeTab, fetchConsultas]);
useEffect(() => {
if (activeTab === "reports") {
fetchLaudos();
}
}, [activeTab, fetchLaudos]);
const getMedicoNome = (medicoId: string) => {
const medico = medicos.find((m) => m._id === medicoId || m.id === medicoId);
return medico?.nome || "Médico";
};
const getMedicoEspecialidade = (medicoId: string) => {
const medico = medicos.find((m) => m._id === medicoId || m.id === medicoId);
return medico?.especialidade || "Especialidade";
};
const handleRemarcar = () => {
setActiveTab("book");
toast.success("Selecione um novo horário para remarcar sua consulta");
};
const handleCancelar = async (consultaId: string) => {
if (!window.confirm("Tem certeza que deseja cancelar esta consulta?")) {
return;
}
try {
await appointmentService.update(consultaId, {
status: "cancelled",
});
toast.success("Consulta cancelada com sucesso");
fetchConsultas();
} catch (error) {
console.error("Erro ao cancelar consulta:", error);
toast.error("Erro ao cancelar consulta. Tente novamente.");
}
};
const todasConsultasProximas = consultas
.filter((c) => c.status === "agendada" || c.status === "confirmada")
.sort(
(a, b) => new Date(a.dataHora).getTime() - new Date(b.dataHora).getTime()
);
const todasConsultasPassadas = consultas
.filter((c) => c.status === "realizada")
.sort(
(a, b) => new Date(b.dataHora).getTime() - new Date(a.dataHora).getTime()
);
// Para o dashboard (apenas 3 consultas)
const consultasProximasDashboard = todasConsultasProximas.slice(0, 3);
const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3);
// Para a página de consultas (com paginação)
const totalPaginasProximas = Math.ceil(
todasConsultasProximas.length / consultasPorPagina
);
const totalPaginasPassadas = Math.ceil(
todasConsultasPassadas.length / consultasPorPagina
);
const consultasProximas = todasConsultasProximas.slice(
(paginaProximas - 1) * consultasPorPagina,
paginaProximas * consultasPorPagina
);
const consultasPassadas = todasConsultasPassadas.slice(
(paginaPassadas - 1) * consultasPorPagina,
paginaPassadas * consultasPorPagina
);
const getStatusColor = (status: string) => {
switch (status) {
case "confirmada":
return "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800";
case "agendada":
return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800";
case "realizada":
return "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-300 dark:border-gray-800";
case "cancelada":
case "faltou":
return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800";
default:
return "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-300 dark:border-gray-800";
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case "confirmada":
return "Confirmada";
case "agendada":
return "Agendada";
case "realizada":
return "Concluída";
case "cancelada":
return "Cancelada";
case "faltou":
return "Não Compareceu";
default:
return status;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "confirmada":
return <CheckCircle className="h-4 w-4" />;
case "agendada":
return <Clock className="h-4 w-4" />;
case "cancelada":
case "faltou":
return <XCircle className="h-4 w-4" />;
default:
return <AlertCircle className="h-4 w-4" />;
}
};
// Menu items
const menuItems = [
{ id: "dashboard", label: "Início", icon: Home },
{ id: "appointments", label: "Minhas Consultas", icon: Calendar },
{ id: "reports", label: "Meus Laudos", icon: FileText },
{ id: "book", label: "Agendar Consulta", icon: Stethoscope },
{ id: "messages", label: "Mensagens", icon: MessageCircle },
{
id: "profile",
label: "Meu Perfil",
icon: User,
isLink: true,
path: "/perfil-paciente",
},
{ id: "help", label: "Ajuda", icon: HelpCircle },
];
// Sidebar
const renderSidebar = () => (
<div className="hidden lg:flex w-64 h-screen bg-white dark:bg-slate-900 border-r border-gray-200 dark:border-slate-700 flex-col">
{/* Patient Profile */}
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center gap-2 sm:gap-3">
<AvatarUpload
userId={user?.id}
currentAvatarUrl={avatarUrl}
name={pacienteNome}
color="blue"
size="lg"
editable={true}
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div className="min-w-0 flex-1">
<p className="font-medium text-gray-900 dark:text-white truncate text-sm sm:text-base">
{pacienteNome}
</p>
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-400">
Paciente
</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-3 sm:p-4">
<div className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => {
if (item.isLink && item.path) {
navigate(item.path);
} else if (item.id === "help") {
navigate("/ajuda");
} else {
setActiveTab(item.id);
}
}}
className={`w-full flex items-center gap-2 sm:gap-3 px-2 sm:px-3 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
isActive
? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400"
: "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800"
}`}
>
<Icon className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
</nav>
{/* Logout */}
<div className="p-3 sm:p-4 border-t border-gray-200 dark:border-slate-700">
<button
onClick={() => {
logout();
navigate("/paciente");
}}
className="w-full flex items-center gap-2 sm:gap-3 px-2 sm:px-3 py-2 rounded-lg text-xs sm:text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
>
<LogOut className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
Sair
</button>
</div>
</div>
);
// Stat Card
const renderStatCard = (
title: string,
value: string | number,
icon: React.ElementType,
description?: string
) => {
const Icon = icon;
return (
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
{title}
</p>
<Icon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white mb-1">
{value}
</div>
{description && (
<p className="text-sm text-gray-600 dark:text-gray-400">
{description}
</p>
)}
</div>
);
};
// Appointment Card
const renderAppointmentCard = (
consulta: Consulta,
isPast: boolean = false
) => {
// Usar dados da consulta local se disponível, senão buscar pelo ID do médico
const medicoNome = consulta.medicoNome || getMedicoNome(consulta.medicoId);
const especialidade =
consulta.especialidade || getMedicoEspecialidade(consulta.medicoId);
return (
<div
key={consulta._id}
className="flex items-start gap-4 p-4 rounded-lg border border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors"
>
<div className="h-14 w-14 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-semibold">
{medicoNome
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
<div className="flex-1 space-y-3">
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-gray-900 dark:text-white">
{medicoNome}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{especialidade}
</p>
</div>
<div
className={`flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium border ${getStatusColor(
consulta.status
)}`}
>
{getStatusIcon(consulta.status)}
{getStatusLabel(consulta.status)}
</div>
</div>
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>
{format(new Date(consulta.dataHora), "dd/MM/yyyy", {
locale: ptBR,
})}
</span>
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>
{format(new Date(consulta.dataHora), "HH:mm", { locale: ptBR })}
</span>
</div>
<div className="flex items-center gap-1">
{consulta.tipoConsulta === "online" ||
consulta.tipoConsulta === "telemedicina" ? (
<>
<Video className="h-4 w-4" />
<span>Online</span>
</>
) : (
<>
<MapPin className="h-4 w-4" />
<span>Presencial</span>
</>
)}
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Motivo: {consulta.motivoConsulta}
</p>
{!isPast && consulta.status !== "cancelada" && (
<div className="flex gap-2">
{consulta.status === "confirmada" &&
(consulta.tipoConsulta === "online" ||
consulta.tipoConsulta === "telemedicina") && (
<button className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500">
<Video className="h-4 w-4" />
Entrar na Consulta
</button>
)}
<button
onClick={handleRemarcar}
className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-slate-800 border border-gray-300 dark:border-slate-600 rounded-md hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
>
Remarcar
</button>
<button
onClick={() => handleCancelar(consulta._id)}
className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
>
<XCircle className="h-4 w-4" />
Cancelar
</button>
</div>
)}
</div>
</div>
);
};
// Dashboard Content
const renderDashboard = () => {
const proximaConsulta = consultasProximas[0];
return (
<div className="space-y-4 sm:space-y-6">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">
Bem-vindo, {pacienteNome.split(" ")[0]}!
</h1>
<p className="text-sm sm:text-base text-gray-600 dark:text-gray-400">
Gerencie suas consultas e cuide da sua saúde
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4">
{renderStatCard(
"Próxima Consulta",
proximaConsulta
? format(new Date(proximaConsulta.dataHora), "dd MMM", {
locale: ptBR,
})
: "Nenhuma",
Calendar,
proximaConsulta
? `${getMedicoEspecialidade(proximaConsulta.medicoId)} - ${format(
new Date(proximaConsulta.dataHora),
"HH:mm"
)}`
: "Agende uma consulta"
)}
{renderStatCard(
"Consultas Agendadas",
consultasProximas.length,
Clock,
"Este mês"
)}
{renderStatCard(
"Médicos Favoritos",
new Set(consultas.map((c) => c.medicoId)).size,
Stethoscope,
"Salvos"
)}
</div>
{/* Próximas Consultas e Ações Rápidas */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Próximas Consultas
</h2>
<button
onClick={() => setActiveTab("appointments")}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded px-2 py-1"
>
Ver todas
</button>
</div>
</div>
<div className="p-6">
{loading ? (
<div className="text-center py-8">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
</div>
) : consultasProximasDashboard.length === 0 ? (
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Nenhuma consulta agendada
</p>
) : (
<>
<div className="space-y-4">
{consultasProximasDashboard.map((c) => (
<div
key={c._id}
className="flex items-start gap-3 p-3 rounded-lg border border-gray-200 dark:border-slate-700"
>
<div className="h-12 w-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<Calendar className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="font-medium text-gray-900 dark:text-white">
{getMedicoNome(c.medicoId)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{getMedicoEspecialidade(c.medicoId)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{format(
new Date(c.dataHora),
"dd/MM/yyyy - HH:mm",
{
locale: ptBR,
}
)}
</p>
</div>
</div>
))}
</div>
{todasConsultasProximas.length > 3 && (
<button
onClick={() => setActiveTab("appointments")}
className="w-full mt-4 px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
>
Ver mais consultas ({todasConsultasProximas.length - 3}{" "}
restantes)
</button>
)}
</>
)}
</div>
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Ações Rápidas
</h2>
</div>
<div className="p-6 space-y-2">
<button
onClick={() => setActiveTab("book")}
className="form-input"
>
<Calendar className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Agendar Nova Consulta</span>
</button>
<button
onClick={() => setActiveTab("messages")}
className="form-input"
>
<MessageCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Mensagens</span>
</button>
<button
onClick={() => setActiveTab("profile")}
className="form-input"
>
<User className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Editar Perfil</span>
</button>
<button onClick={() => navigate("/ajuda")} className="form-input">
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Central de Ajuda</span>
</button>
</div>
</div>
</div>
{/* Dicas de Saúde */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Dicas de Saúde
</h2>
</div>
<div className="p-6">
<div className="space-y-3">
<div className="p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">
💧 Hidratação
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Beba pelo menos 2 litros de água por dia para manter seu corpo
hidratado
</p>
</div>
<div className="p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">
🏃 Exercícios
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
30 minutos de atividade física diária ajudam a prevenir
doenças
</p>
</div>
</div>
</div>
</div>
</div>
);
};
// Appointments Content
const renderAppointments = () => (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Minhas Consultas
</h1>
<p className="text-gray-600 dark:text-gray-400">
Visualize e gerencie todas as suas consultas
</p>
</div>
{/* Próximas */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Próximas Consultas
</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-center py-8">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
</div>
) : consultasProximas.length === 0 ? (
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Nenhuma consulta agendada
</p>
) : (
<>
<div className="space-y-4">
{consultasProximas.map((c) => renderAppointmentCard(c))}
</div>
{/* Paginação Próximas Consultas */}
{totalPaginasProximas > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<div className="text-sm text-gray-600 dark:text-gray-400">
Mostrando {(paginaProximas - 1) * consultasPorPagina + 1} a{" "}
{Math.min(
paginaProximas * consultasPorPagina,
todasConsultasProximas.length
)}{" "}
de {todasConsultasProximas.length} consultas
</div>
<div className="flex items-center gap-2">
<button
onClick={() =>
setPaginaProximas(Math.max(1, paginaProximas - 1))
}
disabled={paginaProximas === 1}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
>
Anterior
</button>
<span className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-slate-800 rounded-lg">
Página {paginaProximas} de {totalPaginasProximas}
</span>
<button
onClick={() =>
setPaginaProximas(
Math.min(totalPaginasProximas, paginaProximas + 1)
)
}
disabled={paginaProximas === totalPaginasProximas}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
>
Próxima
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
{/* Passadas */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Histórico
</h2>
</div>
<div className="p-6">
{consultasPassadas.length === 0 ? (
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Nenhuma consulta realizada
</p>
) : (
<>
<div className="space-y-4">
{consultasPassadas.map((c) => renderAppointmentCard(c, true))}
</div>
{/* Paginação Consultas Passadas */}
{totalPaginasPassadas > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<div className="text-sm text-gray-600 dark:text-gray-400">
Mostrando {(paginaPassadas - 1) * consultasPorPagina + 1} a{" "}
{Math.min(
paginaPassadas * consultasPorPagina,
todasConsultasPassadas.length
)}{" "}
de {todasConsultasPassadas.length} consultas
</div>
<div className="flex items-center gap-2">
<button
onClick={() =>
setPaginaPassadas(Math.max(1, paginaPassadas - 1))
}
disabled={paginaPassadas === 1}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
>
Anterior
</button>
<span className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-slate-800 rounded-lg">
Página {paginaPassadas} de {totalPaginasPassadas}
</span>
<button
onClick={() =>
setPaginaPassadas(
Math.min(totalPaginasPassadas, paginaPassadas + 1)
)
}
disabled={paginaPassadas === totalPaginasPassadas}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
>
Próxima
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
</div>
);
// Book Appointment Content
const renderBookAppointment = () => (
<div className="space-y-6">
<AgendamentoConsulta medicos={medicos} />
</div>
);
// Messages Content
const renderMessages = () => (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Mensagens
</h1>
<p className="text-gray-600 dark:text-gray-400">
Converse com seus médicos
</p>
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<p className="text-center py-16 text-gray-600 dark:text-gray-400">
Sistema de mensagens em desenvolvimento
</p>
</div>
</div>
);
// Help Content
const renderHelp = () => (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Central de Ajuda
</h1>
<p className="text-gray-600 dark:text-gray-400">
Como podemos ajudar você?
</p>
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<p className="text-center py-16 text-gray-600 dark:text-gray-400">
Central de ajuda em desenvolvimento
</p>
</div>
</div>
);
// Profile Content
const renderProfile = () => (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Meu Perfil
</h1>
<p className="text-gray-600 dark:text-gray-400">
Gerencie suas informações pessoais
</p>
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<p className="text-center py-16 text-gray-600 dark:text-gray-400">
Edição de perfil em desenvolvimento
</p>
</div>
</div>
);
const renderReports = () => (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Meus Laudos Médicos
</h1>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
{loadingLaudos ? (
<div className="p-6">
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Carregando laudos...
</p>
</div>
) : laudos.length === 0 ? (
<div className="p-6">
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Você ainda não possui laudos médicos.
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-slate-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Número
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Exame
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Médico Solicitante
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Data
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-slate-700">
{laudos.map((laudo) => (
<tr
key={laudo.id}
className="hover:bg-gray-50 dark:hover:bg-slate-800"
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{laudo.order_number}
</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
{laudo.exam || "-"}
</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
{laudo.requested_by
? (requestedByNames[laudo.requested_by] || laudo.requested_by)
: "-"}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${
laudo.status === "completed"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: laudo.status === "pending"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: laudo.status === "cancelled"
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
}`}
>
{laudo.status === "completed"
? "Concluído"
: laudo.status === "pending"
? "Pendente"
: laudo.status === "cancelled"
? "Cancelado"
: "Rascunho"}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-300">
{new Date(laudo.created_at).toLocaleDateString("pt-BR")}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
onClick={() => {
setSelectedLaudo(laudo);
setShowLaudoModal(true);
}}
className="inline-flex items-center gap-2 px-3 py-1 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors"
title="Ver detalhes"
>
<Eye className="h-4 w-4" />
<span>Ver</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
const renderContent = () => {
switch (activeTab) {
case "dashboard":
return renderDashboard();
case "appointments":
return renderAppointments();
case "reports":
return renderReports();
case "book":
return renderBookAppointment();
case "messages":
return renderMessages();
case "help":
return renderHelp();
case "profile":
return renderProfile();
default:
return null;
}
};
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Acesso Negado
</h1>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Você precisa estar logado para acessar esta página.
</p>
<button
onClick={() => navigate("/paciente")}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors"
>
Fazer Login
</button>
</div>
</div>
);
}
return (
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
{renderSidebar()}
{/* Mobile Header */}
<div className="lg:hidden bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 p-4 sticky top-0 z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<AvatarUpload
userId={user?.id}
currentAvatarUrl={avatarUrl}
name={pacienteNome}
color="blue"
size="lg"
editable={false}
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div className="min-w-0">
<p className="font-medium text-gray-900 dark:text-white text-sm truncate">
{pacienteNome}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
Paciente
</p>
</div>
</div>
<button
onClick={() => {
logout();
navigate("/paciente");
}}
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg"
>
<LogOut className="h-5 w-5" />
</button>
</div>
{/* Mobile Nav */}
<div className="mt-3 flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => {
if (item.isLink && item.path) {
navigate(item.path);
} else if (item.id === "help") {
navigate("/ajuda");
} else {
setActiveTab(item.id);
}
}}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${
isActive
? "bg-blue-600 text-white"
: "bg-gray-100 dark:bg-slate-800 text-gray-700 dark:text-gray-300"
}`}
>
<Icon className="h-4 w-4 flex-shrink-0" />
{item.label}
</button>
);
})}
</div>
</div>
<main className="flex-1 overflow-y-auto">
<div className="container mx-auto p-4 sm:p-6 lg:p-8">
{renderContent()}
</div>
</main>
{/* Modal de Visualização do Laudo */}
{showLaudoModal && selectedLaudo && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto">
{/* Header do Modal */}
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Detalhes do Laudo
</h3>
<button
onClick={() => {
setShowLaudoModal(false);
setSelectedLaudo(null);
}}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Conteúdo do Modal */}
<div className="p-6 space-y-6">
{/* Informações Principais */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Número do Pedido
</label>
<p className="text-sm text-gray-900 dark:text-white font-semibold">
{selectedLaudo.order_number}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<span
className={`inline-block px-2 py-1 text-xs font-semibold rounded-full ${
selectedLaudo.status === "completed"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: selectedLaudo.status === "pending"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: selectedLaudo.status === "cancelled"
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
}`}
>
{selectedLaudo.status === "completed"
? "Concluído"
: selectedLaudo.status === "pending"
? "Pendente"
: selectedLaudo.status === "cancelled"
? "Cancelado"
: "Rascunho"}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Data de Criação
</label>
<p className="text-sm text-gray-900 dark:text-white">
{new Date(selectedLaudo.created_at).toLocaleDateString(
"pt-BR",
{
day: "2-digit",
month: "long",
year: "numeric",
}
)}
</p>
</div>
{selectedLaudo.due_at && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Prazo de Entrega
</label>
<p className="text-sm text-gray-900 dark:text-white">
{new Date(selectedLaudo.due_at).toLocaleDateString(
"pt-BR",
{
day: "2-digit",
month: "long",
year: "numeric",
}
)}
</p>
</div>
)}
</div>
{/* Exame */}
{selectedLaudo.exam && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Exame
</label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white">
{selectedLaudo.exam}
</p>
</div>
</div>
)}
{/* Diagnóstico */}
{selectedLaudo.diagnosis && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Diagnóstico
</label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white whitespace-pre-wrap">
{selectedLaudo.diagnosis}
</p>
</div>
</div>
)}
{/* CID */}
{selectedLaudo.cid_code && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
CID-10
</label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white font-mono">
{selectedLaudo.cid_code}
</p>
</div>
</div>
)}
{/* Conclusão */}
{selectedLaudo.conclusion && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Conclusão
</label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white whitespace-pre-wrap">
{selectedLaudo.conclusion}
</p>
</div>
</div>
)}
{/* Solicitado por */}
{selectedLaudo.requested_by && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Solicitado por
</label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white">
{requestedByNames[selectedLaudo.requested_by] ||
selectedLaudo.requested_by}
</p>
</div>
</div>
)}
{/* Conteúdo HTML (se houver) */}
{selectedLaudo.content_html && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Conteúdo Completo
</label>
<div
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: selectedLaudo.content_html,
}}
/>
</div>
)}
</div>
{/* Footer do Modal */}
<div className="sticky bottom-0 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-end gap-3">
<button
onClick={() => {
setShowLaudoModal(false);
setSelectedLaudo(null);
}}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Fechar
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default AcompanhamentoPaciente;