// ROCA GOLF — Vista de Gestión de Usuarios (solo administrador) const ROLES_RG = { administrador: 'Administrador', contralor: 'Contralor', auxiliarcontable: 'Auxiliar Contable', directorproyectos: 'Director de Proyectos', tesorero: 'Tesorero', supervisor: 'Supervisor', proyectos: 'Proyectos', }; const ROL_COLORS = { administrador: '#b22222', contralor: '#1a4d2e', auxiliarcontable: '#2563eb', directorproyectos: '#7c3aed', tesorero: '#b45309', supervisor: '#64748b', proyectos: '#64748b', }; function UsuariosView({ onNav, user, onLogout }) { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(''); // Modal: nueva contraseña generada const [pwdModal, setPwdModal] = useState(false); const [pwdNombre, setPwdNombre] = useState(''); const [pwdUsuario, setPwdUsuario] = useState(''); const [pwdPlain, setPwdPlain] = useState(''); const [copied, setCopied] = useState(false); // Modal: crear usuario const [createModal, setCreateModal] = useState(false); const [createLoading, setCreateLoading] = useState(false); const [createForm, setCreateForm] = useState({ nombre: '', usuario: '', rol: 'proyectos' }); // Modal: editar usuario const [editModal, setEditModal] = useState(false); const [editLoading, setEditLoading] = useState(false); const [editForm, setEditForm] = useState({ uid: 0, nombre: '', rol: 'proyectos' }); // ── Helpers ─────────────────────────────────────────────────────────────── // apiFetch (rg-api.jsx) refresca el CSRF y reintenta una vez sola si el // servidor lo rechaza por token inválido — antes esta vista hacía su propio // fetch() y se quedaba sin forma de recuperarse de eso sin recargar a mano. function loadUsers() { setLoading(true); setErr(''); apiFetch('usuarios.php') .then(data => setUsers(data)) .catch(e => setErr(e.message || 'Error cargando usuarios')) .finally(() => setLoading(false)); } useEffect(() => { loadUsers(); }, []); // Muestra modal con la contraseña generada function mostrarPassword(nombre, usuario, plain) { setPwdNombre(nombre); setPwdUsuario(usuario); setPwdPlain(plain); setCopied(false); setPwdModal(true); } function copiarPassword() { navigator.clipboard.writeText(pwdPlain).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2500); }); } // ── Acciones ───────────────────────────────────────────────────────────── async function handleGenerar(u) { const ok = window.confirm( `¿${u.tiene_acceso_rg ? 'Resetear' : 'Generar'} contraseña de ROCA GOLF para ${u.nombre}?\n` + (u.tiene_acceso_rg ? 'La contraseña anterior quedará inválida.' : '') ); if (!ok) return; try { const data = await apiFetch('usuarios.php', 'POST', { accion: 'generar', uid: u.id }); mostrarPassword(u.nombre, u.usuario, data.password); loadUsers(); } catch (e) { toast.error(e.message || 'Error generando acceso'); } } async function handleRevocar(u) { if (!window.confirm(`¿Revocar el acceso de ${u.nombre} a ROCA GOLF? No podrá iniciar sesión.`)) return; try { await apiFetch('usuarios.php', 'POST', { accion: 'revocar', uid: u.id }); toast('Acceso revocado'); loadUsers(); } catch (e) { toast.error(e.message); } } async function handleToggle(u) { if (!window.confirm(`¿${u.activo ? 'Desactivar' : 'Activar'} a ${u.nombre}?`)) return; try { await apiFetch('usuarios.php', 'POST', { accion: 'toggle', uid: u.id }); toast(u.activo ? 'Usuario desactivado' : 'Usuario activado'); loadUsers(); } catch (e) { toast.error(e.message); } } async function handleCreate(e) { e.preventDefault(); setCreateLoading(true); try { const data = await apiFetch('usuarios.php', 'POST', { accion: 'crear', ...createForm }); setCreateModal(false); setCreateForm({ nombre: '', usuario: '', rol: 'proyectos' }); mostrarPassword(createForm.nombre, createForm.usuario, data.password); loadUsers(); } catch (err) { toast.error(err.message || 'Error creando usuario'); } finally { setCreateLoading(false); } } async function handleEdit(e) { e.preventDefault(); setEditLoading(true); try { await apiFetch('usuarios.php', 'POST', { accion: 'update', uid: editForm.uid, nombre: editForm.nombre, rol: editForm.rol }); toast('Usuario actualizado'); setEditModal(false); loadUsers(); } catch (err) { toast.error(err.message || 'Error actualizando'); } finally { setEditLoading(false); } } function abrirEditar(u) { setEditForm({ uid: u.id, nombre: u.nombre, rol: u.rol }); setEditModal(true); } // ── Solo administradores ───────────────────────────────────────────────── if (user?.rol !== 'administrador') { return (
Acceso restringido
Solo los administradores pueden gestionar usuarios.
); } // ── Render ──────────────────────────────────────────────────────────────── const sinAcceso = users.filter(u => !u.tiene_acceso_rg).length; const conAcceso = users.filter(u => u.tiene_acceso_rg).length; return (
setCreateModal(true)}> {Icon.plus(13)} Nuevo Usuario } />
{/* KPIs rápidos */}
{[ { label: 'Total usuarios', val: users.length, color: '#1a4d2e' }, { label: 'Con acceso RG', val: conAcceso, color: '#2563eb' }, { label: 'Sin acceso RG', val: sinAcceso, color: '#b45309', warn: sinAcceso > 0 }, { label: 'Inactivos', val: users.filter(u => !u.activo).length, color: '#64748b' }, ].map(k => (
{k.label}
{k.val}
))}
{/* Error global */} {err && (
{err}
)} {/* Tabla */}
Usuarios del sistema Tabla compartida con Contraloría · Las contraseñas son independientes por sistema
{loading ? (
Cargando usuarios…
) : ( {['ID', 'Nombre', 'Usuario', 'Rol', 'Acceso RG', 'Estado', 'Acciones'].map(h => ( ))} {users.length === 0 ? ( ) : users.map(u => ( e.currentTarget.style.background = '#faf9f6'} onMouseLeave={e => e.currentTarget.style.background = ''}> {/* ID */} {/* Nombre */} {/* Usuario */} {/* Rol */} {/* Acceso RG */} {/* Estado */} {/* Acciones */} ))}
{h}
No hay usuarios registrados.
{u.id}
{u.nombre}
{u.id === user.id && ( Tú )}
{u.usuario} {ROLES_RG[u.rol] || u.rol} {u.tiene_acceso_rg ? ( Habilitado ) : ( Sin acceso )} {u.activo ? 'Activo' : 'Inactivo'}
{/* Editar nombre/rol */} {/* Generar / resetear contraseña */} {/* Revocar acceso */} {u.tiene_acceso_rg && u.id !== user.id && ( )} {/* Toggle activo */} {u.id !== user.id && ( )}
)}
{/* Nota informativa */}
Contraseñas independientes: Cada usuario tiene una contraseña para Contraloría y otra para ROCA GOLF. Al crear o resetear una contraseña de ROCA GOLF, la contraseña de Contraloría no se modifica. Los usuarios creados aquí aparecerán en Contraloría sin contraseña activa hasta que el admin de ese sistema la configure.
{/* ── Modal: Contraseña generada ── */} {pwdModal && (
setPwdModal(false)}>
e.stopPropagation()}> {/* Header */}
Contraseña generada
{pwdNombre} · {pwdUsuario}
{/* Body */}
Contraseña ROCA GOLF
{pwdPlain}
⚠ Esta contraseña se muestra solo una vez.
Cópiala y entrégala al usuario. Si se pierde, usa "Resetear contraseña" para generar una nueva.
)} {/* ── Modal: Crear usuario ── */} {createModal && (
setCreateModal(false)}>
e.stopPropagation()}>
Nuevo Usuario
{/* Nombre */}
setCreateForm(f => ({ ...f, nombre: e.target.value }))} placeholder="Ej. Juan Pérez García" style={{ width: '100%', padding: '10px 13px', border: '1.5px solid #e0dbd2', borderRadius: 8, fontSize: 13.5, fontFamily: 'var(--rg-font-body)', outline: 'none', boxSizing: 'border-box' }} onFocus={e => e.target.style.borderColor = '#1a4d2e'} onBlur={e => e.target.style.borderColor = '#e0dbd2'} />
{/* Usuario */}
setCreateForm(f => ({ ...f, usuario: e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '') }))} placeholder="Ej. jperez" style={{ width: '100%', padding: '10px 13px', border: '1.5px solid #e0dbd2', borderRadius: 8, fontSize: 13.5, fontFamily: 'var(--rg-font-mono)', outline: 'none', boxSizing: 'border-box' }} onFocus={e => e.target.style.borderColor = '#1a4d2e'} onBlur={e => e.target.style.borderColor = '#e0dbd2'} />
Solo letras minúsculas, números y guiones. La contraseña se genera automáticamente.
{/* Rol */}
{/* Aviso */}
La contraseña se generará automáticamente y se mostrará una sola vez al crear el usuario.
{/* Footer */}
)} {/* ── Modal: Editar usuario ── */} {editModal && (
setEditModal(false)}>
e.stopPropagation()}>
Editar Usuario
setEditForm(f => ({ ...f, nombre: e.target.value }))} style={{ width: '100%', padding: '10px 13px', border: '1.5px solid #e0dbd2', borderRadius: 8, fontSize: 13.5, fontFamily: 'var(--rg-font-body)', outline: 'none', boxSizing: 'border-box' }} onFocus={e => e.target.style.borderColor = '#1a4d2e'} onBlur={e => e.target.style.borderColor = '#e0dbd2'} />
)}
); } Object.assign(window, { UsuariosView });