128 lines
7.8 KiB
TypeScript
128 lines
7.8 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import type { FormEvent } from 'react';
|
||
import { useAuth } from '../auth/AuthContext';
|
||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||
import { Icon } from '../components/Icon';
|
||
import {
|
||
createRole,
|
||
listPermissions,
|
||
listRoles,
|
||
replaceRolePermissions,
|
||
updateRole,
|
||
} from '../lib/api';
|
||
import type { AdministrativeRole, Permission } from '../lib/api';
|
||
|
||
const groupLabels: Record<string, string> = {
|
||
dashboard: 'Panel general',
|
||
users: 'Usuarios',
|
||
roles: 'Roles y permisos',
|
||
audit: 'Auditoría',
|
||
assets: 'Inventarios',
|
||
asset_types: 'Tipos y atributos de inventario',
|
||
asset_relations: 'Relaciones Área–Organización',
|
||
asset_registry: 'Registro documental y legal',
|
||
asset_imports: 'Importaciones de inventarios',
|
||
inspections: 'Inspecciones',
|
||
inspection_acts: 'Actas de inspección',
|
||
inspection_findings: 'Hallazgos',
|
||
inspection_verifications: 'Planificación de verificaciones',
|
||
finding_catalog: 'Catálogo de hallazgos',
|
||
surveys: 'Relevamientos',
|
||
};
|
||
|
||
|
||
const permissionLabels: Record<string, string> = {
|
||
read: 'Consultar', create: 'Crear', update: 'Modificar', manage: 'Administrar', change_status: 'Cambiar estado',
|
||
change_operational_status: 'Cambiar estado operativo', read_history: 'Ver historial', read_temporal: 'Consultar estado histórico',
|
||
read_geometry: 'Ver ubicación', update_geometry: 'Modificar ubicación', read_media: 'Ver archivos', manage_media: 'Administrar archivos',
|
||
read_provenance: 'Ver procedencia', manage_provenance: 'Administrar procedencia', verify_provenance: 'Validar procedencia',
|
||
plan: 'Planificar',
|
||
};
|
||
function permissionLabel(permission: Permission) {
|
||
const action = permission.code.split('.').slice(1).join('_');
|
||
return permissionLabels[action] ?? permission.description ?? permission.code;
|
||
}
|
||
|
||
export function RolesPage() {
|
||
const { hasPermission } = useAuth();
|
||
const canManage = hasPermission('roles.manage');
|
||
const [roles, setRoles] = useState<AdministrativeRole[]>([]);
|
||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||
const [creating, setCreating] = useState(false);
|
||
const [code, setCode] = useState('');
|
||
const [name, setName] = useState('');
|
||
const [description, setDescription] = useState('');
|
||
const [permissionIds, setPermissionIds] = useState<string[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [success, setSuccess] = useState('');
|
||
|
||
const selected = roles.find((role) => role.id === selectedId) ?? null;
|
||
const grouped = useMemo(() => permissions.reduce<Record<string, Permission[]>>((result, permission) => {
|
||
const group = permission.code.split('.')[0] ?? 'other';
|
||
(result[group] ??= []).push(permission);
|
||
return result;
|
||
}, {}), [permissions]);
|
||
|
||
const selectRole = (role: AdministrativeRole) => {
|
||
setCreating(false); setSelectedId(role.id); setCode(role.code); setName(role.name);
|
||
setDescription(role.description); setPermissionIds(role.permissions.map((permission) => permission.id));
|
||
setError(''); setSuccess('');
|
||
};
|
||
|
||
const startCreate = () => {
|
||
setCreating(true); setSelectedId(null); setCode(''); setName(''); setDescription('');
|
||
setPermissionIds([]); setError(''); setSuccess('');
|
||
};
|
||
|
||
const load = async (preferId?: string) => {
|
||
const [loadedRoles, loadedPermissions] = await Promise.all([listRoles(), listPermissions()]);
|
||
setRoles(loadedRoles); setPermissions(loadedPermissions);
|
||
const next = loadedRoles.find((role) => role.id === preferId) ?? loadedRoles[0];
|
||
if (next) selectRole(next);
|
||
};
|
||
|
||
useEffect(() => {
|
||
load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const togglePermission = (id: string) => setPermissionIds((current) => current.includes(id) ? current.filter((value) => value !== id) : [...current, id]);
|
||
|
||
const save = async (event: FormEvent) => {
|
||
event.preventDefault(); setError(''); setSuccess(''); setSaving(true);
|
||
try {
|
||
let saved: AdministrativeRole;
|
||
if (creating) {
|
||
saved = await createRole({ code, name, description, permissionIds });
|
||
} else if (selected) {
|
||
await updateRole(selected.id, { name, description });
|
||
saved = await replaceRolePermissions(selected.id, permissionIds);
|
||
} else return;
|
||
await load(saved.id);
|
||
setSuccess(creating ? 'Rol creado correctamente' : 'Rol y permisos actualizados');
|
||
setCreating(false);
|
||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||
finally { setSaving(false); }
|
||
};
|
||
|
||
if (loading) return <LoadingBlock label="Cargando roles…" />;
|
||
|
||
return <section>
|
||
<div className="page-heading"><div><span className="eyebrow">ADMINISTRACIÓN</span><h1>Roles y permisos</h1><p>Elegí qué puede consultar o modificar cada perfil. Los códigos técnicos quedan como referencia secundaria.</p></div>{canManage && <button className="button primary" onClick={startCreate}><Icon name="plus" />Nuevo rol</button>}</div>
|
||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
||
<div className="roles-layout">
|
||
<aside className="panel role-list"><div className="role-list-heading"><strong>Roles disponibles</strong><span>{roles.length}</span></div>{roles.map((role) => <button key={role.id} className={`role-list-item ${selectedId === role.id && !creating ? 'active' : ''}`} onClick={() => selectRole(role)}><span><strong>{role.name}</strong><small>{role.code}</small></span><span className="role-count">{role.userCount} usuario{Number(role.userCount) === 1 ? '' : 's'}</span></button>)}</aside>
|
||
|
||
<form className="panel form-panel role-editor" onSubmit={save}>
|
||
<div className="panel-heading"><div><span className="eyebrow">{creating ? 'NUEVO ROL' : selected?.isSystem ? 'ROL DEL SISTEMA' : 'ROL PERSONALIZADO'}</span><h2>{creating ? 'Crear rol' : name}</h2></div>{selected?.isSystem && !creating && <span className="tag">Sistema</span>}</div>
|
||
<div className="form-grid"><label className="field"><span>Código</span><input value={code} onChange={(event) => setCode(event.target.value.toLowerCase())} disabled={!creating || !canManage} required minLength={3} maxLength={80} pattern="[a-z][a-z0-9_-]+" /></label><label className="field"><span>Nombre</span><input value={name} onChange={(event) => setName(event.target.value)} disabled={!canManage} required maxLength={120} /></label></div>
|
||
<label className="field"><span>Descripción</span><textarea value={description} onChange={(event) => setDescription(event.target.value)} disabled={!canManage} required maxLength={1000} rows={3} /></label>
|
||
<div className="permission-matrix"><div><h3>Permisos asignados</h3><p>Los cambios se aplican inmediatamente a los próximos requests del usuario.</p></div>{(Object.entries(grouped) as Array<[string, Permission[]]>).map(([group, items]) => <fieldset key={group}><legend>{groupLabels[group] ?? group}</legend><div className="permission-grid">{items.map((permission) => <label className={`permission-row ${permissionIds.includes(permission.id) ? 'checked' : ''}`} key={permission.id}><input type="checkbox" checked={permissionIds.includes(permission.id)} onChange={() => togglePermission(permission.id)} disabled={!canManage} /><span><strong>{permissionLabel(permission)}</strong><small>{permission.description}<br /><code>{permission.code}</code></small></span><Icon name="check" /></label>)}</div></fieldset>)}</div>
|
||
{canManage && <div className="form-actions">{creating && <button className="button secondary" type="button" onClick={() => roles[0] && selectRole(roles[0])}>Cancelar</button>}<button className="button primary" disabled={saving}>{saving ? 'Guardando…' : creating ? 'Crear rol' : 'Guardar cambios'}</button></div>}
|
||
</form>
|
||
</div>
|
||
</section>;
|
||
}
|