refactor(auth): Centraliza lógica de autenticação e corrige avatares

- Cria o hook customizado 'useAuthLayout' para gerenciar os dados do usuário e as permissões de acesso de forma centralizada.
- Refatora todos os layouts (Manager, Doctor, Secretary, Patient, etc.) para utilizar o novo hook, simplificando o código e eliminando repetição.
- Corrige o bug no fluxo de login de múltiplos perfis, garantindo que a role seja salva corretamente em minúsculas.
- Implementa a exibição correta do avatar do usuário em todos os layouts, corrigindo a montagem da URL do Supabase Storage.
- Corrige o erro de CORS no upload de avatar na página de perfil do paciente, utilizando a API REST para atualizar a tabela 'profiles' diretamente.
- Adiciona a funcionalidade completa de edição de dados e troca de foto na página 'Meus Dados' do paciente.
This commit is contained in:
Gabriel Lira Figueira 2025-11-09 21:10:51 -03:00
parent 93ea8709d6
commit 3f77c52bcd
11 changed files with 574 additions and 1564 deletions

View File

@ -1,52 +1,127 @@
"use client"
// ARQUIVO COMPLETO PARA: app/patient/profile/page.tsx
import { useState, useEffect } from "react"
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { User, Mail, Phone, Calendar, FileText } from "lucide-react"
"use client";
interface PatientData {
name: string
email: string
phone: string
cpf: string
birthDate: string
address: string
import { useState, useEffect, useRef } from "react";
import PatientLayout from "@/components/patient-layout";
import { useAuthLayout } from "@/hooks/useAuthLayout";
import { patientsService } from "@/services/patientsApi.mjs";
import { api } from "@/services/api.mjs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { User, Mail, Phone, Calendar, Upload } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
interface PatientProfileData {
name: string;
email: string;
phone: string;
cpf: string;
birthDate: string;
cep: string;
street: string;
number: string;
city: string;
avatarFullUrl?: string;
}
export default function PatientProfile() {
const [patientData, setPatientData] = useState<PatientData>({
name: "",
email: "",
phone: "",
cpf: "",
birthDate: "",
address: "",
})
const [isEditing, setIsEditing] = useState(false)
const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: 'patient' });
const [patientData, setPatientData] = useState<PatientProfileData | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const data = localStorage.getItem("patientData")
if (data) {
setPatientData(JSON.parse(data))
if (user?.id) {
const fetchPatientDetails = async () => {
try {
const patientDetails = await patientsService.getById(user.id);
setPatientData({
name: patientDetails.full_name || user.name,
email: user.email,
phone: patientDetails.phone_mobile || '',
cpf: patientDetails.cpf || '',
birthDate: patientDetails.birth_date || '',
cep: patientDetails.cep || '',
street: patientDetails.street || '',
number: patientDetails.number || '',
city: patientDetails.city || '',
avatarFullUrl: user.avatarFullUrl,
});
} catch (error) {
console.error("Erro ao buscar detalhes do paciente:", error);
toast({ title: "Erro", description: "Não foi possível carregar seus dados completos.", variant: "destructive" });
}
};
fetchPatientDetails();
}
}, [])
}, [user]);
const handleSave = () => {
localStorage.setItem("patientData", JSON.stringify(patientData))
setIsEditing(false)
alert("Dados atualizados com sucesso!")
}
const handleInputChange = (field: keyof PatientProfileData, value: string) => {
setPatientData((prev) => (prev ? { ...prev, [field]: value } : null));
};
const handleInputChange = (field: keyof PatientData, value: string) => {
setPatientData((prev) => ({
...prev,
[field]: value,
}))
const handleSave = async () => {
if (!patientData || !user) return;
setIsSaving(true);
try {
const patientPayload = {
full_name: patientData.name,
cpf: patientData.cpf,
birth_date: patientData.birthDate,
phone_mobile: patientData.phone,
cep: patientData.cep,
street: patientData.street,
number: patientData.number,
city: patientData.city,
};
await patientsService.update(user.id, patientPayload);
toast({ title: "Sucesso!", description: "Seus dados foram atualizados." });
setIsEditing(false);
} catch (error) {
console.error("Erro ao salvar dados:", error);
toast({ title: "Erro", description: "Não foi possível salvar suas alterações.", variant: "destructive" });
} finally {
setIsSaving(false);
}
};
const handleAvatarClick = () => {
fileInputRef.current?.click();
};
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file || !user) return;
const fileExt = file.name.split('.').pop();
// *** A CORREÇÃO ESTÁ AQUI ***
// O caminho salvo no banco de dados não deve conter o nome do bucket.
const filePath = `${user.id}/avatar.${fileExt}`;
try {
await api.storage.upload('avatars', filePath, file);
await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { avatar_url: filePath });
const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`;
setPatientData(prev => prev ? { ...prev, avatarFullUrl: newFullUrl } : null);
toast({ title: "Sucesso!", description: "Sua foto de perfil foi atualizada." });
} catch (error) {
console.error("Erro no upload do avatar:", error);
toast({ title: "Erro de Upload", description: "Não foi possível enviar sua foto.", variant: "destructive" });
}
};
if (isAuthLoading || !patientData) {
return <PatientLayout><div>Carregando seus dados...</div></PatientLayout>;
}
return (
@ -57,99 +132,37 @@ export default function PatientProfile() {
<h1 className="text-3xl font-bold text-gray-900">Meus Dados</h1>
<p className="text-gray-600">Gerencie suas informações pessoais</p>
</div>
<Button
onClick={() => (isEditing ? handleSave() : setIsEditing(true))}
variant={isEditing ? "default" : "outline"}
>
{isEditing ? "Salvar Alterações" : "Editar Dados"}
<Button onClick={() => (isEditing ? handleSave() : setIsEditing(true))} disabled={isSaving}>
{isEditing ? (isSaving ? "Salvando..." : "Salvar Alterações") : "Editar Dados"}
</Button>
</div>
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<User className="mr-2 h-5 w-5" />
Informações Pessoais
</CardTitle>
<CardDescription>Seus dados pessoais básicos</CardDescription>
</CardHeader>
<CardHeader><CardTitle className="flex items-center"><User className="mr-2 h-5 w-5" />Informações Pessoais</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nome Completo</Label>
<Input
id="name"
value={patientData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
disabled={!isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpf">CPF</Label>
<Input
id="cpf"
value={patientData.cpf}
onChange={(e) => handleInputChange("cpf", e.target.value)}
disabled={!isEditing}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="birthDate">Data de Nascimento</Label>
<Input
id="birthDate"
type="date"
value={patientData.birthDate}
onChange={(e) => handleInputChange("birthDate", e.target.value)}
disabled={!isEditing}
/>
<div><Label htmlFor="name">Nome Completo</Label><Input id="name" value={patientData.name} onChange={(e) => handleInputChange("name", e.target.value)} disabled={!isEditing} /></div>
<div><Label htmlFor="cpf">CPF</Label><Input id="cpf" value={patientData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} disabled={!isEditing} /></div>
</div>
<div><Label htmlFor="birthDate">Data de Nascimento</Label><Input id="birthDate" type="date" value={patientData.birthDate} onChange={(e) => handleInputChange("birthDate", e.target.value)} disabled={!isEditing} /></div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Mail className="mr-2 h-5 w-5" />
Contato
</CardTitle>
<CardDescription>Informações de contato</CardDescription>
</CardHeader>
<CardHeader><CardTitle className="flex items-center"><Mail className="mr-2 h-5 w-5" />Contato e Endereço</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={patientData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
disabled={!isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone">Telefone</Label>
<Input
id="phone"
value={patientData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
disabled={!isEditing}
/>
</div>
<div><Label htmlFor="email">Email</Label><Input id="email" type="email" value={patientData.email} disabled /></div>
<div><Label htmlFor="phone">Telefone</Label><Input id="phone" value={patientData.phone} onChange={(e) => handleInputChange("phone", e.target.value)} disabled={!isEditing} /></div>
</div>
<div className="space-y-2">
<Label htmlFor="address">Endereço</Label>
<Textarea
id="address"
value={patientData.address}
onChange={(e) => handleInputChange("address", e.target.value)}
disabled={!isEditing}
rows={3}
/>
<div className="grid md:grid-cols-3 gap-4">
<div><Label htmlFor="cep">CEP</Label><Input id="cep" value={patientData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} disabled={!isEditing} /></div>
<div className="md:col-span-2"><Label htmlFor="street">Rua / Logradouro</Label><Input id="street" value={patientData.street} onChange={(e) => handleInputChange("street", e.target.value)} disabled={!isEditing} /></div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div><Label htmlFor="number">Número</Label><Input id="number" value={patientData.number} onChange={(e) => handleInputChange("number", e.target.value)} disabled={!isEditing} /></div>
<div><Label htmlFor="city">Cidade</Label><Input id="city" value={patientData.city} onChange={(e) => handleInputChange("city", e.target.value)} disabled={!isEditing} /></div>
</div>
</CardContent>
</Card>
@ -157,66 +170,34 @@ export default function PatientProfile() {
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Resumo do Perfil</CardTitle>
</CardHeader>
<CardHeader><CardTitle>Resumo do Perfil</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<User className="h-6 w-6 text-blue-600" />
<div className="relative">
<Avatar className="w-16 h-16 cursor-pointer" onClick={handleAvatarClick}>
<AvatarImage src={patientData.avatarFullUrl} />
<AvatarFallback className="text-2xl">{patientData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
<div className="absolute bottom-0 right-0 bg-primary text-primary-foreground rounded-full p-1 cursor-pointer hover:bg-primary/80" onClick={handleAvatarClick}>
<Upload className="w-3 h-3" />
</div>
<input type="file" ref={fileInputRef} onChange={handleAvatarUpload} className="hidden" accept="image/png, image/jpeg" />
</div>
<div>
<p className="font-medium">{patientData.name}</p>
<p className="text-sm text-gray-500">Paciente</p>
</div>
</div>
<div className="space-y-3 pt-4 border-t">
<div className="flex items-center text-sm">
<Mail className="mr-2 h-4 w-4 text-gray-500" />
<span className="truncate">{patientData.email}</span>
</div>
<div className="flex items-center text-sm">
<Phone className="mr-2 h-4 w-4 text-gray-500" />
<span>{patientData.phone}</span>
</div>
<div className="flex items-center text-sm">
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
<span>
{patientData.birthDate
? new Date(patientData.birthDate).toLocaleDateString("pt-BR")
: "Não informado"}
</span>
</div>
<div className="flex items-center text-sm"><Mail className="mr-2 h-4 w-4 text-gray-500" /><span className="truncate">{patientData.email}</span></div>
<div className="flex items-center text-sm"><Phone className="mr-2 h-4 w-4 text-gray-500" /><span>{patientData.phone || "Não informado"}</span></div>
<div className="flex items-center text-sm"><Calendar className="mr-2 h-4 w-4 text-gray-500" /><span>{patientData.birthDate ? new Date(patientData.birthDate).toLocaleDateString("pt-BR", { timeZone: 'UTC' }) : "Não informado"}</span></div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<FileText className="mr-2 h-5 w-5" />
Documentos
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button variant="outline" size="sm" className="w-full justify-start bg-transparent">
<FileText className="mr-2 h-4 w-4" />
Carteirinha do Convênio
</Button>
<Button variant="outline" size="sm" className="w-full justify-start bg-transparent">
<FileText className="mr-2 h-4 w-4" />
Histórico Médico
</Button>
<Button variant="outline" size="sm" className="w-full justify-start bg-transparent">
<FileText className="mr-2 h-4 w-4" />
Exames Recentes
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
</PatientLayout>
)
}
);
}

View File

@ -47,35 +47,41 @@ export function LoginForm({ children }: LoginFormProps) {
return;
}
const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: selectedDashboardRole } };
localStorage.setItem("user_info", JSON.stringify(completeUserInfo));
// AQUI ESTÁ A CORREÇÃO:
const roleInLowerCase = selectedDashboardRole.toLowerCase();
// Adicionando o log que você pediu:
console.log("Salvando no localStorage com o perfil:", roleInLowerCase);
let redirectPath = "";
switch (selectedDashboardRole) {
case "manager":
redirectPath = "/manager/home";
break;
case "doctor":
redirectPath = "/doctor/medicos";
break;
case "secretary":
redirectPath = "/secretary/pacientes";
break;
case "patient":
redirectPath = "/patient/dashboard";
break;
case "finance":
redirectPath = "/finance/home";
break;
}
const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: roleInLowerCase } };
localStorage.setItem("user_info", JSON.stringify(completeUserInfo));
if (redirectPath) {
toast({ title: `Entrando como ${selectedDashboardRole}...` });
router.push(redirectPath);
} else {
toast({ title: "Erro", description: "Perfil selecionado inválido.", variant: "destructive" });
}
};
let redirectPath = "";
switch (roleInLowerCase) { // Usamos a variável em minúsculas aqui também
case "manager":
redirectPath = "/manager/home";
break;
case "doctor":
redirectPath = "/doctor/medicos";
break;
case "secretary":
redirectPath = "/secretary/pacientes";
break;
case "patient":
redirectPath = "/patient/dashboard";
break;
case "finance":
redirectPath = "/finance/home";
break;
}
if (redirectPath) {
toast({ title: `Entrando como ${selectedDashboardRole}...` });
router.push(redirectPath);
} else {
toast({ title: "Erro", description: "Perfil selecionado inválido.", variant: "destructive" });
}
};
/**
* --- FUNÇÃO ATUALIZADA ---
@ -111,7 +117,7 @@ export function LoginForm({ children }: LoginFormProps) {
// Caso 1: Usuário é ADMIN, mostra todos os dashboards possíveis.
if (rolesFromApi.includes("admin")) {
setUserRoles(["manager", "doctor", "secretary", "paciente", "finance"]);
setUserRoles(["manager", "doctor", "secretary", "patient", "finance"]);
setIsLoading(false); // Para o loading para mostrar a tela de seleção
return;
}
@ -130,7 +136,7 @@ export function LoginForm({ children }: LoginFormProps) {
case "secretaria":
displayRoles.add("secretary");
break;
case "paciente": // Mapeamento de 'patient' (ou outro nome que você use para paciente)
case "patient": // Mapeamento de 'patient' (ou outro nome que você use para patiente)
displayRoles.add("patient");
break;
}
@ -196,7 +202,13 @@ export function LoginForm({ children }: LoginFormProps) {
<p className="text-sm text-muted-foreground text-center">Selecione com qual perfil deseja entrar:</p>
<div className="flex flex-col space-y-3 pt-2">
{userRoles.map((role) => (
<Button key={role} variant="outline" className="h-11 text-base" onClick={() => handleRoleSelection(role)}>
<Button
key={role}
variant="outline"
className="h-11 text-base"
// AQUI ESTÁ A CORREÇÃO:
onClick={() => handleRoleSelection(role === 'paciente' ? 'patient' : role)}
>
Entrar como: {role.charAt(0).toUpperCase() + role.slice(1)}
</Button>
))}

View File

@ -1,472 +1,128 @@
// CÓDIGO REATORADO PARA: components/doctor-layout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie"; // Manteremos para o logout, se necessário
import { useAuthLayout } from "@/hooks/useAuthLayout"; // 1. Importamos nosso novo hook
import { api } from "@/services/api.mjs";
// Componentes da UI
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 { Home, Calendar, Clock, User, LogOut, ChevronLeft, ChevronRight, Bell, FileText } from "lucide-react";
import { Badge } from "./ui/badge";
interface DoctorData {
id: string;
name: string;
email: string;
phone: string;
cpf: string;
crm: string;
specialty: string;
department: string;
permissions: object;
role: string
}
export default function DoctorLayout({ children }: { children: React.ReactNode }) {
// 2. Usamos o hook para buscar o usuário e controlar o acesso para 'medico'
const { user, isLoading } = useAuthLayout({ requiredRole: 'medico' });
interface PatientLayoutProps {
children: React.ReactNode;
}
export default function DoctorLayout({ children }: PatientLayoutProps) {
const [doctorData, setDoctorData] = useState<DoctorData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [windowWidth, setWindowWidth] = useState(0);
const isMobile = windowWidth < 1024;
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
// --- 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);
setDoctorData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Doutor(a)",
email: userInfo.email || "",
specialty: userInfo.user_metadata?.specialty || "Especialidade",
phone: userInfo.phone || "",
cpf: "",
crm: "",
department: "",
permissions: {},
role: userInfo.role
});
} else {
// 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();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (isMobile) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [isMobile]);
const handleLogout = () => {
setShowLogoutDialog(true);
};
// --- 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 = () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
// ESTA PARTE É ÚNICA DE CADA LAYOUT E DEVE SER MANTIDA
const menuItems = [
{
href: "/doctor/dashboard",
icon: Home,
label: "Dashboard",
// Botão para o dashboard do médico
},
{
href: "/doctor/consultas",
icon: Calendar,
label: "Consultas",
// Botão para página de consultas marcadas do médico atual
},
{
href: "/doctor/medicos/editorlaudo",
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: "/doctor/disponibilidade",
icon: Calendar,
label: "Disponibilidade",
// Botão para o dashboard do médico
},
{ href: "/doctor/dashboard", icon: Home, label: "Dashboard" },
{ href: "/doctor/consultas", icon: Calendar, label: "Consultas" },
{ href: "/doctor/medicos/editorlaudo", icon: Clock, label: "Editor de Laudo" },
{ href: "/doctor/medicos", icon: User, label: "patientes" },
{ href: "/doctor/disponibilidade", icon: Calendar, label: "Disponibilidade" },
];
if (!doctorData) {
return <div>Carregando...</div>;
// 3. Adicionamos o estado de carregamento
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
// O restante do seu código JSX permanece exatamente o mesmo
<div className="min-h-screen bg-background flex">
<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">
{!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">MediConnect</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
</Button>
</div>
<div className="min-h-screen bg-gray-50 flex">
<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"}`}>
{/* Header da Sidebar */}
<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">MediConnect</span>
</div>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
{/* Menu (específico deste layout) */}
<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 || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} 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"
}`}
>
<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"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
</div>
</Link>
);
})}
</nav>
// ... (seu código anterior)
{/* Sidebar para desktop */}
<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-gray-200">
<div className="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">
MediConnect
</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
</Button>
</div>
</div>
<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));
return (
<Link key={item.href} 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"
}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
</div>
</Link>
);
})}
</nav>
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{!sidebarCollapsed && (
<>
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{doctorData.name}
</p>
<p className="text-xs text-gray-500 truncate">
{doctorData.specialty}
</p>
</div>
</>
)}
{sidebarCollapsed && (
<Avatar className="mx-auto">
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
)}
</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" : ""
}`}
onClick={handleLogout}
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">Sair</span>}
</div>
</div>
</div>
</div>
{isMobileMenuOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
onClick={toggleMobileMenu}
></div>
)}
<div
className={`bg-white border-r border-gray-200 fixed left-0 top-0 h-screen flex flex-col z-50 transition-transform duration-300 md:hidden ${
isMobileMenuOpen ? "translate-x-0 w-64" : "-translate-x-full w-64"
}`}
>
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<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">Hospital System</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={toggleMobileMenu}
className="p-1"
>
<X className="w-4 h-4" />
</Button>
</div>
<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));
return (
<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>
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
);
})}
</nav>
{/* Rodapé com Avatar e Logout */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{/* 4. A LÓGICA DO AVATAR AGORA É APLICADA AQUI */}
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{doctorData.name}
</p>
<p className="text-xs text-gray-500 truncate">
{doctorData.specialty}
</p>
</div>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{user.name}</p>
<p className="text-xs text-gray-500 truncate">{user.roles.join(', ')}</p>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
className="w-full bg-transparent"
onClick={() => {
handleLogout();
toggleMobileMenu();
}}
>
<LogOut className="mr-2 h-4 w-4" />
Sair
<Button variant="outline" size="sm" className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} onClick={() => setShowLogoutDialog(true)}>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
<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"></div>
<div className="flex items-center gap-4">
<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>
</Button>
</div>
{/* Main Content */}
<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">
<div className="flex items-center gap-4 flex-1 max-w-md"></div>
<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>
</Button>
</div>
</header>
<main className="flex-1 p-6">{children}</main>
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
{/* Dialog de Logout */}
<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>
</DialogHeader>
<DialogHeader><DialogTitle>Confirmar Saída</DialogTitle><DialogDescription>Deseja realmente sair do sistema?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

