Files
dh-inspeccion-v2/web-v2/src/pages/RolesPage.tsx
T

128 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ÁreaOrganizació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>;
}