diff --git a/app/doctor/login/page.tsx b/app/doctor/login/page.tsx index c9cf646..051706e 100644 --- a/app/doctor/login/page.tsx +++ b/app/doctor/login/page.tsx @@ -1,11 +1,31 @@ // Caminho: app/(doctor)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function DoctorLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return (
- +
+

Área do Médico

+

Acesse o sistema médico

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/finance/login/page.tsx b/app/finance/login/page.tsx index d00e41c..636680a 100644 --- a/app/finance/login/page.tsx +++ b/app/finance/login/page.tsx @@ -1,12 +1,31 @@ // Caminho: app/(finance)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function FinanceLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return ( - // Fundo com gradiente laranja, como no seu código original
- +
+

Área Financeira

+

Acesse o sistema de faturamento

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..b193142 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,82 @@ +// Caminho: app/login/page.tsx + +import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; // Importa o ícone de seta + +export default function LoginPage() { + return ( +
+ + {/* PAINEL ESQUERDO: O Formulário */} +
+ + {/* Link para Voltar */} +
+ + + Voltar à página inicial + +
+ + {/* O contêiner principal que agora terá a sombra e o estilo de card */} +
+
+

Acesse sua conta

+

Bem-vindo(a) de volta ao MedConnect!

+
+ + + {/* Children para o LoginForm */} +
+ + + Esqueceu sua senha? + + +
+
+ +
+ Não tem uma conta de paciente? + + + Crie uma agora + + +
+
+
+ + {/* PAINEL DIREITO: A Imagem e Branding */} +
+ {/* Usamos o componente para otimização e performance */} + Médica utilizando um tablet na clínica MedConnect + {/* Camada de sobreposição para escurecer a imagem e destacar o texto */} +
+ {/* BLOCO DE NOME ADICIONADO */} +
+

+ MedConnect +

+
+

+ Tecnologia e Cuidado a Serviço da Sua Saúde. +

+

+ Acesse seu portal para uma experiência de saúde integrada, segura e eficiente. +

+
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/manager/login/page.tsx b/app/manager/login/page.tsx index db3aadc..cb236d3 100644 --- a/app/manager/login/page.tsx +++ b/app/manager/login/page.tsx @@ -1,12 +1,31 @@ // Caminho: app/(manager)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function ManagerLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return ( - // Mantemos o seu plano de fundo original
- +
+

Área do Gestor

+

Acesse o sistema médico

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index b254689..5c3d1f6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,7 +17,7 @@ export default function InicialPage() {

MedConnect

{} - + - +
+ Não tem uma conta? + + + Crie uma agora + + +
- {/* Conteúdo e espaçamento restaurados */}

Problemas para acessar? Entre em contato conosco

- + ); -} +} \ No newline at end of file diff --git a/app/secretary/login/page.tsx b/app/secretary/login/page.tsx index 3dabba8..fb9310c 100644 --- a/app/secretary/login/page.tsx +++ b/app/secretary/login/page.tsx @@ -1,11 +1,31 @@ // Caminho: app/(secretary)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function SecretaryLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return (
- -
+
+

Área da Secretária

+

