Files
hefesto/frontend/src/pages/Usuarios.tsx
bigtux d8ca580acb HEFESTO v1.0 - Sistema de Controle Orçamentário para Facilities
- Backend NestJS com 12 módulos
- Frontend React com dashboard e gestão
- Manuais técnico e de negócios (MD + PDF)
- Workflow de aprovação com alçadas
- RBAC com 6 perfis de acesso
2026-02-09 14:53:01 -03:00

358 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react'
import {
Users,
Search,
Plus,
Edit2,
Trash2,
Shield,
X,
Loader2,
Mail,
User,
CheckCircle2,
XCircle,
Key
} from 'lucide-react'
import api from '../services/api'
interface Usuario {
id: number;
nome: string;
email: string;
perfil: string;
perfil_id: number;
ativo?: boolean;
ultimo_acesso?: string;
}
const perfilConfig: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
'admin': { label: 'Administrador', color: '#E65100', icon: <Shield className="w-4 h-4" /> },
'administrador': { label: 'Administrador', color: '#E65100', icon: <Shield className="w-4 h-4" /> },
'gestor': { label: 'Gestor', color: '#1A237E', icon: <Users className="w-4 h-4" /> },
'financeiro': { label: 'Financeiro', color: '#2E7D32', icon: <Key className="w-4 h-4" /> },
'solicitante': { label: 'Solicitante', color: '#7B1FA2', icon: <User className="w-4 h-4" /> },
}
const mockUsuarios: Usuario[] = [
{ id: 1, nome: 'Admin Sistema', email: 'admin@hefesto.com', perfil: 'Administrador', perfil_id: 1, ativo: true, ultimo_acesso: '2024-01-16 14:30' },
{ id: 2, nome: 'João Santos', email: 'joao.santos@hefesto.com', perfil: 'Gestor', perfil_id: 2, ativo: true, ultimo_acesso: '2024-01-16 10:15' },
{ id: 3, nome: 'Ana Oliveira', email: 'ana.oliveira@hefesto.com', perfil: 'Financeiro', perfil_id: 3, ativo: true, ultimo_acesso: '2024-01-15 16:45' },
{ id: 4, nome: 'Maria Silva', email: 'maria.silva@hefesto.com', perfil: 'Solicitante', perfil_id: 4, ativo: true, ultimo_acesso: '2024-01-16 09:00' },
{ id: 5, nome: 'Carlos Lima', email: 'carlos.lima@hefesto.com', perfil: 'Solicitante', perfil_id: 4, ativo: false, ultimo_acesso: '2024-01-10 11:30' },
]
export default function Usuarios() {
const [loading, setLoading] = useState(true)
const [usuarios, setUsuarios] = useState<Usuario[]>([])
const [searchTerm, setSearchTerm] = useState('')
const [filterPerfil, setFilterPerfil] = useState('todos')
const [showModal, setShowModal] = useState(false)
const [selectedUser, setSelectedUser] = useState<Usuario | null>(null)
const [formData, setFormData] = useState({ nome: '', email: '', perfil_id: 4, senha: '' })
useEffect(() => {
// Simulate loading
setTimeout(() => {
setUsuarios(mockUsuarios)
setLoading(false)
}, 500)
}, [])
const filteredUsuarios = usuarios.filter(user => {
const matchesSearch = user.nome.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
const matchesPerfil = filterPerfil === 'todos' || user.perfil.toLowerCase() === filterPerfil
return matchesSearch && matchesPerfil
})
const handleOpenModal = (user?: Usuario) => {
if (user) {
setSelectedUser(user)
setFormData({ nome: user.nome, email: user.email, perfil_id: user.perfil_id, senha: '' })
} else {
setSelectedUser(null)
setFormData({ nome: '', email: '', perfil_id: 4, senha: '' })
}
setShowModal(true)
}
const handleCloseModal = () => {
setShowModal(false)
setSelectedUser(null)
setFormData({ nome: '', email: '', perfil_id: 4, senha: '' })
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Mock save
if (selectedUser) {
setUsuarios(usuarios.map(u => u.id === selectedUser.id ? { ...u, ...formData, perfil: getPerfilLabel(formData.perfil_id) } : u))
} else {
const newUser: Usuario = {
id: Date.now(),
...formData,
perfil: getPerfilLabel(formData.perfil_id),
ativo: true,
ultimo_acesso: 'Nunca'
}
setUsuarios([...usuarios, newUser])
}
handleCloseModal()
}
const getPerfilLabel = (id: number) => {
const perfis: Record<number, string> = { 1: 'Administrador', 2: 'Gestor', 3: 'Financeiro', 4: 'Solicitante' }
return perfis[id] || 'Solicitante'
}
const getPerfilConfig = (perfil: string) => {
const key = perfil.toLowerCase()
return perfilConfig[key] || { label: perfil, color: '#757575', icon: <User className="w-4 h-4" /> }
}
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
)
}
return (
<div className="space-y-6 animate-fade-in">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-text">Usuários</h1>
<p className="text-gray mt-1">Gerencie os usuários do sistema</p>
</div>
<button
onClick={() => handleOpenModal()}
className="btn-primary flex items-center gap-2 w-full sm:w-auto justify-center"
>
<Plus className="w-5 h-5" />
Novo Usuário
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="card">
<p className="text-gray text-sm">Total</p>
<p className="text-2xl font-bold text-text">{usuarios.length}</p>
</div>
<div className="card">
<p className="text-gray text-sm">Ativos</p>
<p className="text-2xl font-bold text-green-600">{usuarios.filter(u => u.ativo !== false).length}</p>
</div>
<div className="card">
<p className="text-gray text-sm">Admins</p>
<p className="text-2xl font-bold text-primary">{usuarios.filter(u => u.perfil.toLowerCase().includes('admin')).length}</p>
</div>
<div className="card">
<p className="text-gray text-sm">Inativos</p>
<p className="text-2xl font-bold text-gray">{usuarios.filter(u => u.ativo === false).length}</p>
</div>
</div>
{/* Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-light" />
<input
type="text"
placeholder="Buscar por nome ou e-mail..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input-field pl-12"
/>
</div>
<div className="flex gap-2 flex-wrap">
{['todos', 'administrador', 'gestor', 'financeiro', 'solicitante'].map((filter) => (
<button
key={filter}
onClick={() => setFilterPerfil(filter)}
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
filterPerfil === filter
? 'bg-primary text-white'
: 'bg-gray-100 text-gray hover:bg-gray-200'
}`}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
</div>
</div>
{/* Table */}
<div className="card !p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="table-header">
<tr>
<th className="table-cell">Usuário</th>
<th className="table-cell">E-mail</th>
<th className="table-cell">Perfil</th>
<th className="table-cell">Último Acesso</th>
<th className="table-cell text-center">Status</th>
<th className="table-cell text-center">Ações</th>
</tr>
</thead>
<tbody>
{filteredUsuarios.map((user) => {
const config = getPerfilConfig(user.perfil)
return (
<tr key={user.id} className="table-row">
<td className="table-cell">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-white font-semibold"
style={{ backgroundColor: config.color }}
>
{user.nome.charAt(0).toUpperCase()}
</div>
<span className="font-medium">{user.nome}</span>
</div>
</td>
<td className="table-cell">
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-gray" />
<span>{user.email}</span>
</div>
</td>
<td className="table-cell">
<span
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold"
style={{ backgroundColor: `${config.color}15`, color: config.color }}
>
{config.icon}
{config.label}
</span>
</td>
<td className="table-cell text-gray text-sm">
{user.ultimo_acesso}
</td>
<td className="table-cell text-center">
{user.ativo !== false ? (
<span className="badge-success inline-flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" />
Ativo
</span>
) : (
<span className="badge-neutral inline-flex items-center gap-1">
<XCircle className="w-3 h-3" />
Inativo
</span>
)}
</td>
<td className="table-cell">
<div className="flex items-center justify-center gap-1">
<button
onClick={() => handleOpenModal(user)}
className="p-2 rounded-lg hover:bg-gray-100 text-gray hover:text-primary transition-colors"
>
<Edit2 className="w-4 h-4" />
</button>
<button className="p-2 rounded-lg hover:bg-red-50 text-gray hover:text-red-500 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{filteredUsuarios.length === 0 && (
<div className="text-center py-12">
<Users className="w-12 h-12 text-gray-light mx-auto mb-4" />
<p className="text-gray">Nenhum usuário encontrado</p>
</div>
)}
</div>
{/* Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl animate-fade-in">
<div className="flex items-center justify-between p-6 border-b border-border">
<h2 className="text-xl font-semibold text-text">
{selectedUser ? 'Editar Usuário' : 'Novo Usuário'}
</h2>
<button
onClick={handleCloseModal}
className="p-2 rounded-lg hover:bg-gray-100 text-gray"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-text mb-2">Nome</label>
<input
type="text"
value={formData.nome}
onChange={(e) => setFormData({ ...formData, nome: e.target.value })}
className="input-field"
placeholder="Nome completo"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-text mb-2">E-mail</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="input-field"
placeholder="email@exemplo.com"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-text mb-2">Perfil</label>
<select
value={formData.perfil_id}
onChange={(e) => setFormData({ ...formData, perfil_id: Number(e.target.value) })}
className="input-field"
>
<option value={1}>Administrador</option>
<option value={2}>Gestor</option>
<option value={3}>Financeiro</option>
<option value={4}>Solicitante</option>
</select>
</div>
{!selectedUser && (
<div>
<label className="block text-sm font-medium text-text mb-2">Senha</label>
<input
type="password"
value={formData.senha}
onChange={(e) => setFormData({ ...formData, senha: e.target.value })}
className="input-field"
placeholder="••••••"
required={!selectedUser}
/>
</div>
)}
<div className="flex gap-3 pt-4">
<button type="button" onClick={handleCloseModal} className="btn-ghost flex-1">
Cancelar
</button>
<button type="submit" className="btn-primary flex-1">
{selectedUser ? 'Salvar' : 'Criar Usuário'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}