Merge pull request #12 from m1guelmcf/Sidebar

Sidebar
This commit is contained in:
DaniloSts 2025-11-12 13:31:04 -03:00 committed by GitHub
commit 48b0c409ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 809 additions and 1033 deletions

View File

@ -4,7 +4,6 @@
import type React from "react";
import { useState, useEffect, useMemo } from "react";
import DoctorLayout from "@/components/doctor-layout";
import { useAuthLayout } from "@/hooks/useAuthLayout";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import { patientsService } from "@/services/patientsApi.mjs";
@ -19,6 +18,7 @@ import { Clock, Calendar as CalendarIcon, User, X, RefreshCw, Loader2, MapPin, P
import { format, isFuture, parseISO, isValid, isToday, isTomorrow } from "date-fns";
import { ptBR } from "date-fns/locale";
import { toast } from "sonner";
import Sidebar from "@/components/Sidebar";
// Interfaces (sem alteração)
interface EnrichedAppointment {
@ -129,11 +129,11 @@ export default function DoctorAppointmentsPage() {
};
if (isAuthLoading) {
return <DoctorLayout><div>Carregando...</div></DoctorLayout>;
return <Sidebar><div>Carregando...</div></Sidebar>;
}
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-foreground">Agenda Médica</h1>
@ -224,6 +224,6 @@ export default function DoctorAppointmentsPage() {
</div>
</div>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -1,6 +1,5 @@
"use client";
import DoctorLayout from "@/components/doctor-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, User, Trash2 } from "lucide-react";
@ -14,6 +13,7 @@ import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { exceptionsService } from "@/services/exceptionApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
import Sidebar from "@/components/Sidebar";
type Availability = {
id: string;
@ -231,7 +231,7 @@ export default function PatientDashboard() {
}, [availability]);
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
@ -409,6 +409,6 @@ export default function PatientDashboard() {
</AlertDialogContent>
</AlertDialog>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -3,14 +3,12 @@
import type React from "react";
import Link from "next/link";
import { useState, useEffect } from "react";
import DoctorLayout from "@/components/doctor-layout";
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Calendar as CalendarIcon, RefreshCw } from "lucide-react";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { exceptionsService } from "@/services/exceptionApi.mjs";
@ -19,6 +17,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs";
import { Calendar } from "@/components/ui/calendar";
import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas
import { doctorsService } from "@/services/doctorsApi.mjs";
import Sidebar from "@/components/Sidebar";
type Doctor = {
id: string;
@ -147,7 +146,7 @@ export default function ExceptionPage() {
const displayDate = selectedCalendarDate ? new Date(selectedCalendarDate).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", month: "long" }) : "Selecione uma data";
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Adicione exceções</h1>
@ -254,6 +253,6 @@ export default function ExceptionPage() {
</div>
</div>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import DoctorLayout from "@/components/doctor-layout";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
@ -17,9 +16,10 @@ import { toast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Eye, Edit, Calendar, Trash2 } from "lucide-react";
import { Edit, Trash2 } from "lucide-react";
import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import Sidebar from "@/components/Sidebar";
// ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas)
@ -323,7 +323,7 @@ export default function AvailabilityPage() {
};
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
@ -506,6 +506,6 @@ export default function AvailabilityPage() {
onSubmit={handleEdit}
/>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -12,7 +12,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { ArrowLeft, Save } from "lucide-react";
import Link from "next/link";
import DoctorLayout from "@/components/doctor-layout";
import Sidebar from "@/components/Sidebar";
// Mock data - in a real app, this would come from an API
const mockDoctors = [
@ -124,7 +124,7 @@ export default function EditarMedicoPage() {
};
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/medicos">
@ -512,6 +512,6 @@ export default function EditarMedicoPage() {
</div>
</form>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -2,7 +2,6 @@
import { useParams, useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import DoctorLayout from "@/components/doctor-layout";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
@ -17,6 +16,7 @@ import { format } from "date-fns";
import TiptapEditor from "@/components/ui/tiptap-editor";
import { Skeleton } from "@/components/ui/skeleton";
import { reportsApi } from "@/services/reportsApi.mjs";
import Sidebar from "@/components/Sidebar";
export default function EditarLaudoPage() {
const router = useRouter();
@ -108,7 +108,7 @@ export default function EditarLaudoPage() {
if (loading) {
return (
<DoctorLayout>
<Sidebar>
<div className="container mx-auto p-4">
<Card>
<CardHeader>
@ -130,12 +130,12 @@ export default function EditarLaudoPage() {
</CardContent>
</Card>
</div>
</DoctorLayout>
</Sidebar>
)
}
return (
<DoctorLayout>
<Sidebar>
<div className="container mx-auto p-4">
<Card>
<CardHeader>
@ -232,6 +232,6 @@ export default function EditarLaudoPage() {
</CardContent>
</Card>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -17,7 +17,7 @@ import { format } from "date-fns";
import TiptapEditor from "@/components/ui/tiptap-editor";
import { reportsApi } from "@/services/reportsApi.mjs";
import DoctorLayout from "@/components/doctor-layout";
import Sidebar from "@/components/Sidebar";
@ -97,7 +97,7 @@ export default function NovoLaudoPage() {
};
return (
<DoctorLayout>
<Sidebar>
<div className="container mx-auto p-4">
<Card>
<CardHeader>
@ -189,6 +189,6 @@ export default function NovoLaudoPage() {
</CardContent>
</Card>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -8,7 +8,7 @@ import Link from 'next/link';
import { useParams } from 'next/navigation';
import { api } from '@/services/api.mjs';
import { reportsApi } from '@/services/reportsApi.mjs';
import DoctorLayout from '@/components/doctor-layout';
import Sidebar from '@/components/Sidebar';
export default function LaudosPage() {
const [patient, setPatient] = useState(null);
@ -49,7 +49,7 @@ export default function LaudosPage() {
const paginate = (pageNumber) => setCurrentPage(pageNumber);
return (
<DoctorLayout>
<Sidebar>
<div className="container mx-auto p-4">
{loading ? (
<p>Carregando...</p>
@ -123,6 +123,6 @@ export default function LaudosPage() {
</>
)}
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Upload, Plus, X, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import DoctorLayout from "@/components/doctor-layout";
import Sidebar from "@/components/Sidebar";
export default function NovoMedicoPage() {
const [anexosOpen, setAnexosOpen] = useState(false);
@ -24,7 +24,7 @@ export default function NovoMedicoPage() {
};
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
@ -466,6 +466,6 @@ export default function NovoMedicoPage() {
</div>
</form>
</div>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -2,25 +2,14 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import DoctorLayout from "@/components/doctor-layout";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Eye, Edit, Calendar, Trash2, Loader2 } from "lucide-react";
import { api } from "@/services/api.mjs";
import { PatientDetailsModal } from "@/components/ui/patient-details-modal";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import Sidebar from "@/components/Sidebar";
interface Paciente {
id: string;
@ -171,7 +160,7 @@ export default function PacientesPage() {
}, [fetchPacientes]);
return (
<DoctorLayout>
<Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-6">
{/* Cabeçalho */}
<div className="flex flex-wrap items-center justify-between gap-3">
@ -363,6 +352,6 @@ export default function PacientesPage() {
isOpen={isModalOpen}
onClose={handleCloseModal}
/>
</DoctorLayout>
</Sidebar>
);
}

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import FinancierLayout from "@/components/finance-layout";
import Sidebar from "@/components/Sidebar";
interface Paciente {
id: string;
@ -14,43 +14,10 @@ interface Paciente {
}
export default function PacientesPage() {
const [pacientes, setPacientes] = useState<Paciente[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchPacientes() {
try {
setLoading(true);
setError(null);
const res = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const items = Array.isArray(json?.data) ? json.data : [];
const mapped = items.map((p: any) => ({
id: String(p.id ?? ""),
nome: p.nome ?? "",
telefone: p?.contato?.celular ?? p?.contato?.telefone1 ?? p?.telefone ?? "",
cidade: p?.endereco?.cidade ?? p?.cidade ?? "",
estado: p?.endereco?.estado ?? p?.estado ?? "",
ultimoAtendimento: p.ultimo_atendimento ?? p.ultimoAtendimento ?? "",
proximoAtendimento: p.proximo_atendimento ?? p.proximoAtendimento ?? "",
}));
setPacientes(mapped);
} catch (e: any) {
setError(e?.message || "Erro ao carregar pacientes");
} finally {
setLoading(false);
}
}
fetchPacientes();
}, []);
return (
<FinancierLayout>
<Sidebar>
<div></div>
</FinancierLayout>
</Sidebar>
);
}

View File

@ -1,82 +1,247 @@
// Caminho: app/login/page.tsx
"use client";
import {usersService} from "@/services/usersApi.mjs";
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
import { Input } from "@/components/ui/input";
import { ArrowLeft, X } from "lucide-react";
import { useState } from "react";
import RenderFromTemplateContext from "next/dist/client/components/render-from-template-context";
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>
const [isModalOpen, setIsModalOpen] = useState(false);
const [email, setEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
{/* 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>
const handleOpenModal = () => {
// Tenta pegar o email do input do formulário de login
const emailInput = document.querySelector('input[type="email"]') as HTMLInputElement;
if (emailInput?.value) {
setEmail(emailInput.value);
}
setIsModalOpen(true);
};
const handleResetPassword = async () => {
if (!email.trim()) {
setMessage({ type: "error", text: "Por favor, insira um e-mail válido." });
return;
}
setIsLoading(true);
setMessage(null);
try {
// Chama o método que já faz o fetch corretamente
const data = await usersService.resetPassword(email);
console.log("Resposta resetPassword:", data);
setMessage({
type: "success",
text: "E-mail de recuperação enviado! Verifique sua caixa de entrada.",
});
setTimeout(() => {
setIsModalOpen(false);
setMessage(null);
setEmail("");
}, 2000);
} catch (error) {
console.error("Erro no reset de senha:", error);
setMessage({
type: "error",
text:
error instanceof Error
? error.message
: "Erro ao enviar e-mail. Tente novamente.",
});
} finally {
setIsLoading(false);
}
};
const closeModal = () => {
setIsModalOpen(false);
setMessage(null);
setEmail("");
};
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>
<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">
{/* 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">
<button
onClick={handleOpenModal}
className="text-muted-foreground hover:text-primary cursor-pointer underline bg-transparent border-none"
>
Esqueceu sua senha?
</button>
</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>
</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">
{/* 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"
alt="Médica utilizando um tablet na clínica MedConnect"
fill
style={{ objectFit: 'cover' }}
priority
/>
{/* 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">
<h1 className="text-5xl font-extrabold text-primary-foreground tracking-wider">
MedConnect
</h1>
</h1>
</div>
<h2 className="text-4xl font-bold text-primary-foreground leading-tight">
Tecnologia e Cuidado a Serviço da Sua Saúde.
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.
Acesse seu portal para uma experiência de saúde integrada, segura e eficiente.
</p>
</div>
</div>
</div>
</div>
{/* Modal de Recuperação de Senha */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="relative w-full max-w-md bg-card p-8 rounded-2xl shadow-2xl mx-4">
{/* Botão de fechar */}
<button
onClick={closeModal}
className="absolute top-4 right-4 text-muted-foreground hover:text-foreground transition-colors"
>
<X className="w-5 h-5" />
</button>
{/* Cabeçalho */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-foreground">Recuperar Senha</h2>
<p className="text-muted-foreground mt-2">
Insira seu e-mail e enviaremos um link para redefinir sua senha.
</p>
</div>
{/* Input de e-mail */}
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground mb-2">
E-mail
</label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="seu@email.com"
disabled={isLoading}
className="w-full"
/>
</div>
{/* Mensagem de feedback */}
{message && (
<div
className={`p-3 rounded-lg text-sm ${
message.type === "success"
? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"
: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"
}`}
>
{message.text}
</div>
)}
{/* Botões */}
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={closeModal}
disabled={isLoading}
className="flex-1"
>
Cancelar
</Button>
<Button
onClick={handleResetPassword}
disabled={isLoading}
className="flex-1"
>
{isLoading ? "Enviando..." : "Resetar Senha"}
</Button>
</div>
</div>
</div>
</div>
)}
</>
);
}
}

View File

@ -1,6 +1,5 @@
"use client";
import ManagerLayout from "@/components/manager-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, Plus, User } from "lucide-react";
@ -8,6 +7,7 @@ import Link from "next/link";
import React, { useState, useEffect } from "react";
import { usersService } from "services/usersApi.mjs";
import { doctorsService } from "services/doctorsApi.mjs";
import Sidebar from "@/components/Sidebar";
export default function ManagerDashboard() {
// 🔹 Estados para usuários
@ -55,7 +55,7 @@ export default function ManagerDashboard() {
}, []);
return (
<ManagerLayout>
<Sidebar>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
@ -185,6 +185,6 @@ export default function ManagerDashboard() {
</Card>
</div>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -10,7 +10,7 @@ import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Save, Loader2, ArrowLeft } from "lucide-react"
import ManagerLayout from "@/components/manager-layout"
import Sidebar from "@/components/Sidebar"
import { doctorsService } from "services/doctorsApi.mjs";
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
@ -207,17 +207,17 @@ export default function EditarMedicoPage() {
};
if (loading) {
return (
<ManagerLayout>
<Sidebar>
<div className="flex justify-center items-center h-full w-full py-16">
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
<p className="ml-2 text-gray-600">Carregando dados do médico...</p>
</div>
</ManagerLayout>
</Sidebar>
);
}
return (
<ManagerLayout>
<Sidebar>
<div className="w-full space-y-6 p-4 md:p-8">
<div className="flex items-center justify-between">
<div>
@ -487,6 +487,6 @@ export default function EditarMedicoPage() {
</div>
</form>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -1,25 +1,16 @@
"use client";
import React, { useEffect, useState, useCallback, useMemo } from "react"
import ManagerLayout from "@/components/manager-layout";
import Link from "next/link"
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
import { doctorsService } from "services/doctorsApi.mjs";
import Sidebar from "@/components/Sidebar";
interface Doctor {
@ -193,7 +184,7 @@ export default function DoctorsPage() {
return (
<ManagerLayout>
<Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-6">
{/* Cabeçalho */}
@ -430,6 +421,6 @@ export default function DoctorsPage() {
</AlertDialogContent>
</AlertDialog>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -13,9 +13,8 @@ import { Checkbox } from "@/components/ui/checkbox";
import { ArrowLeft, Save, Trash2, Paperclip, Upload } from "lucide-react";
import Link from "next/link";
import { useToast } from "@/hooks/use-toast";
import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
import { json } from "stream/consumers";
import Sidebar from "@/components/Sidebar";
export default function EditarPacientePage() {
const router = useRouter();
@ -247,7 +246,7 @@ export default function EditarPacientePage() {
};
return (
<SecretaryLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/manager/pacientes">
@ -677,6 +676,6 @@ export default function EditarPacientePage() {
</div>
</form>
</div>
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -6,10 +6,10 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { patientsService } from "@/services/patientsApi.mjs";
import ManagerLayout from "@/components/manager-layout";
import Sidebar from "@/components/Sidebar";
// Defina o tamanho da página.
const PAGE_SIZE = 5;
@ -145,7 +145,7 @@ export default function PacientesPage() {
};
return (
<ManagerLayout>
<Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-8">
{/* Header (Responsividade OK) */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
@ -449,6 +449,6 @@ export default function PacientesPage() {
</AlertDialogContent>
</AlertDialog>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Save, Loader2, ArrowLeft } from "lucide-react"
import ManagerLayout from "@/components/manager-layout"
import Sidebar from "@/components/Sidebar"
// Mock user service for demonstration. Replace with your actual API service.
const usersService = {
@ -155,17 +155,17 @@ export default function EditarUsuarioPage() {
if (loading) {
return (
<ManagerLayout>
<Sidebar>
<div className="flex justify-center items-center h-full w-full py-16">
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
<p className="ml-2 text-gray-600">Carregando dados do usuário...</p>
</div>
</ManagerLayout>
</Sidebar>
);
}
return (
<ManagerLayout>
<Sidebar>
<div className="w-full max-w-2xl mx-auto space-y-6 p-4 md:p-8">
<div className="flex items-center justify-between">
<div>
@ -274,6 +274,6 @@ export default function EditarUsuarioPage() {
</div>
</form>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -10,11 +10,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout";
import { usersService } from "@/services/usersApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import { login } from "services/api.mjs";
import { isValidCPF } from "@/lib/utils"; // 1. IMPORTAÇÃO DA FUNÇÃO DE VALIDAÇÃO
import Sidebar from "@/components/Sidebar";
interface UserFormData {
email: string;
@ -135,7 +135,7 @@ export default function NovoUsuarioPage() {
const isMedico = formData.papel === "medico";
return (
<ManagerLayout>
<Sidebar>
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
<div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4">
@ -236,6 +236,6 @@ export default function NovoUsuarioPage() {
</form>
</div>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -2,28 +2,14 @@
"use client";
import React, { useEffect, useState, useCallback } from "react";
import ManagerLayout from "@/components/manager-layout";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { api, login } from "services/api.mjs";
import { usersService } from "services/usersApi.mjs";
import Sidebar from "@/components/Sidebar";
interface FlatUser {
id: string;
@ -192,7 +178,7 @@ export default function UsersPage() {
return (
<ManagerLayout>
<Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-8">
{/* Header */}
@ -424,6 +410,6 @@ export default function UsersPage() {
</AlertDialogContent>
</AlertDialog>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -1,7 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import PatientLayout from "@/components/patient-layout";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -10,6 +9,7 @@ import { toast } from "sonner";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
import Sidebar from "@/components/Sidebar";
// Tipagem correta para o usuário
interface UserProfile {
@ -129,7 +129,7 @@ export default function PatientAppointmentsPage() {
};
return (
<PatientLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
@ -185,6 +185,6 @@ export default function PatientAppointmentsPage() {
)}
</div>
</div>
</PatientLayout>
</Sidebar>
);
}

View File

@ -1,12 +1,12 @@
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, Clock, User, Plus } from "lucide-react"
import Link from "next/link"
import Sidebar from "@/components/Sidebar"
export default function PatientDashboard() {
return (
<PatientLayout>
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
@ -108,6 +108,6 @@ export default function PatientDashboard() {
</Card>
</div>
</div>
</PatientLayout>
</Sidebar>
)
}

View File

@ -3,7 +3,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
import PatientLayout from "@/components/patient-layout";
import Sidebar from "@/components/Sidebar"
import { useAuthLayout } from "@/hooks/useAuthLayout";
import { patientsService } from "@/services/patientsApi.mjs";
import { api } from "@/services/api.mjs";
@ -121,11 +121,11 @@ export default function PatientProfile() {
};
if (isAuthLoading || !patientData) {
return <PatientLayout><div>Carregando seus dados...</div></PatientLayout>;
return <Sidebar><div>Carregando seus dados...</div></Sidebar>;
}
return (
<PatientLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
@ -198,6 +198,6 @@ export default function PatientProfile() {
</div>
</div>
</div>
</PatientLayout>
);
</Sidebar>
)
}

View File

@ -1,13 +1,13 @@
"use client"
import { useState, useEffect } from "react"
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { toast } from "@/hooks/use-toast"
import { FileText, Download, Eye, Calendar, User, X } from "lucide-react"
import Sidebar from "@/components/Sidebar"
interface Report {
id: string
@ -287,7 +287,7 @@ export default function ReportsPage() {
const pendingReports = reports.filter((report) => report.status === "pendente")
return (
<PatientLayout>
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Meus Laudos</h1>
@ -536,6 +536,6 @@ export default function ReportsPage() {
</DialogContent>
</Dialog>
</div>
</PatientLayout>
</Sidebar>
)
}

View File

@ -1,11 +1,12 @@
// app/patient/appointments/page.tsx
import PatientLayout from "@/components/patient-layout";
import Sidebar from "@/components/Sidebar";
import ScheduleForm from "@/components/schedule/schedule-form";
export default function PatientAppointments() {
return (
<PatientLayout>
<Sidebar>
<ScheduleForm />
</PatientLayout>
</Sidebar>
);
}

View File

@ -1,20 +1,17 @@
"use client";
import { useState, useEffect } from "react";
import SecretaryLayout from "@/components/secretary-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog } from "@/components/ui/dialog";
import { Calendar, Clock, MapPin, Phone, User, Trash2, Pencil } from "lucide-react";
import { toast } from "sonner";
import Link from "next/link";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import { patientsService } from "@/services/patientsApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import Sidebar from "@/components/Sidebar";
export default function SecretaryAppointments() {
const [appointments, setAppointments] = useState<any[]>([]);
@ -144,7 +141,7 @@ export default function SecretaryAppointments() {
const appointmentStatuses = ["requested", "confirmed", "checked_in", "completed", "cancelled", "no_show"];
return (
<SecretaryLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
@ -225,6 +222,6 @@ export default function SecretaryAppointments() {
<Dialog open={deleteModal} onOpenChange={setDeleteModal}>
{/* ... (código do modal de deleção) ... */}
</Dialog>
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -1,10 +1,6 @@
"use client";
import SecretaryLayout from "@/components/secretary-layout";
import {
Card,
CardContent,
CardDescription,
import { Card, CardContent, CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
@ -14,6 +10,7 @@ import Link from "next/link";
import React, { useState, useEffect } from "react";
import { patientsService } from "@/services/patientsApi.mjs";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import Sidebar from "@/components/Sidebar";
export default function SecretaryDashboard() {
// Estados
@ -100,7 +97,7 @@ export default function SecretaryDashboard() {
}, []);
return (
<SecretaryLayout>
<Sidebar>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
@ -299,6 +296,6 @@ export default function SecretaryDashboard() {
</Card>
</div>
</div>
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -13,9 +13,8 @@ import { Checkbox } from "@/components/ui/checkbox";
import { ArrowLeft, Save, Trash2, Paperclip, Upload } from "lucide-react";
import Link from "next/link";
import { useToast } from "@/hooks/use-toast";
import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
import { json } from "stream/consumers";
import Sidebar from "@/components/Sidebar";
export default function EditarPacientePage() {
const router = useRouter();
@ -247,7 +246,7 @@ export default function EditarPacientePage() {
};
return (
<SecretaryLayout>
<Sidebar>
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/secretary/pacientes">
@ -677,6 +676,6 @@ export default function EditarPacientePage() {
</div>
</form>
</div>
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -1,4 +1,3 @@
// Caminho: app/(manager)/usuario/novo/page.tsx
"use client";
import { useState } from "react";
@ -7,13 +6,9 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// O Select foi removido pois não é mais necessário
import { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout";
// Os imports originais foram mantidos, como solicitado
import { usersService } from "services/usersApi.mjs";
import { doctorsService } from "services/doctorsApi.mjs";
import { login } from "services/api.mjs";
import Sidebar from "@/components/Sidebar";
// Interface simplificada para refletir apenas os campos necessários
interface UserFormData {
@ -97,7 +92,7 @@ export default function NovoUsuarioPage() {
};
return (
<ManagerLayout>
<Sidebar>
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
<div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4">
@ -167,6 +162,6 @@ export default function NovoUsuarioPage() {
</form>
</div>
</div>
</ManagerLayout>
</Sidebar>
);
}

View File

@ -8,8 +8,8 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
import Sidebar from "@/components/Sidebar";
// Defina o tamanho da página.
const PAGE_SIZE = 5;
@ -145,7 +145,7 @@ export default function PacientesPage() {
};
return (
<SecretaryLayout>
<Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-8">
{/* Header (Responsividade OK) */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
@ -457,6 +457,6 @@ export default function PacientesPage() {
</AlertDialogContent>
</AlertDialog>
</div>
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -1,11 +1,11 @@
import SecretaryLayout from "@/components/secretary-layout";
import Sidebar from "@/components/Sidebar";
import ScheduleForm from "@/components/schedule/schedule-form";
export default function SecretaryAppointments() {
return (
<SecretaryLayout>
<Sidebar>
<ScheduleForm />
</SecretaryLayout>
</Sidebar>
);
}

View File

@ -13,6 +13,7 @@ import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Eye, EyeOff, Mail, Lock, Loader2 } from "lucide-react";
import { usersService } from "@/services/usersApi.mjs";
interface LoginFormProps {
children?: React.ReactNode;
@ -31,8 +32,12 @@ export function LoginForm({ children }: LoginFormProps) {
const { toast } = useToast();
const [userRoles, setUserRoles] = useState<string[]>([]);
// *** MUDANÇA 1: A função agora recebe o objeto 'user' como parâmetro ***
const [authenticatedUser, setAuthenticatedUser] = useState<any>(null);
/**
* --- NOVA FUNÇÃO ---
* Finaliza o login com o perfil de dashboard escolhido e redireciona.
*/
const handleRoleSelection = (selectedDashboardRole: string, user: any) => {
if (!user) {
toast({ title: "Erro de Sessão", description: "Não foi possível encontrar os dados do usuário. Tente novamente.", variant: "destructive" });
@ -47,12 +52,12 @@ export function LoginForm({ children }: LoginFormProps) {
localStorage.setItem("user_info", JSON.stringify(completeUserInfo));
let redirectPath = "";
switch (roleInLowerCase) {
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;
switch (selectedDashboardRole) {
case "gestor": redirectPath = "/manager/dashboard"; break;
case "admin": redirectPath = "/manager/dashboard"; break;
case "medico": redirectPath = "/doctor/dashboard"; break;
case "secretaria": redirectPath = "/secretary/dashboard"; break;
case "paciente": redirectPath = "/patient/dashboard"; break;
}
if (redirectPath) {
@ -77,55 +82,16 @@ export function LoginForm({ children }: LoginFormProps) {
}
const rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`);
if (!rolesData || rolesData.length === 0) {
const me = await usersService.getMeSimple()
console.log(me.roles)
if (!me.roles || me.roles.length === 0) {
throw new Error("Nenhum perfil de acesso foi encontrado para este usuário.");
}
const rolesFromApi: string[] = rolesData.map((r: any) => r.role);
// *** MUDANÇA 2: Passamos o objeto 'user' diretamente para a função de seleção ***
const handleSelectionWithUser = (role: string) => handleRoleSelection(role, user);
handleRoleSelection(me.roles[0], user);
if (rolesFromApi.includes("admin")) {
const allRoles = ["manager", "doctor", "secretary", "patient", "finance"];
setUserRoles(allRoles);
// Atualizamos o onClick para usar a nova função que já tem o 'user'
const roleButtons = allRoles.map((role) => (
<Button key={role} variant="outline" className="h-11 text-base" onClick={() => handleSelectionWithUser(role)}>
Entrar como: {role.charAt(0).toUpperCase() + role.slice(1)}
</Button>
));
// Precisamos de um estado para renderizar os botões
setRoleSelectionUI(roleButtons);
setIsLoading(false);
return;
}
const displayRoles = new Set<string>();
rolesFromApi.forEach((role) => {
switch (role) {
case "gestor": displayRoles.add("manager"); displayRoles.add("finance"); break;
case "medico": displayRoles.add("doctor"); break;
case "secretaria": displayRoles.add("secretary"); break;
case "paciente": displayRoles.add("patient"); break;
}
});
const finalRoles = Array.from(displayRoles);
if (finalRoles.length === 1) {
handleSelectionWithUser(finalRoles[0]);
} else {
setUserRoles(finalRoles);
// Atualizamos o onClick aqui também
const roleButtons = finalRoles.map((role) => (
<Button key={role} variant="outline" className="h-11 text-base" onClick={() => handleSelectionWithUser(role)}>
Entrar como: {role.charAt(0).toUpperCase() + role.slice(1)}
</Button>
));
setRoleSelectionUI(roleButtons);
setIsLoading(false);
}
} catch (error) {
localStorage.removeItem("token");
localStorage.removeItem("user_info");
@ -172,7 +138,11 @@ export function LoginForm({ children }: LoginFormProps) {
<h3 className="text-lg font-medium text-center text-foreground">Você tem múltiplos perfis</h3>
<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">
{roleSelectionUI}
{userRoles.map((role) => (
<Button key={role} variant="outline" className="h-11 text-base" onClick={() => handleRoleSelection(role, authenticatedUser)}>
Entrar como: {role.charAt(0).toUpperCase() + role.slice(1)}
</Button>
))}
</div>
</div>
)}

291
components/Sidebar.tsx Normal file
View File

@ -0,0 +1,291 @@
// Caminho: [seu-caminho]/ManagerLayout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout
import { api } from "@/services/api.mjs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home, CalendarCheck2, ClipboardPlus, SquareUserRound, CalendarClock, Users, SquareUser, ClipboardList, Stethoscope, ClipboardMinus } from "lucide-react";
import SidebarUserSection from "@/components/ui/userToolTip";
interface UserData {
id: string;
email: string;
app_metadata: {
user_role: string;
};
user_metadata: {
cpf: string;
email_verified: boolean;
full_name: string;
phone_mobile: string;
role: string;
};
identities: {
identity_id: string;
id: string;
user_id: string;
provider: string;
}[];
is_anonymous: boolean;
}
interface MenuItem {
href: string;
icon: React.ElementType;
label: string;
}
interface SidebarProps {
children: React.ReactNode;
}
export default function Sidebar({ children }: SidebarProps) {
const [userData, setUserData] = useState<UserData>();
const [role, setRole] = useState<string>();
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);
setUserData({
id: userInfo.id ?? "",
email: userInfo.email ?? "",
app_metadata: {
user_role: userInfo.app_metadata?.user_role ?? "patient",
},
user_metadata: {
cpf: userInfo.user_metadata?.cpf ?? "",
email_verified: userInfo.user_metadata?.email_verified ?? false,
full_name: userInfo.user_metadata?.full_name ?? "",
phone_mobile: userInfo.user_metadata?.phone_mobile ?? "",
role: userInfo.user_metadata?.role ?? "",
},
identities:
userInfo.identities?.map((identity: any) => ({
identity_id: identity.identity_id ?? "",
id: identity.id ?? "",
user_id: identity.user_id ?? "",
provider: identity.provider ?? "",
})) ?? [],
is_anonymous: userInfo.is_anonymous ?? false,
});
setRole(userInfo.user_metadata?.role)
} 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
}
};
const cancelLogout = () => setShowLogoutDialog(false);
const SetMenuItems = (role: any) => {
const patientItems: MenuItem[] = [
{ href: "/patient/dashboard", icon: Home, label: "Dashboard" },
{ href: "/patient/schedule", icon: CalendarClock, label: "Agendar Consulta" },
{ href: "/patient/appointments", icon: CalendarCheck2, label: "Minhas Consultas" },
{ href: "/patient/reports", icon: ClipboardPlus, label: "Meus Laudos" },
{ href: "/patient/profile", icon: SquareUser, label: "Meus Dados" },
]
const doctorItems: MenuItem[] = [
{ href: "/doctor/dashboard", icon: Home, label: "Dashboard" },
{ href: "/doctor/medicos", icon: Users, label: "Gestão de Pacientes" },
{ href: "/doctor/consultas", icon: CalendarCheck2, label: "Consultas" },
{ href: "/doctor/disponibilidade", icon: ClipboardList, label: "Disponibilidade" },
]
const secretaryItems: MenuItem[] = [
{ href: "/secretary/dashboard", icon: Home, label: "Dashboard" },
{ href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" },
{ href: "/secretary/schedule", icon: CalendarClock, label: "Agendar Consulta" },
{ href: "/secretary/pacientes", icon: Users, label: "Gestão de Pacientes" },
]
const managerItems: MenuItem[] = [
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
{ href: "#", icon: ClipboardMinus, label: "Relatórios gerenciais" },
{ href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" },
{ href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" },
{ href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" },
{ href: "/doctor/consultas", icon: CalendarCheck2, label: "Consultas" }, //adicionar botão de voltar pra pagina anterior
]
let menuItems: MenuItem[];
switch (role) {
case "gestor":
menuItems = managerItems;
break;
case "admin":
menuItems = managerItems;
break;
case "medico":
menuItems = doctorItems;
break;
case "secretaria":
menuItems = secretaryItems;
break;
case "paciente":
menuItems = patientItems;
break;
default:
menuItems = patientItems;
break;
}
return menuItems;
}
const menuItems = SetMenuItems(role)
if (!userData) {
return (
<div className="flex h-screen w-full items-center justify-center">
Carregando...
</div>
);
}
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="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">MedConnect</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>
<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"
}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
</div>
</Link>
);
})}
</nav>
<SidebarUserSection
userData={userData}
sidebarCollapsed={false}
handleLogout={handleLogout}
isActive={role === "paciente"? false: true}>
</SidebarUserSection>
</div>
<div
className={`flex-1 flex flex-col transition-all duration-300 w-full ${
sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
<header className="bg-gray-50 px-4 md:px-6 py-4 flex items-center justify-between"></header>
<main className="flex-1 p-4 md: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>
<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>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,128 +0,0 @@
// CÓDIGO REATORADO PARA: components/doctor-layout.tsx
"use client";
import type React from "react";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
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";
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' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
const confirmLogout = async () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
// ESTA PARTE É ÚNICA DE CADA LAYOUT E DEVE SER MANTIDA
const menuItems = [
{ 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" },
];
// 3. Adicionamos o estado de carregamento
if (isLoading || !user) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
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"}`}>
{/* 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));
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>
{/* 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={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">{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={() => 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 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-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?</DialogDescription></DialogHeader>
<DialogFooter className="flex gap-2">
<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,117 +0,0 @@
// CÓDIGO COMPLETO PARA: components/finance-layout.tsx
"use client";
import type React 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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
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";
export default function FinancierLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'finance' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
const confirmLogout = async () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
const menuItems = [
{ href: "#", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios financeiros" },
{ href: "#", icon: User, label: "Finanças Gerais" },
{ href: "#", icon: Calendar, label: "Configurações" },
];
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">
<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>
)}
<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-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 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<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">{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={() => 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-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>
</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?</DialogDescription></DialogHeader>
<DialogFooter className="flex gap-2">
<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,121 +0,0 @@
// CÓDIGO COMPLETO PARA: components/hospital-layout.tsx
"use client";
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 { 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";
export default function HospitalLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'patiente' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
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" },
];
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">
<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>
<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>
</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));
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"}`}>
<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={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">{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={() => setShowLogoutDialog(true)}>
<LogOut className="mr-2 h-4 w-4" /> Sair
</Button>
</div>
</div>
<div className="flex-1 flex flex-col">
<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 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>
</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?</DialogDescription></DialogHeader>
<DialogFooter className="flex gap-2">
<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,134 +0,0 @@
// CÓDIGO REATORADO PARA: components/manager-layout.tsx
"use client";
import type React from "react";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
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";
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' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
const confirmLogout = async () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
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/patientes", icon: User, label: "Gestão de patientes" },
{ href: "#", icon: Calendar, label: "Configurações" },
];
// 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"}`}>
{/* 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 */}
<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"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
{!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={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">{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={() => 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 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-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?</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2">
<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,118 +0,0 @@
// CÓDIGO COMPLETO PARA: components/patient-layout.tsx
"use client";
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 { 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 } from "lucide-react";
import { Badge } from "./ui/badge";
export default function patientLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'patiente' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
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" },
];
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">
<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>
)}
<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-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 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<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">{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={() => 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-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>
</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?</DialogDescription></DialogHeader>
<DialogFooter className="flex gap-2">
<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,117 +0,0 @@
// CÓDIGO COMPLETO PARA: components/secretary-layout.tsx
"use client";
import type React 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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
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";
export default function SecretaryLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthLayout({ requiredRole: 'secretaria' });
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
const confirmLogout = async () => {
await api.logout();
setShowLogoutDialog(false);
router.push("/");
};
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" },
];
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">
<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>
)}
<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-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 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<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">{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={() => 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-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>
</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?</DialogDescription></DialogHeader>
<DialogFooter className="flex gap-2">
<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

@ -0,0 +1,125 @@
"use client";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { CalendarCheck2, CalendarClock, ClipboardPlus, Home, LogOut, SquareUser } from "lucide-react";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { usePathname } from "next/navigation";
import Link from "next/link";
interface UserData {
user_metadata: {
full_name: string;
};
app_metadata: {
user_role: string;
};
email: string;
}
interface Props {
userData: UserData;
sidebarCollapsed: boolean;
handleLogout: () => void;
isActive: boolean;
}
export default function SidebarUserSection({
userData,
sidebarCollapsed,
handleLogout,
isActive,
}: Props) {
const pathname = usePathname();
const menuItems: any[] = [
{ href: "/patient/schedule", icon: CalendarClock, label: "Agendar Consulta" },
{ href: "/patient/appointments", icon: CalendarCheck2, label: "Minhas Consultas" },
{ href: "/patient/reports", icon: ClipboardPlus, label: "Meus Laudos" },
{ href: "/patient/profile", icon: SquareUser, label: "Meus Dados" },
]
return (
<div className="border-t p-4 mt-auto">
{/* POPUP DE INFORMAÇÕES DO USUÁRIO */}
<Popover>
<PopoverTrigger asChild>
<div
className={`flex items-center space-x-3 mb-4 p-2 rounded-md transition-colors ${
isActive
? "cursor-pointer hover:bg-gray-100"
: "cursor-default pointer-events-none"
}`}>
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{userData.user_metadata.full_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">
{userData.user_metadata.full_name}
</p>
<p className="text-xs text-gray-500 truncate">
{userData.app_metadata.user_role}
</p>
</div>
)}
</div>
</PopoverTrigger>
{/* Card flutuante */}
<PopoverContent
align="center"
side="top"
className="w-64 p-4 shadow-lg border bg-white"
>
<nav>
{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"
}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
</div>
</Link>
);
})}
</nav>
</PopoverContent>
</Popover>
{/* Botão de sair */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{sidebarCollapsed && "Sair"}
</Button>
</div>
);
}

View File

@ -21,6 +21,10 @@ export const usersService = {
return await api.post(`/functions/v1/create-user-with-password`, data);
},
async getMeSimple() {
return await api.post(`/functions/v1/user-info`);
},
async full_data(user_id) {
if (!user_id) throw new Error("user_id é obrigatório");
@ -57,4 +61,40 @@ export const usersService = {
permissions,
};
},
};
async resetPassword(email) {
if (!email) throw new Error("Email é obrigatório para resetar a senha.");
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/recover`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
},
body: JSON.stringify({ email }),
}
);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
console.error("Erro no resetPassword:", res.status, data);
throw new Error(`Erro ${res.status}: ${data.message || "Falha ao resetar senha."}`);
}
console.log("✅ Reset de senha:", data);
return data;
} catch (err) {
console.error("❌ Erro na chamada resetPassword:", err);
throw new Error(err.message || "Erro inesperado na recuperação de senha.");
}
},
};