Compare commits

..

No commits in common. "32af41df4481ea75d438485063031863800dafbf" and "bbce3eb93273640c58a490e6e93d3bec81c761c1" have entirely different histories.

24 changed files with 1123 additions and 1490 deletions

View File

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

View File

@ -10,7 +10,7 @@ export default function HomePage() {
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-foreground mb-4">Central de Operações <br>
</br>
MediConnect
MedConnect
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">

View File

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

View File

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

View File

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

View File

@ -1,105 +1,41 @@
"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";
import Link from "next/link";
import React, { useState, useEffect } from "react";
import { usersService } from "services/usersApi.mjs";
import { doctorsService } from "services/doctorsApi.mjs";
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, User, Plus } from "lucide-react"
import Link from "next/link"
export default function ManagerDashboard() {
// 🔹 Estados para usuários
const [firstUser, setFirstUser] = useState<any>(null);
const [loadingUser, setLoadingUser] = useState(true);
// 🔹 Estados para médicos
const [doctors, setDoctors] = useState<any[]>([]);
const [loadingDoctors, setLoadingDoctors] = useState(true);
// 🔹 Buscar primeiro usuário
useEffect(() => {
async function fetchFirstUser() {
try {
const data = await usersService.list_roles();
if (Array.isArray(data) && data.length > 0) {
setFirstUser(data[0]);
}
} catch (error) {
console.error("Erro ao carregar usuário:", error);
} finally {
setLoadingUser(false);
}
}
fetchFirstUser();
}, []);
// 🔹 Buscar 3 primeiros médicos
useEffect(() => {
async function fetchDoctors() {
try {
const data = await doctorsService.list(); // ajuste se seu service tiver outro método
if (Array.isArray(data)) {
setDoctors(data.slice(0, 3)); // pega os 3 primeiros
}
} catch (error) {
console.error("Erro ao carregar médicos:", error);
} finally {
setLoadingDoctors(false);
}
}
fetchDoctors();
}, []);
return (
<ManagerLayout>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
{/* Cards principais */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Card 1 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground">2 não lidos, 1 lido</p>
</CardContent>
</Card>
{/* Card 2 — Gestão de usuários */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingUser ? (
<div className="text-gray-500 text-sm">Carregando usuário...</div>
) : firstUser ? (
<>
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
<p className="text-xs text-muted-foreground">
{firstUser.email || "Sem e-mail cadastrado"}
</p>
</>
) : (
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
)}
<div className="text-2xl font-bold">João Marques</div>
<p className="text-xs text-muted-foreground">fez login a 13min</p>
</CardContent>
</Card>
{/* Card 3 — Perfil */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
@ -112,79 +48,66 @@ export default function ManagerDashboard() {
</Card>
</div>
{/* Cards secundários */}
<div className="grid md:grid-cols-2 gap-6">
{/* Card — Ações rápidas */}
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/manager/home">
<Link href="##">
<Button className="w-full justify-start">
<User className="mr-2 h-4 w-4" />
Gestão de Médicos
<Plus className="mr-2 h-4 w-4" />
#
</Button>
</Link>
<Link href="/manager/usuario">
<Link href="##">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Calendar className="mr-2 h-4 w-4" />
#
</Button>
</Link>
<Link href="##">
<Button variant="outline" className="w-full justify-start bg-transparent">
<User className="mr-2 h-4 w-4" />
Usuários Cadastrados
</Button>
</Link>
<Link href="/manager/home/novo">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Plus className="mr-2 h-4 w-4" />
Adicionar Novo Médico
</Button>
</Link>
<Link href="/manager/usuario/novo">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Plus className="mr-2 h-4 w-4" />
Criar novo Usuário
#
</Button>
</Link>
</CardContent>
</Card>
{/* Card — Gestão de Médicos */}
<Card>
<CardHeader>
<CardTitle>Gestão de Médicos</CardTitle>
<CardDescription>Médicos cadastrados recentemente</CardDescription>
<CardDescription>Médicos online</CardDescription>
</CardHeader>
<CardContent>
{loadingDoctors ? (
<p className="text-sm text-gray-500">Carregando médicos...</p>
) : doctors.length === 0 ? (
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
) : (
<div className="space-y-4">
{doctors.map((doc, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100"
>
<div>
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
<p className="text-sm text-gray-600">
{doc.specialty || "Sem especialidade"}
</p>
</div>
<div className="text-right">
<p className="font-medium text-green-700">
{doc.active ? "Ativo" : "Inativo"}
</p>
</div>
</div>
))}
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium">Dr. Silva</p>
<p className="text-sm text-gray-600">Cardiologia</p>
</div>
<div className="text-right">
<p className="font-medium">On-line</p>
<p className="text-sm text-gray-600"></p>
</div>
</div>
)}
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">Dra. Santos</p>
<p className="text-sm text-gray-600">Dermatologia</p>
</div>
<div className="text-right">
<p className="font-medium">Off-line</p>
<p className="text-sm text-gray-600">Visto as 8:33</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</ManagerLayout>
);
)
}

View File

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

View File

@ -1,256 +1,231 @@
"use client";
"use client"
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
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 { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout";
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
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 { Save, Loader2 } from "lucide-react"
import ManagerLayout from "@/components/manager-layout"
import { usersService } from "services/usersApi.mjs";
import { login } from "services/api.mjs";
interface UserFormData {
email: string;
nomeCompleto: string;
telefone: string;
papel: string;
senha: string;
confirmarSenha: string;
email: string;
password: string;
nomeCompleto: string;
telefone: string;
papel: string;
}
const defaultFormData: UserFormData = {
email: "",
nomeCompleto: "",
telefone: "",
papel: "",
senha: "",
confirmarSenha: "",
email: '',
password: '',
nomeCompleto: '',
telefone: '',
papel: '',
};
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
// Remove todos os caracteres não numéricos
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
// Definição do requisito mínimo de senha
const MIN_PASSWORD_LENGTH = 8;
const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length === 11)
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 10)
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
return cleaned;
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length === 11) {
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
}
if (cleaned.length === 10) {
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3');
}
return cleaned;
};
export default function NovoUsuarioPage() {
const router = useRouter();
const [formData, setFormData] = useState<UserFormData>(defaultFormData);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const [formData, setFormData] = useState<UserFormData>(defaultFormData);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleInputChange = (key: keyof UserFormData, value: string) => {
const updatedValue = key === "telefone" ? formatPhone(value) : value;
setFormData((prev) => ({ ...prev, [key]: updatedValue }));
};
const handleInputChange = (key: keyof UserFormData, value: string) => {
const updatedValue = key === 'telefone' ? formatPhone(value) : value;
setFormData((prev) => ({ ...prev, [key]: updatedValue }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Handles form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (
!formData.email ||
!formData.nomeCompleto ||
!formData.papel ||
!formData.senha ||
!formData.confirmarSenha
) {
setError("Por favor, preencha todos os campos obrigatórios.");
return;
}
// Basic validation
if (!formData.email || !formData.password || !formData.nomeCompleto || !formData.papel) {
setError("Por favor, preencha todos os campos obrigatórios.");
return;
}
if (formData.senha !== formData.confirmarSenha) {
setError("A Senha e a Confirmação de Senha não coincidem.");
return;
}
// Validação de comprimento mínimo da senha
if (formData.password.length < MIN_PASSWORD_LENGTH) {
setError(`A senha deve ter no mínimo ${MIN_PASSWORD_LENGTH} caracteres.`);
return;
}
setIsSaving(true);
setIsSaving(true);
try {
await login();
// ----------------------------------------------------------------------
// CORREÇÃO FINAL: Usa o formato de telefone que o mock API comprovadamente aceitou.
// ----------------------------------------------------------------------
const phoneValue = formData.telefone.trim();
const payload = {
full_name: formData.nomeCompleto,
email: formData.email.trim().toLowerCase(),
phone: formData.telefone || null,
role: formData.papel,
password: formData.senha,
};
// Prepara o payload com os campos obrigatórios
const payload: any = {
email: formData.email,
password: formData.password,
full_name: formData.nomeCompleto,
role: formData.papel,
};
console.log("📤 Enviando payload:", payload);
// Adiciona o telefone APENAS se estiver preenchido, enviando o formato FORMATADO.
if (phoneValue.length > 0) {
payload.phone = phoneValue;
}
// ----------------------------------------------------------------------
await usersService.create_user(payload);
try {
await usersService.create_user(payload);
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao criar usuário:", e);
// Melhorando a mensagem de erro para o usuário final
const apiErrorMsg = e.message?.includes("500")
? "Erro interno do servidor. Verifique os logs do backend ou tente novamente mais tarde. (Possível problema: E-mail já em uso ou falha de conexão.)"
: e.message || "Ocorreu um erro inesperado. Tente novamente.";
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao criar usuário:", e);
setError(
e?.message ||
"Não foi possível criar o usuário. Verifique os dados e tente novamente."
);
} finally {
setIsSaving(false);
}
};
setError(apiErrorMsg);
} finally {
setIsSaving(false);
}
};
return (
<ManagerLayout>
<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">
<div>
<h1 className="text-3xl font-extrabold text-gray-900">
Novo Usuário
</h1>
<p className="text-md text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
return (
<ManagerLayout>
<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>
<h1 className="text-2xl font-bold text-gray-900">Novo Usuário</h1>
<p className="text-sm text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
</div>
<Link href="/manager/usuario">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<form onSubmit={handleSubmit} className="space-y-8 bg-white p-8 border rounded-lg shadow-sm">
{/* Error Message Display */}
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
<p className="font-medium">Erro no Cadastro:</p>
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
placeholder="Nome e Sobrenome"
required
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha *</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
placeholder="••••••••"
required
minLength={MIN_PASSWORD_LENGTH} // Adiciona validação HTML
/>
{/* MENSAGEM DE AJUDA PARA SENHA */}
<p className="text-xs text-gray-500">Mínimo de {MIN_PASSWORD_LENGTH} caracteres.</p>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input
id="telefone"
value={formData.telefone}
onChange={(e) => handleInputChange("telefone", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label>
<Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)} required>
<SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="secretaria">Secretaria</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-4 pt-4">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Usuário"}
</Button>
</div>
</form>
</div>
<Link href="/manager/usuario">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<form
onSubmit={handleSubmit}
className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg"
>
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
<p className="font-semibold">Erro no Cadastro:</p>
<p className="text-sm break-words">{error}</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2 md:col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) =>
handleInputChange("nomeCompleto", e.target.value)
}
placeholder="Nome e Sobrenome"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label>
<Select
value={formData.papel}
onValueChange={(v) => handleInputChange("papel", v)}
required
>
<SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrador</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="senha">Senha *</Label>
<Input
id="senha"
type="password"
value={formData.senha}
onChange={(e) => handleInputChange("senha", e.target.value)}
placeholder="Mínimo 8 caracteres"
minLength={8}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label>
<Input
id="confirmarSenha"
type="password"
value={formData.confirmarSenha}
onChange={(e) =>
handleInputChange("confirmarSenha", e.target.value)
}
placeholder="Repita a senha"
required
/>
{formData.senha &&
formData.confirmarSenha &&
formData.senha !== formData.confirmarSenha && (
<p className="text-xs text-red-500">
As senhas não coincidem.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input
id="telefone"
value={formData.telefone}
onChange={(e) =>
handleInputChange("telefone", e.target.value)
}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
</div>
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Usuário"}
</Button>
</div>
</form>
</div>
</div>
</ManagerLayout>
);
</ManagerLayout>
);
}