View File

@ -1,124 +1,32 @@
// Caminho: [seu-caminho]/FinancierLayout.tsx
// CÓDIGO COMPLETO PARA: components/finance-layout.tsx
"use client";
import Cookies from "js-cookie";
import type React from "react";
import { useState, useEffect } from "react";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { useAuthLayout } from "@/hooks/useAuthLayout";
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 { Home, Calendar, User, LogOut, ChevronLeft, ChevronRight, Bell } from "lucide-react";
import { Badge } from "./ui/badge";
interface FinancierData {
id: string;
name: string;
email: string;
phone: string;
cpf: string;
department: string;
permissions: object;
}
export default function FinancierLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'finance' });
interface PatientLayoutProps {
children: React.ReactNode;
}
export default function FinancierLayout({ children }: PatientLayoutProps) {
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 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 {
// --- ALTERAÇÃO 2: Redirecionando para o login central ---
router.push("/login");
}
}, [router]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogout = () => {
setShowLogoutDialog(true);
};
// --- 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 = () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const menuItems = [
@ -128,156 +36,82 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
{ href: "#", icon: Calendar, label: "Configurações" },
];
if (!financierData) {
return (
<div className="flex h-screen w-full items-center justify-center">
Carregando...
</div>
);
if (isLoading || !user) {
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">
<div
className={`bg-card border-r border-border transition-all duration-300 ${
sidebarCollapsed ? "w-16" : "w-64"
} fixed left-0 top-0 h-screen flex flex-col z-10`}
>
<div className={`bg-card border-r border-border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-10`}>
<div className="p-4 border-b border-border">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-primary-foreground rounded-sm"></div>
</div>
<span className="font-semibold text-foreground">
MediConnect
</span>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"><div className="w-4 h-4 bg-primary-foreground rounded-sm"></div></div>
<span className="font-semibold text-foreground">MediConnect</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
</div>
<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 || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href}>
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`}>
<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>
<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>
{financierData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{financierData.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{financierData.department}
</p>
<p className="text-sm font-medium text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.roles.join(', ')}</p>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<Button variant="outline" size="sm" className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} onClick={() => setShowLogoutDialog(true)}>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
<div
className={`flex-1 flex flex-col transition-all duration-300 ${
sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<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"></div>
<div className="flex items-center gap-4">
<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">
1
</Badge>
<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">1</Badge>
</Button>
</div>
</div>
</header>
<main className="flex-1 p-6">{children}</main>
</div>
<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>
</DialogHeader>
<DialogHeader><DialogTitle>Confirmar Saída</DialogTitle><DialogDescription>Deseja realmente sair do sistema?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

View File

@ -1,128 +1,55 @@
"use client"
// CÓDIGO COMPLETO PARA: components/hospital-layout.tsx
import type React from "react"
"use client";
import { useState, useEffect } from "react"
import Link from "next/link"
import { useRouter, usePathname } from "next/navigation"
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,
Settings,
Users,
UserCheck,
Calendar,
Clock,
User,
LogOut,
FileText,
BarChart3,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react"
import type React from "react";
import { useState } from "react";
import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { useAuthLayout } from "@/hooks/useAuthLayout";
import { api } from "@/services/api.mjs";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Home, Calendar, Clock, FileText, User, LogOut, ChevronLeft, ChevronRight, Bell, Search } from "lucide-react";
import { Badge } from "./ui/badge";
import { Input } from "./ui/input";
interface PatientData {
name: string
email: string
phone: string
cpf: string
birthDate: string
address: string
}
export default function HospitalLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'patiente' });
interface HospitalLayoutProps {
children: React.ReactNode
}
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
export default function HospitalLayout({ children }: HospitalLayoutProps) {
const [patientData, setPatientData] = useState<PatientData | null>(null)
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter()
const pathname = usePathname()
useEffect(() => {
const data = localStorage.getItem("patientData")
if (data) {
setPatientData(JSON.parse(data))
} else {
router.push("/patient/login")
}
}, [router])
const handleLogout = () => {
setShowLogoutDialog(true)
}
const confirmLogout = () => {
localStorage.removeItem("patientData")
setShowLogoutDialog(false)
router.push("/")
}
const cancelLogout = () => {
setShowLogoutDialog(false)
}
const confirmLogout = async () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const menuItems = [
{
href: "/patient/dashboard",
icon: Home,
label: "Dashboard",
},
{
href: "/patient/appointments",
icon: Calendar,
label: "Minhas Consultas",
},
{
href: "/patient/schedule",
icon: Clock,
label: "Agendar Consulta",
},
{
href: "/patient/reports",
icon: FileText,
label: "Meus Laudos",
},
{
href: "/patient/profile",
icon: User,
label: "Meus Dados",
},
]
{ href: "/patient/dashboard", icon: Home, label: "Dashboard" },
{ href: "/patient/appointments", icon: Calendar, label: "Minhas Consultas" },
{ href: "/patient/schedule", icon: Clock, label: "Agendar Consulta" },
{ href: "/patient/reports", icon: FileText, label: "Meus Laudos" },
{ href: "/patient/profile", icon: User, label: "Meus Dados" },
];
if (!patientData) {
return <div>Carregando...</div>
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<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"} h-screen flex flex-col`}
>
<div className={`bg-card border-r border-border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} h-screen flex flex-col`}>
<div className="p-4 border-b border-border">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-primary-foreground rounded-sm"></div>
</div>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"><div className="w-4 h-4 bg-primary-foreground rounded-sm"></div></div>
<span className="font-semibold text-foreground">MediConnect</span>
</div>
)}
@ -131,97 +58,64 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) {
</Button>
</div>
</div>
<nav className="flex-1 p-2">
{menuItems.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
const Icon = item.icon;
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href}>
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
)
);
})}
</nav>
<div className="border-t p-4">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{patientData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{patientData.name}</p>
<p className="text-xs text-muted-foreground truncate">{patientData.email}</p>
<p className="text-sm font-medium text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
</div>
</div>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sair
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => setShowLogoutDialog(true)}>
<LogOut className="mr-2 h-4 w-4" /> Sair
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* 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">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input placeholder="Buscar paciente" className="pl-10 bg-background border-border" />
<Input placeholder="Buscar patiente" className="pl-10 bg-background border-border" />
</div>
</div>
<div className="flex items-center gap-4">
<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">
1
</Badge>
<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">1</Badge>
</Button>
</div>
</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>
<DialogTitle>Confirmar Saída</DialogTitle>
<DialogDescription>
Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta.
</DialogDescription>
</DialogHeader>
<DialogHeader><DialogTitle>Confirmar Saída</DialogTitle><DialogDescription>Deseja realmente sair do sistema?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
);
}

