riseup-squad18/src/pages/PainelMedico.tsx
Fernando Pirichowski Aguiar 389a191f20 fix: corrige persistência de avatar, agendamento de consulta e download de PDF
- Avatar do paciente agora persiste após reload (adiciona timestamp para evitar cache)
- Agendamento usa patient_id correto ao invés de user_id
- Botão de download de PDF desbloqueado com logs detalhados
2025-11-15 08:36:41 -03:00

2297 lines
86 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import {
Calendar,
Clock,
Mail,
TrendingUp,
Video,
MapPin,
Phone,
FileText,
Settings,
LogOut,
Home,
CheckCircle,
AlertCircle,
XCircle,
HelpCircle,
Plus,
Edit,
Trash2,
User,
Save,
Eye,
} from "lucide-react";
import toast from "react-hot-toast";
import { format, isSameDay } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useNavigate } from "react-router-dom";
import {
appointmentService,
patientService,
reportService,
doctorService,
authService,
type Appointment,
type Patient,
type CreateReportInput,
} from "../services";
import type { Report } from "../services/reports/types";
import { useAuth } from "../hooks/useAuth";
import DisponibilidadeMedico from "../components/DisponibilidadeMedico";
import ConsultaModal from "../components/consultas/ConsultaModal";
import MensagensMedico from "./MensagensMedico";
import { AvatarUpload } from "../components/ui/AvatarUpload";
import { avatarService } from "../services/avatars/avatarService";
// Type aliases para compatibilidade
type ServiceConsulta = Appointment;
type RelatorioCreate = CreateReportInput;
interface ConsultaUI {
id: string;
pacienteId: string;
medicoId: string;
pacienteNome: string;
medicoNome: string;
dataHora: string;
status: string;
tipo?: string;
observacoes?: string;
motivoConsulta?: string; // patient_notes
}
const PainelMedico: React.FC = () => {
const { user, roles, logout } = useAuth();
const navigate = useNavigate();
// Auth
const temAcessoMedico =
user &&
(user.role === "medico" ||
roles.includes("medico") ||
roles.includes("admin"));
const medicoNome = user?.nome || "Médico";
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
// Usar user_id diretamente para perfil e relatórios
const authUserId = user?.id || "";
// Buscar doctor.id da tabela apenas para consultas
const [doctorTableId, setDoctorTableId] = useState<string | null>(null);
// Helpers seguros para formatar data e hora (evitam crash quando value for inválido)
const formatDateSafe = (iso?: string) => {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d.getTime())) return "—";
return format(d, "dd/MM/yyyy", { locale: ptBR });
} catch {
return "—";
}
};
const formatTimeSafe = (iso?: string) => {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d.getTime())) return "—";
return format(d, "HH:mm", { locale: ptBR });
} catch {
return "—";
}
};
// State
const [activeTab, setActiveTab] = useState("dashboard");
const [consultas, setConsultas] = useState<ConsultaUI[]>([]);
const [filtroData, setFiltroData] = useState("todas");
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<ConsultaUI | null>(null);
const [viewingConsulta, setViewingConsulta] = useState<ConsultaUI | null>(
null
);
const [viewModalOpen, setViewModalOpen] = useState(false);
const [viewingReport, setViewingReport] = useState<Report | null>(null);
const [viewReportModalOpen, setViewReportModalOpen] = useState(false);
const [relatorioModalOpen, setRelatorioModalOpen] = useState(false);
const [loadingRelatorio, setLoadingRelatorio] = useState(false);
const [laudos, setLaudos] = useState<Report[]>([]);
const [loadingLaudos, setLoadingLaudos] = useState(false);
const [pacientesDisponiveis, setPacientesDisponiveis] = useState<
Array<{ id: string; nome: string }>
>([]);
const [formRelatorio, setFormRelatorio] = useState({
patient_id: "",
order_number: "",
exam: "",
diagnosis: "",
conclusion: "",
cid_code: "",
content_html: "",
status: "draft" as "draft" | "pending" | "completed" | "cancelled",
requested_by: medicoNome,
due_at: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
hide_date: false,
hide_signature: false,
});
// Estados para perfil do médico
const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileTab, setProfileTab] = useState<
"personal" | "professional" | "security"
>("personal");
const [profileData, setProfileData] = useState({
full_name: "",
email: "",
phone: "",
cpf: "",
birth_date: "",
sex: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
cep: "",
crm: "",
specialty: "",
});
// Estados para alteração de senha na seção de Configurações > Segurança
const [passwordData, setPasswordData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
// Buscar doctor.id da tabela usando user_id (apenas para consultas)
useEffect(() => {
const fetchDoctorTableId = async () => {
if (!authUserId) return;
try {
console.log(
"[PainelMedico] 🔍 Buscando doctor.id (tabela) para consultas. Auth user_id:",
authUserId
);
const doctor = await doctorService.getByUserId(authUserId);
if (doctor) {
setDoctorTableId(doctor.id);
console.log(
"[PainelMedico] ✅ Doctor.id (tabela) encontrado para consultas:",
{
doctorTableId: doctor.id,
authUserId: authUserId,
nome: doctor.full_name,
diferentes: doctor.id !== authUserId,
}
);
} else {
console.warn(
"[PainelMedico] ⚠️ Médico não encontrado na tabela doctors"
);
}
} catch (error) {
console.error("[PainelMedico] ❌ Erro ao buscar doctor.id:", error);
}
};
fetchDoctorTableId();
}, [authUserId]);
useEffect(() => {
if (!user) navigate("/login-medico");
}, [user, navigate]);
// 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 () => {
// Adiciona timestamp para evitar cache do navegador
const timestamp = new Date().getTime();
for (const ext of extensions) {
try {
const url = avatarService.getPublicUrl({
userId: user.id,
ext: ext as "jpg" | "png" | "webp",
});
// Adiciona timestamp como query parameter para forçar reload
const urlWithTimestamp = `${url}?t=${timestamp}`;
const response = await fetch(urlWithTimestamp, { method: "HEAD" });
if (response.ok) {
setAvatarUrl(urlWithTimestamp);
console.log(
`[PainelMedico] Avatar encontrado: ${urlWithTimestamp}`
);
break;
}
} catch {
// Continua testando próxima extensão
}
}
};
testAvatar();
}
}, [user?.id]);
const fetchConsultas = useCallback(async () => {
setLoading(true);
try {
let appointments;
if (user?.role === "admin" || roles.includes("admin")) {
// Admin: busca todas as consultas do sistema
appointments = await appointmentService.list();
} else {
// Médico comum: busca todas as consultas usando doctor.id da tabela
if (!doctorTableId) {
console.warn(
"[PainelMedico] ⚠️ Aguardando doctorTableId para carregar consultas..."
);
setLoading(false);
return;
}
console.log(
"[PainelMedico] 🔍 Buscando consultas para doctor.id (tabela):",
doctorTableId
);
appointments = await appointmentService.list({
doctor_id: doctorTableId,
});
}
if (appointments && appointments.length > 0) {
console.log(
"[PainelMedico] ✅ Consultas encontradas:",
appointments.length
);
// Buscar nomes dos pacientes
const consultasComNomes = await Promise.all(
appointments.map(async (appt: Appointment) => {
let pacienteNome = "Paciente Desconhecido";
try {
const patient = await patientService.getById(appt.patient_id);
if (patient) {
pacienteNome = patient.full_name;
}
} catch (error) {
console.error("Erro ao buscar nome do paciente:", error);
}
return {
id: appt.id,
pacienteId: appt.patient_id,
medicoId: appt.doctor_id,
pacienteNome,
medicoNome: medicoNome,
dataHora: appt.scheduled_at,
status: appt.status,
tipo: appt.appointment_type,
observacoes: appt.notes || undefined,
motivoConsulta: appt.patient_notes || undefined,
};
})
);
setConsultas(consultasComNomes);
} else {
setConsultas([]);
}
} catch (error) {
console.error("Erro ao buscar consultas:", error);
toast.error("Erro ao carregar consultas");
setConsultas([]);
} finally {
setLoading(false);
}
}, [user, roles, doctorTableId, medicoNome]);
const fetchLaudos = useCallback(async () => {
if (!authUserId) {
console.warn(
"[PainelMedico] ⚠️ Aguardando authUserId para carregar laudos..."
);
return;
}
setLoadingLaudos(true);
try {
console.log("[PainelMedico] 🔍 Buscando laudos para auth user_id:", {
authUserId,
authUserIdType: typeof authUserId,
});
// Buscar todos os laudos e filtrar pelo médico criador
const allReports = await reportService.list();
console.log(
"[PainelMedico] 📋 Total de laudos retornados:",
allReports.length
);
// Debug: mostrar alguns relatórios para análise
allReports.slice(0, 3).forEach((report: Report, index: number) => {
console.log(`[PainelMedico] Relatório ${index + 1}:`, {
created_by: report.created_by,
requested_by: report.requested_by,
createdByType: typeof report.created_by,
requestedByType: typeof report.requested_by,
isCreator: report.created_by === authUserId,
isRequester: report.requested_by === authUserId,
match:
report.created_by === authUserId ||
report.requested_by === authUserId,
});
});
// Filtrar apenas laudos criados por este médico usando auth user_id
const meusLaudos = allReports.filter(
(report: Report) => report.created_by === authUserId
);
console.log("[PainelMedico] ✅ Laudos criados por este médico:", {
total: meusLaudos.length,
laudos: meusLaudos,
});
setLaudos(meusLaudos);
} catch (error) {
console.error("[PainelMedico] ❌ Erro ao buscar laudos:", error);
toast.error("Erro ao carregar laudos");
setLaudos([]);
} finally {
setLoadingLaudos(false);
}
}, [authUserId]);
useEffect(() => {
fetchConsultas();
}, [fetchConsultas]);
useEffect(() => {
if (activeTab === "reports") {
fetchLaudos();
}
}, [activeTab, fetchLaudos]);
useEffect(() => {
if (relatorioModalOpen && user?.id) {
const carregarPacientes = async () => {
try {
const patients = await patientService.list();
if (patients && patients.length > 0) {
setPacientesDisponiveis(
patients.map((p: Patient) => ({
id: p.id || "",
nome: p.full_name,
}))
);
}
} catch (error) {
console.error("Erro ao carregar pacientes:", error);
toast.error("Erro ao carregar lista de pacientes");
}
};
carregarPacientes();
}
}, [relatorioModalOpen, user]);
const handleCriarRelatorio = async (e: React.FormEvent) => {
e.preventDefault();
if (!formRelatorio.patient_id) {
toast.error("Selecione um paciente");
return;
}
if (!formRelatorio.exam.trim()) {
toast.error("Informe o tipo de exame");
return;
}
setLoadingRelatorio(true);
try {
const payload: RelatorioCreate = {
patient_id: formRelatorio.patient_id,
exam: formRelatorio.exam,
diagnosis: formRelatorio.diagnosis || undefined,
conclusion: formRelatorio.conclusion || undefined,
cid_code: formRelatorio.cid_code || undefined,
content_html: formRelatorio.content_html || undefined,
status: formRelatorio.status,
requested_by: formRelatorio.requested_by || medicoNome,
due_at: formRelatorio.due_at || undefined,
hide_date: formRelatorio.hide_date,
hide_signature: formRelatorio.hide_signature,
created_by: authUserId || undefined, // Usar auth user_id como criador
};
console.log(
"[PainelMedico] 📝 Criando relatório com auth user_id:",
authUserId
);
const newReport = await reportService.create(payload);
if (newReport) {
toast.success("Relatório criado com sucesso!");
setRelatorioModalOpen(false);
setFormRelatorio({
patient_id: "",
order_number: "",
exam: "",
diagnosis: "",
conclusion: "",
cid_code: "",
content_html: "",
status: "draft",
requested_by: medicoNome,
due_at: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
hide_date: false,
hide_signature: false,
});
} else {
toast.error("Erro ao criar relatório");
}
} catch (error) {
console.error("Erro ao criar relatório:", error);
toast.error("Erro ao criar relatório");
} finally {
setLoadingRelatorio(false);
}
};
const handleNovaConsulta = () => {
setEditing(null);
setModalOpen(true);
};
const handleEditConsulta = (consulta: ConsultaUI) => {
setEditing(consulta);
setModalOpen(true);
};
const handleDeleteConsulta = async (id: string) => {
if (!window.confirm("Deseja realmente excluir esta consulta?")) return;
try {
const raw = localStorage.getItem("consultas_local");
if (raw) {
const lista: ServiceConsulta[] = JSON.parse(raw);
const nova = lista.filter((c) => c.id !== id);
localStorage.setItem("consultas_local", JSON.stringify(nova));
toast.success("Consulta excluída");
fetchConsultas();
}
} catch (error) {
console.error("Erro ao excluir consulta:", error);
toast.error("Erro ao excluir consulta");
}
};
const handleSaveConsulta = () => {
setModalOpen(false);
setEditing(null);
fetchConsultas();
};
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case "confirmada":
case "confirmed":
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":
case "scheduled":
return "bg-indigo-100 text-indigo-800 border-blue-200 dark:bg-indigo-900/30 dark:text-blue-300 dark:border-indigo-800";
case "concluida":
case "completed":
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 "cancelled":
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.toLowerCase()) {
case "confirmada":
case "confirmed":
return "Confirmada";
case "agendada":
case "scheduled":
return "Agendada";
case "concluida":
case "completed":
return "Concluída";
case "cancelada":
case "cancelled":
return "Cancelada";
default:
return status;
}
};
const getStatusIcon = (status: string) => {
switch (status.toLowerCase()) {
case "confirmada":
case "confirmed":
return <CheckCircle className="h-4 w-4" />;
case "agendada":
case "scheduled":
return <Clock className="h-4 w-4" />;
case "cancelada":
case "cancelled":
return <XCircle className="h-4 w-4" />;
default:
return <AlertCircle className="h-4 w-4" />;
}
};
// Stats
// Calcula consultas de hoje (filtra por data local)
const consultasHoje = consultas.filter((c) => {
if (!c?.dataHora) return false;
try {
const d = new Date(c.dataHora);
if (isNaN(d.getTime())) return false;
const today = new Date();
return (
d.getFullYear() === today.getFullYear() &&
d.getMonth() === today.getMonth() &&
d.getDate() === today.getDate()
);
} catch {
return false;
}
});
const consultasConfirmadas = consultas.filter(
(c) =>
c.status.toLowerCase() === "confirmada" ||
c.status.toLowerCase() === "confirmed"
);
const consultasConcluidas = consultas.filter(
(c) =>
c.status.toLowerCase() === "concluida" ||
c.status.toLowerCase() === "completed"
);
// Sidebar
const menuItems = [
{ id: "dashboard", label: "Dashboard", icon: Home },
{ id: "appointments", label: "Consultas", icon: Clock },
{ id: "messages", label: "Mensagens", icon: Mail },
{ id: "availability", label: "Disponibilidade", icon: Calendar },
{ id: "reports", label: "Relatórios", icon: FileText },
{ id: "help", label: "Ajuda", icon: HelpCircle },
{ id: "settings", label: "Meu Perfil", icon: User },
];
const renderSidebar = () => (
<div className="w-64 h-screen bg-white dark:bg-slate-900 border-r border-gray-200 dark:border-slate-700 flex flex-col">
{/* Doctor Profile */}
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center gap-3">
<AvatarUpload
userId={user?.id}
currentAvatarUrl={avatarUrl}
name={medicoNome}
color="green"
size="lg"
editable={true}
userType="doctor"
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{medicoNome}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Médico</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 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-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 ${
isActive
? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400"
: "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800"
}`}
>
<Icon className="h-5 w-5" />
{item.label}
</button>
);
})}
</div>
</nav>
{/* Logout */}
<div className="p-4 border-t border-gray-200 dark:border-slate-700">
<button
onClick={() => {
logout();
navigate("/login-medico");
}}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg 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-5 w-5" />
Sair
</button>
</div>
</div>
);
// Stats Cards
const renderStatCard = (
title: string,
value: string | number,
icon: React.ElementType,
description?: string,
trend?: 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>
)}
{trend && (
<p className="text-sm text-gray-600 dark:text-gray-400">{trend}</p>
)}
</div>
);
};
// Appointment Card
const renderAppointmentCard = (consulta: ConsultaUI) => (
<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-12 w-12 rounded-full bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center text-white font-semibold">
{consulta.pacienteNome
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
<div className="flex-1 space-y-2">
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-gray-900 dark:text-white">
{consulta.pacienteNome}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{consulta.observacoes || "Consulta médica"}
</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 items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<div className="flex flex-col leading-tight">
<span className="font-medium text-gray-900 dark:text-gray-100">
{formatTimeSafe(consulta.dataHora)}
</span>
<span className="text-xs text-gray-500">
{formatDateSafe(consulta.dataHora)}
</span>
</div>
</div>
<div className="flex items-center gap-1">
{consulta.tipo === "online" || consulta.tipo === "telemedicina" ? (
<>
<Video className="h-4 w-4" />
<span>Online</span>
</>
) : (
<>
<MapPin className="h-4 w-4" />
<span>Presencial</span>
</>
)}
</div>
</div>
<div className="flex gap-2">
{consulta.status.toLowerCase() === "confirmada" && (
<>
<button 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-indigo-500">
<Phone className="h-4 w-4" />
Ligar
</button>
{(consulta.tipo === "online" ||
consulta.tipo === "telemedicina") && (
<button className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500">
<Video className="h-4 w-4" />
Iniciar Consulta
</button>
)}
</>
)}
<button
onClick={() => {
setViewingConsulta(consulta);
setViewModalOpen(true);
}}
className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<Eye className="h-4 w-4" />
Visualizar
</button>
<button
onClick={() => handleEditConsulta(consulta)}
className="flex items-center gap-1 px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<Edit className="h-4 w-4" />
Editar
</button>
<button
onClick={() => handleDeleteConsulta(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"
>
<Trash2 className="h-4 w-4" />
Excluir
</button>
</div>
</div>
</div>
);
// Content Sections
const renderDashboard = () => {
console.log("[PainelMedico] 📊 Renderizando dashboard com consultas:", {
totalConsultas: consultas.length,
consultasHoje: consultasHoje.length,
doctorTableId: doctorTableId,
authUserId: authUserId,
});
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{renderStatCard(
"Consultas Hoje",
consultasHoje.length,
Clock,
`${consultasConfirmadas.length} confirmadas`
)}
{renderStatCard(
"Total Consultas",
consultas.length,
Calendar,
"Este período"
)}
{renderStatCard(
"Concluídas",
consultasConcluidas.length,
CheckCircle,
"Este período"
)}
{renderStatCard(
"Taxa Comparecimento",
consultas.length > 0
? `${Math.round(
(consultasConcluidas.length / consultas.length) * 100
)}%`
: "0%",
TrendingUp,
"Baseado em consultas concluídas"
)}
</div>
{/* Today's Appointments */}
<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">
Consultas de Hoje
</h2>
<button
onClick={handleNovaConsulta}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<Plus className="h-4 w-4" />
Nova Consulta
</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-indigo-600 border-r-transparent"></div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Carregando consultas...
</p>
</div>
) : consultasHoje.length === 0 ? (
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
Nenhuma consulta agendada para hoje
</p>
) : (
<div className="space-y-4">
{consultasHoje.map(renderAppointmentCard)}
</div>
)}
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
<h3 className="font-semibold text-gray-900 dark:text-white">
Próximos 7 Dias
</h3>
</div>
<div className="p-6">
<div className="space-y-3">
{(() => {
// Calcula os próximos 7 dias e conta consultas por dia
const days: Array<{ label: string; count: number }>[] =
[] as any;
const today = new Date();
for (let i = 0; i < 7; i++) {
const d = new Date(today);
d.setDate(today.getDate() + i);
const label = d
.toLocaleDateString("pt-BR", { weekday: "long" })
.replace(/(^\w|\s\w)/g, (m) => m.toUpperCase());
const count = consultas.filter((c) => {
if (!c?.dataHora) return false;
const cd = new Date(c.dataHora);
if (isNaN(cd.getTime())) return false;
return (
cd.getFullYear() === d.getFullYear() &&
cd.getMonth() === d.getMonth() &&
cd.getDate() === d.getDate()
);
}).length;
days.push({ label, count });
}
return days.map((day) => (
<div
key={day.label}
className="flex justify-between items-center"
>
<span className="text-sm text-gray-600 dark:text-gray-400">
{day.label}
</span>
<span className="text-sm font-medium text-gray-900 dark:text-white">
{day.count} consulta{day.count !== 1 ? "s" : ""}
</span>
</div>
));
})()}
</div>
</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">
<h3 className="font-semibold text-gray-900 dark:text-white">
Tipos de Consulta
</h3>
</div>
<div className="p-6">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">
Presencial
</span>
<span className="text-sm font-medium text-gray-900 dark:text-white">
{
consultas.filter(
(c) => c.tipo !== "online" && c.tipo !== "telemedicina"
).length
}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">
Online
</span>
<span className="text-sm font-medium text-gray-900 dark:text-white">
{
consultas.filter(
(c) => c.tipo === "online" || c.tipo === "telemedicina"
).length
}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
// Função para filtrar consultas por data
const filtrarConsultasPorData = (consultas: ConsultaUI[]) => {
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const amanha = new Date(hoje);
amanha.setDate(amanha.getDate() + 1);
const fimDaSemana = new Date(hoje);
fimDaSemana.setDate(fimDaSemana.getDate() + 7);
return consultas.filter((consulta) => {
const dataConsulta = new Date(consulta.dataHora);
dataConsulta.setHours(0, 0, 0, 0);
switch (filtroData) {
case "hoje":
return dataConsulta.getTime() === hoje.getTime();
case "amanha":
return dataConsulta.getTime() === amanha.getTime();
case "semana":
return dataConsulta >= hoje && dataConsulta <= fimDaSemana;
case "todas":
default:
return true;
}
});
};
const renderAppointments = () => {
const agora = new Date();
// Separar consultas futuras e passadas
const consultasFuturas = consultas.filter((c) => {
if (!c?.dataHora) return false;
try {
const dataConsulta = new Date(c.dataHora);
if (isNaN(dataConsulta.getTime())) return false;
return dataConsulta >= agora;
} catch {
return false;
}
});
const consultasPassadas = consultas.filter((c) => {
if (!c?.dataHora) return false;
try {
const dataConsulta = new Date(c.dataHora);
if (isNaN(dataConsulta.getTime())) return false;
return dataConsulta < agora;
} catch {
return false;
}
});
// Aplicar filtro de data apenas nas consultas futuras
const consultasFuturasFiltradas = filtrarConsultasPorData(consultasFuturas);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Consultas
</h1>
<button
onClick={handleNovaConsulta}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<Plus className="h-4 w-4" />
Nova Consulta
</button>
</div>
{/* Filters - apenas para consultas futuras */}
<div className="flex gap-2">
{["hoje", "amanha", "semana", "todas"].map((filtro) => (
<button
key={filtro}
onClick={() => setFiltroData(filtro)}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
filtroData === filtro
? "bg-indigo-600 text-white"
: "bg-white dark:bg-slate-900 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800"
}`}
>
{filtro === "hoje"
? "Hoje"
: filtro === "amanha"
? "Amanhã"
: filtro === "semana"
? "Esta Semana"
: "Todas"}
</button>
))}
</div>
{/* Consultas Futuras */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Próximas Consultas ({consultasFuturasFiltradas.length})
</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-indigo-600 border-r-transparent"></div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Carregando consultas...
</p>
</div>
) : consultasFuturasFiltradas.length === 0 ? (
<p className="text-center py-8 text-gray-600 dark:text-gray-400">
{filtroData === "hoje"
? "Nenhuma consulta agendada para hoje"
: filtroData === "amanha"
? "Nenhuma consulta agendada para amanhã"
: filtroData === "semana"
? "Nenhuma consulta agendada para esta semana"
: "Nenhuma consulta futura encontrada"}
</p>
) : (
<div className="space-y-4">
{consultasFuturasFiltradas.map(renderAppointmentCard)}
</div>
)}
</div>
</div>
{/* Histórico de Consultas Passadas */}
{consultasPassadas.length > 0 && (
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Histórico ({consultasPassadas.length})
</h2>
</div>
<div className="p-6">
<div className="space-y-4">
{consultasPassadas
.sort(
(a, b) =>
new Date(b.dataHora).getTime() -
new Date(a.dataHora).getTime()
)
.map(renderAppointmentCard)}
</div>
</div>
</div>
)}
</div>
);
};
const renderAvailability = () => <DisponibilidadeMedico />;
const renderReports = () => (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Meus Laudos
</h1>
<button
onClick={() => setRelatorioModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<Plus className="h-4 w-4" />
Novo Laudo
</button>
</div>
<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 criou nenhum laudo.
</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">
Diagnóstico
</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-center 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.diagnosis || "-"}
</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-center">
<button
onClick={() => {
setViewingReport(laudo);
setViewReportModalOpen(true);
}}
className="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-md transition-colors"
>
<Eye className="h-4 w-4" />
Visualizar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
// Carregar dados do perfil do médico usando auth user_id
const loadDoctorProfile = useCallback(async () => {
if (!authUserId) return;
try {
console.log(
"[PainelMedico] 🔍 Carregando perfil do médico para auth user_id:",
authUserId
);
const doctor = await doctorService.getByUserId(authUserId);
if (!doctor) {
console.warn("[PainelMedico] ⚠️ Perfil do médico não encontrado");
return;
}
setProfileData({
full_name: doctor.full_name || "",
email: doctor.email || "",
phone: doctor.phone2 || "", // Usar phone2 ao invés de phone
cpf: doctor.cpf || "",
birth_date: doctor.birth_date || "",
sex: "", // Campo sex não existe no tipo Doctor
street: doctor.street || "",
number: doctor.number || "",
complement: doctor.complement || "",
neighborhood: doctor.neighborhood || "",
city: doctor.city || "",
state: doctor.state || "",
cep: doctor.cep || "",
crm: doctor.crm || "",
specialty: doctor.specialty || "",
});
console.log("[PainelMedico] ✅ Perfil carregado com sucesso");
} catch (error) {
console.error("[PainelMedico] Erro ao carregar perfil:", error);
toast.error("Erro ao carregar perfil");
}
}, [authUserId]);
useEffect(() => {
if (authUserId) {
loadDoctorProfile();
}
}, [authUserId, loadDoctorProfile]);
const handleSaveProfile = async () => {
if (!authUserId) return;
try {
console.log(
"[PainelMedico] 💾 Salvando perfil do médico para auth user_id:",
authUserId
);
await doctorService.updateByUserId(authUserId, profileData);
toast.success("Perfil atualizado com sucesso!");
setIsEditingProfile(false);
await loadDoctorProfile();
} catch (error) {
console.error("[PainelMedico] Erro ao salvar perfil:", error);
toast.error("Erro ao salvar perfil");
}
};
const handleProfileChange = (field: string, value: string) => {
setProfileData((prev) => ({ ...prev, [field]: value }));
};
const handlePasswordChange = async () => {
if (passwordData.newPassword !== passwordData.confirmPassword) {
toast.error("As senhas não coincidem");
return;
}
if (passwordData.newPassword.length < 6) {
toast.error("A senha deve ter pelo menos 6 caracteres");
return;
}
try {
const token = authService.getAccessToken();
if (!token) {
toast.error("Token de sessão não encontrado. Faça login novamente.");
return;
}
try {
await authService.updatePassword(token, passwordData.newPassword);
} catch (err: any) {
if (err?.response?.status === 401) {
try {
await authService.refreshToken();
const newToken = authService.getAccessToken();
if (newToken) {
await authService.updatePassword(
newToken,
passwordData.newPassword
);
} else {
throw new Error("Falha ao obter novo token após refresh");
}
} catch (refreshErr) {
console.error(
"Falha ao renovar token e atualizar senha:",
refreshErr
);
throw refreshErr;
}
} else {
throw err;
}
}
toast.success("Senha alterada com sucesso!");
setPasswordData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
} catch (error) {
console.error("Erro ao alterar senha:", error);
toast.error("Erro ao alterar senha");
}
};
const renderSettings = () => (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<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 e profissionais
</p>
</div>
{!isEditingProfile ? (
<button
onClick={() => setIsEditingProfile(true)}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
<Edit className="h-4 w-4" />
Editar Perfil
</button>
) : (
<div className="flex gap-2">
<button
onClick={() => {
setIsEditingProfile(false);
loadDoctorProfile();
}}
className="px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors"
>
Cancelar
</button>
<button
onClick={handleSaveProfile}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
<Save className="h-4 w-4" />
Salvar
</button>
</div>
)}
</div>
{/* Avatar Card */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<h2 className="text-lg font-semibold mb-4 dark:text-white">
Foto de Perfil
</h2>
<div className="flex items-center gap-6">
<AvatarUpload
userId={user?.id}
currentAvatarUrl={avatarUrl}
name={profileData.full_name || medicoNome}
color="indigo"
size="xl"
editable={true}
userType="doctor"
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{profileData.full_name || medicoNome}
</p>
<p className="text-gray-500 dark:text-gray-400">
{profileData.email || user?.email || "Sem email"}
</p>
<p className="text-sm text-indigo-600 dark:text-indigo-400 mt-1">
CRM: {profileData.crm || "Não informado"}
</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="border-b border-gray-200 dark:border-slate-700">
<nav className="flex -mb-px">
<button
onClick={() => setProfileTab("personal")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
profileTab === "personal"
? "border-indigo-600 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300"
}`}
>
Dados Pessoais
</button>
<button
onClick={() => setProfileTab("professional")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
profileTab === "professional"
? "border-indigo-600 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300"
}`}
>
Info. Profissionais
</button>
<button
onClick={() => setProfileTab("security")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
profileTab === "security"
? "border-indigo-600 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300"
}`}
>
Segurança
</button>
</nav>
</div>
<div className="p-6">
{/* Tab: Dados Pessoais */}
{profileTab === "personal" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Informações Pessoais
</h3>
<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">
Nome Completo
</label>
<input
type="text"
value={profileData.full_name}
onChange={(e) =>
handleProfileChange("full_name", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<input
type="email"
value={profileData.email}
onChange={(e) =>
handleProfileChange("email", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Telefone
</label>
<input
type="tel"
value={profileData.phone}
onChange={(e) =>
handleProfileChange("phone", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
CPF
</label>
<input
type="text"
value={profileData.cpf}
disabled
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Data de Nascimento
</label>
<input
type="date"
value={profileData.birth_date}
onChange={(e) =>
handleProfileChange("birth_date", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Sexo
</label>
<select
value={profileData.sex}
onChange={(e) =>
handleProfileChange("sex", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
>
<option value="">Selecione</option>
<option value="M">Masculino</option>
<option value="F">Feminino</option>
<option value="O">Outro</option>
</select>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Endereço
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Rua
</label>
<input
type="text"
value={profileData.street}
onChange={(e) =>
handleProfileChange("street", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Número
</label>
<input
type="text"
value={profileData.number}
onChange={(e) =>
handleProfileChange("number", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Complemento
</label>
<input
type="text"
value={profileData.complement}
onChange={(e) =>
handleProfileChange("complement", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Bairro
</label>
<input
type="text"
value={profileData.neighborhood}
onChange={(e) =>
handleProfileChange("neighborhood", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Cidade
</label>
<input
type="text"
value={profileData.city}
onChange={(e) =>
handleProfileChange("city", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Estado
</label>
<input
type="text"
value={profileData.state}
onChange={(e) =>
handleProfileChange("state", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
maxLength={2}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
CEP
</label>
<input
type="text"
value={profileData.cep}
onChange={(e) =>
handleProfileChange("cep", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
</div>
</div>
</div>
)}
{/* Tab: Info Profissionais */}
{profileTab === "professional" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Informações Profissionais
</h3>
<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">
CRM
</label>
<input
type="text"
value={profileData.crm}
onChange={(e) =>
handleProfileChange("crm", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Especialidade
</label>
<input
type="text"
value={profileData.specialty}
onChange={(e) =>
handleProfileChange("specialty", e.target.value)
}
disabled={!isEditingProfile}
className="form-input"
/>
</div>
</div>
</div>
</div>
)}
{/* Tab: Segurança */}
{profileTab === "security" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Alteração de Senha
</h3>
<p className="text-sm text-gray-500 mb-4">
Atualize sua senha de acesso. Após a alteração, use a nova
senha para efetuar login.
</p>
<div className="max-w-md space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Senha Atual
</label>
<input
type="password"
value={passwordData.currentPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
currentPassword: e.target.value,
})
}
placeholder="Digite sua senha atual"
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nova Senha
</label>
<input
type="password"
value={passwordData.newPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
newPassword: e.target.value,
})
}
placeholder="Digite a nova senha"
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Confirmar Nova Senha
</label>
<input
type="password"
value={passwordData.confirmPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
confirmPassword: e.target.value,
})
}
placeholder="Confirme a nova senha"
className="form-input"
/>
</div>
<button
onClick={handlePasswordChange}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
Alterar Senha
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
const renderContent = () => {
switch (activeTab) {
case "dashboard":
return renderDashboard();
case "appointments":
return renderAppointments();
case "messages":
return <MensagensMedico />;
case "availability":
return renderAvailability();
case "reports":
return renderReports();
case "settings":
return renderSettings();
default:
return null;
}
};
if (!temAcessoMedico) {
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ê não tem permissão para acessar esta página.
</p>
<button
onClick={() => navigate("/login-medico")}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-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()}
<main className="flex-1 overflow-y-auto">
<div className="container mx-auto p-4 sm:p-6 lg:p-8">
{renderContent()}
</div>
</main>
{/* Modals */}
{modalOpen && (
<ConsultaModal
isOpen={modalOpen}
onClose={() => {
setModalOpen(false);
setEditing(null);
}}
onSaved={handleSaveConsulta}
editing={editing}
defaultMedicoId={doctorTableId || ""}
lockMedico={false}
/>
)}
{/* Modal de Visualização de Consulta */}
{viewModalOpen && viewingConsulta && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={() => setViewModalOpen(false)}
>
<div
className="bg-white dark:bg-slate-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Detalhes da Consulta
</h2>
<button
onClick={() => setViewModalOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="h-6 w-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Informações do Paciente */}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<User className="h-5 w-5" />
Informações do Paciente
</h3>
<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">
Nome do Paciente
</label>
<p className="text-gray-900 dark:text-white font-medium">
{viewingConsulta.pacienteNome}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<div
className={`inline-flex items-center gap-1 px-3 py-1 rounded-md text-sm font-medium border ${getStatusColor(
viewingConsulta.status
)}`}
>
{getStatusIcon(viewingConsulta.status)}
{getStatusLabel(viewingConsulta.status)}
</div>
</div>
</div>
</div>
{/* Data e Hora */}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Calendar className="h-5 w-5" />
Data e Hora
</h3>
<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">
Data
</label>
<p className="text-gray-900 dark:text-white font-medium flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-400" />
{formatDateSafe(viewingConsulta.dataHora)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Horário
</label>
<p className="text-gray-900 dark:text-white font-medium flex items-center gap-2">
<Clock className="h-4 w-4 text-gray-400" />
{formatTimeSafe(viewingConsulta.dataHora)}
</p>
</div>
</div>
</div>
{/* Tipo de Consulta */}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Tipo de Consulta
</h3>
<p className="text-gray-900 dark:text-white font-medium flex items-center gap-2">
{viewingConsulta.tipo === "online" ||
viewingConsulta.tipo === "telemedicina" ? (
<>
<Video className="h-5 w-5 text-indigo-600" />
Online / Telemedicina
</>
) : (
<>
<MapPin className="h-5 w-5 text-gray-600" />
Presencial
</>
)}
</p>
</div>
{/* Motivo da Consulta */}
{viewingConsulta.motivoConsulta && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<FileText className="h-5 w-5" />
Motivo da Consulta
</h3>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-gray-900 dark:text-white whitespace-pre-wrap">
{viewingConsulta.motivoConsulta}
</p>
</div>
</div>
)}
{/* Observações do Médico */}
{viewingConsulta.observacoes && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<FileText className="h-5 w-5" />
Observações do Médico
</h3>
<div className="bg-gray-50 dark:bg-slate-800 rounded-lg p-4">
<p className="text-gray-900 dark:text-white whitespace-pre-wrap">
{viewingConsulta.observacoes}
</p>
</div>
</div>
)}
{/* Ações */}
<div className="flex gap-3 pt-4 border-t border-gray-200 dark:border-slate-700">
<button
onClick={() => {
setViewModalOpen(false);
handleEditConsulta(viewingConsulta);
}}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors"
>
<Edit className="h-4 w-4" />
Editar Consulta
</button>
<button
onClick={() => setViewModalOpen(false)}
className="px-4 py-2 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"
>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal de Visualização de Relatório */}
{viewReportModalOpen && viewingReport && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={() => setViewReportModalOpen(false)}
>
<div
className="bg-white dark:bg-slate-900 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Detalhes do Relatório
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Número: {viewingReport.order_number}
</p>
</div>
<button
onClick={() => setViewReportModalOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="h-6 w-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Informações Básicas */}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<FileText className="h-5 w-5" />
Informações Básicas
</h3>
<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">
Tipo de Exame
</label>
<p className="text-gray-900 dark:text-white font-medium">
{viewingReport.exam || "-"}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<span
className={`inline-flex px-3 py-1 text-sm font-semibold rounded-full ${
viewingReport.status === "completed"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: viewingReport.status === "pending"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: viewingReport.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"
}`}
>
{viewingReport.status === "completed"
? "Concluído"
: viewingReport.status === "pending"
? "Pendente"
: viewingReport.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-gray-900 dark:text-white font-medium flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-400" />
{new Date(viewingReport.created_at).toLocaleDateString(
"pt-BR"
)}
</p>
</div>
{viewingReport.cid_code && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Código CID
</label>
<p className="text-gray-900 dark:text-white font-medium">
{viewingReport.cid_code}
</p>
</div>
)}
</div>
</div>
{/* Diagnóstico */}
{viewingReport.diagnosis && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Diagnóstico
</h3>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-gray-900 dark:text-white whitespace-pre-wrap">
{viewingReport.diagnosis}
</p>
</div>
</div>
)}
{/* Conclusão */}
{viewingReport.conclusion && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Conclusão
</h3>
<div className="bg-gray-50 dark:bg-slate-800 rounded-lg p-4">
<p className="text-gray-900 dark:text-white whitespace-pre-wrap">
{viewingReport.conclusion}
</p>
</div>
</div>
)}
{/* Conteúdo HTML */}
{viewingReport.content_html && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Detalhes do Relatório
</h3>
<div
className="bg-white dark:bg-slate-800 rounded-lg p-4 border border-gray-200 dark:border-slate-700 prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: viewingReport.content_html,
}}
/>
</div>
)}
{/* Botão Fechar */}
<div className="flex justify-end pt-4 border-t border-gray-200 dark:border-slate-700">
<button
onClick={() => setViewReportModalOpen(false)}
className="px-6 py-2 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"
>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
{relatorioModalOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={() => setRelatorioModalOpen(false)}
>
<div
className="bg-white dark:bg-slate-900 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Criar Novo Relatório
</h2>
</div>
<form onSubmit={handleCriarRelatorio} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Paciente *
</label>
<select
value={formRelatorio.patient_id}
onChange={(e) =>
setFormRelatorio((p) => ({
...p,
patient_id: e.target.value,
}))
}
required
className="form-input"
>
<option value="">Selecione um paciente</option>
{pacientesDisponiveis.map((p) => (
<option key={p.id} value={p.id}>
{p.nome}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tipo de Exame *
</label>
<input
type="text"
value={formRelatorio.exam}
onChange={(e) =>
setFormRelatorio((p) => ({ ...p, exam: e.target.value }))
}
required
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Diagnóstico
</label>
<textarea
value={formRelatorio.diagnosis}
onChange={(e) =>
setFormRelatorio((p) => ({
...p,
diagnosis: e.target.value,
}))
}
rows={3}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Conclusão
</label>
<textarea
value={formRelatorio.conclusion}
onChange={(e) =>
setFormRelatorio((p) => ({
...p,
conclusion: e.target.value,
}))
}
rows={3}
className="form-input"
/>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setRelatorioModalOpen(false)}
className="px-4 py-2 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-indigo-500"
>
Cancelar
</button>
<button
type="submit"
disabled={loadingRelatorio}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
{loadingRelatorio ? "Criando..." : "Criar Relatório"}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default PainelMedico;