View File

@ -1,20 +1,15 @@
// app/manager/usuario/page.tsx
"use client";
import React, { useEffect, useState, useCallback } 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Filter, Loader2 } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
@ -22,144 +17,173 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { api, login } from "services/api.mjs";
import { usersService } from "services/usersApi.mjs";
interface User {
user: {
id: string;
email: string;
email_confirmed_at?: string;
created_at?: string;
last_sign_in_at?: string;
};
profile: {
id?: string;
full_name?: string;
email?: string;
phone?: string | null;
avatar_url?: string;
disabled?: boolean;
created_at?: string;
updated_at?: string;
};
roles: string[];
permissions: {
isAdmin?: boolean;
isManager?: boolean;
isDoctor?: boolean;
isSecretary?: boolean;
isAdminOrManager?: boolean;
[key: string]: boolean | undefined;
};
}
interface FlatUser {
id: string;
user_id: string;
id: string;
user_id: string;
full_name?: string;
email: string;
phone?: string | null;
role: string;
}
interface UserInfoResponse {
user: any;
profile: any;
roles: string[];
permissions: Record<string, boolean>;
}
export default function UsersPage() {
const router = useRouter();
const [users, setUsers] = useState<FlatUser[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(null);
const [userDetails, setUserDetails] = useState<User | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [userToDeleteId, setUserToDeleteId] = useState<number | null>(null);
const [selectedRole, setSelectedRole] = useState<string>("");
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
// 1) pega roles
const rolesData: any[] = await usersService.list_roles();
// Garante que rolesData é array
const rolesArray = Array.isArray(rolesData) ? rolesData : [];
setLoading(true);
setError(null);
try {
const data = await usersService.list_roles(); // já retorna o JSON diretamente
console.log("Resposta da API list_roles:", data);
// 2) pega todos os profiles de uma vez (para evitar muitos requests)
const profilesData: any[] = await api.get(`/rest/v1/profiles?select=id,full_name,email,phone`);
const profilesById = new Map<string, any>();
if (Array.isArray(profilesData)) {
for (const p of profilesData) {
if (p?.id) profilesById.set(p.id, p);
}
}
if (Array.isArray(data)) {
const mappedUsers: FlatUser[] = data.map((item: any) => ({
id: item.id || (item.user_id ?? ""), // id da linha ou fallback
user_id: item.user_id || item.id || "", // garante que user_id exista
full_name: item.full_name || "—",
email: item.email || "—",
phone: item.phone ?? "—",
role: item.role || "—",
}));
// 3) mapear roles -> flat users, usando ID específico de cada item
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
const uid = roleItem.user_id;
const profile = profilesById.get(uid);
return {
id: uid,
user_id: uid,
full_name: profile?.full_name ?? "—",
email: profile?.email ?? "—",
phone: profile?.phone ?? "—",
role: roleItem.role ?? "—",
};
});
setUsers(mapped);
console.log("[fetchUsers] mapped count:", mapped.length);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Veja console.");
setUsers(mappedUsers);
} else {
console.warn("Formato inesperado recebido em list_roles:", data);
setUsers([]);
} finally {
setLoading(false);
}
}, []);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Tente novamente.");
setUsers([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const init = async () => {
try {
await login(); // garante token
} catch (e) {
console.warn("login falhou no init:", e);
}
await fetchUsers();
};
init();
fetchUsers();
}, [fetchUsers]);
const openDetailsDialog = async (flatUser: FlatUser) => {
setDetailsDialogOpen(true);
setUserDetails(null);
setDetailsDialogOpen(true);
setUserDetails(null);
try {
console.log("Buscando detalhes do user_id:", flatUser.user_id);
const fullUserData: User = await usersService.full_data(flatUser.user_id);
setUserDetails(fullUserData);
} catch (err: any) {
console.error("Erro ao buscar detalhes do usuário:", err);
setUserDetails({
user: {
id: flatUser.user_id,
email: flatUser.email || "",
created_at: "Erro ao Carregar",
last_sign_in_at: "Erro ao Carregar",
},
profile: {
full_name: flatUser.full_name || "Erro ao Carregar Detalhes",
phone: flatUser.phone || "—",
},
roles: [],
permissions: {},
} as any);
}
};
const filteredUsers = selectedRole && selectedRole !== "all"
? users.filter((u) => u.role === selectedRole)
: users;
try {
console.log("[openDetailsDialog] user_id:", flatUser.user_id);
const data = await usersService.full_data(flatUser.user_id);
console.log("[openDetailsDialog] full_data returned:", data);
setUserDetails(data);
} catch (err: any) {
console.error("Erro ao carregar detalhes:", err);
// fallback com dados já conhecidos
setUserDetails({
user: { id: flatUser.user_id, email: flatUser.email },
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
roles: [flatUser.role],
permissions: {},
});
}
};
const filteredUsers =
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users;
return (
<ManagerLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
<p className="text-sm text-gray-500">Gerencie usuários.</p>
<h1 className="text-2xl font-bold text-gray-900">Usuários Cadastrados</h1>
<p className="text-sm text-gray-500">Gerencie todos os usuários e seus papéis no sistema.</p>
</div>
<Link href="/manager/usuario/novo">
<Button className="bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" /> Novo Usuário
<Plus className="w-4 h-4 mr-2" /> Adicionar Novo
</Button>
</Link>
</div>
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
<Filter className="w-5 h-5 text-gray-400" />
<Select onValueChange={setSelectedRole} value={selectedRole}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar por papel" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar por Papel" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretaria</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
{loading ? (
<div className="p-8 text-center text-gray-500">
@ -170,7 +194,11 @@ export default function UsersPage() {
<div className="p-8 text-center text-red-600">{error}</div>
) : filteredUsers.length === 0 ? (
<div className="p-8 text-center text-gray-500">
Nenhum usuário encontrado.
Nenhum usuário encontrado.{" "}
<Link href="/manager/usuario/novo" className="text-green-600 hover:underline">
Adicione um novo
</Link>
.
</div>
) : (
<div className="overflow-x-auto">
@ -181,22 +209,31 @@ export default function UsersPage() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Papel</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm text-gray-500">{u.id}</td>
<td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.email}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td>
<td className="px-6 py-4 text-sm text-gray-500 capitalize">{u.role}</td>
{filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm text-gray-700">{user.id}</td>
<td className="px-6 py-4 text-sm text-gray-900">{user.full_name || "—"}</td>
<td className="px-6 py-4 text-sm text-gray-500">{user.email || "—"}</td>
<td className="px-6 py-4 text-sm text-gray-500">{user.phone || "—"}</td>
<td className="px-6 py-4 text-sm text-gray-500 capitalize">{user.role || "—"}</td>
<td className="px-6 py-4 text-right">
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar">
<Eye className="h-4 w-4" />
</Button>
<div className="flex justify-end space-x-1">
<Button
variant="outline"
size="icon"
onClick={() => openDetailsDialog(user)}
title="Visualizar"
>
<Eye className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
@ -206,31 +243,59 @@ export default function UsersPage() {
)}
</div>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
<AlertDialogTitle className="text-2xl">
{userDetails?.profile?.full_name || userDetails?.user?.email || "Detalhes do Usuário"}
</AlertDialogTitle>
<AlertDialogDescription>
{!userDetails ? (
<div className="p-4 text-center text-gray-500">
{!userDetails ? (
<div className="p-4 text-center text-gray-500">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />
Buscando dados completos...
</div>
) : (
<div className="space-y-3 pt-2 text-left text-gray-700">
<div><strong>ID:</strong> {userDetails.user.id}</div>
<div><strong>E-mail:</strong> {userDetails.user.email}</div>
<div><strong>Email confirmado em:</strong> {userDetails.user.email_confirmed_at || "—"}</div>
<div><strong>Último login:</strong> {userDetails.user.last_sign_in_at || "—"}</div>
<div><strong>Criado em:</strong> {userDetails.user.created_at || "—"}</div>
<div><strong>Nome completo:</strong> {userDetails.profile.full_name || "—"}</div>
<div><strong>Telefone:</strong> {userDetails.profile.phone || "—"}</div>
{userDetails.profile.avatar_url && (
<div><strong>Avatar:</strong> <img src={userDetails.profile.avatar_url} className="w-16 h-16 rounded-full mt-1" /></div>
)}
<div><strong>Conta desativada:</strong> {userDetails.profile.disabled ? "Sim" : "Não"}</div>
<div><strong>Profile criado em:</strong> {userDetails.profile.created_at || "—"}</div>
<div><strong>Profile atualizado em:</strong> {userDetails.profile.updated_at || "—"}</div>
<div>
<strong>Roles:</strong>
<ul className="list-disc list-inside">
{userDetails.roles.map((role, idx) => <li key={idx}>{role}</li>)}
</ul>
</div>
) : (
<div className="space-y-3 pt-2 text-left text-gray-700">
<div><strong>ID:</strong> {userDetails.user.id}</div>
<div><strong>E-mail:</strong> {userDetails.user.email}</div>
<div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div>
<div><strong>Telefone:</strong> {userDetails.profile.phone}</div>
<div><strong>Roles:</strong> {userDetails.roles?.join(", ")}</div>
<div>
<strong>Permissões:</strong>
<ul className="list-disc list-inside">
{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: {v ? "Sim" : "Não"}</li>)}
</ul>
</div>
<div>
<strong>Permissões:</strong>
<ul className="list-disc list-inside">
{Object.entries(userDetails.permissions).map(([key, value]) => (
<li key={key}>{key}: {value ? "Sim" : "Não"}</li>
))}
</ul>
</div>
)}
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@ -241,4 +306,4 @@ export default function UsersPage() {
</div>
</ManagerLayout>
);
}
}

View File

@ -10,14 +10,14 @@ export default function InicialPage() {
{}
<div className="bg-primary text-primary-foreground text-sm py-2 px-6 flex justify-between">
<span> Horário: 08h00 - 21h00</span>
<span> Email: contato@mediconnect.com</span>
<span> Email: contato@medconnect.com</span>
</div>
{}
<header className="bg-card shadow-md py-4 px-6 flex justify-between items-center">
<h1 className="text-2xl font-bold text-primary">MediConnect</h1>
<h1 className="text-2xl font-bold text-primary">MedConnect</h1>
<nav className="flex space-x-6 text-muted-foreground font-medium">
<a href="#home" className="hover:text-primary"><Link href="/cadastro">Home</Link></a>
<a href="#home" className="hover:text-primary">Home</a>
<a href="#about" className="hover:text-primary">Sobre</a>
<a href="#departments" className="hover:text-primary">Departamentos</a>
<a href="#doctors" className="hover:text-primary">Médicos</a>
@ -25,7 +25,7 @@ export default function InicialPage() {
</nav>
<div className="flex space-x-4">
{}
<Link href="/login">
<Link href="/cadastro">
<Button
variant="outline"
className="rounded-full px-6 py-2 border-2 transition cursor-pointer"
@ -105,7 +105,7 @@ export default function InicialPage() {
{}
<footer className="bg-primary text-primary-foreground py-6 text-center">
<p>© 2025 MediConnect</p>
<p>© 2025 MedConnect</p>
</footer>
</div>
);

View File

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

View File

@ -1,207 +1,41 @@
"use client";
import SecretaryLayout from "@/components/secretary-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 React, { useState, useEffect } from "react";
import { patientsService } from "@/services/patientsApi.mjs";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import SecretaryLayout from "@/components/secretary-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"
export default function SecretaryDashboard() {
// Estados
const [patients, setPatients] = useState<any[]>([]);
const [loadingPatients, setLoadingPatients] = useState(true);
const [firstConfirmed, setFirstConfirmed] = useState<any>(null);
const [nextAgendada, setNextAgendada] = useState<any>(null);
const [loadingAppointments, setLoadingAppointments] = useState(true);
// 🔹 Buscar pacientes
useEffect(() => {
async function fetchPatients() {
try {
const data = await patientsService.list();
if (Array.isArray(data)) {
setPatients(data.slice(0, 3));
}
} catch (error) {
console.error("Erro ao carregar pacientes:", error);
} finally {
setLoadingPatients(false);
}
}
fetchPatients();
}, []);
// 🔹 Buscar consultas (confirmadas + 1ª do mês)
useEffect(() => {
async function fetchAppointments() {
try {
const hoje = new Date();
const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1);
const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0);
// Mesmo parâmetro de ordenação da página /secretary/appointments
const queryParams = "order=scheduled_at.desc";
const data = await appointmentsService.search_appointment(queryParams);
if (!Array.isArray(data) || data.length === 0) {
setFirstConfirmed(null);
setNextAgendada(null);
return;
}
// 🩵 1⃣ Consultas confirmadas (para o card “Próxima Consulta Confirmada”)
const confirmadas = data.filter((apt: any) => {
const dataConsulta = new Date(apt.scheduled_at || apt.date);
return apt.status === "confirmed" && dataConsulta >= hoje;
});
confirmadas.sort(
(a: any, b: any) =>
new Date(a.scheduled_at || a.date).getTime() -
new Date(b.scheduled_at || b.date).getTime()
);
setFirstConfirmed(confirmadas[0] || null);
// 💙 2⃣ Consultas deste mês — pegar sempre a 1ª (mais próxima)
const consultasMes = data.filter((apt: any) => {
const dataConsulta = new Date(apt.scheduled_at);
return dataConsulta >= inicioMes && dataConsulta <= fimMes;
});
if (consultasMes.length > 0) {
consultasMes.sort(
(a: any, b: any) =>
new Date(a.scheduled_at).getTime() -
new Date(b.scheduled_at).getTime()
);
setNextAgendada(consultasMes[0]);
} else {
setNextAgendada(null);
}
} catch (error) {
console.error("Erro ao carregar consultas:", error);
} finally {
setLoadingAppointments(false);
}
}
fetchAppointments();
}, []);
return (
<SecretaryLayout>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
{/* Cards principais */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Próxima Consulta Confirmada */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Próxima Consulta Confirmada
</CardTitle>
<CardTitle className="text-sm font-medium">Próxima Consulta</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingAppointments ? (
<div className="text-gray-500 text-sm">
Carregando próxima consulta...
</div>
) : firstConfirmed ? (
<>
<div className="text-2xl font-bold">
{new Date(
firstConfirmed.scheduled_at || firstConfirmed.date
).toLocaleDateString("pt-BR")}
</div>
<p className="text-xs text-muted-foreground">
{firstConfirmed.doctor_name
? `Dr(a). ${firstConfirmed.doctor_name}`
: "Médico não informado"}{" "}
-{" "}
{new Date(
firstConfirmed.scheduled_at
).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
})}
</p>
</>
) : (
<div className="text-sm text-gray-500">
Nenhuma consulta confirmada encontrada
</div>
)}
<div className="text-2xl font-bold">15 Jan</div>
<p className="text-xs text-muted-foreground">Dr. Silva - 14:30</p>
</CardContent>
</Card>
{/* Consultas Este Mês */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Consultas Este Mês
</CardTitle>
<CardTitle className="text-sm font-medium">Consultas Este Mês</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingAppointments ? (
<div className="text-gray-500 text-sm">
Carregando consultas...
</div>
) : nextAgendada ? (
<>
<div className="text-lg font-bold text-gray-900">
{new Date(
nextAgendada.scheduled_at
).toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}{" "}
às{" "}
{new Date(
nextAgendada.scheduled_at
).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
})}
</div>
<p className="text-xs text-muted-foreground">
{nextAgendada.doctor_name
? `Dr(a). ${nextAgendada.doctor_name}`
: "Médico não informado"}
</p>
<p className="text-xs text-muted-foreground">
{nextAgendada.patient_name
? `Paciente: ${nextAgendada.patient_name}`
: ""}
</p>
</>
) : (
<div className="text-sm text-gray-500">
Nenhuma consulta agendada neste mês
</div>
)}
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground">2 realizadas, 1 agendada</p>
</CardContent>
</Card>
{/* Perfil */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
@ -214,15 +48,11 @@ export default function SecretaryDashboard() {
</Card>
</div>
{/* Cards Secundários */}
<div className="grid md:grid-cols-2 gap-6">
{/* Ações rápidas */}
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>
Acesse rapidamente as principais funcionalidades
</CardDescription>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/secretary/schedule">
@ -232,73 +62,52 @@ export default function SecretaryDashboard() {
</Button>
</Link>
<Link href="/secretary/appointments">
<Button
variant="outline"
className="w-full justify-start bg-transparent"
>
<Button variant="outline" className="w-full justify-start bg-transparent">
<Calendar className="mr-2 h-4 w-4" />
Ver Consultas
</Button>
</Link>
<Link href="/secretary/pacientes">
<Button
variant="outline"
className="w-full justify-start bg-transparent"
>
<Link href="##">
<Button variant="outline" className="w-full justify-start bg-transparent">
<User className="mr-2 h-4 w-4" />
Gerenciar Pacientes
Atualizar Dados
</Button>
</Link>
</CardContent>
</Card>
{/* Pacientes */}
<Card>
<CardHeader>
<CardTitle>Pacientes</CardTitle>
<CardDescription>
Últimos pacientes cadastrados
</CardDescription>
<CardTitle>Próximas Consultas</CardTitle>
<CardDescription>Suas consultas agendadas</CardDescription>
</CardHeader>
<CardContent>
{loadingPatients ? (
<p className="text-sm text-gray-500">
Carregando pacientes...
</p>
) : patients.length === 0 ? (
<p className="text-sm text-gray-500">
Nenhum paciente cadastrado.
</p>
) : (
<div className="space-y-4">
{patients.map((patient, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-100"
>
<div>
<p className="font-medium text-gray-900">
{patient.full_name || "Sem nome"}
</p>
<p className="text-sm text-gray-600">
{patient.phone_mobile ||
patient.phone1 ||
"Sem telefone"}
</p>
</div>
<div className="text-right">
<p className="font-medium text-blue-700">
{patient.convenio || "Particular"}
</p>
</div>
</div>
))}
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium">Dr. Silva</p>
<p className="text-sm text-gray-600">Cardiologia</p>
</div>
<div className="text-right">
<p className="font-medium">15 Jan</p>
<p className="text-sm text-gray-600">14:30</p>
</div>
</div>
)}
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">Dra. Santos</p>
<p className="text-sm text-gray-600">Dermatologia</p>
</div>
<div className="text-right">
<p className="font-medium">22 Jan</p>
<p className="text-sm text-gray-600">10:00</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</SecretaryLayout>
);
)
}

