refactor(auth): Centraliza e padroniza o fluxo de autenticação

Esta refatoração unifica todo o sistema de login e logout da aplicação, resolvendo inconsistências e eliminando código duplicado.

Problema Anterior:
- A lógica de login estava espalhada por múltiplos componentes e páginas (`/doctor/login`, `/patient/login`, etc.).
- Cada layout de área restrita (`DoctorLayout`, `PatientLayout`, etc.) tinha sua própria lógica de verificação de segurança e logout, resultando em bugs (ex: uso de Cookies vs. localStorage).

Solução Aplicada:
- Foi criado um componente `LoginForm` unificado e inteligente, responsável por toda a interação de login.
- Toda a lógica de comunicação com a API de autenticação foi centralizada no serviço `api.mjs`, incluindo uma nova função `api.logout()`.
- Todos os layouts de áreas restritas (`DoctorLayout`, `PatientLayout`, etc.) foram padronizados para usar `localStorage.getItem('token')` para verificação e para chamar `api.logout()` ao sair.
- As páginas de login específicas de cada perfil foram atualizadas para usar o novo `LoginForm` genérico.
This commit is contained in:
Gabriel Lira Figueira 2025-10-15 23:29:31 -03:00
parent d6a950560f
commit f6f206ff63
14 changed files with 615 additions and 617 deletions

View File

@ -1,11 +1,31 @@
// Caminho: app/(doctor)/login/page.tsx // Caminho: app/(doctor)/login/page.tsx
import { LoginForm } from "@/components/LoginForm"; import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function DoctorLoginPage() { 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 ( return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 flex items-center justify-center p-4">
<LoginForm title="Área do Médico" description="Acesse o sistema médico" role="doctor" themeColor="green" redirectPath="/doctor/medicos" /> <div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área do Médico</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema médico</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div> </div>
); );
} }

View File

@ -1,12 +1,31 @@
// Caminho: app/(finance)/login/page.tsx // Caminho: app/(finance)/login/page.tsx
import { LoginForm } from "@/components/LoginForm"; import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function FinanceLoginPage() { 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 ( return (
// Fundo com gradiente laranja, como no seu código original
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-orange-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-orange-50 flex items-center justify-center p-4">
<LoginForm title="Área Financeira" description="Acesse o sistema de faturamento" role="finance" themeColor="orange" redirectPath="/finance/home" /> <div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área Financeira</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema de faturamento</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div> </div>
); );
} }

82
app/login/page.tsx Normal file
View File

@ -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 (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-2">
{/* PAINEL ESQUERDO: O Formulário */}
<div className="relative flex flex-col items-center justify-center p-8 bg-background">
{/* Link para Voltar */}
<div className="absolute top-8 left-8">
<Link href="/" className="inline-flex items-center text-muted-foreground hover:text-primary transition-colors font-medium">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar à página inicial
</Link>
</div>
{/* O contêiner principal que agora terá a sombra e o estilo de card */}
<div className="w-full max-w-md bg-card p-10 rounded-2xl shadow-xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-foreground">Acesse sua conta</h1>
<p className="text-muted-foreground mt-2">Bem-vindo(a) de volta ao MedConnect!</p>
</div>
<LoginForm>
{/* Children para o LoginForm */}
<div className="mt-4 text-center text-sm">
<Link href="/esqueci-minha-senha">
<span className="text-muted-foreground hover:text-primary cursor-pointer underline">
Esqueceu sua senha?
</span>
</Link>
</div>
</LoginForm>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">Não tem uma conta de paciente? </span>
<Link href="/patient/register">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Crie uma agora
</span>
</Link>
</div>
</div>
</div>
{/* PAINEL DIREITO: A Imagem e Branding */}
<div className="hidden lg:block relative">
{/* Usamos o componente <Image> para otimização e performance */}
<Image
src="https://images.unsplash.com/photo-1576091160550-2173dba999ef?q=80&w=2070" // Uma imagem profissional de alta qualidade
alt="Médica utilizando um tablet na clínica MedConnect"
fill
style={{ objectFit: 'cover' }}
priority // Ajuda a carregar a imagem mais rápido
/>
{/* Camada de sobreposição para escurecer a imagem e destacar o texto */}
<div className="absolute inset-0 bg-primary/80 flex flex-col items-start justify-end p-12 text-left">
{/* BLOCO DE NOME ADICIONADO */}
<div className="mb-6 border-l-4 border-primary-foreground pl-4">
<h1 className="text-5xl font-extrabold text-primary-foreground tracking-wider">
MedConnect
</h1>
</div>
<h2 className="text-4xl font-bold text-primary-foreground leading-tight">
Tecnologia e Cuidado a Serviço da Sua Saúde.
</h2>
<p className="mt-4 text-lg text-primary-foreground/80">
Acesse seu portal para uma experiência de saúde integrada, segura e eficiente.
</p>
</div>
</div>
</div>
);
}

View File

