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>
<button
onClick={logout}
className="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex-shrink-0"
className="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-gray-100 hover:bg-gray-200 hover:scale-105 active:scale-95 text-gray-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex-shrink-0"
aria-label={i18n.t("header.logout")}
>
<LogOut className="w-4 h-4 mr-1" aria-hidden="true" />
@ -106,7 +106,7 @@ const Header: React.FC = () => {
) : (
<Link
to="/paciente"
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 text-white transition-all focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm hover:shadow-md"
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 text-white transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm hover:shadow-md"
aria-label={i18n.t("header.login")}
>
<LogIn className="w-4 h-4 mr-2" aria-hidden="true" />

View File

@ -3,6 +3,7 @@ import { User, Stethoscope, Clipboard, ChevronDown } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { i18n } from "../i18n";
import { telemetry } from "../services/telemetry";
import { useAuth } from "../hooks/useAuth";
export type ProfileType = "patient" | "doctor" | "secretary" | null;
@ -51,6 +52,7 @@ export const ProfileSelector: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate();
const dropdownRef = useRef<HTMLDivElement>(null);
const { isAuthenticated, user } = useAuth();
useEffect(() => {
// Carregar perfil salvo
@ -96,8 +98,49 @@ export const ProfileSelector: React.FC = () => {
// Telemetria
telemetry.trackProfileChange(previousProfile, profile.type || "none");
// Navegar
navigate(profile.path);
// Navegar - condicional baseado em autenticação e role
let targetPath = profile.path; // default: caminho do perfil (login)
if (isAuthenticated && user) {
// Se autenticado, redirecionar para o painel apropriado baseado na role
switch (user.role) {
case "paciente":
if (profile.type === "patient") {
targetPath = "/acompanhamento"; // painel do paciente
}
break;
case "medico":
if (profile.type === "doctor") {
targetPath = "/painel-medico"; // painel do médico
}
break;
case "secretaria":
if (profile.type === "secretary") {
targetPath = "/painel-secretaria"; // painel da secretária
}
break;
case "admin":
// Admin pode ir para qualquer painel
if (profile.type === "secretary") {
targetPath = "/painel-secretaria";
} else if (profile.type === "doctor") {
targetPath = "/painel-medico";
} else if (profile.type === "patient") {
targetPath = "/acompanhamento";
}
break;
}
console.log(
`🔀 ProfileSelector: Usuário autenticado (${user.role}), redirecionando para ${targetPath}`
);
} else {
console.log(
`🔀 ProfileSelector: Usuário NÃO autenticado, redirecionando para ${targetPath}`
);
}
navigate(targetPath);
};
const getCurrentProfile = () => {

View File

@ -12,18 +12,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
roles,
redirectTo = "/",
}) => {
const { isAuthenticated, role, loading } = useAuth();
const { isAuthenticated, role, loading, user } = useAuth();
const location = useLocation();
console.log("[ProtectedRoute]", {
console.log("[ProtectedRoute] VERIFICAÇÃO COMPLETA", {
path: location.pathname,
isAuthenticated,
role,
loading,
requiredRoles: roles,
user: user ? { id: user.id, nome: user.nome, email: user.email } : null,
timestamp: new Date().toISOString(),
});
// Verificar localStorage para debug
try {
const stored = localStorage.getItem("appSession");
console.log(
"[ProtectedRoute] localStorage appSession:",
stored ? JSON.parse(stored) : null
);
} catch (e) {
console.error("[ProtectedRoute] Erro ao ler localStorage:", e);
}
if (loading) {
console.log("[ProtectedRoute] ⏳ Ainda carregando sessão...");
return (
<div className="py-10 text-center text-sm text-gray-500">
Verificando sessão...
@ -33,12 +47,16 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
if (!isAuthenticated) {
console.log(
"[ProtectedRoute] Não autenticado, redirecionando para:",
"[ProtectedRoute] ❌ NÃO AUTENTICADO! User:",
user,
"Redirecionando para:",
redirectTo
);
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
console.log("[ProtectedRoute] ✅ Autenticado! Verificando roles...");
// Admin tem acesso a tudo
if (role === "admin") {
console.log("[ProtectedRoute] Admin detectado, permitindo acesso");

View File

@ -88,43 +88,164 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const [user, setUser] = useState<SessionUser | null>(null);
const [loading, setLoading] = useState(true);
// Restaurar sessão do localStorage e verificar token
// Log sempre que user ou loading mudar
useEffect(() => {
console.log("[AuthContext] 🔄 ESTADO MUDOU:", {
user: user ? { id: user.id, nome: user.nome, role: user.role } : null,
loading,
isAuthenticated: !!user,
timestamp: new Date().toISOString(),
});
}, [user, loading]);
// RE-VERIFICAR sessão quando user estiver null mas localStorage tiver dados
// Isso corrige o problema de navegação entre páginas perdendo o estado
useEffect(() => {
if (!loading && !user) {
console.log(
"[AuthContext] 🔍 User é null mas loading false, verificando localStorage..."
);
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as PersistedSession;
if (parsed?.user?.role) {
console.log(
"[AuthContext] 🔧 RECUPERANDO sessão perdida:",
parsed.user.nome
);
setUser(parsed.user);
// Restaurar tokens também
if (parsed.token) {
import("../services/tokenStore").then((module) => {
module.default.setTokens(parsed.token!, parsed.refreshToken);
});
}
}
} catch (e) {
console.error("[AuthContext] Erro ao recuperar sessão:", e);
}
}
}
}, [user, loading]);
// Monitorar mudanças no localStorage para debug
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) {
console.log("[AuthContext] 📢 localStorage MUDOU externamente!", {
oldValue: e.oldValue ? "TINHA DADOS" : "VAZIO",
newValue: e.newValue ? "TEM DADOS" : "VAZIO",
url: e.url,
});
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
// Restaurar sessão do localStorage e verificar token
// IMPORTANTE: Este useEffect roda apenas UMA VEZ quando o AuthProvider monta
useEffect(() => {
console.log("[AuthContext] 🚀 INICIANDO RESTAURAÇÃO DE SESSÃO (mount)");
console.log("[AuthContext] 🔍 Verificando TODOS os itens no localStorage:");
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
const value = localStorage.getItem(key);
console.log(` - ${key}: ${value?.substring(0, 50)}...`);
}
}
const restoreSession = async () => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
// Tentar localStorage primeiro, depois sessionStorage como backup
let raw = localStorage.getItem(STORAGE_KEY);
console.log(
"[AuthContext] localStorage raw:",
raw ? "EXISTE" : "VAZIO"
);
if (!raw) {
console.log(
"[AuthContext] 🔍 localStorage vazio, tentando sessionStorage..."
);
raw = sessionStorage.getItem(STORAGE_KEY);
console.log(
"[AuthContext] sessionStorage raw:",
raw ? "EXISTE" : "VAZIO"
);
if (raw) {
// Restaurar do sessionStorage para localStorage
console.log(
"[AuthContext] 🔄 Restaurando do sessionStorage para localStorage"
);
localStorage.setItem(STORAGE_KEY, raw);
}
}
if (raw) {
console.log(
"[AuthContext] Conteúdo completo:",
raw.substring(0, 100) + "..."
);
}
if (raw) {
const parsed = JSON.parse(raw) as PersistedSession;
if (parsed?.user?.role) {
console.log("[AuthContext] Restaurando sessão:", parsed.user);
console.log("[AuthContext] ✅ Restaurando sessão:", {
nome: parsed.user.nome,
role: parsed.user.role,
hasToken: !!parsed.token,
});
// Verificar se há tokens válidos salvos
if (parsed.token) {
console.log("[AuthContext] Restaurando tokens no tokenStore");
// Restaurar tokens no tokenStore
const tokenStore = (await import("../services/tokenStore"))
.default;
tokenStore.setTokens(parsed.token, parsed.refreshToken);
} else {
console.warn("[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore...");
// Verificar se há token no tokenStore (pode ter sido salvo diretamente)
console.warn(
"[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore..."
);
const tokenStore = (await import("../services/tokenStore"))
.default;
const existingToken = tokenStore.getAccessToken();
if (existingToken) {
console.log("[AuthContext] Token encontrado no tokenStore, mantendo sessão");
console.log(
"[AuthContext] Token encontrado no tokenStore, mantendo sessão"
);
} else {
console.warn("[AuthContext] Nenhum token encontrado. Sessão pode estar inválida.");
console.warn(
"[AuthContext] ⚠️ Nenhum token encontrado. Sessão pode estar inválida."
);
}
}
console.log(
"[AuthContext] 📝 Chamando setUser com:",
parsed.user.nome
);
setUser(parsed.user);
} else {
console.log(
"[AuthContext] ⚠️ Sessão parseada mas sem user.role válido"
);
}
} else {
console.log("[AuthContext] Nenhuma sessão salva encontrada");
console.log(
"[AuthContext] Nenhuma sessão salva encontrada no localStorage"
);
}
} catch (error) {
console.error("[AuthContext] Erro ao restaurar sessão:", error);
console.error("[AuthContext] Erro ao restaurar sessão:", error);
} finally {
console.log(
"[AuthContext] 🏁 Finalizando restauração, setLoading(false)"
);
setLoading(false);
}
};
@ -133,17 +254,37 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const persist = useCallback((session: PersistedSession) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
} catch {
/* ignore */
console.log(
"[AuthContext] 💾 SALVANDO sessão no localStorage E sessionStorage:",
{
user: session.user.nome,
role: session.user.role,
hasToken: !!session.token,
}
);
const sessionStr = JSON.stringify(session);
localStorage.setItem(STORAGE_KEY, sessionStr);
sessionStorage.setItem(STORAGE_KEY, sessionStr); // BACKUP em sessionStorage
console.log(
"[AuthContext] ✅ Sessão salva com sucesso em ambos storages!"
);
} catch (error) {
console.error("[AuthContext] ❌ ERRO ao salvar sessão:", error);
}
}, []);
const clearPersisted = useCallback(() => {
try {
console.log(
"[AuthContext] 🗑️ REMOVENDO sessão do localStorage E sessionStorage"
);
localStorage.removeItem(STORAGE_KEY);
} catch {
/* ignore */
sessionStorage.removeItem(STORAGE_KEY);
console.log(
"[AuthContext] ✅ Sessão removida com sucesso de ambos storages!"
);
} catch (error) {
console.error("[AuthContext] ❌ ERRO ao remover sessão:", error);
}
}, []);
@ -353,18 +494,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
);
const logout = useCallback(async () => {
console.log("[AuthContext] Iniciando logout...");
try {
const resp = await authService.logout(); // chama /auth/v1/logout (204 esperado)
if (!resp.success && resp.error) {
console.warn("[AuthContext] Falha no logout remoto:", resp.error);
toast.error(`Falha no logout remoto: ${resp.error}`);
} else {
toast.success("Sessão encerrada no servidor");
console.log("[AuthContext] Logout remoto bem-sucedido");
}
} catch (e) {
console.warn("Erro inesperado ao executar logout remoto", e);
toast("Logout local (falha remota)");
console.warn(
"[AuthContext] Erro inesperado ao executar logout remoto",
e
);
} finally {
// Limpa contexto local
console.log("[AuthContext] Limpando estado local...");
setUser(null);
clearPersisted();
authService.clearLocalAuth();
@ -373,6 +519,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
} catch {
// ignore
}
console.log("[AuthContext] Logout completo - usuário removido do estado");
// Modelo somente Supabase: nenhum token técnico para invalidar
}
}, [clearPersisted]);

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">
<button
onClick={() => handleCTA("Agendar consulta", "/paciente")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600"
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600"
aria-label={i18n.t(
"home.actionCards.scheduleAppointment.ctaAriaLabel"
)}
@ -122,7 +122,7 @@ const Home: React.FC = () => {
<button
onClick={() => handleCTA("Ver próximas consultas", "/consultas")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl border-2 border-white/20 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-blue-600"
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-blue-600"
aria-label="Ver lista de próximas consultas"
>
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
@ -278,7 +278,7 @@ const ActionCard: React.FC<ActionCardProps> = ({
</p>
<button
onClick={onAction}
className="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 text-white rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 group-hover:shadow-lg"
className="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 text-white rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 group-hover:shadow-lg"
aria-label={ctaAriaLabel}
>
{ctaLabel}

View File

@ -4,8 +4,6 @@ import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
// interface Medico is not required in this component
const LoginMedico: React.FC = () => {
const [formData, setFormData] = useState({
email: "",
@ -14,30 +12,61 @@ const LoginMedico: React.FC = () => {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { loginComEmailSenha } = useAuth();
const { loginMedico, loginComEmailSenha } = useAuth();
// Credenciais fixas para LOGIN LOCAL de médico
const LOCAL_MEDICO = {
email: "fernando.pirichowski@souunit.com.br",
senha: "fernando",
nome: "Dr. Fernando Pirichowski",
id: "fernando.pirichowski@souunit.com.br",
} as const;
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Primeiro tenta fluxo real Supabase (grant_type=password)
let ok = await loginComEmailSenha(formData.email, formData.senha);
// Se falhar (ex: usuário não mapeado ainda), cai no fallback legado de médico
if (!ok) {
ok = await loginMedico(formData.email, formData.senha);
console.log("[LoginMedico] Fazendo login com email:", formData.email);
const authService = (await import("../services/authService")).default;
const loginResult = await authService.login({
email: formData.email,
password: formData.senha,
});
if (!loginResult.success) {
console.log("[LoginMedico] Erro no login:", loginResult.error);
toast.error(loginResult.error || "Email ou senha incorretos");
setLoading(false);
return;
}
console.log("[LoginMedico] Login bem-sucedido!", loginResult.data);
const tokenStore = (await import("../services/tokenStore")).default;
const token = tokenStore.getAccessToken();
console.log("[LoginMedico] Token salvo:", token ? "SIM" : "NÃO");
if (!token) {
console.error("[LoginMedico] Token não foi salvo!");
toast.error("Erro ao salvar credenciais de autenticação");
setLoading(false);
return;
}
const ok = await loginComEmailSenha(formData.email, formData.senha);
if (ok) {
// Login bem-sucedido, redirecionar para painel médico
// A verificação de permissões será feita pelo ProtectedRoute
console.log("[LoginMedico] Login realizado, redirecionando...");
console.log("[LoginMedico] Navegando para /painel-medico");
toast.success("Login realizado com sucesso!");
navigate("/painel-medico");
} else {
console.error("[LoginMedico] loginComEmailSenha retornou false");
toast.error("Erro ao processar login");
}
} catch (error) {
console.error("Erro no login:", error);
console.error("[LoginMedico] Erro no login:", error);
toast.error("Erro ao fazer login. Tente novamente.");
} finally {
setLoading(false);
@ -47,7 +76,6 @@ const LoginMedico: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
<div className="max-w-md w-full">
{/* Header */}
<div className="text-center mb-8">
<div className="bg-gradient-to-r from-indigo-600 to-indigo-400 dark:from-indigo-700 dark:to-indigo-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
<Stethoscope className="w-8 h-8 text-white" />
@ -60,7 +88,6 @@ const LoginMedico: React.FC = () => {
</p>
</div>
{/* Formulário */}
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors"
aria-live="polite"
@ -83,7 +110,7 @@ const LoginMedico: React.FC = () => {
setFormData((prev) => ({ ...prev, email: e.target.value }))
}
className="form-input pl-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-100"
placeholder="dr.medico@clinica.com"
placeholder="seu@email.com"
required
autoComplete="email"
/>
@ -117,22 +144,16 @@ const LoginMedico: React.FC = () => {
<button
type="submit"
disabled={loading}
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
>
{loading ? "Entrando..." : "Entrar"}
</button>
</form>
{/* Informações de demonstração */}
<div className="mt-6 p-4 bg-indigo-50 dark:bg-gray-700/40 rounded-lg">
<h3 className="text-sm font-medium text-indigo-800 dark:text-indigo-300 mb-2">
Para Demonstração:
</h3>
<p className="text-sm text-indigo-700 dark:text-indigo-200">
Email:riseup@popcode.com.br <br />
Senha: riseup
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
<strong>{LOCAL_MEDICO.email}</strong> /{" "}
<strong>{LOCAL_MEDICO.senha}</strong>
</p>
</div>
</form>
</div>
</div>
</div>

View File

@ -59,10 +59,10 @@ const LoginPaciente: React.FC = () => {
// Credenciais fixas para LOGIN LOCAL de paciente
const LOCAL_PATIENT = {
email: "pedro.araujo@mediconnect.com",
senha: "local123",
nome: "Pedro Araujo",
id: "pedro.araujo@mediconnect.com",
email: "guilhermesilvagomes1020@gmail.com",
senha: "guilherme123",
nome: "Guilherme Silva Gomes",
id: "guilhermesilvagomes1020@gmail.com",
} as const;
const handleLogin = async (e: React.FormEvent) => {
@ -359,13 +359,13 @@ const LoginPaciente: React.FC = () => {
type="button"
onClick={handleLoginLocal}
disabled={loading}
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
>
{loading ? "Entrando..." : "Entrar"}
</button>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
Credenciais locais: <strong>{LOCAL_PATIENT.email}</strong> /
<strong> {LOCAL_PATIENT.senha}</strong>
<strong>{LOCAL_PATIENT.email}</strong> /{" "}
<strong>{LOCAL_PATIENT.senha}</strong>
</p>
</form>
) : (

View File

@ -10,40 +10,64 @@ const LoginSecretaria: React.FC = () => {
senha: "",
});
const [loading, setLoading] = useState(false);
const [showCadastro, setShowCadastro] = useState(false);
const [cadastroData, setCadastroData] = useState({
nome: "",
email: "",
senha: "",
confirmarSenha: "",
telefone: "",
cpf: "",
});
const navigate = useNavigate();
const { loginComEmailSenha } = useAuth();
// Credenciais fixas para LOGIN LOCAL de secretaria
const LOCAL_SECRETARIA = {
email: "secretaria.mediconnect@gmail.com",
senha: "secretaria@mediconnect",
nome: "Secretaria MediConnect",
id: "secretaria.mediconnect@gmail.com",
} as const;
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
console.log("[LoginSecretaria] Tentando login com:", formData.email);
// Tenta login real via authService primeiro
console.log("[LoginSecretaria] Fazendo login com email:", formData.email);
const authService = (await import("../services/authService")).default;
const loginResult = await authService.login({
email: formData.email,
password: formData.senha,
});
if (!loginResult.success) {
console.log("[LoginSecretaria] Erro no login:", loginResult.error);
toast.error(loginResult.error || "Email ou senha incorretos");
setLoading(false);
return;
}
console.log("[LoginSecretaria] Login bem-sucedido!", loginResult.data);
const tokenStore = (await import("../services/tokenStore")).default;
const token = tokenStore.getAccessToken();
console.log("[LoginSecretaria] Token salvo:", token ? "SIM" : "NÃO");
if (!token) {
console.error("[LoginSecretaria] Token não foi salvo!");
toast.error("Erro ao salvar credenciais de autenticação");
setLoading(false);
return;
}
const ok = await loginComEmailSenha(formData.email, formData.senha);
console.log("[LoginSecretaria] Resultado login:", ok);
if (ok) {
console.log("[LoginSecretaria] Login bem-sucedido, redirecionando...");
console.log("[LoginSecretaria] Navegando para /painel-secretaria");
toast.success("Login realizado com sucesso!");
navigate("/painel-secretaria");
} else {
console.error("[LoginSecretaria] Login falhou - credenciais inválidas");
toast.error("Email ou senha incorretos");
console.error("[LoginSecretaria] loginComEmailSenha retornou false");
toast.error("Erro ao processar login");
}
} catch (error) {
console.error("[LoginSecretaria] Erro no login:", error);
toast.error("Erro ao fazer login. Verifique suas credenciais.");
toast.error("Erro ao fazer login. Tente novamente.");
} finally {
setLoading(false);
}
@ -52,279 +76,84 @@ const LoginSecretaria: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
<div className="max-w-md w-full">
{/* Header */}
<div className="text-center mb-8">
<div className="bg-gradient-to-r from-green-600 to-green-400 dark:from-green-700 dark:to-green-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
<Clipboard className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
{showCadastro ? "Criar Conta de Secretária" : "Área da Secretaria"}
Área da Secretaria
</h1>
<p className="text-gray-600 dark:text-gray-400">
{showCadastro
? "Preencha os dados para criar uma conta de secretária"
: "Faça login para acessar o sistema de gestão"}
Faça login para acessar o sistema de gestão
</p>
</div>
{/* Formulário */}
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors"
aria-live="polite"
>
{!showCadastro ? (
<form onSubmit={handleLogin} className="space-y-6" noValidate>
<div>
<label
htmlFor="sec_email"
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"
<form onSubmit={handleLogin} className="space-y-6" noValidate>
<div>
<label
htmlFor="sec_email"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{loading ? "Entrando..." : "Entrar"}
</button>
</form>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
toast(
"Cadastro de secretária não disponível. Entre em contato com o administrador."
);
}}
className="space-y-4"
noValidate
>
<div>
<label
htmlFor="sec_cad_nome"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Nome Completo
</label>
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_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"
id="sec_email"
type="email"
value={cadastroData.email}
value={formData.email}
onChange={(e) =>
setCadastroData((prev) => ({
...prev,
email: e.target.value,
}))
setFormData((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"
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
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>
)}
</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 className="flex space-x-4">
<button
type="button"
onClick={() => setShowCadastro(false)}
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors"
>
Voltar
</button>
<button
type="submit"
disabled={loading}
className="flex-1 bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{loading ? "Cadastrando..." : "Cadastrar"}
</button>
</div>
</form>
)}
</div>
<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 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
>
{loading ? "Entrando..." : "Entrar"}
</button>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 text-center">
<strong>{LOCAL_SECRETARIA.email}</strong> /{" "}
<strong>{LOCAL_SECRETARIA.senha}</strong>
</p>
</form>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -163,15 +163,23 @@ class MedicoService {
];
for (const ep of candidates) {
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, {
params: {
select: "*",
...((params?.status && { active: params.status === "ativo" }) ||
{}),
...(params?.especialidade && {
specialty: `ilike.%${params.especialidade}%`,
}),
},
params: queryParams,
});
endpointTried.push(ep);
if (response.success && response.data) {

View File

@ -365,12 +365,24 @@ export async function createPatient(payload: {
const cleanEndereco: EnderecoPaciente | undefined = payload.endereco
? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") }
: undefined;
const peso = typeof payload.pesoKg === "number" && payload.pesoKg > 0 && payload.pesoKg < 500 ? payload.pesoKg : undefined;
const altura = typeof payload.alturaM === "number" && payload.alturaM > 0 && payload.alturaM < 3 ? payload.alturaM : undefined;
const peso =
typeof payload.pesoKg === "number" &&
payload.pesoKg > 0 &&
payload.pesoKg < 500
? payload.pesoKg
: undefined;
const altura =
typeof payload.alturaM === "number" &&
payload.alturaM > 0 &&
payload.alturaM < 3
? payload.alturaM
: undefined;
if (!payload.nome?.trim()) return { success: false, error: "Nome é obrigatório" };
if (!payload.nome?.trim())
return { success: false, error: "Nome é obrigatório" };
if (!rawCpf) return { success: false, error: "CPF é obrigatório" };
if (!payload.email?.trim()) return { success: false, error: "Email é obrigatório" };
if (!payload.email?.trim())
return { success: false, error: "Email é obrigatório" };
if (!phone) return { success: false, error: "Telefone é obrigatório" };
const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({
@ -397,7 +409,8 @@ export async function createPatient(payload: {
const prune = () => {
Object.keys(body).forEach((k) => {
const v = (body as Record<string, unknown>)[k];
if (v === undefined || v === "") delete (body as Record<string, unknown>)[k];
if (v === undefined || v === "")
delete (body as Record<string, unknown>)[k];
});
};
prune();
@ -409,21 +422,36 @@ export async function createPatient(payload: {
{ headers: { Prefer: "return=representation" } }
);
if (response.success && response.data) {
const raw = Array.isArray(response.data) ? response.data[0] : response.data;
const raw = Array.isArray(response.data)
? response.data[0]
: response.data;
return { success: true, data: mapPacienteFromApi(raw) };
}
return { success: false, error: response.error || "Erro ao criar paciente" };
return {
success: false,
error: response.error || "Erro ao criar paciente",
};
};
const handleOverflowFallbacks = async (baseError: string): Promise<ApiResponse<Paciente>> => {
const handleOverflowFallbacks = async (
baseError: string
): Promise<ApiResponse<Paciente>> => {
// 1) tentar com CPF formatado
if (/numeric field overflow/i.test(baseError) && rawCpf.length === 11) {
body = buildBody(rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"));
body = buildBody(
rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")
);
prune();
let r = await attempt();
if (r.success) return r;
// 2) remover campos opcionais progressivamente
const optional: Array<keyof PatientInputSchema> = ["weight_kg", "height_m", "blood_type", "cep", "number"];
const optional: Array<keyof PatientInputSchema> = [
"weight_kg",
"height_m",
"blood_type",
"cep",
"number",
];
for (const key of optional) {
if (key in body) {
delete (body as Record<string, unknown>)[key];
@ -439,14 +467,19 @@ export async function createPatient(payload: {
try {
let first = await attempt();
if (!first.success && /numeric field overflow/i.test(first.error || "")) {
first = await handleOverflowFallbacks(first.error || "numeric field overflow");
first = await handleOverflowFallbacks(
first.error || "numeric field overflow"
);
}
return first;
} catch (err: unknown) {
const e = err as { response?: { status?: number; data?: { message?: string } } };
const e = err as {
response?: { status?: number; data?: { message?: string } };
};
let msg = "Erro ao criar paciente";
if (e.response?.status === 401) msg = "Não autorizado";
else if (e.response?.status === 400) msg = e.response.data?.message || "Dados inválidos";
else if (e.response?.status === 400)
msg = e.response.data?.message || "Dados inválidos";
else if (e.response?.data?.message) msg = e.response.data.message;
if (/numeric field overflow/i.test(msg)) {
const overflowAttempt = await handleOverflowFallbacks(msg);
@ -535,26 +568,70 @@ export async function updatePatient(
export async function deletePatient(id: string): Promise<ApiResponse<void>> {
if (!id) return { success: false, error: "ID é obrigatório" };
try {
const resp = await http.delete<unknown>(
`${ENDPOINTS.PATIENTS}/${encodeURIComponent(id)}`
console.log("[deletePatient] Tentando deletar paciente:", id);
console.log(
"[deletePatient] Endpoint:",
`${ENDPOINTS.PATIENTS}?id=eq.${encodeURIComponent(id)}`
);
// Supabase REST API DELETE usa query params com filtro
// Formato: /rest/v1/patients?id=eq.<uuid>
// É necessário adicionar header Prefer: return=representation ou return=minimal
const resp = await http.delete<unknown>(
`${ENDPOINTS.PATIENTS}?id=eq.${encodeURIComponent(id)}`,
{
headers: {
Prefer: "return=minimal",
},
}
);
console.log("[deletePatient] Resposta:", resp);
if (!resp.success) {
console.error("[deletePatient] Falha ao deletar:", resp.error);
return {
success: false,
error: resp.error || "Falha ao deletar paciente",
};
}
console.log("[deletePatient] Paciente deletado com sucesso");
return { success: true };
} catch (error: unknown) {
const err = error as {
response?: { status?: number; data?: { message?: string } };
response?: {
status?: number;
data?: {
message?: string;
hint?: string;
details?: string;
error?: string;
};
};
};
let msg = "Erro ao deletar paciente";
if (err.response?.status === 404) msg = "Paciente não encontrado";
else if (err.response?.status === 401) msg = "Não autorizado";
else if (err.response?.status === 403) msg = "Acesso negado";
else if (err.response?.data?.message) msg = err.response.data.message;
console.error(msg, error);
const status = err.response?.status;
const errorData = err.response?.data;
console.error("[deletePatient] Erro capturado:", {
status,
message: errorData?.message,
hint: errorData?.hint,
details: errorData?.details,
error: errorData?.error,
fullError: error,
});
if (status === 404) msg = "Paciente não encontrado";
else if (status === 401) msg = "Não autorizado - faça login novamente";
else if (status === 403)
msg = "Acesso negado - você não tem permissão para excluir pacientes";
else if (status === 406) msg = "Formato de requisição inválido";
else if (errorData?.error) msg = errorData.error;
else if (errorData?.message) msg = errorData.message;
else if (errorData?.hint) msg = `${msg}: ${errorData.hint}`;
console.error("[deletePatient]", msg, error);
return { success: false, error: msg };
}
}

View File

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