Acesse o sistema de gerenciamento

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
+ ); -} +} \ No newline at end of file diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 71a6ab4..1176948 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -1,223 +1,157 @@ // Caminho: components/LoginForm.tsx -"use client"; +"use client" -import type React from "react"; -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import Cookies from "js-cookie"; -import { jwtDecode } from "jwt-decode"; -import { cn } from "@/lib/utils"; +import type React from "react" +import { useState } from "react" +import { useRouter } from "next/navigation" +import Link from "next/link" +import { cn } from "@/lib/utils" + +// Nossos serviços de API centralizados +import { loginWithEmailAndPassword, api } from "@/services/api"; // Componentes Shadcn UI -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { apikey } from "@/services/api.mjs"; - -// Hook customizado -import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Card, CardContent } from "@/components/ui/card" +import { useToast } from "@/hooks/use-toast" // Ícones -import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react"; +import { Eye, EyeOff, Loader2, Mail, Lock } from "lucide-react" interface LoginFormProps { - title: string; - description: string; - role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance"; - themeColor: "blue" | "green" | "orange"; - redirectPath: string; - children?: React.ReactNode; + children?: React.ReactNode } interface FormState { - email: string; - password: string; + email: string + password: string } -// Supondo que o payload do seu token tenha esta estrutura -interface DecodedToken { - name: string; - email: string; - role: string; - exp: number; - // Adicione outros campos que seu token possa ter -} +export function LoginForm({ children }: LoginFormProps) { + const [form, setForm] = useState({ email: "", password: "" }) + const [showPassword, setShowPassword] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const { toast } = useToast() -const themeClasses = { - blue: { - iconBg: "bg-blue-100", - iconText: "text-blue-600", - button: "bg-blue-600 hover:bg-blue-700", - link: "text-blue-600 hover:text-blue-700", - focus: "focus:border-blue-500 focus:ring-blue-500", - }, - green: { - iconBg: "bg-green-100", - iconText: "text-green-600", - button: "bg-green-600 hover:bg-green-700", - link: "text-green-600 hover:text-green-700", - focus: "focus:border-green-500 focus:ring-green-500", - }, - orange: { - iconBg: "bg-orange-100", - iconText: "text-orange-600", - button: "bg-orange-600 hover:bg-orange-700", - link: "text-orange-600 hover:text-orange-700", - focus: "focus:border-orange-500 focus:ring-orange-500", - }, -}; + // ================================================================== + // LÓGICA DE LOGIN INTELIGENTE E CENTRALIZADA + // ================================================================== + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + localStorage.removeItem("token"); + localStorage.removeItem("user_info"); -const roleIcons = { - secretary: UserCheck, - patient: Stethoscope, - doctor: Stethoscope, - admin: UserCheck, - manager: IdCard, - finance: Receipt, -}; - -export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) { - const [form, setForm] = useState({ email: "", password: "" }); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - const { toast } = useToast(); - - const currentTheme = themeClasses[themeColor]; - const Icon = roleIcons[role]; - - // ================================================================== - // AJUSTE PRINCIPAL NA LÓGICA DE LOGIN - // ================================================================== - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password"; - const API_KEY = apikey; - - if (!API_KEY) { - toast({ - title: "Erro de Configuração", - description: "A chave da API não foi encontrada.", - }); - setIsLoading(false); - return; + try { + const authData = await loginWithEmailAndPassword(form.email, form.password); + const user = authData.user; + if (!user || !user.id) { + throw new Error("Resposta de autenticação inválida: ID do usuário não encontrado."); } - try { - const response = await fetch(LOGIN_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - apikey: API_KEY, - }, - body: JSON.stringify({ email: form.email, password: form.password }), - }); + const rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error_description || "Credenciais inválidas. Tente novamente."); - } - - const accessToken = data.access_token; - const user = data.user; - - /* =================== Verificação de Role Desativada Temporariamente =================== */ - // if (user.user_metadata.role !== role) { - // toast({ title: "Acesso Negado", ... }); - // return; - // } - /* ===================================================================================== */ - - Cookies.set("access_token", accessToken, { expires: 1, secure: true }); - localStorage.setItem("user_info", JSON.stringify(user)); - - toast({ - title: "Login bem-sucedido!", - description: `Bem-vindo(a), ${user.user_metadata.full_name || "usuário"}! Redirecionando...`, - }); - - router.push(redirectPath); - } catch (error) { - toast({ - title: "Erro no Login", - description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", - }); - } finally { - setIsLoading(false); + if (!rolesData || rolesData.length === 0) { + throw new Error("Login bem-sucedido, mas nenhum perfil de acesso foi encontrado para este usuário."); } - }; - // O JSX do return permanece exatamente o mesmo, preservando seus ajustes. - return ( - - -
- -
-
- {title} - {description} -
-
- -
- {/* Inputs e Botão */} -
- -
- - setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> -
-
-
- -
- - setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> - -
-
- -
- {/* Conteúdo Extra (children) */} -
- {children ? ( -
-
-
-
-
-
- Novo por aqui? -
-
- {children} -
- ) : ( - <> -
- - ou -
-
- - Voltar à página inicial - -
- - )} -
-
-
- ); -} + const userRole = rolesData[0].role; + const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: userRole } }; + localStorage.setItem('user_info', JSON.stringify(completeUserInfo)); + + let redirectPath = ""; + switch (userRole) { + case "admin": + case "manager": redirectPath = "/manager/home"; break; + case "medico": redirectPath = "/doctor/medicos"; break; + case "secretary": redirectPath = "/secretary/pacientes"; break; + case "patient": redirectPath = "/patient/dashboard"; break; + case "finance": redirectPath = "/finance/home"; break; + } + + if (!redirectPath) { + throw new Error(`O perfil de acesso '${userRole}' não é válido para login. Contate o suporte.`); + } + + toast({ + title: "Login bem-sucedido!", + description: `Bem-vindo(a)! Redirecionando...`, + }); + + router.push(redirectPath); + + } catch (error) { + localStorage.removeItem("token"); + localStorage.removeItem("user_info"); + + console.error("ERRO DETALHADO NO CATCH:", error); + + toast({ + title: "Erro no Login", + description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", + }); + } finally { + setIsLoading(false); + } + } + + // ================================================================== + // JSX VISUALMENTE RICO E UNIFICADO + // ================================================================== + return ( + // Usamos Card e CardContent para manter a consistência, mas o estilo principal + // virá da página 'app/login/page.tsx' que envolve este componente. + + {/* Removemos o padding para dar controle à página pai */} +
+
+ +
+ + setForm({ ...form, email: e.target.value })} + className="pl-10 h-11" + required + disabled={isLoading} + autoComplete="username" // Boa prática de acessibilidade + /> +
+
+
+ +
+ + setForm({ ...form, password: e.target.value })} + className="pl-10 pr-12 h-11" + required + disabled={isLoading} + autoComplete="current-password" // Boa prática de acessibilidade + /> + +
+
+ +
+ + {/* O children permite que a página de login adicione links extras aqui */} + {children} +
+
+ ) +} \ No newline at end of file diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 767db28..c9134ba 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -4,7 +4,8 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; -import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA +import Cookies from "js-cookie"; // Manteremos para o logout, se necessário +import { api } from '@/services/api.mjs'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -39,23 +40,20 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { const router = useRouter(); const pathname = usePathname(); - // ================================================================== - // 2. BLOCO DE SEGURANÇA CORRIGIDO - // ================================================================== useEffect(() => { const userInfoString = localStorage.getItem("user_info"); - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO PRINCIPAL AQUI --- + // Procurando o token no localStorage, onde ele foi realmente salvo. + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera setDoctorData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Doutor(a)", email: userInfo.email || "", specialty: userInfo.user_metadata?.specialty || "Especialidade", - // Campos que não vêm do login, definidos como vazios para não quebrar phone: userInfo.phone || "", cpf: "", crm: "", @@ -63,35 +61,49 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { permissions: {}, }); } else { - // Se faltar o token ou os dados, volta para o login - router.push("/doctor/login"); + // Se não encontrar, aí sim redireciona. + router.push("/login"); } }, [router]); + // O restante do seu código permanece exatamente o mesmo... useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - handleResize(); // inicializa com a largura atual - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); -}, []); + const handleResize = () => setWindowWidth(window.innerWidth); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); -useEffect(() => { - if (isMobile) { - setSidebarCollapsed(true); - } else { - setSidebarCollapsed(false); - } -}, [isMobile]); + useEffect(() => { + if (isMobile) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } + }, [isMobile]); const handleLogout = () => { setShowLogoutDialog(true); }; - const confirmLogout = () => { - localStorage.removeItem("doctorData"); + + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + setShowLogoutDialog(false); - router.push("/"); - }; + router.push("/"); // Redireciona para a home + } + }; const cancelLogout = () => { setShowLogoutDialog(false); @@ -102,30 +114,10 @@ useEffect(() => { }; const menuItems = [ - { - href: "#", - icon: Home, - label: "Dashboard", - // Botão para o dashboard do médico - }, - { - href: "/doctor/medicos/consultas", - icon: Calendar, - label: "Consultas", - // Botão para página de consultas marcadas do médico atual - }, - { - href: "#", - icon: Clock, - label: "Editor de Laudo", - // Botão para página do editor de laudo - }, - { - href: "/doctor/medicos", - icon: User, - label: "Pacientes", - // Botão para a página de visualização de todos os pacientes - }, + { href: "#", icon: Home, label: "Dashboard" }, + { href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" }, + { href: "#", icon: Clock, label: "Editor de Laudo" }, + { href: "/doctor/medicos", icon: User, label: "Pacientes" }, ]; if (!doctorData) { @@ -133,8 +125,8 @@ useEffect(() => { } return ( + // O restante do seu código JSX permanece exatamente o mesmo
- {/* Sidebar para desktop */}
@@ -170,7 +162,6 @@ useEffect(() => {
- {/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */} {!sidebarCollapsed && ( <> @@ -189,7 +180,7 @@ useEffect(() => { )} {sidebarCollapsed && ( - {/* Centraliza o avatar quando recolhido */} + {doctorData.name @@ -201,7 +192,6 @@ useEffect(() => { )}
- {/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
{ const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href)); return ( - {/* Fechar menu ao clicar */} +
{item.label} @@ -259,17 +249,14 @@ useEffect(() => {

{doctorData.specialty}

-
- - {/* Main Content */} -
- {/* Header */} +
@@ -288,11 +275,9 @@ useEffect(() => {
- {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} diff --git a/components/finance-layout.tsx b/components/finance-layout.tsx index abcc7eb..d8b4bd2 100644 --- a/components/finance-layout.tsx +++ b/components/finance-layout.tsx @@ -1,3 +1,4 @@ +// Caminho: [seu-caminho]/FinancierLayout.tsx "use client"; import Cookies from "js-cookie"; @@ -5,32 +6,14 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; +import { api } from '@/services/api.mjs'; + import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Search, - Bell, - Calendar, - Clock, - User, - LogOut, - Menu, - X, - Home, - FileText, - ChevronLeft, - ChevronRight, -} from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react"; interface FinancierData { id: string; @@ -47,37 +30,45 @@ interface PatientLayoutProps { } export default function FinancierLayout({ children }: PatientLayoutProps) { - const [financierData, setFinancierData] = useState( - null - ); + const [financierData, setFinancierData] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const router = useRouter(); const pathname = usePathname(); useEffect(() => { - const data = localStorage.getItem("financierData"); - if (data) { - setFinancierData(JSON.parse(data)); + const userInfoString = localStorage.getItem("user_info"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + setFinancierData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Financeiro", + email: userInfo.email || "", + department: userInfo.user_metadata?.department || "Departamento Financeiro", + phone: userInfo.phone || "", + cpf: "", + permissions: {}, + }); } else { - router.push("/finance/login"); + // --- ALTERAÇÃO 2: Redirecionando para o login central --- + router.push("/login"); } }, [router]); - // 🔥 Responsividade automática da sidebar useEffect(() => { const handleResize = () => { - // Ajuste o breakpoint conforme necessário. 1024px (lg) ou 768px (md) são comuns. if (window.innerWidth < 1024) { setSidebarCollapsed(true); } else { setSidebarCollapsed(false); } }; - - handleResize(); // executa na primeira carga + handleResize(); window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); }, []); @@ -85,10 +76,22 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { setShowLogoutDialog(true); }; - const confirmLogout = () => { - localStorage.removeItem("financierData"); - setShowLogoutDialog(false); - router.push("/"); + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home + } }; const cancelLogout = () => { @@ -96,35 +99,19 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { }; const menuItems = [ - { - href: "#", - icon: Home, - label: "Dashboard", - }, - { - href: "#", - icon: Calendar, - label: "Relatórios financeiros", - }, - { - href: "#", - icon: User, - label: "Finanças Gerais", - }, - { - href: "#", - icon: Calendar, - label: "Configurações", - }, + { href: "#", icon: Home, label: "Dashboard" }, + { href: "#", icon: Calendar, label: "Relatórios financeiros" }, + { href: "#", icon: User, label: "Finanças Gerais" }, + { href: "#", icon: Calendar, label: "Configurações" }, ]; if (!financierData) { - return
Carregando...
; + return
Carregando...
; } return ( + // O restante do seu código JSX permanece inalterado
- {/* Sidebar */}
- {/* Footer user info */}
@@ -206,34 +192,29 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
)}
- {/* Botão Sair - ajustado para responsividade */}
- {/* Main Content */}
- {/* Header */}
@@ -257,11 +238,9 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
- {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} diff --git a/components/manager-layout.tsx b/components/manager-layout.tsx index 9d2600b..3af99ef 100644 --- a/components/manager-layout.tsx +++ b/components/manager-layout.tsx @@ -1,33 +1,19 @@ +// Caminho: [seu-caminho]/ManagerLayout.tsx "use client"; import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; -import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA +import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout +import { api } from '@/services/api.mjs'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Search, - Bell, - Calendar, - User, - LogOut, - ChevronLeft, - ChevronRight, - Home, -} from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react"; interface ManagerData { id: string; @@ -39,7 +25,7 @@ interface ManagerData { permissions: object; } -interface ManagerLayoutProps { // Corrigi o nome da prop aqui +interface ManagerLayoutProps { children: React.ReactNode; } @@ -50,89 +36,88 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { const router = useRouter(); const pathname = usePathname(); - // ================================================================== - // 2. BLOCO DE SEGURANÇA CORRIGIDO - // ================================================================== useEffect(() => { const userInfoString = localStorage.getItem("user_info"); - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera setManagerData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Gestor(a)", email: userInfo.email || "", department: userInfo.user_metadata?.role || "Gestão", - // Campos que não vêm do login, definidos como vazios para não quebrar phone: userInfo.phone || "", cpf: "", permissions: {}, }); } else { - // Se faltar o token ou os dados, volta para o login - router.push("/manager/login"); + // O redirecionamento para /login já estava correto. Ótimo! + router.push("/login"); } }, [router]); - - // 🔥 Responsividade automática da sidebar useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true); // colapsa em telas pequenas (lg breakpoint ~ 1024px) + setSidebarCollapsed(true); } else { - setSidebarCollapsed(false); // expande em desktop + setSidebarCollapsed(false); } }; - - handleResize(); // roda na primeira carga + handleResize(); window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); }, []); const handleLogout = () => setShowLogoutDialog(true); - const confirmLogout = () => { - localStorage.removeItem("managerData"); - setShowLogoutDialog(false); - router.push("/"); + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home + } }; const cancelLogout = () => setShowLogoutDialog(false); const menuItems = [ - { href: "#", icon: Home, label: "Dashboard" }, - { href: "#", icon: Calendar, label: "Relatórios gerenciais" }, - { href: "#", icon: User, label: "Gestão de Usuários" }, - { href: "#", icon: User, label: "Gestão de Médicos" }, - { href: "#", icon: Calendar, label: "Configurações" }, + { href: "#dashboard", icon: Home, label: "Dashboard" }, + { href: "#reports", icon: Calendar, label: "Relatórios gerenciais" }, + { href: "#users", icon: User, label: "Gestão de Usuários" }, + { href: "#doctors", icon: User, label: "Gestão de Médicos" }, + { href: "#settings", icon: Calendar, label: "Configurações" }, ]; if (!managerData) { - return
Carregando...
; + return
Carregando...
; } return (
- {/* Sidebar */}
- {/* Logo + collapse button */}
{!sidebarCollapsed && (
- - MidConnecta - + MidConnecta
)}
- {/* Menu Items */} - {/* Perfil no rodapé */}
- - {managerData.name - .split(" ") - .map((n) => n[0]) - .join("")} - + {managerData.name.split(" ").map((n) => n[0]).join("")} {!sidebarCollapsed && (
-

- {managerData.name} -

-

- {managerData.department} -

+

{managerData.name}

+

{managerData.department}

)}
- {/* Botão Sair - ajustado para responsividade */}
- {/* Conteúdo principal */} -
- {/* Header */} +
- {/* Search */}
- +
- - {/* Notifications */}
- - {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} Confirmar Saída - - Deseja realmente sair do sistema? Você precisará fazer login - novamente para acessar sua conta. - + Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta. - - + + diff --git a/components/patient-layout.tsx b/components/patient-layout.tsx index 9ff3aa1..775dc5f 100644 --- a/components/patient-layout.tsx +++ b/components/patient-layout.tsx @@ -1,36 +1,18 @@ "use client" - import Cookies from "js-cookie"; import type React from "react" import { useState, useEffect } from "react" import Link from "next/link" import { useRouter, usePathname } from "next/navigation" +import { api } from "@/services/api.mjs"; // Importando nosso cliente de API + import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { - Search, - Bell, - User, - LogOut, - FileText, - Clock, - Calendar, - Home, - ChevronLeft, - ChevronRight, -} from "lucide-react" - -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" +import { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" interface PatientData { name: string @@ -41,65 +23,72 @@ interface PatientData { address: string } -interface HospitalLayoutProps { +interface PatientLayoutProps { children: React.ReactNode } -export default function HospitalLayout({ children }: HospitalLayoutProps) { +// --- ALTERAÇÃO 1: Renomeando o componente para maior clareza --- +export default function PatientLayout({ children }: PatientLayoutProps) { const [patientData, setPatientData] = useState(null) const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false) const router = useRouter() const pathname = usePathname() - // 🔹 Ajuste automático no resize useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true) // colapsa no mobile + setSidebarCollapsed(true) } else { - setSidebarCollapsed(false) // expande no desktop + setSidebarCollapsed(false) } } - handleResize() window.addEventListener("resize", handleResize) return () => window.removeEventListener("resize", handleResize) }, []) useEffect(() => { - // 1. Procuramos pela chave correta: 'user_info' const userInfoString = localStorage.getItem("user_info"); - // 2. Para mais segurança, verificamos também se o token de acesso existe no cookie - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO 2: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - - // 3. Adaptamos os dados para a estrutura que seu layout espera (PatientData) - // Usamos os dados do objeto 'user' que a API do Supabase nos deu + setPatientData({ name: userInfo.user_metadata?.full_name || "Paciente", email: userInfo.email || "", - // Os campos abaixo não vêm do login, então os deixamos vazios por enquanto phone: userInfo.phone || "", cpf: "", birthDate: "", address: "", }); } else { - // Se as informações do usuário ou o token não forem encontrados, mandamos para o login. - router.push("/patient/login"); + // --- ALTERAÇÃO 3: Redirecionando para o login central --- + router.push("/login"); } }, [router]); const handleLogout = () => setShowLogoutDialog(true) - const confirmLogout = () => { - localStorage.removeItem("patientData") - setShowLogoutDialog(false) - router.push("/") - } + // --- ALTERAÇÃO 4: Função de logout completa e padronizada --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + console.error("Erro ao tentar fazer logout no servidor:", error); + } finally { + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial + } + }; const cancelLogout = () => setShowLogoutDialog(false) @@ -112,7 +101,7 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) { ] if (!patientData) { - return
Carregando...
+ return
Carregando...
; } return ( diff --git a/components/secretary-layout.tsx b/components/secretary-layout.tsx index a52b7bf..1ccb2b3 100644 --- a/components/secretary-layout.tsx +++ b/components/secretary-layout.tsx @@ -1,3 +1,4 @@ +// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo) "use client" import type React from "react" @@ -5,30 +6,14 @@ import { useState, useEffect } from "react" import { useRouter, usePathname } from "next/navigation" import Link from "next/link" import Cookies from "js-cookie"; +import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central + import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" - -import { - Search, - Bell, - Calendar, - Clock, - User, - LogOut, - Home, - ChevronLeft, - ChevronRight, -} from "lucide-react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react" interface SecretaryData { id: string @@ -46,12 +31,36 @@ interface SecretaryLayoutProps { } export default function SecretaryLayout({ children }: SecretaryLayoutProps) { + const [secretaryData, setSecretaryData] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false) const router = useRouter() const pathname = usePathname() - // 🔹 Colapsar no mobile e expandir no desktop automaticamente + useEffect(() => { + const userInfoString = localStorage.getItem("user_info"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + setSecretaryData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Secretária", + email: userInfo.email || "", + department: userInfo.user_metadata?.department || "Atendimento", + phone: userInfo.phone || "", + cpf: "", + employeeId: "", + permissions: {}, + }); + } else { + // --- ALTERAÇÃO 2: Redirecionando para o login central --- + router.push("/login"); + } + }, [router]); + useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { @@ -66,10 +75,25 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { }, []) const handleLogout = () => setShowLogoutDialog(true) - const confirmLogout = () => { - setShowLogoutDialog(false) - router.push("/") - } + + // --- ALTERAÇÃO 3: Função de logout completa e padronizada --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + console.error("Erro ao tentar fazer logout no servidor:", error); + } finally { + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial + } + }; + const cancelLogout = () => setShowLogoutDialog(false) const menuItems = [ @@ -79,17 +103,11 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { { href: "/secretary/pacientes", icon: User, label: "Pacientes" }, ] - const secretaryData: SecretaryData = { - id: "1", - name: "Secretária Exemplo", - email: "secretaria@hospital.com", - phone: "999999999", - cpf: "000.000.000-00", - employeeId: "12345", - department: "Atendimento", - permissions: {}, + if (!secretaryData) { + return
Carregando...
; } + return (
{/* Sidebar */} @@ -165,23 +183,20 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
)}
- {/* Botão Sair - ajustado para responsividade */}
@@ -191,7 +206,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64" }`} > - {/* Header */}
@@ -205,13 +219,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
- {/* Este botão no header parece ter sido uma cópia do botão "Sair" da sidebar. - Removi a lógica de sidebarCollapsed aqui, pois o header é independente. - Se a intenção era ter um botão de logout no header, ele não deve ser afetado pela sidebar. - Ajustei para ser um botão de sino de notificação, como nos exemplos anteriores, - já que você tem o ícone Bell importado e uma badge para notificação. - Se você quer um botão de LogOut aqui, por favor, me avise! - */}
- {/* Page Content */}
{children}
diff --git a/services/api.mjs b/services/api.mjs index 6c053b9..8fe59e9 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -1,47 +1,59 @@ +// Caminho: [seu-caminho]/services/api.mjs + const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; -export const apikey = API_KEY; -var tempToken; -export async function login() { - const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { +export async function loginWithEmailAndPassword(email, password) { + const response = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, { method: "POST", headers: { "Content-Type": "application/json", - Prefer: "return=representation", - apikey: API_KEY, // valor fixo + "apikey": API_KEY, }, - body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }), + body: JSON.stringify({ email, password }), }); const data = await response.json(); - if (typeof window !== 'undefined') { + if (!response.ok) { + throw new Error(data.error_description || "Credenciais inválidas."); + } + + if (data.access_token && typeof window !== 'undefined') { + // Padronizando para salvar o token no localStorage localStorage.setItem("token", data.access_token); } return data; } -let loginPromise = login(); +// --- NOVA FUNÇÃO DE LOGOUT CENTRALIZADA --- +async function logout() { + const token = localStorage.getItem("token"); + if (!token) return; // Se não há token, não há o que fazer + + try { + await fetch(`${BASE_URL}/auth/v1/logout`, { + method: "POST", + headers: { + "apikey": API_KEY, + "Authorization": `Bearer ${token}`, + }, + }); + } catch (error) { + // Mesmo que a chamada falhe, o logout no cliente deve continuar. + // O token pode já ter expirado no servidor, por exemplo. + console.error("Falha ao invalidar token no servidor (isso pode ser normal se o token já expirou):", error); + } +} async function request(endpoint, options = {}) { - if (loginPromise) { - try { - await loginPromise; - } catch (error) { - console.error("Falha na autenticação inicial:", error); - } - - loginPromise = null; - } - const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null; const headers = { "Content-Type": "application/json", - apikey: API_KEY, - ...(token ? { Authorization: `Bearer ${token}` } : {}), + "apikey": API_KEY, + ...(token ? { "Authorization": `Bearer ${token}` } : {}), ...options.headers, }; @@ -52,35 +64,29 @@ async function request(endpoint, options = {}) { }); if (!response.ok) { - let errorBody = `Status: ${response.status}`; + let errorBody; try { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - const jsonError = await response.json(); - - errorBody = jsonError.message || JSON.stringify(jsonError); - } else { - errorBody = await response.text(); - } + errorBody = await response.json(); } catch (e) { - errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`; + errorBody = await response.text(); } - - throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${errorBody}`); - } - const contentType = response.headers.get("content-type"); - if (response.status === 204 || (contentType && !contentType.includes("application/json")) || !contentType) { - return {}; + throw new Error(`Erro HTTP: ${response.status} - ${JSON.stringify(errorBody)}`); } + + if (response.status === 204) return {}; return await response.json(); + } catch (error) { console.error("Erro na requisição:", error); throw error; } } + +// Adicionamos a função de logout ao nosso objeto de API exportado export const api = { get: (endpoint, options) => request(endpoint, { method: "GET", ...options }), - post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }), - patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), - delete: (endpoint) => request(endpoint, { method: "DELETE" }), -}; + post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }), + patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }), + delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }), + logout: logout, // <-- EXPORTANDO A NOVA FUNÇÃO +}; \ No newline at end of file