guisilvagomes 3a3e4c1f55 fix: corrige exibição de nomes de médicos em laudos e relatórios
- Adiciona resolução de IDs para nomes nos laudos do painel do paciente
- Implementa dropdown de médicos nos formulários de relatórios
- Corrige API PATCH para retornar dados atualizados (header Prefer)
- Adiciona fallback para buscar relatório após update
- Limpa cache de nomes ao atualizar relatórios
- Trata dados legados (nomes diretos vs UUIDs)
- Exibe 'Médico não cadastrado' para IDs inexistentes
2025-11-05 18:25:13 -03:00

290 lines
9.5 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Calendar, Users, UserCheck, Clock, ArrowRight } from "lucide-react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { patientService, doctorService, appointmentService } from "../services";
import { MetricCard } from "../components/MetricCard";
import { HeroBanner } from "../components/HeroBanner";
import { i18n } from "../i18n";
import { useAuth } from "../hooks/useAuth";
import RecoveryRedirect from "../components/auth/RecoveryRedirect";
const Home: React.FC = () => {
const [stats, setStats] = useState({
totalPacientes: 0,
totalMedicos: 0,
consultasHoje: 0,
consultasPendentes: 0,
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { user } = useAuth();
// Verificar se há parâmetros de magic link e redirecionar para AuthCallback
useEffect(() => {
const hash = window.location.hash;
if (
hash &&
(hash.includes("access_token") || hash.includes("type=magiclink"))
) {
console.log(
"[Home] Detectado magic link, redirecionando para /auth/callback"
);
navigate(`/auth/callback${hash}`, { replace: true });
return;
}
}, [navigate]);
// Limpar cache se houver parâmetro ?clear=true
useEffect(() => {
if (searchParams.get("clear") === "true") {
console.log("🧹 Limpando cache via URL...");
localStorage.clear();
sessionStorage.clear();
// Remove o parâmetro da URL e recarrega
window.location.href = "/";
}
}, [searchParams]);
useEffect(() => {
// Só buscar estatísticas se o usuário estiver autenticado
if (user) {
fetchStats();
} else {
setLoading(false);
}
}, [user]);
const fetchStats = async () => {
try {
setLoading(true);
setError(false);
// Silenciar erros 401 (não autenticado) - são esperados na home pública
const [pacientes, medicos, consultasRaw] = await Promise.all([
patientService.list().catch((err) => {
if (err.response?.status !== 401)
console.error("Erro ao buscar pacientes:", err);
return [];
}),
doctorService.list().catch((err) => {
if (err.response?.status !== 401)
console.error("Erro ao buscar médicos:", err);
return [];
}),
appointmentService.list().catch((err) => {
if (err.response?.status !== 401)
console.error("Erro ao buscar consultas:", err);
return [];
}),
]);
// Ensure consultas is an array
const consultas = Array.isArray(consultasRaw) ? consultasRaw : [];
const hoje = new Date().toISOString().split("T")[0];
const consultasHoje = consultas.filter((c) =>
c.scheduled_at?.startsWith(hoje)
).length;
const consultasPendentes = consultas.filter(
(c) => c.status === "requested" || c.status === "confirmed"
).length;
setStats({
totalPacientes: pacientes.length,
totalMedicos: medicos.length,
consultasHoje,
consultasPendentes,
});
} catch (err) {
console.error("Erro ao carregar estatísticas:", err);
setError(true);
} finally {
setLoading(false);
}
};
const handleCTA = (action: string, destination: string) => {
console.log(`CTA clicked: ${action} -> ${destination}`);
navigate(destination);
};
return (
<div
className="space-y-6 sm:space-y-8 px-4 sm:px-6 lg:px-8"
id="main-content"
>
{/* Componente invisível que detecta tokens de recuperação e redireciona */}
<RecoveryRedirect />
{/* Hero Section com Background Rotativo */}
<HeroBanner />
{/* Métricas */}
<div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6"
role="region"
aria-label="Estatísticas do sistema"
>
<MetricCard
title={i18n.t("home.metrics.totalPatients")}
value={stats.totalPacientes}
icon={Users}
iconColor="text-blue-600"
iconBgColor="bg-blue-100"
description={i18n.t("home.metrics.totalPatientsDescription")}
loading={loading}
error={error}
ariaLabel={`${i18n.t("home.metrics.totalPatients")}: ${
stats.totalPacientes
}`}
/>
<MetricCard
title={i18n.t("home.metrics.activeDoctors")}
value={stats.totalMedicos}
icon={UserCheck}
iconColor="text-green-500"
iconBgColor="bg-green-50"
description={i18n.t("home.metrics.activeDoctorsDescription")}
loading={loading}
error={error}
ariaLabel={`${i18n.t("home.metrics.activeDoctors")}: ${
stats.totalMedicos
}`}
/>
<MetricCard
title={i18n.t("home.metrics.todayAppointments")}
value={stats.consultasHoje}
icon={Calendar}
iconColor="text-yellow-500"
iconBgColor="bg-yellow-50"
description={i18n.t("home.metrics.todayAppointmentsDescription")}
loading={loading}
error={error}
ariaLabel={`${i18n.t("home.metrics.todayAppointments")}: ${
stats.consultasHoje
}`}
/>
<MetricCard
title={i18n.t("home.metrics.pendingAppointments")}
value={stats.consultasPendentes}
icon={Clock}
iconColor="text-purple-500"
iconBgColor="bg-purple-50"
description={i18n.t("home.metrics.pendingAppointmentsDescription")}
loading={loading}
error={error}
ariaLabel={`${i18n.t("home.metrics.pendingAppointments")}: ${
stats.consultasPendentes
}`}
/>
</div>
{/* Cards de Ação */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 md:gap-6"
role="region"
aria-label="Ações rápidas"
>
<ActionCard
icon={Calendar}
iconColor="text-blue-600"
iconBgColor="bg-gradient-to-br from-blue-700 to-blue-400"
title={i18n.t("home.actionCards.scheduleAppointment.title")}
description={i18n.t(
"home.actionCards.scheduleAppointment.description"
)}
ctaLabel={i18n.t("home.actionCards.scheduleAppointment.cta")}
ctaAriaLabel={i18n.t(
"home.actionCards.scheduleAppointment.ctaAriaLabel"
)}
onAction={() => handleCTA("Card Agendar", "/paciente")}
/>
<ActionCard
icon={UserCheck}
iconColor="text-indigo-600"
iconBgColor="bg-gradient-to-br from-indigo-600 to-indigo-400"
title={i18n.t("home.actionCards.doctorPanel.title")}
description={i18n.t("home.actionCards.doctorPanel.description")}
ctaLabel={i18n.t("home.actionCards.doctorPanel.cta")}
ctaAriaLabel={i18n.t("home.actionCards.doctorPanel.ctaAriaLabel")}
onAction={() => handleCTA("Card Médico", "/login-medico")}
/>
<ActionCard
icon={Users}
iconColor="text-green-600"
iconBgColor="bg-gradient-to-br from-green-600 to-green-400"
title={i18n.t("home.actionCards.patientManagement.title")}
description={i18n.t("home.actionCards.patientManagement.description")}
ctaLabel={i18n.t("home.actionCards.patientManagement.cta")}
ctaAriaLabel={i18n.t(
"home.actionCards.patientManagement.ctaAriaLabel"
)}
onAction={() => handleCTA("Card Secretaria", "/login-secretaria")}
/>
</div>
</div>
);
};
// Action Card Component
interface ActionCardProps {
icon: React.ComponentType<{ className?: string }>;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
ctaLabel: string;
ctaAriaLabel: string;
onAction: () => void;
}
const ActionCard: React.FC<ActionCardProps> = ({
icon: Icon,
iconBgColor,
title,
description,
ctaLabel,
ctaAriaLabel,
onAction,
}) => {
return (
<div className="bg-white rounded-lg shadow-md p-4 sm:p-5 md:p-6 hover:shadow-xl transition-all duration-200 group border border-gray-100 focus-within:ring-2 focus-within:ring-blue-500/50 focus-within:ring-offset-2">
<div
className={`w-10 h-10 sm:w-12 sm:h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`}
>
<Icon
className={`w-5 h-5 sm:w-6 sm:h-6 text-white`}
aria-hidden="true"
/>
</div>
<h3 className="text-base sm:text-lg font-semibold mb-2 text-gray-900">
{title}
</h3>
<p className="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4 leading-relaxed">
{description}
</p>
<button
onClick={onAction}
className="w-full inline-flex items-center justify-center px-3 sm:px-4 py-2 sm:py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 text-white rounded-lg text-sm sm:text-base font-medium transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 group-hover:shadow-lg"
aria-label={ctaAriaLabel}
>
{ctaLabel}
<ArrowRight
className="w-3.5 h-3.5 sm:w-4 sm:h-4 ml-2 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</button>
</div>
);
};
export default Home;