@ -1,12 +1,31 @@
// Caminho: app/(manager)/login/page.tsx // Caminho: app/(manager)/login/page.tsx
import { LoginForm } from "@/components/LoginForm"; import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function ManagerLoginPage() { 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 ( return (
// Mantemos o seu plano de fundo original
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área do Gestor" description="Acesse o sistema médico" role="manager" themeColor="blue" redirectPath="/manager/home" /> <div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área do Gestor</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema médico</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div> </div>
); );
} }

View File

@ -17,7 +17,7 @@ export default function InicialPage() {
<header className="bg-card shadow-md py-4 px-6 flex justify-between items-center"> <header className="bg-card shadow-md py-4 px-6 flex justify-between items-center">
<h1 className="text-2xl font-bold text-primary">MedConnect</h1> <h1 className="text-2xl font-bold text-primary">MedConnect</h1>
<nav className="flex space-x-6 text-muted-foreground font-medium"> <nav className="flex space-x-6 text-muted-foreground font-medium">
<a href="#home" className="hover:text-primary">Home</a> <a href="#home" className="hover:text-primary"><Link href="/cadastro">Home</Link></a>
<a href="#about" className="hover:text-primary">Sobre</a> <a href="#about" className="hover:text-primary">Sobre</a>
<a href="#departments" className="hover:text-primary">Departamentos</a> <a href="#departments" className="hover:text-primary">Departamentos</a>
<a href="#doctors" className="hover:text-primary">Médicos</a> <a href="#doctors" className="hover:text-primary">Médicos</a>
@ -25,7 +25,7 @@ export default function InicialPage() {
</nav> </nav>
<div className="flex space-x-4"> <div className="flex space-x-4">
{} {}
<Link href="/cadastro"> <Link href="/login">
<Button <Button
variant="outline" variant="outline"
className="rounded-full px-6 py-2 border-2 transition cursor-pointer" className="rounded-full px-6 py-2 border-2 transition cursor-pointer"

View File

@ -1,4 +1,4 @@
// Caminho: app/(patient)/login/page.tsx // Caminho: app/patient/login/page.tsx
import Link from "next/link"; import Link from "next/link";
import { LoginForm } from "@/components/LoginForm"; import { LoginForm } from "@/components/LoginForm";
@ -6,6 +6,12 @@ import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
export default function PatientLoginPage() { export default function PatientLoginPage() {
// NOTA: Esta página de login específica para pacientes se tornou obsoleta
// com a criação da nossa página de login central em /login.
// Mantemos este arquivo por enquanto para evitar quebrar outras partes do código,
// mas o ideal no futuro seria deletar esta página e redirecionar
// /patient/login para /login.
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex flex-col items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex flex-col items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
@ -16,20 +22,25 @@ export default function PatientLoginPage() {
</Link> </Link>
</div> </div>
<LoginForm title="Área do Paciente" description="Acesse sua conta para gerenciar consultas" role="patient" themeColor="blue" redirectPath="/patient/dashboard"> {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Removemos as props desnecessárias (title, description, role, etc.) */}
{/* O novo LoginForm é autônomo e não precisa mais delas. */}
<LoginForm>
{/* Este bloco é passado como 'children' para o LoginForm */} {/* Este bloco é passado como 'children' para o LoginForm */}
<Link href="/patient/register" passHref> <div className="mt-6 text-center text-sm">
<Button variant="outline" className="w-full h-12 text-base"> <span className="text-muted-foreground">Não tem uma conta? </span>
Criar nova conta <Link href="/patient/register">
</Button> <span className="font-semibold text-primary hover:underline cursor-pointer">
</Link> Crie uma agora
</span>
</Link>
</div>
</LoginForm> </LoginForm>
{/* Conteúdo e espaçamento restaurados */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p> <p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -1,11 +1,31 @@
// Caminho: app/(secretary)/login/page.tsx // Caminho: app/(secretary)/login/page.tsx
import { LoginForm } from "@/components/LoginForm"; import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function SecretaryLoginPage() { 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 ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área da Secretária" description="Acesse o sistema de gerenciamento" role="secretary" themeColor="blue" redirectPath="/secretary/pacientes" /> <div className="w-full max-w-md text-center">
</div> <h1 className="text-3xl font-bold text-foreground mb-2">Área da Secretária</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema de gerenciamento</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div>
); );
} }

View File

@ -1,223 +1,157 @@
// Caminho: components/LoginForm.tsx // Caminho: components/LoginForm.tsx
"use client"; "use client"
import type React from "react"; import type React from "react"
import { useState } from "react"; import { useState } from "react"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
import Link from "next/link"; import Link from "next/link"
import Cookies from "js-cookie"; import { cn } from "@/lib/utils"
import { jwtDecode } from "jwt-decode";
import { cn } from "@/lib/utils"; // Nossos serviços de API centralizados
import { loginWithEmailAndPassword, api } from "@/services/api";
// Componentes Shadcn UI // Componentes Shadcn UI
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"; import { useToast } from "@/hooks/use-toast"
import { apikey } from "@/services/api.mjs";
// Hook customizado
import { useToast } from "@/hooks/use-toast";
// Ícones // Í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 { interface LoginFormProps {
title: string; children?: React.ReactNode
description: string;
role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance";
themeColor: "blue" | "green" | "orange";
redirectPath: string;
children?: React.ReactNode;
} }
interface FormState { interface FormState {
email: string; email: string
password: string; password: string
} }
// Supondo que o payload do seu token tenha esta estrutura export function LoginForm({ children }: LoginFormProps) {
interface DecodedToken { const [form, setForm] = useState<FormState>({ email: "", password: "" })
name: string; const [showPassword, setShowPassword] = useState(false)
email: string; const [isLoading, setIsLoading] = useState(false)
role: string; const router = useRouter()
exp: number; const { toast } = useToast()
// Adicione outros campos que seu token possa ter
}
const themeClasses = { // ==================================================================
blue: { // LÓGICA DE LOGIN INTELIGENTE E CENTRALIZADA
iconBg: "bg-blue-100", // ==================================================================
iconText: "text-blue-600", const handleSubmit = async (e: React.FormEvent) => {
button: "bg-blue-600 hover:bg-blue-700", e.preventDefault();
link: "text-blue-600 hover:text-blue-700", setIsLoading(true);
focus: "focus:border-blue-500 focus:ring-blue-500", localStorage.removeItem("token");
}, localStorage.removeItem("user_info");
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",
},
};
const roleIcons = { try {
secretary: UserCheck, const authData = await loginWithEmailAndPassword(form.email, form.password);
patient: Stethoscope, const user = authData.user;
doctor: Stethoscope, if (!user || !user.id) {
admin: UserCheck, throw new Error("Resposta de autenticação inválida: ID do usuário não encontrado.");
manager: IdCard,
finance: Receipt,
};
export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) {
const [form, setForm] = useState<FormState>({ 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 rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`);
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 data = await response.json(); if (!rolesData || rolesData.length === 0) {
throw new Error("Login bem-sucedido, mas nenhum perfil de acesso foi encontrado para este usuário.");
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);
} }
};
// O JSX do return permanece exatamente o mesmo, preservando seus ajustes. const userRole = rolesData[0].role;
return ( const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: userRole } };
<Card className="w-full max-w-md shadow-xl border-0 bg-white/80 backdrop-blur-sm"> localStorage.setItem('user_info', JSON.stringify(completeUserInfo));
<CardHeader className="text-center space-y-4 pb-8">
<div className={cn("mx-auto w-16 h-16 rounded-full flex items-center justify-center", currentTheme.iconBg)}> let redirectPath = "";
<Icon className={cn("w-8 h-8", currentTheme.iconText)} /> switch (userRole) {
</div> case "admin":
<div> case "manager": redirectPath = "/manager/home"; break;
<CardTitle className="text-2xl font-bold text-gray-900">{title}</CardTitle> case "medico": redirectPath = "/doctor/medicos"; break;
<CardDescription className="text-gray-600 mt-2">{description}</CardDescription> case "secretary": redirectPath = "/secretary/pacientes"; break;
</div> case "patient": redirectPath = "/patient/dashboard"; break;
</CardHeader> case "finance": redirectPath = "/finance/home"; break;
<CardContent className="px-8 pb-8"> }
<form onSubmit={handleSubmit} className="space-y-6">
{/* Inputs e Botão */} if (!redirectPath) {
<div className="space-y-2"> throw new Error(`O perfil de acesso '${userRole}' não é válido para login. Contate o suporte.`);
<Label htmlFor="email">E-mail</Label> }
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> toast({
<Input id="email" type="email" placeholder="seu.email@clinica.com" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> title: "Login bem-sucedido!",
</div> description: `Bem-vindo(a)! Redirecionando...`,
</div> });
<div className="space-y-2">
<Label htmlFor="password">Senha</Label> router.push(redirectPath);
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> } catch (error) {
<Input id="password" type={showPassword ? "text" : "password"} placeholder="Digite sua senha" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> localStorage.removeItem("token");
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-gray-400 hover:text-gray-600" disabled={isLoading}> localStorage.removeItem("user_info");
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> console.error("ERRO DETALHADO NO CATCH:", error);
</div>
</div> toast({
<Button type="submit" className={cn("w-full h-12 text-base font-semibold", currentTheme.button)} disabled={isLoading}> title: "Erro no Login",
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"} description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
</Button> });
</form> } finally {
{/* Conteúdo Extra (children) */} setIsLoading(false);
<div className="mt-8"> }
{children ? ( }
<div className="space-y-4">
<div className="relative"> // ==================================================================
<div className="absolute inset-0 flex items-center"> // JSX VISUALMENTE RICO E UNIFICADO
<div className="w-full border-t border-slate-200"></div> // ==================================================================
</div> return (
<div className="relative flex justify-center text-sm"> // Usamos Card e CardContent para manter a consistência, mas o estilo principal
<span className="px-4 bg-white text-slate-500">Novo por aqui?</span> // virá da página 'app/login/page.tsx' que envolve este componente.
</div> <Card className="w-full bg-transparent border-0 shadow-none">
</div> <CardContent className="p-0"> {/* Removemos o padding para dar controle à página pai */}
{children} <form onSubmit={handleSubmit} className="space-y-6">
</div> <div className="space-y-2">
) : ( <Label htmlFor="email">E-mail</Label>
<> <div className="relative">
<div className="relative"> <Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Separator className="my-6" /> <Input
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-3 text-sm text-gray-500">ou</span> id="email"
</div> type="email"
<div className="text-center"> placeholder="seu.email@exemplo.com"
<Link href="/" className={cn("text-sm font-medium hover:underline", currentTheme.link)}> value={form.email}
Voltar à página inicial onChange={(e) => setForm({ ...form, email: e.target.value })}
</Link> className="pl-10 h-11"
</div> required
</> disabled={isLoading}
)} autoComplete="username" // Boa prática de acessibilidade
</div> />
</CardContent> </div>
</Card> </div>
); <div className="space-y-2">
} <Label htmlFor="password">Senha</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-12 h-11"
required
disabled={isLoading}
autoComplete="current-password" // Boa prática de acessibilidade
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-muted-foreground hover:text-foreground" disabled={isLoading}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button type="submit" className="w-full h-11 text-base font-semibold" disabled={isLoading}>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"}
</Button>
</form>
{/* O children permite que a página de login adicione links extras aqui */}
{children}
</CardContent>
</Card>
)
}

View File

@ -4,7 +4,8 @@ import type React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -39,23 +40,20 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
// ==================================================================
// 2. BLOCO DE SEGURANÇA CORRIGIDO
// ==================================================================
useEffect(() => { useEffect(() => {
const userInfoString = localStorage.getItem("user_info"); 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) { if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString); const userInfo = JSON.parse(userInfoString);
// 3. "TRADUZIMOS" os dados da API para o formato que o layout espera
setDoctorData({ setDoctorData({
id: userInfo.id || "", id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Doutor(a)", name: userInfo.user_metadata?.full_name || "Doutor(a)",
email: userInfo.email || "", email: userInfo.email || "",
specialty: userInfo.user_metadata?.specialty || "Especialidade", specialty: userInfo.user_metadata?.specialty || "Especialidade",
// Campos que não vêm do login, definidos como vazios para não quebrar
phone: userInfo.phone || "", phone: userInfo.phone || "",
cpf: "", cpf: "",
crm: "", crm: "",
@ -63,35 +61,49 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
permissions: {}, permissions: {},
}); });
} else { } else {
// Se faltar o token ou os dados, volta para o login // Se não encontrar, aí sim redireciona.
router.push("/doctor/login"); router.push("/login");
} }
}, [router]); }, [router]);
// O restante do seu código permanece exatamente o mesmo...
useEffect(() => { useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth); const handleResize = () => setWindowWidth(window.innerWidth);
handleResize(); // inicializa com a largura atual handleResize();
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isMobile) { if (isMobile) {
setSidebarCollapsed(true); setSidebarCollapsed(true);
} else { } else {
setSidebarCollapsed(false); setSidebarCollapsed(false);
} }
}, [isMobile]); }, [isMobile]);
const handleLogout = () => { const handleLogout = () => {
setShowLogoutDialog(true); 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); setShowLogoutDialog(false);
router.push("/"); router.push("/"); // Redireciona para a home
}; }
};
const cancelLogout = () => { const cancelLogout = () => {
setShowLogoutDialog(false); setShowLogoutDialog(false);
@ -102,30 +114,10 @@ useEffect(() => {
}; };
const menuItems = [ const menuItems = [
{ { href: "#", icon: Home, label: "Dashboard" },
href: "#", { href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
icon: Home, { href: "#", icon: Clock, label: "Editor de Laudo" },
label: "Dashboard", { href: "/doctor/medicos", icon: User, label: "Pacientes" },
// 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
},
]; ];
if (!doctorData) { if (!doctorData) {
@ -133,8 +125,8 @@ useEffect(() => {
} }
return ( return (
// O restante do seu código JSX permanece exatamente o mesmo
<div className="min-h-screen bg-background flex"> <div className="min-h-screen bg-background flex">
{/* Sidebar para desktop */}
<div className={`bg-card border-r border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}> <div className={`bg-card border-r border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
<div className="p-4 border-b border"> <div className="p-4 border-b border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -170,7 +162,6 @@ useEffect(() => {
<div className="border-t p-4 mt-auto"> <div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4"> <div className="flex items-center space-x-3 mb-4">
{/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */}
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<> <>
<Avatar> <Avatar>
@ -189,7 +180,7 @@ useEffect(() => {
</> </>
)} )}
{sidebarCollapsed && ( {sidebarCollapsed && (
<Avatar className="mx-auto"> {/* Centraliza o avatar quando recolhido */} <Avatar className="mx-auto">
<AvatarImage src="/placeholder.svg?height=40&width=40" /> <AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback> <AvatarFallback>
{doctorData.name {doctorData.name
@ -201,7 +192,6 @@ useEffect(() => {
)} )}
</div> </div>
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
<div <div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`} className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`}
onClick={handleLogout} onClick={handleLogout}
@ -233,7 +223,7 @@ useEffect(() => {
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href)); const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return ( return (
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */} <Link key={item.href} href={item.href} onClick={toggleMobileMenu}>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}> <div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}>
<Icon className="w-5 h-5 flex-shrink-0" /> <Icon className="w-5 h-5 flex-shrink-0" />
<span className="font-medium">{item.label}</span> <span className="font-medium">{item.label}</span>
@ -259,17 +249,14 @@ useEffect(() => {
<p className="text-xs text-muted-foreground truncate">{doctorData.specialty}</p> <p className="text-xs text-muted-foreground truncate">{doctorData.specialty}</p>
</div> </div>
</div> </div>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */} <Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}>
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
Sair Sair
</Button> </Button>
</div> </div>
</div> </div>
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Main Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Header */}
<header className="bg-card border-b border px-6 py-4"> <header className="bg-card border-b border px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
@ -288,11 +275,9 @@ useEffect(() => {
</div> </div>
</header> </header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main> <main className="flex-1 p-6">{children}</main>
</div> </div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}> <Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>

View File

@ -1,3 +1,4 @@
// Caminho: [seu-caminho]/FinancierLayout.tsx
"use client"; "use client";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@ -5,32 +6,14 @@ import type React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { api } from '@/services/api.mjs';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
Dialog, import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react";
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 { interface FinancierData {
id: string; id: string;
@ -47,37 +30,45 @@ interface PatientLayoutProps {
} }
export default function FinancierLayout({ children }: PatientLayoutProps) { export default function FinancierLayout({ children }: PatientLayoutProps) {
const [financierData, setFinancierData] = useState<FinancierData | null>( const [financierData, setFinancierData] = useState<FinancierData | null>(null);
null
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
useEffect(() => { useEffect(() => {
const data = localStorage.getItem("financierData"); const userInfoString = localStorage.getItem("user_info");
if (data) { // --- ALTERAÇÃO 1: Buscando o token no localStorage ---
setFinancierData(JSON.parse(data)); 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 { } else {
router.push("/finance/login"); // --- ALTERAÇÃO 2: Redirecionando para o login central ---
router.push("/login");
} }
}, [router]); }, [router]);
// 🔥 Responsividade automática da sidebar
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
// Ajuste o breakpoint conforme necessário. 1024px (lg) ou 768px (md) são comuns.
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
setSidebarCollapsed(true); setSidebarCollapsed(true);
} else { } else {
setSidebarCollapsed(false); setSidebarCollapsed(false);
} }
}; };
handleResize();
handleResize(); // executa na primeira carga
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
@ -85,10 +76,22 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
setShowLogoutDialog(true); setShowLogoutDialog(true);
}; };
const confirmLogout = () => { // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples ---
localStorage.removeItem("financierData"); const confirmLogout = async () => {
setShowLogoutDialog(false); try {
router.push("/"); // 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 = () => { const cancelLogout = () => {
@ -96,35 +99,19 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
}; };
const menuItems = [ const menuItems = [
{ { href: "#", icon: Home, label: "Dashboard" },
href: "#", { href: "#", icon: Calendar, label: "Relatórios financeiros" },
icon: Home, { href: "#", icon: User, label: "Finanças Gerais" },
label: "Dashboard", { href: "#", icon: Calendar, label: "Configurações" },
},
{
href: "#",
icon: Calendar,
label: "Relatórios financeiros",
},
{
href: "#",
icon: User,
label: "Finanças Gerais",
},
{
href: "#",
icon: Calendar,
label: "Configurações",
},
]; ];
if (!financierData) { if (!financierData) {
return <div>Carregando...</div>; return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
} }
return ( return (
// O restante do seu código JSX permanece inalterado
<div className="min-h-screen bg-background flex"> <div className="min-h-screen bg-background flex">
{/* Sidebar */}
<div <div
className={`bg-card border-r border-border transition-all duration-300 ${ className={`bg-card border-r border-border transition-all duration-300 ${
sidebarCollapsed ? "w-16" : "w-64" sidebarCollapsed ? "w-16" : "w-64"
@ -183,7 +170,6 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
})} })}
</nav> </nav>
{/* Footer user info */}
<div className="border-t p-4 mt-auto"> <div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4"> <div className="flex items-center space-x-3 mb-4">
<Avatar> <Avatar>
@ -206,34 +192,29 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div> </div>
)} )}
</div> </div>
{/* Botão Sair - ajustado para responsividade */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={ className={
sidebarCollapsed sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado ? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent" : "w-full bg-transparent"
} }
onClick={handleLogout} onClick={handleLogout}
> >
<LogOut <LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "} />
{/* Remove margem quando colapsado */} {!sidebarCollapsed && "Sair"}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
</Button> </Button>
</div> </div>
</div> </div>
{/* Main Content */}
<div <div
className={`flex-1 flex flex-col transition-all duration-300 ${ className={`flex-1 flex flex-col transition-all duration-300 ${
sidebarCollapsed ? "ml-16" : "ml-64" sidebarCollapsed ? "ml-16" : "ml-64"
}`} }`}
> >
{/* Header */}
<header className="bg-card border-b border-border px-6 py-4"> <header className="bg-card border-b border-border px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md"> <div className="flex items-center gap-4 flex-1 max-w-md">
@ -257,11 +238,9 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div> </div>
</header> </header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main> <main className="flex-1 p-6">{children}</main>
</div> </div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}> <Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>

View File

@ -1,33 +1,19 @@
// Caminho: [seu-caminho]/ManagerLayout.tsx
"use client"; "use client";
import type React from "react"; import type React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
Dialog, import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react";
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Search,
Bell,
Calendar,
User,
LogOut,
ChevronLeft,
ChevronRight,
Home,
} from "lucide-react";
interface ManagerData { interface ManagerData {
id: string; id: string;
@ -39,7 +25,7 @@ interface ManagerData {
permissions: object; permissions: object;
} }
interface ManagerLayoutProps { // Corrigi o nome da prop aqui interface ManagerLayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
@ -50,89 +36,88 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
// ==================================================================
// 2. BLOCO DE SEGURANÇA CORRIGIDO
// ==================================================================
useEffect(() => { useEffect(() => {
const userInfoString = localStorage.getItem("user_info"); 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) { if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString); const userInfo = JSON.parse(userInfoString);
// 3. "TRADUZIMOS" os dados da API para o formato que o layout espera
setManagerData({ setManagerData({
id: userInfo.id || "", id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Gestor(a)", name: userInfo.user_metadata?.full_name || "Gestor(a)",
email: userInfo.email || "", email: userInfo.email || "",
department: userInfo.user_metadata?.role || "Gestão", 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 || "", phone: userInfo.phone || "",
cpf: "", cpf: "",
permissions: {}, permissions: {},
}); });
} else { } else {
// Se faltar o token ou os dados, volta para o login // O redirecionamento para /login já estava correto. Ótimo!
router.push("/manager/login"); router.push("/login");
} }
}, [router]); }, [router]);
// 🔥 Responsividade automática da sidebar
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
setSidebarCollapsed(true); // colapsa em telas pequenas (lg breakpoint ~ 1024px) setSidebarCollapsed(true);
} else { } else {
setSidebarCollapsed(false); // expande em desktop setSidebarCollapsed(false);
} }
}; };
handleResize();
handleResize(); // roda na primeira carga
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
const handleLogout = () => setShowLogoutDialog(true); const handleLogout = () => setShowLogoutDialog(true);
const confirmLogout = () => { // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples ---
localStorage.removeItem("managerData"); const confirmLogout = async () => {
setShowLogoutDialog(false); try {
router.push("/"); // 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 cancelLogout = () => setShowLogoutDialog(false);
const menuItems = [ const menuItems = [
{ href: "#", icon: Home, label: "Dashboard" }, { href: "#dashboard", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios gerenciais" }, { href: "#reports", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "#", icon: User, label: "Gestão de Usuários" }, { href: "#users", icon: User, label: "Gestão de Usuários" },
{ href: "#", icon: User, label: "Gestão de Médicos" }, { href: "#doctors", icon: User, label: "Gestão de Médicos" },
{ href: "#", icon: Calendar, label: "Configurações" }, { href: "#settings", icon: Calendar, label: "Configurações" },
]; ];
if (!managerData) { if (!managerData) {
return <div>Carregando...</div>; return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
} }
return ( return (
<div className="min-h-screen bg-gray-50 flex"> <div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
<div <div
className={`bg-white border-r border-gray-200 transition-all duration-300 fixed top-0 h-screen flex flex-col z-30 className={`bg-white border-r border-gray-200 transition-all duration-300 fixed top-0 h-screen flex flex-col z-30 ${sidebarCollapsed ? "w-16" : "w-64"}`}
${sidebarCollapsed ? "w-16" : "w-64"}`}
> >
{/* Logo + collapse button */}
<div className="p-4 border-b border-gray-200 flex items-center justify-between"> <div className="p-4 border-b border-gray-200 flex items-center justify-between">
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-sm"></div> <div className="w-4 h-4 bg-white rounded-sm"></div>
</div> </div>
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900">MidConnecta</span>
MidConnecta
</span>
</div> </div>
)} )}
<Button <Button
@ -141,136 +126,79 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
onClick={() => setSidebarCollapsed(!sidebarCollapsed)} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1" className="p-1"
> >
{sidebarCollapsed ? ( {sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
</Button> </Button>
</div> </div>
{/* Menu Items */}
<nav className="flex-1 p-2 overflow-y-auto"> <nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => { {menuItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = const isActive = pathname === item.href;
pathname === item.href ||
(item.href !== "/" && pathname.startsWith(item.href));
return ( return (
<Link key={item.href} href={item.href}> <Link key={item.label} href={item.href}>
<div <div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${ className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}
isActive
? "bg-blue-50 text-blue-600 border-r-2 border-blue-600"
: "text-gray-600 hover:bg-gray-50"
}`}
> >
<Icon className="w-5 h-5 flex-shrink-0" /> <Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && ( {!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
<span className="font-medium">{item.label}</span>
)}
</div> </div>
</Link> </Link>
); );
})} })}
</nav> </nav>
{/* Perfil no rodapé */}
<div className="border-t p-4 mt-auto"> <div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4"> <div className="flex items-center space-x-3 mb-4">
<Avatar> <Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" /> <AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback> <AvatarFallback>{managerData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
{managerData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar> </Avatar>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"> <p className="text-sm font-medium text-gray-900 truncate">{managerData.name}</p>
{managerData.name} <p className="text-xs text-gray-500 truncate">{managerData.department}</p>
</p>
<p className="text-xs text-gray-500 truncate">
{managerData.department}
</p>
</div> </div>
)} )}
</div> </div>
{/* Botão Sair - ajustado para responsividade */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={ className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"}
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
: "w-full bg-transparent"
}
onClick={handleLogout} onClick={handleLogout}
> >
<LogOut <LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} {!sidebarCollapsed && "Sair"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
</Button> </Button>
</div> </div>
</div> </div>
{/* Conteúdo principal */} <div className={`flex-1 flex flex-col transition-all duration-300 w-full ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<div
className={`flex-1 flex flex-col transition-all duration-300 w-full
${sidebarCollapsed ? "ml-16" : "ml-64"}`}
>
{/* Header */}
<header className="bg-white border-b border-gray-200 px-4 md:px-6 py-4 flex items-center justify-between"> <header className="bg-white border-b border-gray-200 px-4 md:px-6 py-4 flex items-center justify-between">
{/* Search */}
<div className="flex items-center gap-4 flex-1 max-w-md"> <div className="flex items-center gap-4 flex-1 max-w-md">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input <Input placeholder="Buscar paciente" className="pl-10 bg-gray-50 border-gray-200" />
placeholder="Buscar paciente"
className="pl-10 bg-gray-50 border-gray-200"
/>
</div> </div>
</div> </div>
{/* Notifications */}
<div className="flex items-center gap-4 ml-auto"> <div className="flex items-center gap-4 ml-auto">
<Button variant="ghost" size="sm" className="relative"> <Button variant="ghost" size="sm" className="relative">
<Bell className="w-5 h-5" /> <Bell className="w-5 h-5" />
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-red-500 text-white text-xs"> <Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-red-500 text-white text-xs">1</Badge>
1
</Badge>
</Button> </Button>
</div> </div>
</header> </header>
{/* Page Content */}
<main className="flex-1 p-4 md:p-6">{children}</main> <main className="flex-1 p-4 md:p-6">{children}</main>
</div> </div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}> <Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Confirmar Saída</DialogTitle> <DialogTitle>Confirmar Saída</DialogTitle>
<DialogDescription> <DialogDescription>Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta.</DialogDescription>
Deseja realmente sair do sistema? Você precisará fazer login
novamente para acessar sua conta.
</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="flex gap-2"> <DialogFooter className="flex gap-2">
<Button variant="outline" onClick={cancelLogout}> <Button variant="outline" onClick={cancelLogout}>Cancelar</Button>
Cancelar <Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</Button>
<Button variant="destructive" onClick={confirmLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sair
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,36 +1,18 @@
"use client" "use client"
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import type React from "react" import type React from "react"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import Link from "next/link" import Link from "next/link"
import { useRouter, usePathname } from "next/navigation" import { useRouter, usePathname } from "next/navigation"
import { api } from "@/services/api.mjs"; // Importando nosso cliente de API
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { import { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react"
Search, import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
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 { interface PatientData {
name: string name: string
@ -41,65 +23,72 @@ interface PatientData {
address: string address: string
} }
interface HospitalLayoutProps { interface PatientLayoutProps {
children: React.ReactNode 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<PatientData | null>(null) const [patientData, setPatientData] = useState<PatientData | null>(null)
const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
// 🔹 Ajuste automático no resize
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
setSidebarCollapsed(true) // colapsa no mobile setSidebarCollapsed(true)
} else { } else {
setSidebarCollapsed(false) // expande no desktop setSidebarCollapsed(false)
} }
} }
handleResize() handleResize()
window.addEventListener("resize", handleResize) window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize) return () => window.removeEventListener("resize", handleResize)
}, []) }, [])
useEffect(() => { useEffect(() => {
// 1. Procuramos pela chave correta: 'user_info'
const userInfoString = localStorage.getItem("user_info"); const userInfoString = localStorage.getItem("user_info");
// 2. Para mais segurança, verificamos também se o token de acesso existe no cookie // --- ALTERAÇÃO 2: Buscando o token no localStorage ---
const token = Cookies.get("access_token"); const token = localStorage.getItem("token");
if (userInfoString && token) { if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString); 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({ setPatientData({
name: userInfo.user_metadata?.full_name || "Paciente", name: userInfo.user_metadata?.full_name || "Paciente",
email: userInfo.email || "", email: userInfo.email || "",
// Os campos abaixo não vêm do login, então os deixamos vazios por enquanto
phone: userInfo.phone || "", phone: userInfo.phone || "",
cpf: "", cpf: "",
birthDate: "", birthDate: "",
address: "", address: "",
}); });
} else { } else {
// Se as informações do usuário ou o token não forem encontrados, mandamos para o login. // --- ALTERAÇÃO 3: Redirecionando para o login central ---
router.push("/patient/login"); router.push("/login");
} }
}, [router]); }, [router]);
const handleLogout = () => setShowLogoutDialog(true) const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => { // --- ALTERAÇÃO 4: Função de logout completa e padronizada ---
localStorage.removeItem("patientData") const confirmLogout = async () => {
setShowLogoutDialog(false) try {
router.push("/") // 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 cancelLogout = () => setShowLogoutDialog(false)
@ -112,7 +101,7 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) {
] ]
if (!patientData) { if (!patientData) {
return <div>Carregando...</div> return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
} }
return ( return (

View File

@ -1,3 +1,4 @@
// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo)
"use client" "use client"
import type React from "react" import type React from "react"
@ -5,30 +6,14 @@ import { useState, useEffect } from "react"
import { useRouter, usePathname } from "next/navigation" import { useRouter, usePathname } from "next/navigation"
import Link from "next/link" import Link from "next/link"
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
Dialog, import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react"
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Search,
Bell,
Calendar,
Clock,
User,
LogOut,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react"
interface SecretaryData { interface SecretaryData {
id: string id: string
@ -46,12 +31,36 @@ interface SecretaryLayoutProps {
} }
export default function SecretaryLayout({ children }: SecretaryLayoutProps) { export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
const [secretaryData, setSecretaryData] = useState<SecretaryData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter() const router = useRouter()
const pathname = usePathname() 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(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
@ -66,10 +75,25 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
}, []) }, [])
const handleLogout = () => setShowLogoutDialog(true) const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => {
setShowLogoutDialog(false) // --- ALTERAÇÃO 3: Função de logout completa e padronizada ---
router.push("/") 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 cancelLogout = () => setShowLogoutDialog(false)
const menuItems = [ const menuItems = [
@ -79,17 +103,11 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" }, { href: "/secretary/pacientes", icon: User, label: "Pacientes" },
] ]
const secretaryData: SecretaryData = { if (!secretaryData) {
id: "1", return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
name: "Secretária Exemplo",
email: "secretaria@hospital.com",
phone: "999999999",
cpf: "000.000.000-00",
employeeId: "12345",
department: "Atendimento",
permissions: {},
} }
return ( return (
<div className="min-h-screen bg-background flex"> <div className="min-h-screen bg-background flex">
{/* Sidebar */} {/* Sidebar */}
@ -165,23 +183,20 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div> </div>
)} )}
</div> </div>
{/* Botão Sair - ajustado para responsividade */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={ className={
sidebarCollapsed sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado ? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent" : "w-full bg-transparent"
} }
onClick={handleLogout} onClick={handleLogout}
> >
<LogOut <LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "} />
{/* Remove margem quando colapsado */} {!sidebarCollapsed && "Sair"}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
</Button> </Button>
</div> </div>
</div> </div>
@ -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" className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"
}`} }`}
> >
{/* Header */}
<header className="bg-card border-b border-border px-6 py-4"> <header className="bg-card border-b border-border px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md"> <div className="flex items-center gap-4 flex-1 max-w-md">
@ -205,13 +219,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* 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,
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!
*/}
<Button variant="ghost" size="sm" className="relative"> <Button variant="ghost" size="sm" className="relative">
<Bell className="w-5 h-5" /> <Bell className="w-5 h-5" />
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-destructive text-destructive-foreground text-xs"> <Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-destructive text-destructive-foreground text-xs">
@ -222,7 +229,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div> </div>
</header> </header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main> <main className="flex-1 p-6">{children}</main>
</div> </div>

View File

@ -1,47 +1,59 @@
// Caminho: [seu-caminho]/services/api.mjs
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const apikey = API_KEY;
var tempToken;
export async function login() { export async function loginWithEmailAndPassword(email, password) {
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { const response = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Prefer: "return=representation", "apikey": API_KEY,
apikey: API_KEY, // valor fixo
}, },
body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }), body: JSON.stringify({ email, password }),
}); });
const data = await response.json(); 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); localStorage.setItem("token", data.access_token);
} }
return data; 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 = {}) { 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 token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
apikey: API_KEY, "apikey": API_KEY,
...(token ? { Authorization: `Bearer ${token}` } : {}), ...(token ? { "Authorization": `Bearer ${token}` } : {}),
...options.headers, ...options.headers,
}; };
@ -52,35 +64,29 @@ async function request(endpoint, options = {}) {
}); });
if (!response.ok) { if (!response.ok) {
let errorBody = `Status: ${response.status}`; let errorBody;
try { try {
const contentType = response.headers.get("content-type"); errorBody = await response.json();
if (contentType && contentType.includes("application/json")) {
const jsonError = await response.json();
errorBody = jsonError.message || JSON.stringify(jsonError);
} else {
errorBody = await response.text();
}
} catch (e) { } catch (e) {
errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`; errorBody = await response.text();
} }
throw new Error(`Erro HTTP: ${response.status} - ${JSON.stringify(errorBody)}`);
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 {};
} }
if (response.status === 204) return {};
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Erro na requisição:", error); console.error("Erro na requisição:", error);
throw error; throw error;
} }
} }
// Adicionamos a função de logout ao nosso objeto de API exportado
export const api = { export const api = {
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }), get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }), post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }),
patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }),
delete: (endpoint) => request(endpoint, { method: "DELETE" }), delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }),
}; logout: logout, // <-- EXPORTANDO A NOVA FUNÇÃO
};