View File

@ -1,139 +1,55 @@
// Caminho: [seu-caminho]/ManagerLayout.tsx
// CÓDIGO REATORADO PARA: components/manager-layout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout
import { useAuthLayout } from "@/hooks/useAuthLayout"; // 1. Importamos nosso novo hook
import { api } from "@/services/api.mjs";
// Componentes da UI (Button, Avatar, etc.)
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 { Home, Calendar, User, LogOut, ChevronLeft, ChevronRight, Bell } from "lucide-react";
import { Badge } from "./ui/badge";
interface ManagerData {
id: string;
name: string;
email: string;
phone: string;
cpf: string;
department: string;
permissions: object;
}
export default function ManagerLayout({ children }: { children: React.ReactNode }) {
// 2. Usamos o hook para buscar o usuário e controlar o acesso
const { user, isLoading } = useAuthLayout({ requiredRole: 'gestor' });
interface ManagerLayoutProps {
children: React.ReactNode;
}
export default function ManagerLayout({ children }: ManagerLayoutProps) {
const [managerData, setManagerData] = useState<ManagerData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
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);
setManagerData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Gestor(a)",
email: userInfo.email || "",
department: userInfo.user_metadata?.role || "Gestão",
phone: userInfo.phone || "",
cpf: "",
permissions: {},
});
} else {
// O redirecionamento para /login já estava correto. Ótimo!
router.push("/login");
}
}, [router]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogout = () => setShowLogoutDialog(true);
// --- 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
}
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const cancelLogout = () => setShowLogoutDialog(false);
const menuItems = [
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "/manager/usuario", icon: User, label: "Gestão de Usuários" },
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
{ href: "/manager/pacientes", icon: User, label: "Gestão de Pacientes" },
{ href: "/manager/patientes", icon: User, label: "Gestão de patientes" },
{ href: "#", icon: Calendar, label: "Configurações" },
];
if (!managerData) {
return (
<div className="flex h-screen w-full items-center justify-center">
Carregando...
</div>
);
// 3. Enquanto o hook está carregando, mostramos uma tela de loading
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
// O resto do seu JSX continua igual, mas agora usando a variável 'user' do hook
return (
<div className="min-h-screen bg-gray-50 flex">
<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"
}`}
>
<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"}`}>
{/* Header da Sidebar */}
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
@ -143,120 +59,76 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
<span className="font-semibold text-gray-900">MediConnect</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
{/* Menu */}
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<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"
}`}
>
<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"}`}>
<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>
{/* Rodapé com Avatar e Logout */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{/* 4. A LÓGICA DO AVATAR AGORA É APLICADA AQUI */}
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{managerData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.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">{user.name}</p>
<p className="text-xs text-gray-500 truncate">{user.roles.join(', ')}</p>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<Button variant="outline" size="sm" className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} onClick={() => setShowLogoutDialog(true)}>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
<div
className={`flex-1 flex flex-col transition-all duration-300 w-full ${
sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
{/* Main Content */}
<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">
<div className="flex items-center gap-4 flex-1 max-w-md"></div>
<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>
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
{/* Dialog de Logout */}
<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?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

View File

@ -1,287 +1,118 @@
// CÓDIGO COMPLETO PARA: components/patient-layout.tsx
"use client";
import Cookies from "js-cookie";
import type React from "react";
import { useState, useEffect } from "react";
import { useState } 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 { useAuthLayout } from "@/hooks/useAuthLayout";
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 {
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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Home, Calendar, Clock, FileText, User, LogOut, ChevronLeft, ChevronRight, Bell } from "lucide-react";
import { Badge } from "./ui/badge";
interface PatientData {
name: string;
email: string;
phone: string;
cpf: string;
birthDate: string;
address: string;
}
export default function patientLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'patiente' });
interface PatientLayoutProps {
children: React.ReactNode;
}
// --- 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();
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
// --- ALTERAÇÃO 2: Buscando o token no localStorage ---
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
setPatientData({
name: userInfo.user_metadata?.full_name || "Paciente",
email: userInfo.email || "",
phone: userInfo.phone || "",
cpf: "",
birthDate: "",
address: "",
});
} else {
// --- ALTERAÇÃO 3: Redirecionando para o login central ---
router.push("/login");
}
}, [router]);
const handleLogout = () => setShowLogoutDialog(true);
// --- 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
}
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const cancelLogout = () => setShowLogoutDialog(false);
const menuItems = [
{ href: "/patient/dashboard", icon: Home, label: "Dashboard" },
{
href: "/patient/appointments",
icon: Calendar,
label: "Minhas Consultas",
},
{ href: "/patient/appointments", icon: Calendar, label: "Minhas Consultas" },
{ href: "/patient/schedule", icon: Clock, label: "Agendar Consulta" },
{ href: "/patient/reports", icon: FileText, label: "Meus Laudos" },
{ href: "/patient/profile", icon: User, label: "Meus Dados" },
];
if (!patientData) {
return (
<div className="flex h-screen w-full items-center justify-center">
Carregando...
</div>
);
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<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"
} fixed left-0 top-0 h-screen flex flex-col z-10`}
>
{/* Header da Sidebar */}
<div className={`bg-card border-r border-border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-10`}>
<div className="p-4 border-b border-border">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-primary-foreground rounded-sm"></div>
</div>
<span className="font-semibold text-foreground">
MediConnect
</span>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"><div className="w-4 h-4 bg-primary-foreground rounded-sm"></div></div>
<span className="font-semibold text-foreground">MediConnect</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
</div>
{/* Menu */}
<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 || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href}>
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`}>
<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>
{/* Rodapé com Avatar e Logout */}
<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>
{patientData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{patientData.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{patientData.email}
</p>
<p className="text-sm font-medium text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.email}</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"
}
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 */}
<Button variant="outline" size="sm" className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} onClick={() => setShowLogoutDialog(true)}>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "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-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md"></div>
<div className="flex items-center gap-4">
<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">
1
</Badge>
<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">1</Badge>
</Button>
</div>
</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>
<DialogTitle>Confirmar Saída</DialogTitle>
<DialogDescription>
Deseja realmente sair do sistema? Você precisará fazer login
novamente para acessar sua conta.
</DialogDescription>
</DialogHeader>
<DialogHeader><DialogTitle>Confirmar Saída</DialogTitle><DialogDescription>Deseja realmente sair do sistema?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

View File

@ -1,279 +1,117 @@
// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo)
// CÓDIGO COMPLETO PARA: components/secretary-layout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useState } 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 { useAuthLayout } from "@/hooks/useAuthLayout";
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,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Home, Calendar, Clock, User, LogOut, ChevronLeft, ChevronRight, Bell } from "lucide-react";
import { Badge } from "./ui/badge";
interface SecretaryData {
id: string;
name: string;
email: string;
phone: string;
cpf: string;
employeeId: string;
department: string;
permissions: object;
}
export default function SecretaryLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'secretaria' });
interface SecretaryLayoutProps {
children: React.ReactNode;
}
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();
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) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogout = () => setShowLogoutDialog(true);
// --- 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
}
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const cancelLogout = () => setShowLogoutDialog(false);
const menuItems = [
{ href: "/secretary/dashboard", icon: Home, label: "Dashboard" },
{ href: "/secretary/appointments", icon: Calendar, label: "Consultas" },
{ href: "/secretary/schedule", icon: Clock, label: "Agendar Consulta" },
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" },
{ href: "/secretary/pacientes", icon: User, label: "pacientes" },
];
if (!secretaryData) {
return (
<div className="flex h-screen w-full items-center justify-center">
Carregando...
</div>
);
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<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"}
fixed left-0 top-0 h-screen flex flex-col z-10`}
>
<div className={`bg-card border-r border-border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-10`}>
<div className="p-4 border-b border-border">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-primary-foreground rounded-sm"></div>
</div>
<span className="font-semibold text-foreground">
MediConnect
</span>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"><div className="w-4 h-4 bg-primary-foreground rounded-sm"></div></div>
<span className="font-semibold text-foreground">MediConnect</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
</div>
<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 || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href}>
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`}>
<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>
<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>
{secretaryData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarImage src={user.avatarFullUrl} />
<AvatarFallback>{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{secretaryData.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{secretaryData.email}
</p>
<p className="text-sm font-medium text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<Button variant="outline" size="sm" className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"} onClick={() => setShowLogoutDialog(true)}>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
{/* Main Content */}
<div
className={`flex-1 flex flex-col transition-all duration-300 ${
sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<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"></div>
<div className="flex items-center gap-4">
<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">
1
</Badge>
<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">1</Badge>
</Button>
</div>
</div>
</header>
<main className="flex-1 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>
</DialogHeader>
<DialogHeader><DialogTitle>Confirmar Saída</DialogTitle><DialogDescription>Deseja realmente sair do sistema?</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={() => setShowLogoutDialog(false)}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

View File

@ -1,3 +1,5 @@
// CÓDIGO CORRIGIDO PARA: components/ui/button.tsx
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
@ -9,16 +11,11 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
@ -35,25 +32,24 @@ const buttonVariants = cva(
},
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export { Button, buttonVariants }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }

