2025-10-22 20:55:23 -03:00

246 lines
11 KiB
TypeScript

"use client";
import React, { useEffect, useState, useCallback } from "react";
// REMOVIDO: import ManagerLayout, pois a página já é envolvida pelo layout pai.
import Link from "next/link";
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 { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
// Assumindo caminhos de importação do seu projeto
import { api } from "services/api";
import { usuariosApi } from "@/services/usuariosApi";
import { perfisApi } from "@/services/perfisApi";
import { UserRole } from "@/services/usuariosApi";
interface FlatUser {
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 [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 [selectedRole, setSelectedRole] = useState<string>("");
// Estado para armazenar papéis disponíveis dinamicamente
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
// Função utilitária para capitalizar a primeira letra
const capitalize = (s: string) => {
if (!s) return s;
return s.charAt(0).toUpperCase() + s.slice(1);
};
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
// 1) Pega papéis e perfis em paralelo para melhor performance
const [rolesData, profilesData] = await Promise.all([
usuariosApi.listRoles(),
perfisApi.list()
]);
const rolesArray: UserRole[] = Array.isArray(rolesData) ? rolesData : [];
// 2) Extrair e salvar papéis únicos para o filtro (NOVO)
const uniqueRoles = new Set<string>();
rolesArray.forEach(roleItem => {
// Usa roleItem.role, se existir
if (roleItem.role) {
uniqueRoles.add(roleItem.role);
}
});
// Converter para array, ordenar e atualizar o estado
setAvailableRoles(Array.from(uniqueRoles).sort());
const profilesById = new Map<string, any>();
if (Array.isArray(profilesData)) {
for (const p of profilesData) {
// A chave do perfil deve ser o user_id, conforme a lógica anterior
if (p?.id) profilesById.set(p.id, p);
}
}
// 3) Mapear roles -> flat users
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
const uid = roleItem.user_id;
const profile = profilesById.get(uid);
// Determina o role a ser usado. Prioriza roleItem.role, se não, '—'
const role = roleItem.role ?? "—";
return {
id: uid,
user_id: uid,
full_name: profile?.full_name ?? "—",
email: profile?.email ?? "—",
phone: profile?.phone ?? "—",
role: role,
};
});
setUsers(mapped);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Verifique o console.");
setUsers([]);
setAvailableRoles([]); // Limpa os papéis em caso de erro
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const init = async () => {
// Garante que o interceptor da API seja executado para ler o cookie
try {
await api.get('/');
} catch (e) {
console.warn("API setup (leitura de cookie via interceptor) falhou ou o endpoint raiz não existe:", e);
}
await fetchUsers();
};
init();
}, [fetchUsers]);
const openDetailsDialog = async (flatUser: FlatUser) => {
setDetailsDialogOpen(true);
setUserDetails(null);
try {
// O getFullData usa user_id
const data = await usuariosApi.getFullData(flatUser.user_id);
setUserDetails(data);
} catch (err: any) {
console.error("Erro ao carregar detalhes:", err);
// Fallback details em caso de falha na API
setUserDetails({
user: { id: flatUser.user_id, email: flatUser.email },
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
roles: [flatUser.role],
permissions: { "read:self": true, "write:profile": false },
});
}
};
const filteredUsers =
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users;
// REMOVIDO: mockUserProfile e mockMenuItems não são mais necessários
// pois o ManagerLayout deve ser fornecido pelo arquivo app/manager/layout.tsx
return (
// CORRIGIDO: Retornando apenas o conteúdo da página, sem o ManagerLayout
<div className="space-y-6">
{/* Conteúdo da página */}
<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>
</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
</Button>
</Link>
</div>
{/* Filtros */}
<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>
{availableRoles.map(role => (
<SelectItem key={role} value={role}>
{capitalize(role)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Tabela de Usuários */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
{loading
? <div className="p-8 text-center text-gray-500"><Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />Carregando usuários...</div>
: error
? <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{selectedRole && selectedRole !== 'all' ? ` para o papel: ${capitalize(selectedRole)}` : '.'}</div>
: <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<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-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">{capitalize(u.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>
</td>
</tr>
))}
</tbody>
</table>
</div>}
</div>
{/* Diálogo de Detalhes */}
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
<AlertDialogDescription>
{!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>Nome completo:</strong> {userDetails.profile.full_name}</div>
<div><strong>Telefone:</strong> {userDetails.profile.phone}</div>
<div><strong>Roles:</strong> {userDetails.roles?.map(capitalize).join(", ")}</div>
<div><strong>Permissões:</strong>
<ul className="list-disc list-inside mt-1 ml-4 text-sm">
{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: <strong className={`${v ? 'text-green-600' : 'text-red-600'}`}>{v ? "Sim" : "Não"}</strong></li>)}
</ul>
</div>
</div>}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter><AlertDialogCancel>Fechar</AlertDialogCancel></AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}