Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
310 lines
15 KiB
TypeScript
310 lines
15 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { AppUser, ViewState } from '../types';
|
|
import {
|
|
Plus, Search, Shield, ShieldAlert, CheckCircle2,
|
|
XCircle, Edit2, Trash2, X, Save, User as UserIcon, Lock
|
|
} from 'lucide-react';
|
|
|
|
interface UserManagementViewProps {
|
|
users: AppUser[];
|
|
setUsers: (users: AppUser[]) => void;
|
|
availableModules: { id: ViewState; label: string }[];
|
|
currentUser: AppUser;
|
|
}
|
|
|
|
const ToggleSwitch: React.FC<{ checked: boolean, onChange: (val: boolean) => void, label: string }> = ({ checked, onChange, label }) => (
|
|
<div className="flex items-center justify-between py-3 border-b border-slate-50 last:border-0">
|
|
<span className="text-sm font-medium text-slate-700">{label}</span>
|
|
<button
|
|
onClick={() => onChange(!checked)}
|
|
className={`relative w-11 h-6 rounded-full transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
checked ? 'bg-primary-500' : 'bg-slate-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block w-4 h-4 transform bg-white rounded-full shadow transition-transform duration-200 ease-in-out mt-1 ml-1 ${
|
|
checked ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
export const UserManagementView: React.FC<UserManagementViewProps> = ({ users, setUsers, availableModules, currentUser }) => {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingUser, setEditingUser] = useState<Partial<AppUser>>({
|
|
role: 'user',
|
|
active: true,
|
|
permissions: []
|
|
});
|
|
|
|
const handleSaveUser = () => {
|
|
if (!editingUser.name || !editingUser.email) return;
|
|
|
|
if (editingUser.id) {
|
|
// Editar existente
|
|
setUsers(users.map(u => u.id === editingUser.id ? { ...u, ...editingUser } as AppUser : u));
|
|
} else {
|
|
// Criar novo
|
|
const newUser: AppUser = {
|
|
...editingUser,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
avatar: `https://ui-avatars.com/api/?name=${editingUser.name}&background=random`,
|
|
permissions: editingUser.role === 'super_admin' ? availableModules.map(m => m.id) : (editingUser.permissions || [])
|
|
} as AppUser;
|
|
setUsers([...users, newUser]);
|
|
}
|
|
setIsModalOpen(false);
|
|
setEditingUser({ role: 'user', active: true, permissions: [] });
|
|
};
|
|
|
|
const handleDeleteUser = (id: string) => {
|
|
if (id === currentUser.id) {
|
|
alert("Você não pode excluir a si mesmo.");
|
|
return;
|
|
}
|
|
if (window.confirm("Tem certeza que deseja remover este usuário?")) {
|
|
setUsers(users.filter(u => u.id !== id));
|
|
}
|
|
};
|
|
|
|
const togglePermission = (moduleId: ViewState) => {
|
|
const currentPermissions = editingUser.permissions || [];
|
|
if (currentPermissions.includes(moduleId)) {
|
|
setEditingUser({ ...editingUser, permissions: currentPermissions.filter(p => p !== moduleId) });
|
|
} else {
|
|
setEditingUser({ ...editingUser, permissions: [...currentPermissions, moduleId] });
|
|
}
|
|
};
|
|
|
|
const openModal = (user?: AppUser) => {
|
|
if (user) {
|
|
setEditingUser(user);
|
|
} else {
|
|
setEditingUser({ role: 'user', active: true, permissions: [], name: '', email: '' });
|
|
}
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const inputClass = "w-full p-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-200 outline-none text-slate-800 text-sm";
|
|
const labelClass = "block text-xs font-bold text-slate-700 mb-1 uppercase tracking-wide";
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fade-in">
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-800">Gerenciamento de Usuários</h1>
|
|
<p className="text-slate-500">Controle de acesso e permissões do sistema.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => openModal()}
|
|
className="flex items-center gap-2 px-5 py-3 bg-primary-500 text-white rounded-xl shadow-lg shadow-primary-200/50 hover:bg-primary-600 font-bold transition-all"
|
|
>
|
|
<Plus size={20} /> Novo Usuário
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-slate-50/50 border-b border-slate-100">
|
|
<tr>
|
|
<th className="p-6 text-xs font-bold uppercase text-slate-500">Usuário</th>
|
|
<th className="p-6 text-xs font-bold uppercase text-slate-500">Função</th>
|
|
<th className="p-6 text-xs font-bold uppercase text-slate-500">Status</th>
|
|
<th className="p-6 text-xs font-bold uppercase text-slate-500">Permissões</th>
|
|
<th className="p-6 text-right"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-50">
|
|
{users.map(user => (
|
|
<tr key={user.id} className="hover:bg-slate-50 transition-colors">
|
|
<td className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-full bg-slate-200 overflow-hidden">
|
|
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
|
|
</div>
|
|
<div>
|
|
<div className="font-bold text-slate-800">{user.name}</div>
|
|
<div className="text-xs text-slate-400">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="p-6">
|
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase ${
|
|
user.role === 'super_admin' ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-100 text-slate-600'
|
|
}`}>
|
|
{user.role === 'super_admin' ? <ShieldAlert size={14}/> : <UserIcon size={14}/>}
|
|
{user.role === 'super_admin' ? 'Super Admin' : 'Usuário'}
|
|
</span>
|
|
</td>
|
|
<td className="p-6">
|
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase ${
|
|
user.active ? 'bg-green-50 text-green-600' : 'bg-red-50 text-red-600'
|
|
}`}>
|
|
{user.active ? <CheckCircle2 size={14}/> : <XCircle size={14}/>}
|
|
{user.active ? 'Ativo' : 'Inativo'}
|
|
</span>
|
|
</td>
|
|
<td className="p-6">
|
|
{user.role === 'super_admin' ? (
|
|
<span className="text-xs text-indigo-500 font-bold">Acesso Total</span>
|
|
) : (
|
|
<div className="flex flex-wrap gap-1 max-w-xs">
|
|
{user.permissions.length === 0 && <span className="text-xs text-slate-400">Sem acesso</span>}
|
|
{user.permissions.slice(0, 3).map(p => (
|
|
<span key={p} className="px-2 py-0.5 bg-slate-100 border border-slate-200 rounded text-[10px] text-slate-500 capitalize">
|
|
{availableModules.find(m => m.id === p)?.label || p}
|
|
</span>
|
|
))}
|
|
{user.permissions.length > 3 && (
|
|
<span className="px-2 py-0.5 bg-slate-100 border border-slate-200 rounded text-[10px] text-slate-500">
|
|
+{user.permissions.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="p-6 text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<button onClick={() => openModal(user)} className="p-2 text-slate-400 hover:text-primary-500 hover:bg-primary-50 rounded-lg transition-colors">
|
|
<Edit2 size={18} />
|
|
</button>
|
|
{user.id !== currentUser.id && (
|
|
<button onClick={() => handleDeleteUser(user.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors">
|
|
<Trash2 size={18} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal User Edit/Create */}
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
|
|
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-2xl overflow-hidden animate-scale-up flex flex-col max-h-[90vh]">
|
|
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
|
<h3 className="font-bold text-slate-800 text-lg flex items-center gap-2">
|
|
{editingUser.id ? 'Editar Usuário' : 'Novo Usuário'}
|
|
</h3>
|
|
<button onClick={() => setIsModalOpen(false)} className="text-slate-400 hover:text-slate-600"><X size={20}/></button>
|
|
</div>
|
|
|
|
<div className="p-8 overflow-y-auto flex-1">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div className="col-span-2 md:col-span-1">
|
|
<label className={labelClass}>Nome Completo</label>
|
|
<input
|
|
type="text"
|
|
className={inputClass}
|
|
value={editingUser.name || ''}
|
|
onChange={e => setEditingUser({...editingUser, name: e.target.value})}
|
|
placeholder="Ex: João Silva"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2 md:col-span-1">
|
|
<label className={labelClass}>Email</label>
|
|
<input
|
|
type="email"
|
|
className={inputClass}
|
|
value={editingUser.email || ''}
|
|
onChange={e => setEditingUser({...editingUser, email: e.target.value})}
|
|
placeholder="joao@empresa.com"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2 md:col-span-1">
|
|
<label className={labelClass}>Nível de Acesso</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setEditingUser({...editingUser, role: 'user'})}
|
|
className={`flex-1 py-2 px-3 rounded-xl border text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
editingUser.role === 'user' ? 'bg-primary-50 border-primary-200 text-primary-700' : 'bg-white border-slate-200 text-slate-500'
|
|
}`}
|
|
>
|
|
<UserIcon size={16}/> Usuário
|
|
</button>
|
|
<button
|
|
onClick={() => setEditingUser({...editingUser, role: 'super_admin'})}
|
|
className={`flex-1 py-2 px-3 rounded-xl border text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
editingUser.role === 'super_admin' ? 'bg-indigo-50 border-indigo-200 text-indigo-700' : 'bg-white border-slate-200 text-slate-500'
|
|
}`}
|
|
>
|
|
<Shield size={16}/> Admin
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-2 md:col-span-1">
|
|
<label className={labelClass}>Status da Conta</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setEditingUser({...editingUser, active: true})}
|
|
className={`flex-1 py-2 px-3 rounded-xl border text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
editingUser.active ? 'bg-green-50 border-green-200 text-green-700' : 'bg-white border-slate-200 text-slate-500'
|
|
}`}
|
|
>
|
|
<CheckCircle2 size={16}/> Ativo
|
|
</button>
|
|
<button
|
|
onClick={() => setEditingUser({...editingUser, active: false})}
|
|
className={`flex-1 py-2 px-3 rounded-xl border text-sm font-medium transition-all flex items-center justify-center gap-2 ${
|
|
!editingUser.active ? 'bg-red-50 border-red-200 text-red-700' : 'bg-white border-slate-200 text-slate-500'
|
|
}`}
|
|
>
|
|
<XCircle size={16}/> Bloqueado
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Permissions Area */}
|
|
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100">
|
|
<h4 className="font-bold text-slate-800 mb-4 flex items-center gap-2">
|
|
<Lock size={18} className="text-slate-400"/>
|
|
Permissões de Acesso
|
|
</h4>
|
|
|
|
{editingUser.role === 'super_admin' ? (
|
|
<div className="text-center py-6 text-indigo-600 bg-indigo-50 rounded-xl border border-indigo-100">
|
|
<ShieldAlert size={32} className="mx-auto mb-2"/>
|
|
<p className="font-bold">Acesso Irrestrito</p>
|
|
<p className="text-xs opacity-75">Super Admins têm acesso a todos os módulos.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{availableModules.map(module => (
|
|
<ToggleSwitch
|
|
key={module.id}
|
|
label={module.label}
|
|
checked={(editingUser.permissions || []).includes(module.id)}
|
|
onChange={() => togglePermission(module.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="px-8 py-5 bg-slate-50 border-t border-slate-100 flex justify-end gap-3">
|
|
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-slate-600 font-medium hover:bg-slate-200 rounded-xl">Cancelar</button>
|
|
<button onClick={handleSaveUser} className="px-6 py-2 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600 flex items-center gap-2 shadow-lg shadow-primary-200">
|
|
<Save size={18} /> Salvar Usuário
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
};
|