View File

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

View File

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

View File

@ -4,8 +4,7 @@ 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"; // Manteremos para o logout, se necessário
import { api } from '@/services/api.mjs';
import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -40,20 +39,23 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
const router = useRouter();
const pathname = usePathname();
// ==================================================================
// 2. BLOCO DE SEGURANÇA CORRIGIDO
// ==================================================================
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
// --- ALTERAÇÃO PRINCIPAL AQUI ---
// Procurando o token no localStorage, onde ele foi realmente salvo.
const token = localStorage.getItem("token");
const token = Cookies.get("access_token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
// 3. "TRADUZIMOS" os dados da API para o formato que o layout espera
setDoctorData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Doutor(a)",
email: userInfo.email || "",
specialty: userInfo.user_metadata?.specialty || "Especialidade",
// Campos que não vêm do login, definidos como vazios para não quebrar
phone: userInfo.phone || "",
cpf: "",
crm: "",
@ -61,49 +63,35 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
permissions: {},
});
} else {
// Se não encontrar, aí sim redireciona.
router.push("/login");
// Se faltar o token ou os dados, volta para o login
router.push("/doctor/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);
}, []);
const handleResize = () => setWindowWidth(window.innerWidth);
handleResize(); // inicializa com a largura atual
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (isMobile) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [isMobile]);
useEffect(() => {
if (isMobile) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [isMobile]);
const handleLogout = () => {
setShowLogoutDialog(true);
};
// --- 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
const confirmLogout = () => {
localStorage.removeItem("doctorData");
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a home
}
};
router.push("/");
};
const cancelLogout = () => {
setShowLogoutDialog(false);
@ -114,10 +102,30 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
};
const menuItems = [
{ href: "#", icon: Home, label: "Dashboard" },
{ href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
{ href: "#", icon: Clock, label: "Editor de Laudo" },
{ href: "/doctor/medicos", icon: User, label: "Pacientes" },
{
href: "#",
icon: Home,
label: "Dashboard",
// Botão para o dashboard do médico
},
{
href: "/doctor/medicos/consultas",
icon: Calendar,
label: "Consultas",
// Botão para página de consultas marcadas do médico atual
},
{
href: "#",
icon: Clock,
label: "Editor de Laudo",
// Botão para página do editor de laudo
},
{
href: "/doctor/medicos",
icon: User,
label: "Pacientes",
// Botão para a página de visualização de todos os pacientes
},
];
if (!doctorData) {
@ -125,17 +133,17 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
}
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="min-h-screen bg-gray-50 flex">
{/* 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>
<span className="font-semibold text-gray-900">MidConnecta</span>
</div>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
@ -171,7 +179,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
<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>
<span className="font-semibold text-gray-900">MedConnect</span>
</div>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
@ -198,6 +206,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */}
{!sidebarCollapsed && (
<>
<Avatar>
@ -216,7 +225,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
</>
)}
{sidebarCollapsed && (
<Avatar className="mx-auto">
<Avatar className="mx-auto"> {/* Centraliza o avatar quando recolhido */}
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
@ -228,6 +237,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
)}
</div>
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`}
onClick={handleLogout}
@ -261,7 +271,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}>
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */}
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
<span className="font-medium">{item.label}</span>
@ -287,14 +297,17 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
</div>
</div>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */}
<LogOut className="mr-2 h-4 w-4" />
Sair
</Button>
</div>
</div>
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Main Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Header */}
<header className="bg-card border-b border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
@ -313,9 +326,11 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>

View File

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

View File

@ -123,7 +123,7 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) {
<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>
<span className="font-semibold text-foreground">MedConnect</span>
</div>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">

View File

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

View File

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

View File

@ -1,4 +1,3 @@
// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo)
"use client"
import type React from "react"
@ -6,14 +5,30 @@ import { useState, useEffect } from "react"
import { useRouter, usePathname } from "next/navigation"
import Link from "next/link"
import Cookies from "js-cookie";
import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Search,
Bell,
Calendar,
Clock,
User,
LogOut,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react"
interface SecretaryData {
id: string
@ -31,36 +46,12 @@ interface SecretaryLayoutProps {
}
export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
const [secretaryData, setSecretaryData] = useState<SecretaryData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter()
const pathname = usePathname()
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]);
// 🔹 Colapsar no mobile e expandir no desktop automaticamente
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
@ -75,25 +66,10 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
}, [])
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
}
};
const confirmLogout = () => {
setShowLogoutDialog(false)
router.push("/")
}
const cancelLogout = () => setShowLogoutDialog(false)
const menuItems = [
@ -103,11 +79,17 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" },
]
if (!secretaryData) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
const secretaryData: SecretaryData = {
id: "1",
name: "Secretária Exemplo",
email: "secretaria@hospital.com",
phone: "999999999",
cpf: "000.000.000-00",
employeeId: "12345",
department: "Atendimento",
permissions: {},
}
return (
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
@ -123,7 +105,7 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
<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>
<span className="font-semibold text-foreground">MedConnect</span>
</div>
)}
<Button
@ -183,20 +165,23 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2"
? "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"}
/>
{!sidebarCollapsed && "Sair"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
</Button>
</div>
</div>
@ -206,6 +191,7 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
{/* 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">
@ -219,6 +205,13 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
<div className="flex items-center gap-4">
{/* Este botão no header parece ter sido uma cópia do botão "Sair" da sidebar.
Removi a lógica de sidebarCollapsed aqui, pois o header é independente.
Se a intenção era ter um botão de logout no header, ele não deve ser afetado pela sidebar.
Ajustei para ser um botão de sino de notificação, como nos exemplos anteriores,
que você tem o ícone Bell importado e uma badge para notificação.
Se você quer um botão de LogOut aqui, por favor, me avise!
*/}
<Button variant="ghost" size="sm" className="relative">
<Bell className="w-5 h-5" />
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-destructive text-destructive-foreground text-xs">
@ -229,6 +222,7 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>

10
package-lock.json generated
View File

@ -52,7 +52,7 @@
"input-otp": "1.4.1",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.545.0",
"lucide-react": "^0.454.0",
"next": "^14.2.33",
"next-themes": "^0.4.6",
"react": "^18.3.1",
@ -3381,12 +3381,12 @@
}
},
"node_modules/lucide-react": {
"version": "0.545.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
"version": "0.454.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
"integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/magic-string": {

View File

@ -53,7 +53,7 @@
"input-otp": "1.4.1",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.545.0",
"lucide-react": "^0.454.0",
"next": "^14.2.33",
"next-themes": "^0.4.6",
"react": "^18.3.1",

View File

@ -1,181 +1,86 @@
// Caminho: [seu-caminho]/services/api.mjs
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const apikey = API_KEY;
var tempToken;
export async function loginWithEmailAndPassword(email, password) {
const response = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
export async function login() {
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": API_KEY,
Prefer: "return=representation",
apikey: API_KEY, // valor fixo
},
body: JSON.stringify({ email, password }),
body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error_description || "Credenciais inválidas.");
}
if (data.access_token && typeof window !== 'undefined') {
// Padronizando para salvar o token no localStorage
if (typeof window !== 'undefined') {
localStorage.setItem("token", data.access_token);
}
return data;
}
// --- NOVA FUNÇÃO DE LOGOUT CENTRALIZADA ---
async function logout() {
const token = localStorage.getItem("token");
if (!token) return; // Se não há token, não há o que fazer
try {
await fetch(`${BASE_URL}/auth/v1/logout`, {
method: "POST",
headers: {
"apikey": API_KEY,
"Authorization": `Bearer ${token}`,
},
});
} catch (error) {
// Mesmo que a chamada falhe, o logout no cliente deve continuar.
// O token pode já ter expirado no servidor, por exemplo.
console.error("Falha ao invalidar token no servidor (isso pode ser normal se o token já expirou):", error);
}
}
let loginPromise = login();
async function request(endpoint, options = {}) {
if (loginPromise) {
try {
await loginPromise;
} catch (error) {
console.error("Falha na autenticação inicial:", error);
}
loginPromise = null;
}
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
const headers = {
"Content-Type": "application/json",
"apikey": API_KEY,
...(token ? { "Authorization": `Bearer ${token}` } : {}),
apikey: API_KEY,
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
};
const API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const apikey = API_KEY;
let loginPromise = null;
export async function login() {
console.log("🔐 Iniciando login...");
const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: API_KEY,
Prefer: "return=representation",
},
body: JSON.stringify({
email: "riseup@popcode.com.br",
password: "riseup",
}),
});
if (!res.ok) {
const msg = await res.text();
console.error("❌ Erro no login:", res.status, msg);
throw new Error(`Erro ao autenticar: ${res.status} - ${msg}`);
}
const data = await res.json();
console.log("✅ Login bem-sucedido:", data);
if (typeof window !== "undefined" && data.access_token) {
localStorage.setItem("token", data.access_token);
}
return data;
}
async function request(endpoint, options = {}) {
if (!loginPromise) loginPromise = login();
try {
await loginPromise;
} catch (error) {
console.error("⚠️ Falha ao autenticar:", error);
} finally {
loginPromise = null;
}
let token =
typeof window !== "undefined" ? localStorage.getItem("token") : null;
if (!token) {
console.warn("⚠️ Token não encontrado, refazendo login...");
const data = await login();
token = data.access_token;
}
const headers = {
"Content-Type": "application/json",
apikey: API_KEY,
Authorization: `Bearer ${token}`,
...options.headers,
};
const fullUrl =
endpoint.startsWith("/rest/v1") || endpoint.startsWith("/functions/")
? `${BASE_URL}${endpoint}`
: `${BASE_URL}/rest/v1${endpoint}`;
console.log("🌐 Requisição para:", fullUrl, "com headers:", headers);
const response = await fetch(fullUrl, {
...options,
headers,
});
try {
const response = await fetch(`${BASE_URL}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
let errorBody;
let errorBody = `Status: ${response.status}`;
try {
errorBody = await response.json();
} catch (e) {
errorBody = await response.text();
}
throw new Error(`Erro HTTP: ${response.status} - ${JSON.stringify(errorBody)}`);
}
if (response.status === 204) return {};
return await response.json();
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const jsonError = await response.json();
errorBody = jsonError.message || JSON.stringify(jsonError);
} else {
errorBody = await response.text();
}
} catch (e) {
errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`;
}
throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${errorBody}`);
}
const contentType = response.headers.get("content-type");
if (response.status === 204 || (contentType && !contentType.includes("application/json")) || !contentType) {
return {};
}
return await response.json();
} catch (error) {
console.error("Erro na requisição:", error);
throw error;
}
}
// Adicionamos a função de logout ao nosso objeto de API exportado
export const api = {
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }),
patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }),
delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }),
logout: logout, // <-- EXPORTANDO A NOVA FUNÇÃO
post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }),
patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }),
delete: (endpoint) => request(endpoint, { method: "DELETE" }),
};
if (!response.ok) {
const msg = await response.text();
console.error("❌ Erro HTTP:", response.status, msg);
throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${msg}`);
}
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) return {};
return await response.json();
}
export const api = {
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data) =>
request(endpoint, { method: "POST", body: JSON.stringify(data) }),
patch: (endpoint, data) =>
request(endpoint, { method: "PATCH", body: JSON.stringify(data) }),
delete: (endpoint) => request(endpoint, { method: "DELETE" }),
};

View File

@ -1,55 +1,10 @@
import { api } from "./api.mjs";
export const usersService = {
async list_roles() {
// continua usando /rest/v1 normalmente
return await api.get(`/rest/v1/user_roles?select=id,user_id,role,created_at`);
create_user: (data) => api.post(`/functions/v1/create-user`),
list_roles: () => api.get(`/rest/v1/user_roles`),
full_data: (id) => {
const endpoint = `/functions/v1/user-info?user_id=${id}`;
return api.get(endpoint);
},
async create_user(data) {
// continua usando a Edge Function corretamente
return await api.post(`/functions/v1/user-create`, data);
},
// 🚀 Busca dados completos do usuário direto do banco
async full_data(user_id) {
if (!user_id) throw new Error("user_id é obrigatório");
// Busca o perfil
const [profile] = await api.get(`/rest/v1/profiles?id=eq.${user_id}`);
// Busca o papel (role)
const [role] = await api.get(`/rest/v1/user_roles?user_id=eq.${user_id}`);
// Busca as permissões se existirem em alguma tabela
const permissions = {
isAdmin: role?.role === "admin",
isManager: role?.role === "gestor",
isDoctor: role?.role === "medico",
isSecretary: role?.role === "secretaria",
isAdminOrManager:
role?.role === "admin" || role?.role === "gestor" ? true : false,
};
// Monta o objeto no mesmo formato do endpoint `user-info`
return {
user: {
id: user_id,
email: profile?.email ?? "—",
email_confirmed_at: null,
created_at: profile?.created_at ?? "—",
last_sign_in_at: null,
},
profile: {
id: profile?.id ?? user_id,
full_name: profile?.full_name ?? "—",
email: profile?.email ?? "—",
phone: profile?.phone ?? "—",
avatar_url: profile?.avatar_url ?? null,
disabled: profile?.disabled ?? false,
created_at: profile?.created_at ?? null,
updated_at: profile?.updated_at ?? null,
},
roles: [role?.role ?? "—"],
permissions,
};
},
};
summary_data: () => api.get(`/auth/v1/user`)
}