Compare commits

..

No commits in common. "e6c6a20842f7ad22075140b3c807af6a07df8b29" and "a41a378befbe9ce95dd1fb1149c36d2eb1eb7439" have entirely different histories.

15 changed files with 568 additions and 651 deletions

View File

@ -1,2 +0,0 @@
{
}

View File

@ -1,31 +1,11 @@
// 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">
<div className="w-full max-w-md text-center"> <LoginForm title="Área do Médico" description="Acesse o sistema médico" role="doctor" themeColor="green" redirectPath="/doctor/medicos" />
<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,31 +1,12 @@
// 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">
<div className="w-full max-w-md text-center"> <LoginForm title="Área Financeira" description="Acesse o sistema de faturamento" role="finance" themeColor="orange" redirectPath="/finance/home" />
<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>
); );
} }

View File

@ -1,82 +0,0 @@
// 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,31 +1,12 @@
// 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">
<div className="w-full max-w-md text-center"> <LoginForm title="Área do Gestor" description="Acesse o sistema médico" role="manager" themeColor="blue" redirectPath="/manager/home" />
<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">MediConnect</h1> <h1 className="text-2xl font-bold text-primary">MediConnect</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"><Link href="/cadastro">Home</Link></a> <a href="#home" className="hover:text-primary">Home</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="/login"> <Link href="/cadastro">
<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,12 +6,6 @@ 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">
@ -22,25 +16,20 @@ export default function PatientLoginPage() {
</Link> </Link>
</div> </div>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} <LoginForm title="Área do Paciente" description="Acesse sua conta para gerenciar consultas" role="patient" themeColor="blue" redirectPath="/patient/dashboard">
{/* 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 */}
<div className="mt-6 text-center text-sm"> <Link href="/patient/register" passHref>
<span className="text-muted-foreground">Não tem uma conta? </span> <Button variant="outline" className="w-full h-12 text-base">
<Link href="/patient/register"> Criar nova conta
<span className="font-semibold text-primary hover:underline cursor-pointer"> </Button>
Crie uma agora </Link>
</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,31 +1,11 @@
// 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">
<div className="w-full max-w-md text-center"> <LoginForm title="Área da Secretária" description="Acesse o sistema de gerenciamento" role="secretary" themeColor="blue" redirectPath="/secretary/pacientes" />
<h1 className="text-3xl font-bold text-foreground mb-2">Área da Secretária</h1> </div>
<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,21 +1,21 @@
// 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 { cn } from "@/lib/utils" import Cookies from "js-cookie";
import { jwtDecode } from "jwt-decode";
// Nossos serviços de API centralizados import { cn } from "@/lib/utils";
import { loginWithEmailAndPassword, 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 { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { apikey } from "@/services/api.mjs";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@ -24,137 +24,200 @@ import { useToast } from "@/hooks/use-toast";
import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react"; import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react";
interface LoginFormProps { interface LoginFormProps {
children?: React.ReactNode title: string;
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;
} }
export function LoginForm({ children }: LoginFormProps) { // Supondo que o payload do seu token tenha esta estrutura
const [form, setForm] = useState<FormState>({ email: "", password: "" }) interface DecodedToken {
const [showPassword, setShowPassword] = useState(false) name: string;
const [isLoading, setIsLoading] = useState(false) email: string;
const router = useRouter() role: string;
const { toast } = useToast() exp: number;
// Adicione outros campos que seu token possa ter
// ================================================================== }
// LÓGICA DE LOGIN INTELIGENTE E CENTRALIZADA
// ================================================================== const themeClasses = {
const handleSubmit = async (e: React.FormEvent) => { blue: {
e.preventDefault(); iconBg: "bg-blue-100",
setIsLoading(true); iconText: "text-blue-600",
localStorage.removeItem("token"); button: "bg-blue-600 hover:bg-blue-700",
localStorage.removeItem("user_info"); link: "text-blue-600 hover:text-blue-700",
focus: "focus:border-blue-500 focus:ring-blue-500",
try { },
const authData = await loginWithEmailAndPassword(form.email, form.password); green: {
const user = authData.user; iconBg: "bg-green-100",
if (!user || !user.id) { iconText: "text-green-600",
throw new Error("Resposta de autenticação inválida: ID do usuário não encontrado."); 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",
const rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`); },
orange: {
if (!rolesData || rolesData.length === 0) { iconBg: "bg-orange-100",
throw new Error("Login bem-sucedido, mas nenhum perfil de acesso foi encontrado para este usuário."); iconText: "text-orange-600",
} button: "bg-orange-600 hover:bg-orange-700",
link: "text-orange-600 hover:text-orange-700",
const userRole = rolesData[0].role; focus: "focus:border-orange-500 focus:ring-orange-500",
const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: userRole } }; },
localStorage.setItem('user_info', JSON.stringify(completeUserInfo)); };
let redirectPath = ""; const roleIcons = {
switch (userRole) { secretary: UserCheck,
case "admin": patient: Stethoscope,
case "manager": redirectPath = "/manager/home"; break; doctor: Stethoscope,
case "medico": redirectPath = "/doctor/medicos"; break; admin: UserCheck,
case "secretary": redirectPath = "/secretary/pacientes"; break; manager: IdCard,
case "patient": redirectPath = "/patient/dashboard"; break; finance: Receipt,
case "finance": redirectPath = "/finance/home"; break; };
}
export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) {
if (!redirectPath) { const [form, setForm] = useState<FormState>({ email: "", password: "" });
throw new Error(`O perfil de acesso '${userRole}' não é válido para login. Contate o suporte.`); const [showPassword, setShowPassword] = useState(false);
} const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
toast({ const { toast } = useToast();
title: "Login bem-sucedido!",
description: `Bem-vindo(a)! Redirecionando...`, const currentTheme = themeClasses[themeColor];
}); const Icon = roleIcons[role];
router.push(redirectPath); // ==================================================================
// AJUSTE PRINCIPAL NA LÓGICA DE LOGIN
} catch (error) { // ==================================================================
localStorage.removeItem("token"); const handleSubmit = async (e: React.FormEvent) => {
localStorage.removeItem("user_info"); e.preventDefault();
setIsLoading(true);
console.error("ERRO DETALHADO NO CATCH:", error);
const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password";
toast({ const API_KEY = apikey;
title: "Erro no Login",
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", if (!API_KEY) {
}); toast({
} finally { title: "Erro de Configuração",
setIsLoading(false); description: "A chave da API não foi encontrada.",
} });
} setIsLoading(false);
return;
// ================================================================== }
// JSX VISUALMENTE RICO E UNIFICADO
// ================================================================== try {
return ( const response = await fetch(LOGIN_URL, {
// Usamos Card e CardContent para manter a consistência, mas o estilo principal method: "POST",
// virá da página 'app/login/page.tsx' que envolve este componente. headers: {
<Card className="w-full bg-transparent border-0 shadow-none"> "Content-Type": "application/json",
<CardContent className="p-0"> {/* Removemos o padding para dar controle à página pai */} apikey: API_KEY,
<form onSubmit={handleSubmit} className="space-y-6"> },
<div className="space-y-2"> body: JSON.stringify({ email: form.email, password: form.password }),
<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" /> const data = await response.json();
<Input
id="email" if (!response.ok) {
type="email" throw new Error(data.error_description || "Credenciais inválidas. Tente novamente.");
placeholder="seu.email@exemplo.com" }
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })} const accessToken = data.access_token;
className="pl-10 h-11" const user = data.user;
required
disabled={isLoading} /* =================== Verificação de Role Desativada Temporariamente =================== */
autoComplete="username" // Boa prática de acessibilidade // if (user.user_metadata.role !== role) {
/> // toast({ title: "Acesso Negado", ... });
</div> // return;
</div> // }
<div className="space-y-2"> /* ===================================================================================== */
<Label htmlFor="password">Senha</Label>
<div className="relative"> Cookies.set("access_token", accessToken, { expires: 1, secure: true });
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" /> localStorage.setItem("user_info", JSON.stringify(user));
<Input
id="password" toast({
type={showPassword ? "text" : "password"} title: "Login bem-sucedido!",
placeholder="Digite sua senha" description: `Bem-vindo(a), ${user.user_metadata.full_name || "usuário"}! Redirecionando...`,
value={form.password} });
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-12 h-11" router.push(redirectPath);
required } catch (error) {
disabled={isLoading} toast({
autoComplete="current-password" // Boa prática de acessibilidade title: "Erro no Login",
/> description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
<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" />} } finally {
</button> setIsLoading(false);
</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"} // O JSX do return permanece exatamente o mesmo, preservando seus ajustes.
</Button> return (
</form> <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">
{/* O children permite que a página de login adicione links extras aqui */} <div className={cn("mx-auto w-16 h-16 rounded-full flex items-center justify-center", currentTheme.iconBg)}>
{children} <Icon className={cn("w-8 h-8", currentTheme.iconText)} />
</CardContent> </div>
</Card> <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>
);
} }

View File

@ -4,8 +4,7 @@ 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"; // Manteremos para o logout, se necessário import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA
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";
@ -40,20 +39,23 @@ 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");
// --- ALTERAÇÃO PRINCIPAL AQUI --- const token = Cookies.get("access_token");
// 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: "",
@ -61,49 +63,35 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
permissions: {}, permissions: {},
}); });
} else { } else {
// Se não encontrar, aí sim redireciona. // Se faltar o token ou os dados, volta para o login
router.push("/login"); router.push("/doctor/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(); handleResize(); // inicializa com a largura atual
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 = () => {
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- localStorage.removeItem("doctorData");
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("/"); // Redireciona para a home router.push("/");
} };
};
const cancelLogout = () => { const cancelLogout = () => {
setShowLogoutDialog(false); setShowLogoutDialog(false);
@ -114,10 +102,30 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
}; };
const menuItems = [ const menuItems = [
{ href: "#", icon: Home, label: "Dashboard" }, {
{ href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" }, href: "#",
{ href: "#", icon: Clock, label: "Editor de Laudo" }, icon: Home,
{ href: "/doctor/medicos", icon: User, label: "Pacientes" }, 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
},
]; ];
if (!doctorData) { if (!doctorData) {
@ -125,10 +133,10 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
} }
return ( return (
// O restante do seu código JSX permanece exatamente o mesmo <div className="min-h-screen bg-gray-50 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-white border-r border-gray-200 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-gray-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -198,6 +206,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
<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>
@ -216,7 +225,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
</> </>
)} )}
{sidebarCollapsed && ( {sidebarCollapsed && (
<Avatar className="mx-auto"> <Avatar className="mx-auto"> {/* Centraliza o avatar quando recolhido */}
<AvatarImage src="/placeholder.svg?height=40&width=40" /> <AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback> <AvatarFallback>
{doctorData.name {doctorData.name
@ -228,6 +237,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
)} )}
</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}
@ -261,7 +271,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
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}> <Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */}
<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>
@ -287,14 +297,17 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p> <p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
</div> </div>
</div> </div>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> <Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */}
<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">
@ -313,9 +326,11 @@ export default function DoctorLayout({ 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,4 +1,3 @@
// Caminho: [seu-caminho]/FinancierLayout.tsx
"use client"; "use client";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@ -6,14 +5,32 @@ 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import {
import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react"; 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 { interface FinancierData {
id: string; id: string;
@ -30,45 +47,37 @@ interface PatientLayoutProps {
} }
export default function FinancierLayout({ children }: 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 [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 userInfoString = localStorage.getItem("user_info"); const data = localStorage.getItem("financierData");
// --- ALTERAÇÃO 1: Buscando o token no localStorage --- if (data) {
const token = localStorage.getItem("token"); setFinancierData(JSON.parse(data));
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 {
// --- ALTERAÇÃO 2: Redirecionando para o login central --- router.push("/finance/login");
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);
}, []); }, []);
@ -76,22 +85,10 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
setShowLogoutDialog(true); setShowLogoutDialog(true);
}; };
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- const confirmLogout = () => {
const confirmLogout = async () => { localStorage.removeItem("financierData");
try { setShowLogoutDialog(false);
// Chama a função centralizada para fazer o logout no servidor router.push("/");
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 = () => {
@ -99,19 +96,35 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
}; };
const menuItems = [ const menuItems = [
{ href: "#", icon: Home, label: "Dashboard" }, {
{ href: "#", icon: Calendar, label: "Relatórios financeiros" }, href: "#",
{ href: "#", icon: User, label: "Finanças Gerais" }, icon: Home,
{ href: "#", icon: Calendar, label: "Configurações" }, label: "Dashboard",
},
{
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 className="flex h-screen w-full items-center justify-center">Carregando...</div>; return <div>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"
@ -170,6 +183,7 @@ 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>
@ -192,29 +206,34 @@ 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" ? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
: "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"}
/> />{" "}
{!sidebarCollapsed && "Sair"} {/* Remove margem quando colapsado */}
{!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">
@ -238,9 +257,11 @@ 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,19 +1,33 @@
// 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"; // Mantido apenas para a limpeza de segurança no logout import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA
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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import {
import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react"; Dialog,
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;
@ -25,7 +39,7 @@ interface ManagerData {
permissions: object; permissions: object;
} }
interface ManagerLayoutProps { interface ManagerLayoutProps { // Corrigi o nome da prop aqui
children: React.ReactNode; children: React.ReactNode;
} }
@ -36,81 +50,80 @@ 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");
// --- ALTERAÇÃO 1: 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. "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 {
// O redirecionamento para /login já estava correto. Ótimo! // Se faltar o token ou os dados, volta para o login
router.push("/login"); router.push("/manager/login");
} }
}, [router]); }, [router]);
// 🔥 Responsividade automática da sidebar
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
setSidebarCollapsed(true); setSidebarCollapsed(true); // colapsa em telas pequenas (lg breakpoint ~ 1024px)
} else { } else {
setSidebarCollapsed(false); setSidebarCollapsed(false); // expande em desktop
} }
}; };
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);
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- const confirmLogout = () => {
const confirmLogout = async () => { localStorage.removeItem("managerData");
try { setShowLogoutDialog(false);
// Chama a função centralizada para fazer o logout no servidor router.push("/");
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: "#dashboard", icon: Home, label: "Dashboard" }, { href: "/manager/dashboard/", icon: Home, label: "Dashboard" },
{ href: "#reports", icon: Calendar, label: "Relatórios gerenciais" }, { href: "#", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "#users", icon: User, label: "Gestão de Usuários" }, { href: "/manager/usuario/", icon: User, label: "Gestão de Usuários" },
{ href: "#doctors", icon: User, label: "Gestão de Médicos" }, { href: "/manager/home", icon: User, label: "Gestão de Médicos" },
{ href: "#settings", icon: Calendar, label: "Configurações" }, { href: "#", icon: Calendar, label: "Configurações" },
]; ];
if (!managerData) { if (!managerData) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>; return <div>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 ${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"> <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">
@ -128,79 +141,136 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
onClick={() => setSidebarCollapsed(!sidebarCollapsed)} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1" 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> </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 = pathname === item.href; const isActive =
pathname === item.href ||
(item.href !== "/" && pathname.startsWith(item.href));
return ( return (
<Link key={item.label} href={item.href}> <Link key={item.href} href={item.href}>
<div <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" /> <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> </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>{managerData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback> <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">{managerData.name}</p> <p className="text-sm font-medium text-gray-900 truncate">
<p className="text-xs text-gray-500 truncate">{managerData.department}</p> {managerData.name}
</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={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} className={
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 className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} /> <LogOut
{!sidebarCollapsed && "Sair"} 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 */}
</Button> </Button>
</div> </div>
</div> </div>
<div className={`flex-1 flex flex-col transition-all duration-300 w-full ${sidebarCollapsed ? "ml-16" : "ml-64"}`}> {/* Conteúdo principal */}
<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 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>
</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">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> </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>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> </DialogHeader>
<DialogFooter className="flex gap-2"> <DialogFooter className="flex gap-2">
<Button variant="outline" onClick={cancelLogout}>Cancelar</Button> <Button variant="outline" onClick={cancelLogout}>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button> Cancelar
</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,18 +1,36 @@
"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 { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react" import {
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" 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 { interface PatientData {
name: string name: string
@ -23,72 +41,65 @@ interface PatientData {
address: string address: string
} }
interface PatientLayoutProps { interface HospitalLayoutProps {
children: React.ReactNode children: React.ReactNode
} }
// --- ALTERAÇÃO 1: Renomeando o componente para maior clareza --- export default function HospitalLayout({ children }: HospitalLayoutProps) {
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) setSidebarCollapsed(true) // colapsa no mobile
} else { } else {
setSidebarCollapsed(false) setSidebarCollapsed(false) // expande no desktop
} }
} }
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");
// --- ALTERAÇÃO 2: Buscando o token no localStorage --- // 2. Para mais segurança, verificamos também se o token de acesso existe no cookie
const token = localStorage.getItem("token"); const token = Cookies.get("access_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 {
// --- ALTERAÇÃO 3: Redirecionando para o login central --- // Se as informações do usuário ou o token não forem encontrados, mandamos para o login.
router.push("/login"); router.push("/patient/login");
} }
}, [router]); }, [router]);
const handleLogout = () => setShowLogoutDialog(true) const handleLogout = () => setShowLogoutDialog(true)
// --- ALTERAÇÃO 4: Função de logout completa e padronizada --- const confirmLogout = () => {
const confirmLogout = async () => { localStorage.removeItem("patientData")
try { setShowLogoutDialog(false)
// Chama a função centralizada para fazer o logout no servidor router.push("/")
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)
@ -101,7 +112,7 @@ export default function PatientLayout({ children }: PatientLayoutProps) {
] ]
if (!patientData) { if (!patientData) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>; return <div>Carregando...</div>
} }
return ( return (

View File

@ -1,4 +1,3 @@
// 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"
@ -6,14 +5,30 @@ 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import {
import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react" 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 { interface SecretaryData {
id: string id: string
@ -31,36 +46,12 @@ 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()
useEffect(() => { // 🔹 Colapsar no mobile e expandir no desktop automaticamente
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) {
@ -75,25 +66,10 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
}, []) }, [])
const handleLogout = () => setShowLogoutDialog(true) const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => {
// --- ALTERAÇÃO 3: Função de logout completa e padronizada --- setShowLogoutDialog(false)
const confirmLogout = async () => { router.push("/")
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 = [
@ -103,11 +79,17 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" }, { href: "/secretary/pacientes", icon: User, label: "Pacientes" },
] ]
if (!secretaryData) { const secretaryData: SecretaryData = {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>; id: "1",
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 */}
@ -183,20 +165,23 @@ 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" ? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
: "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"}
/> />{" "}
{!sidebarCollapsed && "Sair"} {/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
</Button> </Button>
</div> </div>
</div> </div>
@ -206,6 +191,7 @@ 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">
@ -219,6 +205,13 @@ 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">
@ -229,6 +222,7 @@ 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,61 +1,4 @@
// 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";
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",
"apikey": API_KEY,
},
body: JSON.stringify({ email, password }),
});
const data = await response.json();
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;
}
// --- 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 = {}) {
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
const headers = {
"Content-Type": "application/json",
"apikey": API_KEY,
...(token ? { "Authorization": `Bearer ${token}` } : {}),
...options.headers,
};
const API_KEY = const API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
@ -110,33 +53,6 @@ async function request(endpoint, options = {}) {
headers, headers,
}); });
if (!response.ok) {
let errorBody;
try {
errorBody = await response.json();
} catch (e) {
errorBody = await response.text();
}
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, 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
};
if (!response.ok) { if (!response.ok) {
const msg = await response.text(); const msg = await response.text();
throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${msg}`); throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${msg}`);