75
hooks/useAuthLayout.ts Normal file
View File

@ -0,0 +1,75 @@
// ARQUIVO COMPLETO PARA: hooks/useAuthLayout.ts
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { usersService } from '@/services/usersApi.mjs';
import { toast } from "@/hooks/use-toast";
interface UserLayoutData {
id: string;
name: string;
email: string;
roles: string[];
avatar_url?: string;
avatarFullUrl?: string;
}
interface UseAuthLayoutOptions {
requiredRole?: string;
}
export function useAuthLayout({ requiredRole }: UseAuthLayoutOptions = {}) {
const [user, setUser] = useState<UserLayoutData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const fetchUserData = async () => {
try {
const fullUserData = await usersService.getMe();
if (
requiredRole &&
!fullUserData.roles.includes(requiredRole) &&
!fullUserData.roles.includes('admin')
) {
console.error(`Acesso negado. Requer perfil '${requiredRole}', mas o usuário tem '${fullUserData.roles.join(', ')}'.`);
toast({
title: "Acesso Negado",
description: "Você não tem permissão para acessar esta página.",
variant: "destructive",
});
router.push('/');
return;
}
const avatarPath = fullUserData.profile.avatar_url;
// *** A CORREÇÃO ESTÁ AQUI ***
// Adicionamos o nome do bucket 'avatars' na URL final.
const avatarFullUrl = avatarPath
? `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${avatarPath}`
: undefined;
setUser({
id: fullUserData.user.id,
name: fullUserData.profile.full_name || 'Usuário',
email: fullUserData.user.email,
roles: fullUserData.roles,
avatar_url: avatarPath,
avatarFullUrl: avatarFullUrl,
});
} catch (error) {
console.error("Falha na autenticação do layout:", error);
router.push("/login");
} finally {
setIsLoading(false);
}
};
fetchUserData();
}, [router, requiredRole]);
return { user, isLoading };
}

View File

@ -106,4 +106,25 @@ export const api = {
patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }),
delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }),
logout: logout,
storage: {
async upload(bucket, path, file) {
const token = localStorage.getItem("token");
const response = await fetch(`${BASE_URL}/storage/v1/object/${bucket}/${path}`, {
method: 'POST',
headers: {
'Content-Type': file.type,
'apikey': API_KEY,
'Authorization': `Bearer ${token}`,
'x-upsert': 'true' // Isso faz com que o arquivo seja substituído se já existir
},
body: file,
});
if (!response.ok) {
const errorBody = await response.json();
throw new Error(`Erro no upload: ${errorBody.message}`);
}
return response.json();
}
},
};