Mudanças
This commit is contained in:
parent
9ec07aeea3
commit
62b741ff7a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
@ -93,7 +93,7 @@ const Header: React.FC = () => {
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex-shrink-0"
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-gray-100 hover:bg-gray-200 hover:scale-105 active:scale-95 text-gray-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex-shrink-0"
|
||||
aria-label={i18n.t("header.logout")}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||
@ -106,7 +106,7 @@ const Header: React.FC = () => {
|
||||
) : (
|
||||
<Link
|
||||
to="/paciente"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 text-white transition-all focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm hover:shadow-md"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md 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 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm hover:shadow-md"
|
||||
aria-label={i18n.t("header.login")}
|
||||
>
|
||||
<LogIn className="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
|
||||
@ -3,6 +3,7 @@ import { User, Stethoscope, Clipboard, ChevronDown } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { i18n } from "../i18n";
|
||||
import { telemetry } from "../services/telemetry";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
|
||||
export type ProfileType = "patient" | "doctor" | "secretary" | null;
|
||||
|
||||
@ -51,6 +52,7 @@ export const ProfileSelector: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// Carregar perfil salvo
|
||||
@ -96,8 +98,49 @@ export const ProfileSelector: React.FC = () => {
|
||||
// Telemetria
|
||||
telemetry.trackProfileChange(previousProfile, profile.type || "none");
|
||||
|
||||
// Navegar
|
||||
navigate(profile.path);
|
||||
// Navegar - condicional baseado em autenticação e role
|
||||
let targetPath = profile.path; // default: caminho do perfil (login)
|
||||
|
||||
if (isAuthenticated && user) {
|
||||
// Se autenticado, redirecionar para o painel apropriado baseado na role
|
||||
switch (user.role) {
|
||||
case "paciente":
|
||||
if (profile.type === "patient") {
|
||||
targetPath = "/acompanhamento"; // painel do paciente
|
||||
}
|
||||
break;
|
||||
case "medico":
|
||||
if (profile.type === "doctor") {
|
||||
targetPath = "/painel-medico"; // painel do médico
|
||||
}
|
||||
break;
|
||||
case "secretaria":
|
||||
if (profile.type === "secretary") {
|
||||
targetPath = "/painel-secretaria"; // painel da secretária
|
||||
}
|
||||
break;
|
||||
case "admin":
|
||||
// Admin pode ir para qualquer painel
|
||||
if (profile.type === "secretary") {
|
||||
targetPath = "/painel-secretaria";
|
||||
} else if (profile.type === "doctor") {
|
||||
targetPath = "/painel-medico";
|
||||
} else if (profile.type === "patient") {
|
||||
targetPath = "/acompanhamento";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🔀 ProfileSelector: Usuário autenticado (${user.role}), redirecionando para ${targetPath}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`🔀 ProfileSelector: Usuário NÃO autenticado, redirecionando para ${targetPath}`
|
||||
);
|
||||
}
|
||||
|
||||
navigate(targetPath);
|
||||
};
|
||||
|
||||
const getCurrentProfile = () => {
|
||||
|
||||
@ -12,18 +12,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
roles,
|
||||
redirectTo = "/",
|
||||
}) => {
|
||||
const { isAuthenticated, role, loading } = useAuth();
|
||||
const { isAuthenticated, role, loading, user } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
console.log("[ProtectedRoute]", {
|
||||
console.log("[ProtectedRoute] VERIFICAÇÃO COMPLETA", {
|
||||
path: location.pathname,
|
||||
isAuthenticated,
|
||||
role,
|
||||
loading,
|
||||
requiredRoles: roles,
|
||||
user: user ? { id: user.id, nome: user.nome, email: user.email } : null,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Verificar localStorage para debug
|
||||
try {
|
||||
const stored = localStorage.getItem("appSession");
|
||||
console.log(
|
||||
"[ProtectedRoute] localStorage appSession:",
|
||||
stored ? JSON.parse(stored) : null
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[ProtectedRoute] Erro ao ler localStorage:", e);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
console.log("[ProtectedRoute] ⏳ Ainda carregando sessão...");
|
||||
return (
|
||||
<div className="py-10 text-center text-sm text-gray-500">
|
||||
Verificando sessão...
|
||||
@ -33,12 +47,16 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
|
||||
if (!isAuthenticated) {
|
||||
console.log(
|
||||
"[ProtectedRoute] Não autenticado, redirecionando para:",
|
||||
"[ProtectedRoute] ❌ NÃO AUTENTICADO! User:",
|
||||
user,
|
||||
"Redirecionando para:",
|
||||
redirectTo
|
||||
);
|
||||
return <Navigate to={redirectTo} state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
console.log("[ProtectedRoute] ✅ Autenticado! Verificando roles...");
|
||||
|
||||
// Admin tem acesso a tudo
|
||||
if (role === "admin") {
|
||||
console.log("[ProtectedRoute] Admin detectado, permitindo acesso");
|
||||
|
||||
@ -88,43 +88,164 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
const [user, setUser] = useState<SessionUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Restaurar sessão do localStorage e verificar token
|
||||
// Log sempre que user ou loading mudar
|
||||
useEffect(() => {
|
||||
console.log("[AuthContext] 🔄 ESTADO MUDOU:", {
|
||||
user: user ? { id: user.id, nome: user.nome, role: user.role } : null,
|
||||
loading,
|
||||
isAuthenticated: !!user,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}, [user, loading]);
|
||||
|
||||
// RE-VERIFICAR sessão quando user estiver null mas localStorage tiver dados
|
||||
// Isso corrige o problema de navegação entre páginas perdendo o estado
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
console.log(
|
||||
"[AuthContext] 🔍 User é null mas loading false, verificando localStorage..."
|
||||
);
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as PersistedSession;
|
||||
if (parsed?.user?.role) {
|
||||
console.log(
|
||||
"[AuthContext] 🔧 RECUPERANDO sessão perdida:",
|
||||
parsed.user.nome
|
||||
);
|
||||
setUser(parsed.user);
|
||||
|
||||
// Restaurar tokens também
|
||||
if (parsed.token) {
|
||||
import("../services/tokenStore").then((module) => {
|
||||
module.default.setTokens(parsed.token!, parsed.refreshToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[AuthContext] Erro ao recuperar sessão:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [user, loading]);
|
||||
|
||||
// Monitorar mudanças no localStorage para debug
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === STORAGE_KEY) {
|
||||
console.log("[AuthContext] 📢 localStorage MUDOU externamente!", {
|
||||
oldValue: e.oldValue ? "TINHA DADOS" : "VAZIO",
|
||||
newValue: e.newValue ? "TEM DADOS" : "VAZIO",
|
||||
url: e.url,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
return () => window.removeEventListener("storage", handleStorageChange);
|
||||
}, []);
|
||||
|
||||
// Restaurar sessão do localStorage e verificar token
|
||||
// IMPORTANTE: Este useEffect roda apenas UMA VEZ quando o AuthProvider monta
|
||||
useEffect(() => {
|
||||
console.log("[AuthContext] 🚀 INICIANDO RESTAURAÇÃO DE SESSÃO (mount)");
|
||||
console.log("[AuthContext] 🔍 Verificando TODOS os itens no localStorage:");
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
const value = localStorage.getItem(key);
|
||||
console.log(` - ${key}: ${value?.substring(0, 50)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
const restoreSession = async () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
// Tentar localStorage primeiro, depois sessionStorage como backup
|
||||
let raw = localStorage.getItem(STORAGE_KEY);
|
||||
console.log(
|
||||
"[AuthContext] localStorage raw:",
|
||||
raw ? "EXISTE" : "VAZIO"
|
||||
);
|
||||
|
||||
if (!raw) {
|
||||
console.log(
|
||||
"[AuthContext] 🔍 localStorage vazio, tentando sessionStorage..."
|
||||
);
|
||||
raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
console.log(
|
||||
"[AuthContext] sessionStorage raw:",
|
||||
raw ? "EXISTE" : "VAZIO"
|
||||
);
|
||||
|
||||
if (raw) {
|
||||
// Restaurar do sessionStorage para localStorage
|
||||
console.log(
|
||||
"[AuthContext] 🔄 Restaurando do sessionStorage para localStorage"
|
||||
);
|
||||
localStorage.setItem(STORAGE_KEY, raw);
|
||||
}
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
console.log(
|
||||
"[AuthContext] Conteúdo completo:",
|
||||
raw.substring(0, 100) + "..."
|
||||
);
|
||||
}
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as PersistedSession;
|
||||
if (parsed?.user?.role) {
|
||||
console.log("[AuthContext] Restaurando sessão:", parsed.user);
|
||||
console.log("[AuthContext] ✅ Restaurando sessão:", {
|
||||
nome: parsed.user.nome,
|
||||
role: parsed.user.role,
|
||||
hasToken: !!parsed.token,
|
||||
});
|
||||
|
||||
// Verificar se há tokens válidos salvos
|
||||
if (parsed.token) {
|
||||
console.log("[AuthContext] Restaurando tokens no tokenStore");
|
||||
// Restaurar tokens no tokenStore
|
||||
const tokenStore = (await import("../services/tokenStore"))
|
||||
.default;
|
||||
tokenStore.setTokens(parsed.token, parsed.refreshToken);
|
||||
} else {
|
||||
console.warn("[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore...");
|
||||
// Verificar se há token no tokenStore (pode ter sido salvo diretamente)
|
||||
console.warn(
|
||||
"[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore..."
|
||||
);
|
||||
const tokenStore = (await import("../services/tokenStore"))
|
||||
.default;
|
||||
const existingToken = tokenStore.getAccessToken();
|
||||
if (existingToken) {
|
||||
console.log("[AuthContext] Token encontrado no tokenStore, mantendo sessão");
|
||||
console.log(
|
||||
"[AuthContext] Token encontrado no tokenStore, mantendo sessão"
|
||||
);
|
||||
} else {
|
||||
console.warn("[AuthContext] Nenhum token encontrado. Sessão pode estar inválida.");
|
||||
console.warn(
|
||||
"[AuthContext] ⚠️ Nenhum token encontrado. Sessão pode estar inválida."
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
"[AuthContext] 📝 Chamando setUser com:",
|
||||
parsed.user.nome
|
||||
);
|
||||
setUser(parsed.user);
|
||||
} else {
|
||||
console.log(
|
||||
"[AuthContext] ⚠️ Sessão parseada mas sem user.role válido"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("[AuthContext] Nenhuma sessão salva encontrada");
|
||||
console.log(
|
||||
"[AuthContext] ℹ️ Nenhuma sessão salva encontrada no localStorage"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AuthContext] Erro ao restaurar sessão:", error);
|
||||
console.error("[AuthContext] ❌ Erro ao restaurar sessão:", error);
|
||||
} finally {
|
||||
console.log(
|
||||
"[AuthContext] 🏁 Finalizando restauração, setLoading(false)"
|
||||
);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@ -133,17 +254,37 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
const persist = useCallback((session: PersistedSession) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
||||
} catch {
|
||||
/* ignore */
|
||||
console.log(
|
||||
"[AuthContext] 💾 SALVANDO sessão no localStorage E sessionStorage:",
|
||||
{
|
||||
user: session.user.nome,
|
||||
role: session.user.role,
|
||||
hasToken: !!session.token,
|
||||
}
|
||||
);
|
||||
const sessionStr = JSON.stringify(session);
|
||||
localStorage.setItem(STORAGE_KEY, sessionStr);
|
||||
sessionStorage.setItem(STORAGE_KEY, sessionStr); // BACKUP em sessionStorage
|
||||
console.log(
|
||||
"[AuthContext] ✅ Sessão salva com sucesso em ambos storages!"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[AuthContext] ❌ ERRO ao salvar sessão:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearPersisted = useCallback(() => {
|
||||
try {
|
||||
console.log(
|
||||
"[AuthContext] 🗑️ REMOVENDO sessão do localStorage E sessionStorage"
|
||||
);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
console.log(
|
||||
"[AuthContext] ✅ Sessão removida com sucesso de ambos storages!"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[AuthContext] ❌ ERRO ao remover sessão:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -353,18 +494,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
console.log("[AuthContext] Iniciando logout...");
|
||||
try {
|
||||
const resp = await authService.logout(); // chama /auth/v1/logout (204 esperado)
|
||||
if (!resp.success && resp.error) {
|
||||
console.warn("[AuthContext] Falha no logout remoto:", resp.error);
|
||||
toast.error(`Falha no logout remoto: ${resp.error}`);
|
||||
} else {
|
||||
toast.success("Sessão encerrada no servidor");
|
||||
console.log("[AuthContext] Logout remoto bem-sucedido");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Erro inesperado ao executar logout remoto", e);
|
||||
toast("Logout local (falha remota)");
|
||||
console.warn(
|
||||
"[AuthContext] Erro inesperado ao executar logout remoto",
|
||||
e
|
||||
);
|
||||
} finally {
|
||||
// Limpa contexto local
|
||||
console.log("[AuthContext] Limpando estado local...");
|
||||
setUser(null);
|
||||
clearPersisted();
|
||||
authService.clearLocalAuth();
|
||||
@ -373,6 +519,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
console.log("[AuthContext] Logout completo - usuário removido do estado");
|
||||
// Modelo somente Supabase: nenhum token técnico para invalidar
|
||||
}
|
||||
}, [clearPersisted]);
|
||||
|
||||
@ -104,7 +104,7 @@ const Home: React.FC = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
|
||||
<button
|
||||
onClick={() => handleCTA("Agendar consulta", "/paciente")}
|
||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600"
|
||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600"
|
||||
aria-label={i18n.t(
|
||||
"home.actionCards.scheduleAppointment.ctaAriaLabel"
|
||||
)}
|
||||
@ -122,7 +122,7 @@ const Home: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={() => handleCTA("Ver próximas consultas", "/consultas")}
|
||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl border-2 border-white/20 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-blue-600"
|
||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-blue-600"
|
||||
aria-label="Ver lista de próximas consultas"
|
||||
>
|
||||
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
|
||||
@ -278,7 +278,7 @@ const ActionCard: React.FC<ActionCardProps> = ({
|
||||
</p>
|
||||
<button
|
||||
onClick={onAction}
|
||||
className="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 text-white rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 group-hover:shadow-lg"
|
||||
className="w-full inline-flex items-center justify-center px-4 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 font-medium transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 group-hover:shadow-lg"
|
||||
aria-label={ctaAriaLabel}
|
||||
>
|
||||
{ctaLabel}
|
||||
|
||||
@ -4,8 +4,6 @@ import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
|
||||
// interface Medico is not required in this component
|
||||
|
||||
const LoginMedico: React.FC = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
@ -14,30 +12,61 @@ const LoginMedico: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { loginComEmailSenha } = useAuth();
|
||||
|
||||
const { loginMedico, loginComEmailSenha } = useAuth();
|
||||
// Credenciais fixas para LOGIN LOCAL de médico
|
||||
const LOCAL_MEDICO = {
|
||||
email: "fernando.pirichowski@souunit.com.br",
|
||||
senha: "fernando",
|
||||
nome: "Dr. Fernando Pirichowski",
|
||||
id: "fernando.pirichowski@souunit.com.br",
|
||||
} as const;
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Primeiro tenta fluxo real Supabase (grant_type=password)
|
||||
let ok = await loginComEmailSenha(formData.email, formData.senha);
|
||||
// Se falhar (ex: usuário não mapeado ainda), cai no fallback legado de médico
|
||||
if (!ok) {
|
||||
ok = await loginMedico(formData.email, formData.senha);
|
||||
console.log("[LoginMedico] Fazendo login com email:", formData.email);
|
||||
|
||||
const authService = (await import("../services/authService")).default;
|
||||
const loginResult = await authService.login({
|
||||
email: formData.email,
|
||||
password: formData.senha,
|
||||
});
|
||||
|
||||
if (!loginResult.success) {
|
||||
console.log("[LoginMedico] Erro no login:", loginResult.error);
|
||||
toast.error(loginResult.error || "Email ou senha incorretos");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[LoginMedico] Login bem-sucedido!", loginResult.data);
|
||||
|
||||
const tokenStore = (await import("../services/tokenStore")).default;
|
||||
const token = tokenStore.getAccessToken();
|
||||
console.log("[LoginMedico] Token salvo:", token ? "SIM" : "NÃO");
|
||||
|
||||
if (!token) {
|
||||
console.error("[LoginMedico] Token não foi salvo!");
|
||||
toast.error("Erro ao salvar credenciais de autenticação");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await loginComEmailSenha(formData.email, formData.senha);
|
||||
|
||||
if (ok) {
|
||||
// Login bem-sucedido, redirecionar para painel médico
|
||||
// A verificação de permissões será feita pelo ProtectedRoute
|
||||
console.log("[LoginMedico] Login realizado, redirecionando...");
|
||||
console.log("[LoginMedico] Navegando para /painel-medico");
|
||||
toast.success("Login realizado com sucesso!");
|
||||
navigate("/painel-medico");
|
||||
} else {
|
||||
console.error("[LoginMedico] loginComEmailSenha retornou false");
|
||||
toast.error("Erro ao processar login");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro no login:", error);
|
||||
console.error("[LoginMedico] Erro no login:", error);
|
||||
toast.error("Erro ao fazer login. Tente novamente.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -47,7 +76,6 @@ const LoginMedico: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-indigo-400 dark:from-indigo-700 dark:to-indigo-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
|
||||
<Stethoscope className="w-8 h-8 text-white" />
|
||||
@ -60,7 +88,6 @@ const LoginMedico: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Formulário */}
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors"
|
||||
aria-live="polite"
|
||||
@ -83,7 +110,7 @@ const LoginMedico: React.FC = () => {
|
||||
setFormData((prev) => ({ ...prev, email: e.target.value }))
|
||||
}
|
||||
className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
placeholder="dr.medico@clinica.com"
|
||||
placeholder="seu@email.com"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
@ -117,22 +144,16 @@ const LoginMedico: React.FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
||||
>
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Informações de demonstração */}
|
||||
<div className="mt-6 p-4 bg-indigo-50 dark:bg-gray-700/40 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-indigo-800 dark:text-indigo-300 mb-2">
|
||||
Para Demonstração:
|
||||
</h3>
|
||||
<p className="text-sm text-indigo-700 dark:text-indigo-200">
|
||||
Email:riseup@popcode.com.br <br />
|
||||
Senha: riseup
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
<strong>{LOCAL_MEDICO.email}</strong> /{" "}
|
||||
<strong>{LOCAL_MEDICO.senha}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -59,10 +59,10 @@ const LoginPaciente: React.FC = () => {
|
||||
|
||||
// Credenciais fixas para LOGIN LOCAL de paciente
|
||||
const LOCAL_PATIENT = {
|
||||
email: "pedro.araujo@mediconnect.com",
|
||||
senha: "local123",
|
||||
nome: "Pedro Araujo",
|
||||
id: "pedro.araujo@mediconnect.com",
|
||||
email: "guilhermesilvagomes1020@gmail.com",
|
||||
senha: "guilherme123",
|
||||
nome: "Guilherme Silva Gomes",
|
||||
id: "guilhermesilvagomes1020@gmail.com",
|
||||
} as const;
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
@ -359,13 +359,13 @@ const LoginPaciente: React.FC = () => {
|
||||
type="button"
|
||||
onClick={handleLoginLocal}
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
||||
>
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</button>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
Credenciais locais: <strong>{LOCAL_PATIENT.email}</strong> /
|
||||
<strong> {LOCAL_PATIENT.senha}</strong>
|
||||
<strong>{LOCAL_PATIENT.email}</strong> /{" "}
|
||||
<strong>{LOCAL_PATIENT.senha}</strong>
|
||||
</p>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
@ -10,40 +10,64 @@ const LoginSecretaria: React.FC = () => {
|
||||
senha: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCadastro, setShowCadastro] = useState(false);
|
||||
const [cadastroData, setCadastroData] = useState({
|
||||
nome: "",
|
||||
email: "",
|
||||
senha: "",
|
||||
confirmarSenha: "",
|
||||
telefone: "",
|
||||
cpf: "",
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { loginComEmailSenha } = useAuth();
|
||||
|
||||
// Credenciais fixas para LOGIN LOCAL de secretaria
|
||||
const LOCAL_SECRETARIA = {
|
||||
email: "secretaria.mediconnect@gmail.com",
|
||||
senha: "secretaria@mediconnect",
|
||||
nome: "Secretaria MediConnect",
|
||||
id: "secretaria.mediconnect@gmail.com",
|
||||
} as const;
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
console.log("[LoginSecretaria] Tentando login com:", formData.email);
|
||||
// Tenta login real via authService primeiro
|
||||
console.log("[LoginSecretaria] Fazendo login com email:", formData.email);
|
||||
|
||||
const authService = (await import("../services/authService")).default;
|
||||
const loginResult = await authService.login({
|
||||
email: formData.email,
|
||||
password: formData.senha,
|
||||
});
|
||||
|
||||
if (!loginResult.success) {
|
||||
console.log("[LoginSecretaria] Erro no login:", loginResult.error);
|
||||
toast.error(loginResult.error || "Email ou senha incorretos");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[LoginSecretaria] Login bem-sucedido!", loginResult.data);
|
||||
|
||||
const tokenStore = (await import("../services/tokenStore")).default;
|
||||
const token = tokenStore.getAccessToken();
|
||||
console.log("[LoginSecretaria] Token salvo:", token ? "SIM" : "NÃO");
|
||||
|
||||
if (!token) {
|
||||
console.error("[LoginSecretaria] Token não foi salvo!");
|
||||
toast.error("Erro ao salvar credenciais de autenticação");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await loginComEmailSenha(formData.email, formData.senha);
|
||||
console.log("[LoginSecretaria] Resultado login:", ok);
|
||||
|
||||
if (ok) {
|
||||
console.log("[LoginSecretaria] Login bem-sucedido, redirecionando...");
|
||||
console.log("[LoginSecretaria] Navegando para /painel-secretaria");
|
||||
toast.success("Login realizado com sucesso!");
|
||||
navigate("/painel-secretaria");
|
||||
} else {
|
||||
console.error("[LoginSecretaria] Login falhou - credenciais inválidas");
|
||||
toast.error("Email ou senha incorretos");
|
||||
console.error("[LoginSecretaria] loginComEmailSenha retornou false");
|
||||
toast.error("Erro ao processar login");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[LoginSecretaria] Erro no login:", error);
|
||||
toast.error("Erro ao fazer login. Verifique suas credenciais.");
|
||||
toast.error("Erro ao fazer login. Tente novamente.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -52,27 +76,22 @@ const LoginSecretaria: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="bg-gradient-to-r from-green-600 to-green-400 dark:from-green-700 dark:to-green-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
|
||||
<Clipboard className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{showCadastro ? "Criar Conta de Secretária" : "Área da Secretaria"}
|
||||
Área da Secretaria
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{showCadastro
|
||||
? "Preencha os dados para criar uma conta de secretária"
|
||||
: "Faça login para acessar o sistema de gestão"}
|
||||
Faça login para acessar o sistema de gestão
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Formulário */}
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors"
|
||||
aria-live="polite"
|
||||
>
|
||||
{!showCadastro ? (
|
||||
<form onSubmit={handleLogin} className="space-y-6" noValidate>
|
||||
<div>
|
||||
<label
|
||||
@ -88,13 +107,10 @@ const LoginSecretaria: React.FC = () => {
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}))
|
||||
setFormData((prev) => ({ ...prev, email: e.target.value }))
|
||||
}
|
||||
className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
placeholder="secretaria@clinica.com"
|
||||
placeholder="seu@email.com"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
@ -115,10 +131,7 @@ const LoginSecretaria: React.FC = () => {
|
||||
type="password"
|
||||
value={formData.senha}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
senha: e.target.value,
|
||||
}))
|
||||
setFormData((prev) => ({ ...prev, senha: e.target.value }))
|
||||
}
|
||||
className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
placeholder="Sua senha"
|
||||
@ -127,204 +140,20 @@ const LoginSecretaria: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
Email:riseup@popcode.com.br <br />
|
||||
Senha: riseup
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="w-full bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
||||
>
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast(
|
||||
"Cadastro de secretária não disponível. Entre em contato com o administrador."
|
||||
);
|
||||
}}
|
||||
className="space-y-4"
|
||||
noValidate
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_nome"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Nome Completo
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_nome"
|
||||
type="text"
|
||||
value={cadastroData.nome}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
nome: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
required
|
||||
autoComplete="name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_cpf"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
CPF
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_cpf"
|
||||
type="text"
|
||||
value={cadastroData.cpf}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
cpf: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
placeholder="000.000.000-00"
|
||||
required
|
||||
inputMode="numeric"
|
||||
pattern="^\d{3}\.\d{3}\.\d{3}-\d{2}$|^\d{11}$"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_tel"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Telefone
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_tel"
|
||||
type="tel"
|
||||
value={cadastroData.telefone}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
telefone: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
placeholder="(11) 99999-9999"
|
||||
required
|
||||
inputMode="numeric"
|
||||
pattern="^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$"
|
||||
autoComplete="tel"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_email"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_email"
|
||||
type="email"
|
||||
value={cadastroData.email}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_senha"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Senha
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_senha"
|
||||
type="password"
|
||||
value={cadastroData.senha}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
senha: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
minLength={6}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sec_cad_confirma"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Confirmar Senha
|
||||
</label>
|
||||
<input
|
||||
id="sec_cad_confirma"
|
||||
type="password"
|
||||
value={cadastroData.confirmarSenha}
|
||||
onChange={(e) =>
|
||||
setCadastroData((prev) => ({
|
||||
...prev,
|
||||
confirmarSenha: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
aria-invalid={
|
||||
cadastroData.confirmarSenha !== "" &&
|
||||
cadastroData.confirmarSenha !== cadastroData.senha
|
||||
}
|
||||
aria-describedby={
|
||||
cadastroData.confirmarSenha !== "" &&
|
||||
cadastroData.confirmarSenha !== cadastroData.senha
|
||||
? "sec_senha_help"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{cadastroData.confirmarSenha !== "" &&
|
||||
cadastroData.confirmarSenha !== cadastroData.senha && (
|
||||
<p
|
||||
id="sec_senha_help"
|
||||
className="mt-1 text-xs text-red-400"
|
||||
>
|
||||
As senhas não coincidem.
|
||||
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
<strong>{LOCAL_SECRETARIA.email}</strong> /{" "}
|
||||
<strong>{LOCAL_SECRETARIA.senha}</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCadastro(false)}
|
||||
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Voltar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{loading ? "Cadastrando..." : "Cadastrar"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -379,14 +379,21 @@ const PainelAdmin: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[PainelAdmin] Deletando paciente:", { id, nome });
|
||||
|
||||
const response = await deletePatient(id);
|
||||
|
||||
console.log("[PainelAdmin] Resultado da deleção:", response);
|
||||
|
||||
if (response.success) {
|
||||
toast.success("Paciente deletado com sucesso!");
|
||||
loadPacientes();
|
||||
} else {
|
||||
console.error("[PainelAdmin] Falha ao deletar:", response.error);
|
||||
toast.error(response.error || "Erro ao deletar paciente");
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error("[PainelAdmin] Erro ao deletar paciente:", error);
|
||||
toast.error("Erro ao deletar paciente");
|
||||
}
|
||||
};
|
||||
@ -395,12 +402,12 @@ const PainelAdmin: React.FC = () => {
|
||||
const handleEditMedico = (medico: Doctor) => {
|
||||
setEditingMedico(medico);
|
||||
setFormMedico({
|
||||
crm: medico.crm,
|
||||
crm_uf: medico.crm_uf,
|
||||
crm: medico.crm || "",
|
||||
crm_uf: medico.crm_uf || "SP",
|
||||
specialty: medico.specialty || "",
|
||||
full_name: medico.full_name,
|
||||
cpf: medico.cpf,
|
||||
email: medico.email,
|
||||
full_name: medico.full_name || "",
|
||||
cpf: medico.cpf || "",
|
||||
email: medico.email || "",
|
||||
phone_mobile: medico.phone_mobile || "",
|
||||
phone2: medico.phone2 || "",
|
||||
cep: medico.cep || "",
|
||||
@ -982,7 +989,10 @@ const PainelAdmin: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleDeleteMedico(m.id!, m.full_name)
|
||||
handleDeleteMedico(
|
||||
m.id!,
|
||||
m.full_name || "Médico sem nome"
|
||||
)
|
||||
}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Deletar"
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import {
|
||||
Activity,
|
||||
Calendar,
|
||||
@ -41,6 +42,7 @@ import {
|
||||
type EnderecoPaciente,
|
||||
type Paciente as PacienteServiceModel,
|
||||
} from "../services/pacienteService";
|
||||
import relatorioService, { type Relatorio } from "../services/relatorioService";
|
||||
|
||||
// Tipos e constantes reinseridos após refatoração
|
||||
type TabId = "dashboard" | "pacientes" | "medicos" | "consultas" | "relatorios";
|
||||
@ -402,7 +404,6 @@ const maskCep = (value: string) => {
|
||||
return digits;
|
||||
};
|
||||
|
||||
|
||||
const splitTelefone = (telefone?: string) => {
|
||||
if (!telefone) {
|
||||
return { codigoPais: "55", ddd: "", numeroTelefone: "" };
|
||||
@ -557,6 +558,7 @@ const buildMedicoTelefone = (value: string) => {
|
||||
|
||||
const PainelSecretaria = () => {
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuth();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabId>("pacientes");
|
||||
@ -648,13 +650,13 @@ const PainelSecretaria = () => {
|
||||
try {
|
||||
const response = await relatorioService.listarRelatorios();
|
||||
if (response.success && response.data) {
|
||||
setRelatorios(response.data.data);
|
||||
setRelatorios(response.data);
|
||||
} else {
|
||||
toast.error('Erro ao carregar relatórios');
|
||||
toast.error("Erro ao carregar relatórios");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar relatórios:', error);
|
||||
toast.error('Erro ao carregar relatórios');
|
||||
console.error("Erro ao carregar relatórios:", error);
|
||||
toast.error("Erro ao carregar relatórios");
|
||||
} finally {
|
||||
setLoadingRelatorios(false);
|
||||
}
|
||||
@ -663,7 +665,11 @@ const PainelSecretaria = () => {
|
||||
const carregarDados = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await Promise.all([carregarPacientes(), carregarMedicos(), carregarRelatorios()]);
|
||||
await Promise.all([
|
||||
carregarPacientes(),
|
||||
carregarMedicos(),
|
||||
carregarRelatorios(),
|
||||
]);
|
||||
setConsultas([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -675,7 +681,7 @@ const PainelSecretaria = () => {
|
||||
}, [carregarDados]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'relatorios') {
|
||||
if (activeTab === "relatorios") {
|
||||
void carregarRelatorios();
|
||||
}
|
||||
}, [activeTab, carregarRelatorios]);
|
||||
@ -689,11 +695,11 @@ const PainelSecretaria = () => {
|
||||
}, []);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
localStorage.removeItem("authToken");
|
||||
localStorage.removeItem("token");
|
||||
console.log("[PainelSecretaria] Fazendo logout...");
|
||||
logout();
|
||||
toast.success("Sessão encerrada");
|
||||
navigate("/login-secretaria");
|
||||
}, [navigate]);
|
||||
}, [logout, navigate]);
|
||||
|
||||
const openCreatePacienteModal = useCallback(() => {
|
||||
resetPacienteForm();
|
||||
@ -774,11 +780,24 @@ const PainelSecretaria = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deletePatient(paciente.id);
|
||||
console.log("[PainelSecretaria] Deletando paciente:", {
|
||||
id: paciente.id,
|
||||
nome: paciente.nome,
|
||||
});
|
||||
|
||||
const result = await deletePatient(paciente.id);
|
||||
|
||||
console.log("[PainelSecretaria] Resultado da deleção:", result);
|
||||
|
||||
if (result.success) {
|
||||
setPacientes((prev) => prev.filter((p) => p.id !== paciente.id));
|
||||
toast.success("Paciente removido");
|
||||
toast.success("Paciente removido com sucesso");
|
||||
} else {
|
||||
console.error("[PainelSecretaria] Falha ao deletar:", result.error);
|
||||
toast.error(result.error || "Erro ao remover paciente");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao remover paciente:", error);
|
||||
console.error("[PainelSecretaria] Erro ao remover paciente:", error);
|
||||
toast.error("Erro ao remover paciente");
|
||||
}
|
||||
}, []);
|
||||
@ -801,10 +820,13 @@ const PainelSecretaria = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCpfChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleCpfChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { formatted } = maskCpf(event.target.value);
|
||||
setFormDataPaciente((prev) => ({ ...prev, cpf: formatted }));
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCepLookup = useCallback(async (rawCep: string) => {
|
||||
const digits = rawCep.replace(/\D/g, "");
|
||||
@ -1855,7 +1877,9 @@ const PainelSecretaria = () => {
|
||||
<section className="bg-white rounded-lg shadow">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Relatórios</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Relatórios
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{loadingRelatorios ? (
|
||||
@ -1871,11 +1895,21 @@ const PainelSecretaria = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Número</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exame</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Paciente</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Número
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Exame
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Paciente
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Data
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
@ -1888,20 +1922,37 @@ const PainelSecretaria = () => {
|
||||
{relatorio.exam}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{pacientes.find(p => p.id === relatorio.patient_id)?.nome || relatorio.patient_id}
|
||||
{pacientes.find(
|
||||
(p) => p.id === relatorio.patient_id
|
||||
)?.nome || relatorio.patient_id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
relatorio.status === 'draft' ? 'bg-gray-100 text-gray-800' :
|
||||
relatorio.status === 'final' ? 'bg-green-100 text-green-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{relatorio.status === 'draft' ? 'Rascunho' :
|
||||
relatorio.status === 'final' ? 'Final' : 'Preliminar'}
|
||||
<span
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
relatorio.status === "draft"
|
||||
? "bg-gray-100 text-gray-800"
|
||||
: relatorio.status === "completed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: relatorio.status === "pending"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{relatorio.status === "draft"
|
||||
? "Rascunho"
|
||||
: relatorio.status === "completed"
|
||||
? "Concluído"
|
||||
: relatorio.status === "pending"
|
||||
? "Pendente"
|
||||
: "Cancelado"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(relatorio.created_at).toLocaleDateString('pt-BR')}
|
||||
{relatorio.created_at
|
||||
? new Date(
|
||||
relatorio.created_at
|
||||
).toLocaleDateString("pt-BR")
|
||||
: "-"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@ -1936,7 +1987,10 @@ const PainelSecretaria = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
<form onSubmit={handleSubmitPaciente} className="space-y-4 max-h-[70vh] overflow-y-auto px-1">
|
||||
<form
|
||||
onSubmit={handleSubmitPaciente}
|
||||
className="space-y-4 max-h-[70vh] overflow-y-auto px-1"
|
||||
>
|
||||
{/* Seção: Dados Pessoais */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-green-600 uppercase tracking-wide border-b pb-1">
|
||||
@ -2251,7 +2305,9 @@ const PainelSecretaria = () => {
|
||||
type="text"
|
||||
value={maskCep(formDataPaciente.endereco.cep || "")}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/\D/g, "").slice(0, 8);
|
||||
const digits = event.target.value
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 8);
|
||||
setFormDataPaciente((prev) => ({
|
||||
...prev,
|
||||
endereco: {
|
||||
@ -2272,7 +2328,9 @@ const PainelSecretaria = () => {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCepLookup(formDataPaciente.endereco.cep || "")}
|
||||
onClick={() =>
|
||||
handleCepLookup(formDataPaciente.endereco.cep || "")
|
||||
}
|
||||
className="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors shadow-sm hover:shadow-md"
|
||||
title="Buscar endereço pelo CEP"
|
||||
>
|
||||
@ -2489,8 +2547,10 @@ const PainelSecretaria = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
|
||||
<form onSubmit={handleSubmitMedico} className="space-y-4 max-h-[70vh] overflow-y-auto px-1">
|
||||
<form
|
||||
onSubmit={handleSubmitMedico}
|
||||
className="space-y-4 max-h-[70vh] overflow-y-auto px-1"
|
||||
>
|
||||
{/* Seção: Dados Pessoais */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-blue-600 uppercase tracking-wide border-b pb-1">
|
||||
@ -2618,7 +2678,35 @@ const PainelSecretaria = () => {
|
||||
required
|
||||
>
|
||||
<option value="">Selecione</option>
|
||||
{["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"].map((uf) => (
|
||||
{[
|
||||
"AC",
|
||||
"AL",
|
||||
"AP",
|
||||
"AM",
|
||||
"BA",
|
||||
"CE",
|
||||
"DF",
|
||||
"ES",
|
||||
"GO",
|
||||
"MA",
|
||||
"MT",
|
||||
"MS",
|
||||
"MG",
|
||||
"PA",
|
||||
"PB",
|
||||
"PR",
|
||||
"PE",
|
||||
"PI",
|
||||
"RJ",
|
||||
"RN",
|
||||
"RS",
|
||||
"RO",
|
||||
"RR",
|
||||
"SC",
|
||||
"SP",
|
||||
"SE",
|
||||
"TO",
|
||||
].map((uf) => (
|
||||
<option key={uf} value={uf}>
|
||||
{uf}
|
||||
</option>
|
||||
@ -2743,7 +2831,9 @@ const PainelSecretaria = () => {
|
||||
type="text"
|
||||
value={maskCep(formDataMedico.cep)}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/\D/g, "").slice(0, 8);
|
||||
const digits = event.target.value
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 8);
|
||||
setFormDataMedico((prev) => ({
|
||||
...prev,
|
||||
cep: digits,
|
||||
@ -2762,7 +2852,9 @@ const PainelSecretaria = () => {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCepLookupMedico(formDataMedico.cep)}
|
||||
onClick={() =>
|
||||
handleCepLookupMedico(formDataMedico.cep)
|
||||
}
|
||||
className="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors shadow-sm hover:shadow-md"
|
||||
title="Buscar endereço pelo CEP"
|
||||
>
|
||||
|
||||
@ -163,15 +163,23 @@ class MedicoService {
|
||||
];
|
||||
for (const ep of candidates) {
|
||||
try {
|
||||
const response = await http.get<MedicoApi[] | MedicoApi>(ep, {
|
||||
params: {
|
||||
// Construir params manualmente para evitar valores booleanos diretos
|
||||
const queryParams: Record<string, string> = {
|
||||
select: "*",
|
||||
...((params?.status && { active: params.status === "ativo" }) ||
|
||||
{}),
|
||||
...(params?.especialidade && {
|
||||
specialty: `ilike.%${params.especialidade}%`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Supabase PostgREST usa formato: active=eq.true ou active=is.true
|
||||
if (params?.status) {
|
||||
queryParams.active =
|
||||
params.status === "ativo" ? "eq.true" : "eq.false";
|
||||
}
|
||||
|
||||
if (params?.especialidade) {
|
||||
queryParams.specialty = `ilike.%${params.especialidade}%`;
|
||||
}
|
||||
|
||||
const response = await http.get<MedicoApi[] | MedicoApi>(ep, {
|
||||
params: queryParams,
|
||||
});
|
||||
endpointTried.push(ep);
|
||||
if (response.success && response.data) {
|
||||
|
||||
@ -365,12 +365,24 @@ export async function createPatient(payload: {
|
||||
const cleanEndereco: EnderecoPaciente | undefined = payload.endereco
|
||||
? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") }
|
||||
: undefined;
|
||||
const peso = typeof payload.pesoKg === "number" && payload.pesoKg > 0 && payload.pesoKg < 500 ? payload.pesoKg : undefined;
|
||||
const altura = typeof payload.alturaM === "number" && payload.alturaM > 0 && payload.alturaM < 3 ? payload.alturaM : undefined;
|
||||
const peso =
|
||||
typeof payload.pesoKg === "number" &&
|
||||
payload.pesoKg > 0 &&
|
||||
payload.pesoKg < 500
|
||||
? payload.pesoKg
|
||||
: undefined;
|
||||
const altura =
|
||||
typeof payload.alturaM === "number" &&
|
||||
payload.alturaM > 0 &&
|
||||
payload.alturaM < 3
|
||||
? payload.alturaM
|
||||
: undefined;
|
||||
|
||||
if (!payload.nome?.trim()) return { success: false, error: "Nome é obrigatório" };
|
||||
if (!payload.nome?.trim())
|
||||
return { success: false, error: "Nome é obrigatório" };
|
||||
if (!rawCpf) return { success: false, error: "CPF é obrigatório" };
|
||||
if (!payload.email?.trim()) return { success: false, error: "Email é obrigatório" };
|
||||
if (!payload.email?.trim())
|
||||
return { success: false, error: "Email é obrigatório" };
|
||||
if (!phone) return { success: false, error: "Telefone é obrigatório" };
|
||||
|
||||
const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({
|
||||
@ -397,7 +409,8 @@ export async function createPatient(payload: {
|
||||
const prune = () => {
|
||||
Object.keys(body).forEach((k) => {
|
||||
const v = (body as Record<string, unknown>)[k];
|
||||
if (v === undefined || v === "") delete (body as Record<string, unknown>)[k];
|
||||
if (v === undefined || v === "")
|
||||
delete (body as Record<string, unknown>)[k];
|
||||
});
|
||||
};
|
||||
prune();
|
||||
@ -409,21 +422,36 @@ export async function createPatient(payload: {
|
||||
{ headers: { Prefer: "return=representation" } }
|
||||
);
|
||||
if (response.success && response.data) {
|
||||
const raw = Array.isArray(response.data) ? response.data[0] : response.data;
|
||||
const raw = Array.isArray(response.data)
|
||||
? response.data[0]
|
||||
: response.data;
|
||||
return { success: true, data: mapPacienteFromApi(raw) };
|
||||
}
|
||||
return { success: false, error: response.error || "Erro ao criar paciente" };
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || "Erro ao criar paciente",
|
||||
};
|
||||
};
|
||||
|
||||
const handleOverflowFallbacks = async (baseError: string): Promise<ApiResponse<Paciente>> => {
|
||||
const handleOverflowFallbacks = async (
|
||||
baseError: string
|
||||
): Promise<ApiResponse<Paciente>> => {
|
||||
// 1) tentar com CPF formatado
|
||||
if (/numeric field overflow/i.test(baseError) && rawCpf.length === 11) {
|
||||
body = buildBody(rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"));
|
||||
body = buildBody(
|
||||
rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")
|
||||
);
|
||||
prune();
|
||||
let r = await attempt();
|
||||
if (r.success) return r;
|
||||
// 2) remover campos opcionais progressivamente
|
||||
const optional: Array<keyof PatientInputSchema> = ["weight_kg", "height_m", "blood_type", "cep", "number"];
|
||||
const optional: Array<keyof PatientInputSchema> = [
|
||||
"weight_kg",
|
||||
"height_m",
|
||||
"blood_type",
|
||||
"cep",
|
||||
"number",
|
||||
];
|
||||
for (const key of optional) {
|
||||
if (key in body) {
|
||||
delete (body as Record<string, unknown>)[key];
|
||||
@ -439,14 +467,19 @@ export async function createPatient(payload: {
|
||||
try {
|
||||
let first = await attempt();
|
||||
if (!first.success && /numeric field overflow/i.test(first.error || "")) {
|
||||
first = await handleOverflowFallbacks(first.error || "numeric field overflow");
|
||||
first = await handleOverflowFallbacks(
|
||||
first.error || "numeric field overflow"
|
||||
);
|
||||
}
|
||||
return first;
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { status?: number; data?: { message?: string } } };
|
||||
const e = err as {
|
||||
response?: { status?: number; data?: { message?: string } };
|
||||
};
|
||||
let msg = "Erro ao criar paciente";
|
||||
if (e.response?.status === 401) msg = "Não autorizado";
|
||||
else if (e.response?.status === 400) msg = e.response.data?.message || "Dados inválidos";
|
||||
else if (e.response?.status === 400)
|
||||
msg = e.response.data?.message || "Dados inválidos";
|
||||
else if (e.response?.data?.message) msg = e.response.data.message;
|
||||
if (/numeric field overflow/i.test(msg)) {
|
||||
const overflowAttempt = await handleOverflowFallbacks(msg);
|
||||
@ -535,26 +568,70 @@ export async function updatePatient(
|
||||
export async function deletePatient(id: string): Promise<ApiResponse<void>> {
|
||||
if (!id) return { success: false, error: "ID é obrigatório" };
|
||||
try {
|
||||
const resp = await http.delete<unknown>(
|
||||
`${ENDPOINTS.PATIENTS}/${encodeURIComponent(id)}`
|
||||
console.log("[deletePatient] Tentando deletar paciente:", id);
|
||||
console.log(
|
||||
"[deletePatient] Endpoint:",
|
||||
`${ENDPOINTS.PATIENTS}?id=eq.${encodeURIComponent(id)}`
|
||||
);
|
||||
|
||||
// Supabase REST API DELETE usa query params com filtro
|
||||
// Formato: /rest/v1/patients?id=eq.<uuid>
|
||||
// É necessário adicionar header Prefer: return=representation ou return=minimal
|
||||
const resp = await http.delete<unknown>(
|
||||
`${ENDPOINTS.PATIENTS}?id=eq.${encodeURIComponent(id)}`,
|
||||
{
|
||||
headers: {
|
||||
Prefer: "return=minimal",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[deletePatient] Resposta:", resp);
|
||||
|
||||
if (!resp.success) {
|
||||
console.error("[deletePatient] Falha ao deletar:", resp.error);
|
||||
return {
|
||||
success: false,
|
||||
error: resp.error || "Falha ao deletar paciente",
|
||||
};
|
||||
}
|
||||
console.log("[deletePatient] Paciente deletado com sucesso");
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const err = error as {
|
||||
response?: { status?: number; data?: { message?: string } };
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
message?: string;
|
||||
hint?: string;
|
||||
details?: string;
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
let msg = "Erro ao deletar paciente";
|
||||
if (err.response?.status === 404) msg = "Paciente não encontrado";
|
||||
else if (err.response?.status === 401) msg = "Não autorizado";
|
||||
else if (err.response?.status === 403) msg = "Acesso negado";
|
||||
else if (err.response?.data?.message) msg = err.response.data.message;
|
||||
console.error(msg, error);
|
||||
const status = err.response?.status;
|
||||
const errorData = err.response?.data;
|
||||
|
||||
console.error("[deletePatient] Erro capturado:", {
|
||||
status,
|
||||
message: errorData?.message,
|
||||
hint: errorData?.hint,
|
||||
details: errorData?.details,
|
||||
error: errorData?.error,
|
||||
fullError: error,
|
||||
});
|
||||
|
||||
if (status === 404) msg = "Paciente não encontrado";
|
||||
else if (status === 401) msg = "Não autorizado - faça login novamente";
|
||||
else if (status === 403)
|
||||
msg = "Acesso negado - você não tem permissão para excluir pacientes";
|
||||
else if (status === 406) msg = "Formato de requisição inválido";
|
||||
else if (errorData?.error) msg = errorData.error;
|
||||
else if (errorData?.message) msg = errorData.message;
|
||||
else if (errorData?.hint) msg = `${msg}: ${errorData.hint}`;
|
||||
|
||||
console.error("[deletePatient]", msg, error);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,42 +1,40 @@
|
||||
/**
|
||||
* DEPRECATED: Substituído por content_json?: Record<string, unknown>;
|
||||
status: "draft" | "final" | "preliminary";
|
||||
requested_by?: string;eportService.ts` (nomes em inglês e mapping padronizado).
|
||||
* Manter temporariamente para compatibilidade até remoção.
|
||||
* Service para gerenciar relatórios médicos
|
||||
* Endpoint: /rest/v1/reports
|
||||
*/
|
||||
import api from "./api";
|
||||
import { ApiResponse } from "./http";
|
||||
import { http, ApiResponse } from "./http";
|
||||
import ENDPOINTS from "./endpoints";
|
||||
|
||||
export interface Relatorio {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
order_number: string;
|
||||
exam: string;
|
||||
diagnosis: string;
|
||||
conclusion: string;
|
||||
id?: string;
|
||||
patient_id?: string;
|
||||
order_number?: string;
|
||||
exam?: string;
|
||||
diagnosis?: string;
|
||||
conclusion?: string;
|
||||
cid_code?: string;
|
||||
content_html?: string;
|
||||
content_json?: Record<string, unknown>;
|
||||
status: "draft" | "final" | "preliminary";
|
||||
status?: "draft" | "pending" | "completed" | "cancelled";
|
||||
requested_by?: string;
|
||||
due_at?: string;
|
||||
hide_date?: boolean;
|
||||
hide_signature?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface RelatorioCreate {
|
||||
patient_id: string;
|
||||
order_number?: string;
|
||||
exam: string;
|
||||
diagnosis: string;
|
||||
conclusion: string;
|
||||
order_number: string;
|
||||
exam?: string;
|
||||
diagnosis?: string;
|
||||
conclusion?: string;
|
||||
cid_code?: string;
|
||||
content_html?: string;
|
||||
content_json?: Record<string, unknown>;
|
||||
status?: "draft" | "final" | "approved";
|
||||
status?: "draft" | "pending" | "completed" | "cancelled";
|
||||
requested_by?: string;
|
||||
due_at?: string;
|
||||
hide_date?: boolean;
|
||||
@ -52,87 +50,155 @@ export interface RelatorioUpdate {
|
||||
cid_code?: string;
|
||||
content_html?: string;
|
||||
content_json?: Record<string, unknown>;
|
||||
status?: "draft" | "final" | "approved";
|
||||
status?: "draft" | "pending" | "completed" | "cancelled";
|
||||
requested_by?: string;
|
||||
due_at?: string;
|
||||
hide_date?: boolean;
|
||||
hide_signature?: boolean;
|
||||
}
|
||||
|
||||
export interface RelatorioListResponse {
|
||||
data: Relatorio[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
}
|
||||
|
||||
class RelatorioService {
|
||||
// Listar relatórios com filtros opcionais
|
||||
async listarRelatorios(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
tipo?: string;
|
||||
}): Promise<ApiResponse<RelatorioListResponse>> {
|
||||
patient_id?: string;
|
||||
status?: "draft" | "pending" | "completed" | "cancelled";
|
||||
}): Promise<ApiResponse<Relatorio[]>> {
|
||||
try {
|
||||
const response = await api.get("/rest/v1/reports", { params });
|
||||
return { success: true, data: response.data };
|
||||
} catch (error: unknown) {
|
||||
const queryParams: Record<string, string> = { select: "*" };
|
||||
|
||||
if (params?.patient_id) {
|
||||
queryParams["patient_id"] = `eq.${params.patient_id}`;
|
||||
}
|
||||
|
||||
if (params?.status) {
|
||||
queryParams["status"] = `eq.${params.status}`;
|
||||
}
|
||||
|
||||
const response = await http.get<Relatorio[]>(ENDPOINTS.REPORTS, {
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: Array.isArray(response.data) ? response.data : [response.data],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || "Erro ao listar relatórios",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao listar relatórios:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error && "response" in error
|
||||
? (error as { response?: { data?: { message?: string } } }).response
|
||||
?.data?.message || "Erro ao listar relatórios"
|
||||
: "Erro ao listar relatórios";
|
||||
return { success: false, error: errorMessage };
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Criar novo relatório
|
||||
async criarRelatorio(
|
||||
relatorio: RelatorioCreate
|
||||
): Promise<ApiResponse<Relatorio>> {
|
||||
try {
|
||||
const response = await api.post("/rest/v1/reports", relatorio);
|
||||
return { success: true, data: response.data };
|
||||
} catch (error: unknown) {
|
||||
const response = await http.post<Relatorio>(
|
||||
ENDPOINTS.REPORTS,
|
||||
relatorio,
|
||||
{
|
||||
headers: { Prefer: "return=representation" },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const report = Array.isArray(response.data)
|
||||
? response.data[0]
|
||||
: response.data;
|
||||
return {
|
||||
success: true,
|
||||
data: report,
|
||||
message: "Relatório criado com sucesso",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || "Erro ao criar relatório",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar relatório:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error && "response" in error
|
||||
? (error as { response?: { data?: { message?: string } } }).response
|
||||
?.data?.message || "Erro ao criar relatório"
|
||||
: "Erro ao criar relatório";
|
||||
return { success: false, error: errorMessage };
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar relatório por ID
|
||||
async buscarRelatorioPorId(id: string): Promise<ApiResponse<Relatorio>> {
|
||||
try {
|
||||
const response = await api.get(`/rest/v1/reports/${id}`);
|
||||
return { success: true, data: response.data };
|
||||
} catch (error: unknown) {
|
||||
console.error("Erro ao buscar relatório:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error && "response" in error
|
||||
? (error as { response?: { data?: { message?: string } } }).response
|
||||
?.data?.message || "Erro ao buscar relatório"
|
||||
: "Erro ao buscar relatório";
|
||||
return { success: false, error: errorMessage };
|
||||
const response = await http.get<Relatorio[]>(
|
||||
`${ENDPOINTS.REPORTS}?id=eq.${id}`,
|
||||
{
|
||||
params: { select: "*" },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const reports = Array.isArray(response.data)
|
||||
? response.data
|
||||
: [response.data];
|
||||
if (reports.length > 0) {
|
||||
return { success: true, data: reports[0] };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: "Relatório não encontrado" };
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar relatório:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar relatório existente
|
||||
async atualizarRelatorio(
|
||||
id: string,
|
||||
updates: RelatorioUpdate
|
||||
): Promise<ApiResponse<Relatorio>> {
|
||||
try {
|
||||
const response = await api.patch(`/rest/v1/reports/${id}`, updates);
|
||||
return { success: true, data: response.data };
|
||||
} catch (error: unknown) {
|
||||
const response = await http.patch<Relatorio>(
|
||||
`${ENDPOINTS.REPORTS}?id=eq.${id}`,
|
||||
updates,
|
||||
{
|
||||
headers: { Prefer: "return=representation" },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const report = Array.isArray(response.data)
|
||||
? response.data[0]
|
||||
: response.data;
|
||||
return {
|
||||
success: true,
|
||||
data: report,
|
||||
message: "Relatório atualizado com sucesso",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || "Erro ao atualizar relatório",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar relatório:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error && "response" in error
|
||||
? (error as { response?: { data?: { message?: string } } }).response
|
||||
?.data?.message || "Erro ao atualizar relatório"
|
||||
: "Erro ao atualizar relatório";
|
||||
return { success: false, error: errorMessage };
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user