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>
|
</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" />
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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,27 +76,22 @@ 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
|
||||||
@ -88,13 +107,10 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, email: e.target.value }))
|
||||||
...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="secretaria@clinica.com"
|
placeholder="seu@email.com"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
/>
|
/>
|
||||||
@ -115,10 +131,7 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={formData.senha}
|
value={formData.senha}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, senha: e.target.value }))
|
||||||
...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"
|
className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
|
||||||
placeholder="Sua senha"
|
placeholder="Sua senha"
|
||||||
@ -127,204 +140,20 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Email:riseup@popcode.com.br <br />
|
|
||||||
Senha: riseup
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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"}
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
) : (
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||||
<form
|
<strong>{LOCAL_SECRETARIA.email}</strong> /{" "}
|
||||||
onSubmit={(e) => {
|
<strong>{LOCAL_SECRETARIA.senha}</strong>
|
||||||
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>
|
</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>
|
</form>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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:", {
|
||||||
|
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));
|
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) {
|
} 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(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const { formatted } = maskCpf(event.target.value);
|
const { formatted } = maskCpf(event.target.value);
|
||||||
setFormDataPaciente((prev) => ({ ...prev, cpf: formatted }));
|
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, "");
|
||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -163,15 +163,23 @@ class MedicoService {
|
|||||||
];
|
];
|
||||||
for (const ep of candidates) {
|
for (const ep of candidates) {
|
||||||
try {
|
try {
|
||||||
const response = await http.get<MedicoApi[] | MedicoApi>(ep, {
|
// Construir params manualmente para evitar valores booleanos diretos
|
||||||
params: {
|
const queryParams: Record<string, string> = {
|
||||||
select: "*",
|
select: "*",
|
||||||
...((params?.status && { active: params.status === "ativo" }) ||
|
};
|
||||||
{}),
|
|
||||||
...(params?.especialidade && {
|
// Supabase PostgREST usa formato: active=eq.true ou active=is.true
|
||||||
specialty: `ilike.%${params.especialidade}%`,
|
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);
|
endpointTried.push(ep);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
|
|||||||
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
{
|
||||||
console.error("Erro ao buscar relatório:", error);
|
params: { select: "*" },
|
||||||
const errorMessage =
|
}
|
||||||
error instanceof Error && "response" in error
|
);
|
||||||
? (error as { response?: { data?: { message?: string } } }).response
|
|
||||||
?.data?.message || "Erro ao buscar relatório"
|
if (response.success && response.data) {
|
||||||
: "Erro ao buscar relatório";
|
const reports = Array.isArray(response.data)
|
||||||
return { success: false, error: errorMessage };
|
? 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(
|
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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user