chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
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>;
}