1416 lines
55 KiB
TypeScript
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;
|