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
This commit is contained in:
357
frontend/src/pages/Usuarios.tsx
Normal file
357
frontend/src/pages/Usuarios.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user