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
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 (
<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>
);
}
}

View File

@ -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
<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>
);
}
}

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
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
<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>
);
}
}

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">
<h1 className="text-2xl font-bold text-primary">MedConnect</h1>
<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="#departments" className="hover:text-primary">Departamentos</a>
<a href="#doctors" className="hover:text-primary">Médicos</a>
@ -25,7 +25,7 @@ export default function InicialPage() {
</nav>
<div className="flex space-x-4">
{}
<Link href="/cadastro">
<Link href="/login">
<Button
variant="outline"
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 { LoginForm } from "@/components/LoginForm";
@ -6,6 +6,12 @@ import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
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 (
<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">
@ -16,20 +22,25 @@ export default function PatientLoginPage() {
</Link>
</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 */}
<Link href="/patient/register" passHref>
<Button variant="outline" className="w-full h-12 text-base">
Criar nova conta
</Button>
</Link>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">Não tem uma conta? </span>
<Link href="/patient/register">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Crie uma agora
</span>
</Link>
</div>
</LoginForm>
{/* Conteúdo e espaçamento restaurados */}
<div className="mt-8 text-center">
<p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -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 (
<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>
<div className="w-full max-w-md text-center">
<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
"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<FormState>({ 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<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 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 (
<Card className="w-full max-w-md shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<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)}>
<Icon className={cn("w-8 h-8", currentTheme.iconText)} />
</div>
<div>
<CardTitle className="text-2xl font-bold text-gray-900">{title}</CardTitle>
<CardDescription className="text-gray-600 mt-2">{description}</CardDescription>
</div>
</CardHeader>
<CardContent className="px-8 pb-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Inputs e Botão */}
<div className="space-y-2">
<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" />
<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} />
</div>
</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-gray-400 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={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} />
<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}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button type="submit" className={cn("w-full h-12 text-base font-semibold", currentTheme.button)} disabled={isLoading}>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"}
</Button>
</form>
{/* Conteúdo Extra (children) */}
<div className="mt-8">
{children ? (
<div className="space-y-4">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-white text-slate-500">Novo por aqui?</span>
</div>
</div>
{children}
</div>
) : (
<>
<div className="relative">
<Separator className="my-6" />
<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>
</div>
<div className="text-center">
<Link href="/" className={cn("text-sm font-medium hover:underline", currentTheme.link)}>
Voltar à página inicial
</Link>
</div>
</>
)}
</div>
</CardContent>
</Card>
);
}
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.
<Card className="w-full bg-transparent border-0 shadow-none">
<CardContent className="p-0"> {/* Removemos o padding para dar controle à página pai */}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="email"
type="email"
placeholder="seu.email@exemplo.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11"
required
disabled={isLoading}
autoComplete="username" // Boa prática de acessibilidade
/>
</div>
</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 { 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
<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="p-4 border-b border">
<div className="flex items-center justify-between">
@ -170,7 +162,6 @@ useEffect(() => {
<div className="border-t p-4 mt-auto">
<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 && (
<>
<Avatar>
@ -189,7 +180,7 @@ useEffect(() => {
</>
)}
{sidebarCollapsed && (
<Avatar className="mx-auto"> {/* Centraliza o avatar quando recolhido */}
<Avatar className="mx-auto">
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
@ -201,7 +192,6 @@ useEffect(() => {
)}
</div>
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
<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" : ""}`}
onClick={handleLogout}
@ -233,7 +223,7 @@ useEffect(() => {
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
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"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
<span className="font-medium">{item.label}</span>
@ -259,17 +249,14 @@ useEffect(() => {
<p className="text-xs text-muted-foreground truncate">{doctorData.specialty}</p>
</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" />
Sair
</Button>
</div>
</div>
{/* Main Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Header */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<header className="bg-card border-b border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
@ -288,11 +275,9 @@ useEffect(() => {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>

View File

@ -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<FinancierData | null>(
null
);
const [financierData, setFinancierData] = useState<FinancierData | null>(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 <div>Carregando...</div>;
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
// O restante do seu código JSX permanece inalterado
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
<div
className={`bg-card border-r border-border transition-all duration-300 ${
sidebarCollapsed ? "w-16" : "w-64"
@ -183,7 +170,6 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
})}
</nav>
{/* Footer user info */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
@ -206,34 +192,29 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
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"
}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
/>
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
{/* 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-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md">
@ -257,11 +238,9 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>

View File

@ -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 <div>Carregando...</div>;
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
<div
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"}`}
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"}`}
>
{/* Logo + collapse button */}
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
{!sidebarCollapsed && (
<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-4 h-4 bg-white rounded-sm"></div>
</div>
<span className="font-semibold text-gray-900">
MidConnecta
</span>
<span className="font-semibold text-gray-900">MidConnecta</span>
</div>
)}
<Button
@ -141,136 +126,79 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
{/* Menu Items */}
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive =
pathname === item.href ||
(item.href !== "/" && pathname.startsWith(item.href));
const isActive = pathname === item.href;
return (
<Link key={item.href} href={item.href}>
<Link key={item.label} href={item.href}>
<div
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"
}`}
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"}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
);
})}
</nav>
{/* Perfil no rodapé */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{managerData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarFallback>{managerData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{managerData.name}
</p>
<p className="text-xs text-gray-500 truncate">
{managerData.department}
</p>
<p className="text-sm font-medium text-gray-900 truncate">{managerData.name}</p>
<p className="text-xs text-gray-500 truncate">{managerData.department}</p>
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
: "w-full bg-transparent"
}
className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
{/* Conteúdo principal */}
<div
className={`flex-1 flex flex-col transition-all duration-300 w-full
${sidebarCollapsed ? "ml-16" : "ml-64"}`}
>
{/* Header */}
<div className={`flex-1 flex flex-col transition-all duration-300 w-full ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<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="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Buscar paciente"
className="pl-10 bg-gray-50 border-gray-200"
/>
<Input placeholder="Buscar paciente" className="pl-10 bg-gray-50 border-gray-200" />
</div>
</div>
{/* Notifications */}
<div className="flex items-center gap-4 ml-auto">
<Button variant="ghost" size="sm" className="relative">
<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">
1
</Badge>
<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>
</Button>
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Confirmar Saída</DialogTitle>
<DialogDescription>
Deseja realmente sair do sistema? Você precisará fazer login
novamente para acessar sua conta.
</DialogDescription>
<DialogDescription>Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta.</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={cancelLogout}>
Cancelar
</Button>
<Button variant="destructive" onClick={confirmLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sair
</Button>
<Button variant="outline" onClick={cancelLogout}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -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<PatientData | null>(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 <div>Carregando...</div>
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (

View File

@ -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<SecretaryData | null>(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 <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
@ -165,23 +183,20 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
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"
}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
/>
{!sidebarCollapsed && "Sair"}
</Button>
</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"
}`}
>
{/* Header */}
<header className="bg-card border-b border-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md">
@ -205,13 +219,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
<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">
<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">
@ -222,7 +229,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>

View File

@ -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
};