Mudanças

This commit is contained in:
guisilvagomes 2025-10-10 16:39:57 -03:00
parent 9ec07aeea3
commit 62b741ff7a
14 changed files with 792 additions and 479 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Local Netlify folder
.netlify

View File

@ -93,7 +93,7 @@ const Header: React.FC = () => {
</div> </div>
<button <button
onClick={logout} 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")} aria-label={i18n.t("header.logout")}
> >
<LogOut className="w-4 h-4 mr-1" aria-hidden="true" /> <LogOut className="w-4 h-4 mr-1" aria-hidden="true" />
@ -106,7 +106,7 @@ const Header: React.FC = () => {
) : ( ) : (
<Link <Link
to="/paciente" 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")} aria-label={i18n.t("header.login")}
> >
<LogIn className="w-4 h-4 mr-2" aria-hidden="true" /> <LogIn className="w-4 h-4 mr-2" aria-hidden="true" />

View File

@ -3,6 +3,7 @@ import { User, Stethoscope, Clipboard, ChevronDown } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { i18n } from "../i18n"; import { i18n } from "../i18n";
import { telemetry } from "../services/telemetry"; import { telemetry } from "../services/telemetry";
import { useAuth } from "../hooks/useAuth";
export type ProfileType = "patient" | "doctor" | "secretary" | null; export type ProfileType = "patient" | "doctor" | "secretary" | null;
@ -51,6 +52,7 @@ export const ProfileSelector: React.FC = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const { isAuthenticated, user } = useAuth();
useEffect(() => { useEffect(() => {
// Carregar perfil salvo // Carregar perfil salvo
@ -96,8 +98,49 @@ export const ProfileSelector: React.FC = () => {
// Telemetria // Telemetria
telemetry.trackProfileChange(previousProfile, profile.type || "none"); telemetry.trackProfileChange(previousProfile, profile.type || "none");
// Navegar // Navegar - condicional baseado em autenticação e role
navigate(profile.path); 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 = () => { const getCurrentProfile = () => {

View File

@ -12,18 +12,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
roles, roles,
redirectTo = "/", redirectTo = "/",
}) => { }) => {
const { isAuthenticated, role, loading } = useAuth(); const { isAuthenticated, role, loading, user } = useAuth();
const location = useLocation(); const location = useLocation();
console.log("[ProtectedRoute]", { console.log("[ProtectedRoute] VERIFICAÇÃO COMPLETA", {
path: location.pathname, path: location.pathname,
isAuthenticated, isAuthenticated,
role, role,
loading, loading,
requiredRoles: roles, 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) { if (loading) {
console.log("[ProtectedRoute] ⏳ Ainda carregando sessão...");
return ( return (
<div className="py-10 text-center text-sm text-gray-500"> <div className="py-10 text-center text-sm text-gray-500">
Verificando sessão... Verificando sessão...
@ -33,12 +47,16 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
if (!isAuthenticated) { if (!isAuthenticated) {
console.log( console.log(
"[ProtectedRoute] Não autenticado, redirecionando para:", "[ProtectedRoute] ❌ NÃO AUTENTICADO! User:",
user,
"Redirecionando para:",
redirectTo redirectTo
); );
return <Navigate to={redirectTo} state={{ from: location }} replace />; return <Navigate to={redirectTo} state={{ from: location }} replace />;
} }
console.log("[ProtectedRoute] ✅ Autenticado! Verificando roles...");
// Admin tem acesso a tudo // Admin tem acesso a tudo
if (role === "admin") { if (role === "admin") {
console.log("[ProtectedRoute] Admin detectado, permitindo acesso"); console.log("[ProtectedRoute] Admin detectado, permitindo acesso");

View File

@ -88,43 +88,164 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const [user, setUser] = useState<SessionUser | null>(null); const [user, setUser] = useState<SessionUser | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Restaurar sessão do localStorage e verificar token // Log sempre que user ou loading mudar
useEffect(() => { 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 () => { const restoreSession = async () => {
try { 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) { if (raw) {
const parsed = JSON.parse(raw) as PersistedSession; const parsed = JSON.parse(raw) as PersistedSession;
if (parsed?.user?.role) { 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 // Verificar se há tokens válidos salvos
if (parsed.token) { if (parsed.token) {
console.log("[AuthContext] Restaurando tokens no tokenStore"); console.log("[AuthContext] Restaurando tokens no tokenStore");
// Restaurar tokens no tokenStore
const tokenStore = (await import("../services/tokenStore")) const tokenStore = (await import("../services/tokenStore"))
.default; .default;
tokenStore.setTokens(parsed.token, parsed.refreshToken); tokenStore.setTokens(parsed.token, parsed.refreshToken);
} else { } else {
console.warn("[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore..."); console.warn(
// Verificar se há token no tokenStore (pode ter sido salvo diretamente) "[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore..."
);
const tokenStore = (await import("../services/tokenStore")) const tokenStore = (await import("../services/tokenStore"))
.default; .default;
const existingToken = tokenStore.getAccessToken(); const existingToken = tokenStore.getAccessToken();
if (existingToken) { if (existingToken) {
console.log("[AuthContext] Token encontrado no tokenStore, mantendo sessão"); console.log(
"[AuthContext] Token encontrado no tokenStore, mantendo sessão"
);
} else { } 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); setUser(parsed.user);
} else {
console.log(
"[AuthContext] ⚠️ Sessão parseada mas sem user.role válido"
);
} }
} else { } else {
console.log("[AuthContext] Nenhuma sessão salva encontrada"); console.log(
"[AuthContext] Nenhuma sessão salva encontrada no localStorage"
);
} }
} catch (error) { } catch (error) {
console.error("[AuthContext] Erro ao restaurar sessão:", error); console.error("[AuthContext] Erro ao restaurar sessão:", error);
} finally { } finally {
console.log(
"[AuthContext] 🏁 Finalizando restauração, setLoading(false)"
);
setLoading(false); setLoading(false);
} }
}; };
@ -133,17 +254,37 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const persist = useCallback((session: PersistedSession) => { const persist = useCallback((session: PersistedSession) => {
try { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); console.log(
} catch { "[AuthContext] 💾 SALVANDO sessão no localStorage E sessionStorage:",
/* ignore */ {
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(() => { const clearPersisted = useCallback(() => {
try { try {
console.log(
"[AuthContext] 🗑️ REMOVENDO sessão do localStorage E sessionStorage"
);
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
} catch { sessionStorage.removeItem(STORAGE_KEY);
/* ignore */ 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 () => { const logout = useCallback(async () => {
console.log("[AuthContext] Iniciando logout...");
try { try {
const resp = await authService.logout(); // chama /auth/v1/logout (204 esperado) const resp = await authService.logout(); // chama /auth/v1/logout (204 esperado)
if (!resp.success && resp.error) { if (!resp.success && resp.error) {
console.warn("[AuthContext] Falha no logout remoto:", resp.error);
toast.error(`Falha no logout remoto: ${resp.error}`); toast.error(`Falha no logout remoto: ${resp.error}`);
} else { } else {
toast.success("Sessão encerrada no servidor"); console.log("[AuthContext] Logout remoto bem-sucedido");
} }
} catch (e) { } catch (e) {
console.warn("Erro inesperado ao executar logout remoto", e); console.warn(
toast("Logout local (falha remota)"); "[AuthContext] Erro inesperado ao executar logout remoto",
e
);
} finally { } finally {
// Limpa contexto local // Limpa contexto local
console.log("[AuthContext] Limpando estado local...");
setUser(null); setUser(null);
clearPersisted(); clearPersisted();
authService.clearLocalAuth(); authService.clearLocalAuth();
@ -373,6 +519,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
} catch { } catch {
// ignore // ignore
} }
console.log("[AuthContext] Logout completo - usuário removido do estado");
// Modelo somente Supabase: nenhum token técnico para invalidar // Modelo somente Supabase: nenhum token técnico para invalidar
} }
}, [clearPersisted]); }, [clearPersisted]);

View File

@ -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"> <div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
<button <button
onClick={() => handleCTA("Agendar consulta", "/paciente")} 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( aria-label={i18n.t(
"home.actionCards.scheduleAppointment.ctaAriaLabel" "home.actionCards.scheduleAppointment.ctaAriaLabel"
)} )}
@ -122,7 +122,7 @@ const Home: React.FC = () => {
<button <button
onClick={() => handleCTA("Ver próximas consultas", "/consultas")} 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" aria-label="Ver lista de próximas consultas"
> >
<Clock className="w-5 h-5 mr-2" aria-hidden="true" /> <Clock className="w-5 h-5 mr-2" aria-hidden="true" />
@ -278,7 +278,7 @@ const ActionCard: React.FC<ActionCardProps> = ({
</p> </p>
<button <button
onClick={onAction} 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} aria-label={ctaAriaLabel}
> >
{ctaLabel} {ctaLabel}

View File

@ -4,8 +4,6 @@ import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
// interface Medico is not required in this component
const LoginMedico: React.FC = () => { const LoginMedico: React.FC = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: "",
@ -14,30 +12,61 @@ const LoginMedico: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); 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) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
// Primeiro tenta fluxo real Supabase (grant_type=password) console.log("[LoginMedico] Fazendo login com email:", formData.email);
let ok = await loginComEmailSenha(formData.email, formData.senha);
// Se falhar (ex: usuário não mapeado ainda), cai no fallback legado de médico const authService = (await import("../services/authService")).default;
if (!ok) { const loginResult = await authService.login({
ok = await loginMedico(formData.email, formData.senha); 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) { if (ok) {
// Login bem-sucedido, redirecionar para painel médico console.log("[LoginMedico] Navegando para /painel-medico");
// A verificação de permissões será feita pelo ProtectedRoute
console.log("[LoginMedico] Login realizado, redirecionando...");
toast.success("Login realizado com sucesso!"); toast.success("Login realizado com sucesso!");
navigate("/painel-medico"); navigate("/painel-medico");
} else {
console.error("[LoginMedico] loginComEmailSenha retornou false");
toast.error("Erro ao processar login");
} }
} catch (error) { } catch (error) {
console.error("Erro no login:", error); console.error("[LoginMedico] Erro no login:", error);
toast.error("Erro ao fazer login. Tente novamente."); toast.error("Erro ao fazer login. Tente novamente.");
} finally { } finally {
setLoading(false); setLoading(false);
@ -47,7 +76,6 @@ const LoginMedico: React.FC = () => {
return ( 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="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"> <div className="max-w-md w-full">
{/* Header */}
<div className="text-center mb-8"> <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"> <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" /> <Stethoscope className="w-8 h-8 text-white" />
@ -60,7 +88,6 @@ const LoginMedico: React.FC = () => {
</p> </p>
</div> </div>
{/* Formulário */}
<div <div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors" 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" aria-live="polite"
@ -83,7 +110,7 @@ const LoginMedico: React.FC = () => {
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" 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 required
autoComplete="email" autoComplete="email"
/> />
@ -117,22 +144,16 @@ const LoginMedico: React.FC = () => {
<button <button
type="submit" type="submit"
disabled={loading} 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"} {loading ? "Entrando..." : "Entrar"}
</button> </button>
</form>
{/* Informações de demonstração */} <p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
<div className="mt-6 p-4 bg-indigo-50 dark:bg-gray-700/40 rounded-lg"> <strong>{LOCAL_MEDICO.email}</strong> /{" "}
<h3 className="text-sm font-medium text-indigo-800 dark:text-indigo-300 mb-2"> <strong>{LOCAL_MEDICO.senha}</strong>
Para Demonstração:
</h3>
<p className="text-sm text-indigo-700 dark:text-indigo-200">
Email:riseup@popcode.com.br <br />
Senha: riseup
</p> </p>
</div> </form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -59,10 +59,10 @@ const LoginPaciente: React.FC = () => {
// Credenciais fixas para LOGIN LOCAL de paciente // Credenciais fixas para LOGIN LOCAL de paciente
const LOCAL_PATIENT = { const LOCAL_PATIENT = {
email: "pedro.araujo@mediconnect.com", email: "guilhermesilvagomes1020@gmail.com",
senha: "local123", senha: "guilherme123",
nome: "Pedro Araujo", nome: "Guilherme Silva Gomes",
id: "pedro.araujo@mediconnect.com", id: "guilhermesilvagomes1020@gmail.com",
} as const; } as const;
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
@ -359,13 +359,13 @@ const LoginPaciente: React.FC = () => {
type="button" type="button"
onClick={handleLoginLocal} onClick={handleLoginLocal}
disabled={loading} 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"} {loading ? "Entrando..." : "Entrar"}
</button> </button>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center"> <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.email}</strong> /{" "}
<strong> {LOCAL_PATIENT.senha}</strong> <strong>{LOCAL_PATIENT.senha}</strong>
</p> </p>
</form> </form>
) : ( ) : (

View File

@ -10,40 +10,64 @@ const LoginSecretaria: React.FC = () => {
senha: "", senha: "",
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showCadastro, setShowCadastro] = useState(false);
const [cadastroData, setCadastroData] = useState({
nome: "",
email: "",
senha: "",
confirmarSenha: "",
telefone: "",
cpf: "",
});
const navigate = useNavigate(); const navigate = useNavigate();
const { loginComEmailSenha } = useAuth(); 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) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
console.log("[LoginSecretaria] Tentando login com:", formData.email); console.log("[LoginSecretaria] Fazendo login com email:", formData.email);
// Tenta login real via authService primeiro
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); const ok = await loginComEmailSenha(formData.email, formData.senha);
console.log("[LoginSecretaria] Resultado login:", ok);
if (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"); navigate("/painel-secretaria");
} else { } else {
console.error("[LoginSecretaria] Login falhou - credenciais inválidas"); console.error("[LoginSecretaria] loginComEmailSenha retornou false");
toast.error("Email ou senha incorretos"); toast.error("Erro ao processar login");
} }
} catch (error) { } catch (error) {
console.error("[LoginSecretaria] Erro no login:", 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 { } finally {
setLoading(false); setLoading(false);
} }
@ -52,279 +76,84 @@ const LoginSecretaria: React.FC = () => {
return ( 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="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"> <div className="max-w-md w-full">
{/* Header */}
<div className="text-center mb-8"> <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"> <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" /> <Clipboard className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2"> <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> </h1>
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
{showCadastro Faça login para acessar o sistema de gestão
? "Preencha os dados para criar uma conta de secretária"
: "Faça login para acessar o sistema de gestão"}
</p> </p>
</div> </div>
{/* Formulário */}
<div <div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors" 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" aria-live="polite"
> >
{!showCadastro ? ( <form onSubmit={handleLogin} className="space-y-6" noValidate>
<form onSubmit={handleLogin} className="space-y-6" noValidate> <div>
<div> <label
<label htmlFor="sec_email"
htmlFor="sec_email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
id="sec_email"
type="email"
value={formData.email}
onChange={(e) =>
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"
required
autoComplete="email"
/>
</div>
</div>
<div>
<label
htmlFor="sec_password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Senha
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
id="sec_password"
type="password"
value={formData.senha}
onChange={(e) =>
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"
required
autoComplete="current-password"
/>
</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"
> >
{loading ? "Entrando..." : "Entrar"} Email
</button> </label>
</form> <div className="relative">
) : ( <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<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 <input
id="sec_cad_nome" id="sec_email"
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" type="email"
value={cadastroData.email} value={formData.email}
onChange={(e) => onChange={(e) =>
setCadastroData((prev) => ({ setFormData((prev) => ({ ...prev, email: e.target.value }))
...prev,
email: e.target.value,
}))
} }
className="form-input dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100" className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
placeholder="seu@email.com"
required required
autoComplete="email" autoComplete="email"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> </div>
<div>
<label <div>
htmlFor="sec_cad_senha" <label
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="sec_password"
> className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
Senha >
</label> Senha
<input </label>
id="sec_cad_senha" <div className="relative">
type="password" <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
value={cadastroData.senha} <input
onChange={(e) => id="sec_password"
setCadastroData((prev) => ({ type="password"
...prev, value={formData.senha}
senha: e.target.value, onChange={(e) =>
})) setFormData((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" className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
minLength={6} placeholder="Sua senha"
required required
autoComplete="new-password" autoComplete="current-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>
)}
</div>
</div> </div>
<div className="flex space-x-4"> </div>
<button
type="button" <button
onClick={() => setShowCadastro(false)} type="submit"
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors" 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 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
Voltar >
</button> {loading ? "Entrando..." : "Entrar"}
<button </button>
type="submit"
disabled={loading} <p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
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" <strong>{LOCAL_SECRETARIA.email}</strong> /{" "}
> <strong>{LOCAL_SECRETARIA.senha}</strong>
{loading ? "Cadastrando..." : "Cadastrar"} </p>
</button> </form>
</div>
</form>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -379,14 +379,21 @@ const PainelAdmin: React.FC = () => {
} }
try { try {
console.log("[PainelAdmin] Deletando paciente:", { id, nome });
const response = await deletePatient(id); const response = await deletePatient(id);
console.log("[PainelAdmin] Resultado da deleção:", response);
if (response.success) { if (response.success) {
toast.success("Paciente deletado com sucesso!"); toast.success("Paciente deletado com sucesso!");
loadPacientes(); loadPacientes();
} else { } else {
console.error("[PainelAdmin] Falha ao deletar:", response.error);
toast.error(response.error || "Erro ao deletar paciente"); 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"); toast.error("Erro ao deletar paciente");
} }
}; };
@ -395,12 +402,12 @@ const PainelAdmin: React.FC = () => {
const handleEditMedico = (medico: Doctor) => { const handleEditMedico = (medico: Doctor) => {
setEditingMedico(medico); setEditingMedico(medico);
setFormMedico({ setFormMedico({
crm: medico.crm, crm: medico.crm || "",
crm_uf: medico.crm_uf, crm_uf: medico.crm_uf || "SP",
specialty: medico.specialty || "", specialty: medico.specialty || "",
full_name: medico.full_name, full_name: medico.full_name || "",
cpf: medico.cpf, cpf: medico.cpf || "",
email: medico.email, email: medico.email || "",
phone_mobile: medico.phone_mobile || "", phone_mobile: medico.phone_mobile || "",
phone2: medico.phone2 || "", phone2: medico.phone2 || "",
cep: medico.cep || "", cep: medico.cep || "",
@ -982,7 +989,10 @@ const PainelAdmin: React.FC = () => {
</button> </button>
<button <button
onClick={() => 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" className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Deletar" title="Deletar"

View File

@ -8,6 +8,7 @@ import {
} from "react"; } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useAuth } from "../hooks/useAuth";
import { import {
Activity, Activity,
Calendar, Calendar,
@ -41,6 +42,7 @@ import {
type EnderecoPaciente, type EnderecoPaciente,
type Paciente as PacienteServiceModel, type Paciente as PacienteServiceModel,
} from "../services/pacienteService"; } from "../services/pacienteService";
import relatorioService, { type Relatorio } from "../services/relatorioService";
// Tipos e constantes reinseridos após refatoração // Tipos e constantes reinseridos após refatoração
type TabId = "dashboard" | "pacientes" | "medicos" | "consultas" | "relatorios"; type TabId = "dashboard" | "pacientes" | "medicos" | "consultas" | "relatorios";
@ -402,7 +404,6 @@ const maskCep = (value: string) => {
return digits; return digits;
}; };
const splitTelefone = (telefone?: string) => { const splitTelefone = (telefone?: string) => {
if (!telefone) { if (!telefone) {
return { codigoPais: "55", ddd: "", numeroTelefone: "" }; return { codigoPais: "55", ddd: "", numeroTelefone: "" };
@ -557,6 +558,7 @@ const buildMedicoTelefone = (value: string) => {
const PainelSecretaria = () => { const PainelSecretaria = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { logout } = useAuth();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState<TabId>("pacientes"); const [activeTab, setActiveTab] = useState<TabId>("pacientes");
@ -648,13 +650,13 @@ const PainelSecretaria = () => {
try { try {
const response = await relatorioService.listarRelatorios(); const response = await relatorioService.listarRelatorios();
if (response.success && response.data) { if (response.success && response.data) {
setRelatorios(response.data.data); setRelatorios(response.data);
} else { } else {
toast.error('Erro ao carregar relatórios'); toast.error("Erro ao carregar relatórios");
} }
} catch (error) { } catch (error) {
console.error('Erro ao carregar relatórios:', error); console.error("Erro ao carregar relatórios:", error);
toast.error('Erro ao carregar relatórios'); toast.error("Erro ao carregar relatórios");
} finally { } finally {
setLoadingRelatorios(false); setLoadingRelatorios(false);
} }
@ -663,7 +665,11 @@ const PainelSecretaria = () => {
const carregarDados = useCallback(async () => { const carregarDados = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
await Promise.all([carregarPacientes(), carregarMedicos(), carregarRelatorios()]); await Promise.all([
carregarPacientes(),
carregarMedicos(),
carregarRelatorios(),
]);
setConsultas([]); setConsultas([]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -675,7 +681,7 @@ const PainelSecretaria = () => {
}, [carregarDados]); }, [carregarDados]);
useEffect(() => { useEffect(() => {
if (activeTab === 'relatorios') { if (activeTab === "relatorios") {
void carregarRelatorios(); void carregarRelatorios();
} }
}, [activeTab, carregarRelatorios]); }, [activeTab, carregarRelatorios]);
@ -689,11 +695,11 @@ const PainelSecretaria = () => {
}, []); }, []);
const handleLogout = useCallback(() => { const handleLogout = useCallback(() => {
localStorage.removeItem("authToken"); console.log("[PainelSecretaria] Fazendo logout...");
localStorage.removeItem("token"); logout();
toast.success("Sessão encerrada"); toast.success("Sessão encerrada");
navigate("/login-secretaria"); navigate("/login-secretaria");
}, [navigate]); }, [logout, navigate]);
const openCreatePacienteModal = useCallback(() => { const openCreatePacienteModal = useCallback(() => {
resetPacienteForm(); resetPacienteForm();
@ -774,11 +780,24 @@ const PainelSecretaria = () => {
return; return;
} }
try { try {
await deletePatient(paciente.id); console.log("[PainelSecretaria] Deletando paciente:", {
setPacientes((prev) => prev.filter((p) => p.id !== paciente.id)); id: paciente.id,
toast.success("Paciente removido"); 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 com sucesso");
} else {
console.error("[PainelSecretaria] Falha ao deletar:", result.error);
toast.error(result.error || "Erro ao remover paciente");
}
} catch (error) { } catch (error) {
console.error("Erro ao remover paciente:", error); console.error("[PainelSecretaria] Erro ao remover paciente:", error);
toast.error("Erro ao remover paciente"); toast.error("Erro ao remover paciente");
} }
}, []); }, []);
@ -801,10 +820,13 @@ const PainelSecretaria = () => {
} }
}, []); }, []);
const handleCpfChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleCpfChange = useCallback(
const { formatted } = maskCpf(event.target.value); (event: ChangeEvent<HTMLInputElement>) => {
setFormDataPaciente((prev) => ({ ...prev, cpf: formatted })); const { formatted } = maskCpf(event.target.value);
}, []); setFormDataPaciente((prev) => ({ ...prev, cpf: formatted }));
},
[]
);
const handleCepLookup = useCallback(async (rawCep: string) => { const handleCepLookup = useCallback(async (rawCep: string) => {
const digits = rawCep.replace(/\D/g, ""); const digits = rawCep.replace(/\D/g, "");
@ -993,7 +1015,7 @@ const PainelSecretaria = () => {
setLoading(false); setLoading(false);
} }
}, },
[formDataPaciente, patientModalMode, resetPacienteForm] [formDataPaciente, patientModalMode, resetPacienteForm]
); );
const handleSubmitMedico = useCallback( const handleSubmitMedico = useCallback(
@ -1855,7 +1877,9 @@ const PainelSecretaria = () => {
<section className="bg-white rounded-lg shadow"> <section className="bg-white rounded-lg shadow">
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center mb-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> </div>
{loadingRelatorios ? ( {loadingRelatorios ? (
@ -1871,11 +1895,21 @@ const PainelSecretaria = () => {
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <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">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Exame</th> Número
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Paciente</th> </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">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th> 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> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
@ -1888,20 +1922,37 @@ const PainelSecretaria = () => {
{relatorio.exam} {relatorio.exam}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <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>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${ <span
relatorio.status === 'draft' ? 'bg-gray-100 text-gray-800' : className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
relatorio.status === 'final' ? 'bg-green-100 text-green-800' : relatorio.status === "draft"
'bg-yellow-100 text-yellow-800' ? "bg-gray-100 text-gray-800"
}`}> : relatorio.status === "completed"
{relatorio.status === 'draft' ? 'Rascunho' : ? "bg-green-100 text-green-800"
relatorio.status === 'final' ? 'Final' : 'Preliminar'} : 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> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <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> </td>
</tr> </tr>
))} ))}
@ -1936,7 +1987,10 @@ const PainelSecretaria = () => {
</p> </p>
</div> </div>
<div className="p-6 overflow-y-auto flex-1"> <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 */} {/* Seção: Dados Pessoais */}
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold text-green-600 uppercase tracking-wide border-b pb-1"> <h4 className="text-sm font-semibold text-green-600 uppercase tracking-wide border-b pb-1">
@ -2251,7 +2305,9 @@ const PainelSecretaria = () => {
type="text" type="text"
value={maskCep(formDataPaciente.endereco.cep || "")} value={maskCep(formDataPaciente.endereco.cep || "")}
onChange={(event) => { 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) => ({ setFormDataPaciente((prev) => ({
...prev, ...prev,
endereco: { endereco: {
@ -2272,7 +2328,9 @@ const PainelSecretaria = () => {
/> />
<button <button
type="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" 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" title="Buscar endereço pelo CEP"
> >
@ -2489,8 +2547,10 @@ const PainelSecretaria = () => {
</p> </p>
</div> </div>
<div className="p-6 overflow-y-auto flex-1"> <div className="p-6 overflow-y-auto flex-1">
<form
<form onSubmit={handleSubmitMedico} className="space-y-4 max-h-[70vh] overflow-y-auto px-1"> onSubmit={handleSubmitMedico}
className="space-y-4 max-h-[70vh] overflow-y-auto px-1"
>
{/* Seção: Dados Pessoais */} {/* Seção: Dados Pessoais */}
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold text-blue-600 uppercase tracking-wide border-b pb-1"> <h4 className="text-sm font-semibold text-blue-600 uppercase tracking-wide border-b pb-1">
@ -2618,7 +2678,35 @@ const PainelSecretaria = () => {
required required
> >
<option value="">Selecione</option> <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}> <option key={uf} value={uf}>
{uf} {uf}
</option> </option>
@ -2743,7 +2831,9 @@ const PainelSecretaria = () => {
type="text" type="text"
value={maskCep(formDataMedico.cep)} value={maskCep(formDataMedico.cep)}
onChange={(event) => { 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) => ({ setFormDataMedico((prev) => ({
...prev, ...prev,
cep: digits, cep: digits,
@ -2762,7 +2852,9 @@ const PainelSecretaria = () => {
/> />
<button <button
type="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" 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" title="Buscar endereço pelo CEP"
> >

View File

@ -163,15 +163,23 @@ class MedicoService {
]; ];
for (const ep of candidates) { for (const ep of candidates) {
try { try {
// Construir params manualmente para evitar valores booleanos diretos
const queryParams: Record<string, string> = {
select: "*",
};
// 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, { const response = await http.get<MedicoApi[] | MedicoApi>(ep, {
params: { params: queryParams,
select: "*",
...((params?.status && { active: params.status === "ativo" }) ||
{}),
...(params?.especialidade && {
specialty: `ilike.%${params.especialidade}%`,
}),
},
}); });
endpointTried.push(ep); endpointTried.push(ep);
if (response.success && response.data) { if (response.success && response.data) {

View File

@ -365,12 +365,24 @@ export async function createPatient(payload: {
const cleanEndereco: EnderecoPaciente | undefined = payload.endereco const cleanEndereco: EnderecoPaciente | undefined = payload.endereco
? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") } ? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") }
: undefined; : undefined;
const peso = typeof payload.pesoKg === "number" && payload.pesoKg > 0 && payload.pesoKg < 500 ? payload.pesoKg : undefined; const peso =
const altura = typeof payload.alturaM === "number" && payload.alturaM > 0 && payload.alturaM < 3 ? payload.alturaM : undefined; 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 (!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" }; if (!phone) return { success: false, error: "Telefone é obrigatório" };
const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({ const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({
@ -397,7 +409,8 @@ export async function createPatient(payload: {
const prune = () => { const prune = () => {
Object.keys(body).forEach((k) => { Object.keys(body).forEach((k) => {
const v = (body as Record<string, unknown>)[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(); prune();
@ -409,21 +422,36 @@ export async function createPatient(payload: {
{ headers: { Prefer: "return=representation" } } { headers: { Prefer: "return=representation" } }
); );
if (response.success && response.data) { 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: 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 // 1) tentar com CPF formatado
if (/numeric field overflow/i.test(baseError) && rawCpf.length === 11) { 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(); prune();
let r = await attempt(); let r = await attempt();
if (r.success) return r; if (r.success) return r;
// 2) remover campos opcionais progressivamente // 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) { for (const key of optional) {
if (key in body) { if (key in body) {
delete (body as Record<string, unknown>)[key]; delete (body as Record<string, unknown>)[key];
@ -439,14 +467,19 @@ export async function createPatient(payload: {
try { try {
let first = await attempt(); let first = await attempt();
if (!first.success && /numeric field overflow/i.test(first.error || "")) { 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; return first;
} catch (err: unknown) { } 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"; let msg = "Erro ao criar paciente";
if (e.response?.status === 401) msg = "Não autorizado"; 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; else if (e.response?.data?.message) msg = e.response.data.message;
if (/numeric field overflow/i.test(msg)) { if (/numeric field overflow/i.test(msg)) {
const overflowAttempt = await handleOverflowFallbacks(msg); const overflowAttempt = await handleOverflowFallbacks(msg);
@ -535,26 +568,70 @@ export async function updatePatient(
export async function deletePatient(id: string): Promise<ApiResponse<void>> { export async function deletePatient(id: string): Promise<ApiResponse<void>> {
if (!id) return { success: false, error: "ID é obrigatório" }; if (!id) return { success: false, error: "ID é obrigatório" };
try { try {
const resp = await http.delete<unknown>( console.log("[deletePatient] Tentando deletar paciente:", id);
`${ENDPOINTS.PATIENTS}/${encodeURIComponent(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) { if (!resp.success) {
console.error("[deletePatient] Falha ao deletar:", resp.error);
return { return {
success: false, success: false,
error: resp.error || "Falha ao deletar paciente", error: resp.error || "Falha ao deletar paciente",
}; };
} }
console.log("[deletePatient] Paciente deletado com sucesso");
return { success: true }; return { success: true };
} catch (error: unknown) { } catch (error: unknown) {
const err = error as { 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"; let msg = "Erro ao deletar paciente";
if (err.response?.status === 404) msg = "Paciente não encontrado"; const status = err.response?.status;
else if (err.response?.status === 401) msg = "Não autorizado"; const errorData = err.response?.data;
else if (err.response?.status === 403) msg = "Acesso negado";
else if (err.response?.data?.message) msg = err.response.data.message; console.error("[deletePatient] Erro capturado:", {
console.error(msg, error); 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 }; return { success: false, error: msg };
} }
} }

View File

@ -1,42 +1,40 @@
/** /**
* DEPRECATED: Substituído por content_json?: Record<string, unknown>; * Service para gerenciar relatórios médicos
status: "draft" | "final" | "preliminary"; * Endpoint: /rest/v1/reports
requested_by?: string;eportService.ts` (nomes em inglês e mapping padronizado).
* Manter temporariamente para compatibilidade até remoção.
*/ */
import api from "./api"; import { http, ApiResponse } from "./http";
import { ApiResponse } from "./http"; import ENDPOINTS from "./endpoints";
export interface Relatorio { export interface Relatorio {
id: string; id?: string;
patient_id: string; patient_id?: string;
order_number: string; order_number?: string;
exam: string; exam?: string;
diagnosis: string; diagnosis?: string;
conclusion: string; conclusion?: string;
cid_code?: string; cid_code?: string;
content_html?: string; content_html?: string;
content_json?: Record<string, unknown>; content_json?: Record<string, unknown>;
status: "draft" | "final" | "preliminary"; status?: "draft" | "pending" | "completed" | "cancelled";
requested_by?: string; requested_by?: string;
due_at?: string; due_at?: string;
hide_date?: boolean; hide_date?: boolean;
hide_signature?: boolean; hide_signature?: boolean;
created_at: string; created_at?: string;
updated_at: string; updated_at?: string;
created_by: string; created_by?: string;
} }
export interface RelatorioCreate { export interface RelatorioCreate {
patient_id: string; patient_id: string;
order_number?: string; order_number: string;
exam: string; exam?: string;
diagnosis: string; diagnosis?: string;
conclusion: string; conclusion?: string;
cid_code?: string; cid_code?: string;
content_html?: string; content_html?: string;
content_json?: Record<string, unknown>; content_json?: Record<string, unknown>;
status?: "draft" | "final" | "approved"; status?: "draft" | "pending" | "completed" | "cancelled";
requested_by?: string; requested_by?: string;
due_at?: string; due_at?: string;
hide_date?: boolean; hide_date?: boolean;
@ -52,87 +50,155 @@ export interface RelatorioUpdate {
cid_code?: string; cid_code?: string;
content_html?: string; content_html?: string;
content_json?: Record<string, unknown>; content_json?: Record<string, unknown>;
status?: "draft" | "final" | "approved"; status?: "draft" | "pending" | "completed" | "cancelled";
requested_by?: string; requested_by?: string;
due_at?: string; due_at?: string;
hide_date?: boolean; hide_date?: boolean;
hide_signature?: boolean; hide_signature?: boolean;
} }
export interface RelatorioListResponse {
data: Relatorio[];
total: number;
page: number;
per_page: number;
}
class RelatorioService { class RelatorioService {
// Listar relatórios com filtros opcionais
async listarRelatorios(params?: { async listarRelatorios(params?: {
page?: number; patient_id?: string;
per_page?: number; status?: "draft" | "pending" | "completed" | "cancelled";
tipo?: string; }): Promise<ApiResponse<Relatorio[]>> {
}): Promise<ApiResponse<RelatorioListResponse>> {
try { try {
const response = await api.get("/rest/v1/reports", { params }); const queryParams: Record<string, string> = { select: "*" };
return { success: true, data: response.data };
} catch (error: unknown) { 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); console.error("Erro ao listar relatórios:", error);
const errorMessage = return {
error instanceof Error && "response" in error success: false,
? (error as { response?: { data?: { message?: string } } }).response error: error instanceof Error ? error.message : "Erro desconhecido",
?.data?.message || "Erro ao listar relatórios" };
: "Erro ao listar relatórios";
return { success: false, error: errorMessage };
} }
} }
// Criar novo relatório
async criarRelatorio( async criarRelatorio(
relatorio: RelatorioCreate relatorio: RelatorioCreate
): Promise<ApiResponse<Relatorio>> { ): Promise<ApiResponse<Relatorio>> {
try { try {
const response = await api.post("/rest/v1/reports", relatorio); const response = await http.post<Relatorio>(
return { success: true, data: response.data }; ENDPOINTS.REPORTS,
} catch (error: unknown) { 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); console.error("Erro ao criar relatório:", error);
const errorMessage = return {
error instanceof Error && "response" in error success: false,
? (error as { response?: { data?: { message?: string } } }).response error: error instanceof Error ? error.message : "Erro desconhecido",
?.data?.message || "Erro ao criar relatório" };
: "Erro ao criar relatório";
return { success: false, error: errorMessage };
} }
} }
// Buscar relatório por ID
async buscarRelatorioPorId(id: string): Promise<ApiResponse<Relatorio>> { async buscarRelatorioPorId(id: string): Promise<ApiResponse<Relatorio>> {
try { try {
const response = await api.get(`/rest/v1/reports/${id}`); const response = await http.get<Relatorio[]>(
return { success: true, data: response.data }; `${ENDPOINTS.REPORTS}?id=eq.${id}`,
} catch (error: unknown) { {
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); console.error("Erro ao buscar relatório:", error);
const errorMessage = return {
error instanceof Error && "response" in error success: false,
? (error as { response?: { data?: { message?: string } } }).response error: error instanceof Error ? error.message : "Erro desconhecido",
?.data?.message || "Erro ao buscar relatório" };
: "Erro ao buscar relatório";
return { success: false, error: errorMessage };
} }
} }
// Atualizar relatório existente
async atualizarRelatorio( async atualizarRelatorio(
id: string, id: string,
updates: RelatorioUpdate updates: RelatorioUpdate
): Promise<ApiResponse<Relatorio>> { ): Promise<ApiResponse<Relatorio>> {
try { try {
const response = await api.patch(`/rest/v1/reports/${id}`, updates); const response = await http.patch<Relatorio>(
return { success: true, data: response.data }; `${ENDPOINTS.REPORTS}?id=eq.${id}`,
} catch (error: unknown) { 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); console.error("Erro ao atualizar relatório:", error);
const errorMessage = return {
error instanceof Error && "response" in error success: false,
? (error as { response?: { data?: { message?: string } } }).response error: error instanceof Error ? error.message : "Erro desconhecido",
?.data?.message || "Erro ao atualizar relatório" };
: "Erro ao atualizar relatório";
return { success: false, error: errorMessage };
} }
} }
} }