feat(f6): show exact classification findings in asset dossier
This commit is contained in:
@@ -1,59 +1,95 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||
import { Icon } from '../../components/Icon';
|
||||
import { getFindingCatalogAssetSelection, replaceFindingCatalogAssetSelection } from '../../lib/api';
|
||||
import type { FindingCatalogAssetSelection } from '../../lib/api';
|
||||
import {
|
||||
getInventoryFamilyFindings,
|
||||
getInventoryTechnicalValues,
|
||||
} from '../../lib/inventoryStructureApi';
|
||||
import type {
|
||||
InventoryFamilyFinding,
|
||||
InventoryTechnicalValues,
|
||||
} from '../../lib/inventoryStructureApi';
|
||||
import { AssetTechnicalDataPanel } from './AssetTechnicalDataPanel';
|
||||
|
||||
export function AssetFindingCatalogPanel({ assetId, canManage }: { assetId: string; canManage: boolean }) {
|
||||
const [selection, setSelection] = useState<FindingCatalogAssetSelection | null>(null);
|
||||
const [enabled, setEnabled] = useState<Set<string>>(new Set());
|
||||
const [reason, setReason] = useState('');
|
||||
const [technical, setTechnical] = useState<InventoryTechnicalValues | null>(null);
|
||||
const [items, setItems] = useState<InventoryFamilyFinding[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [unsupported, setUnsupported] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
const load = () => getFindingCatalogAssetSelection(assetId).then((loaded) => {
|
||||
setSelection(loaded);
|
||||
setEnabled(new Set(loaded.items.filter((item) => item.enabled).map((item) => item.id)));
|
||||
});
|
||||
|
||||
useEffect(() => { load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, [assetId]);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
setUnsupported(false);
|
||||
setError('');
|
||||
getInventoryTechnicalValues(assetId)
|
||||
.then(async (loaded) => {
|
||||
const findings = await getInventoryFamilyFindings(loaded.family.id);
|
||||
if (!active) return;
|
||||
setTechnical(loaded);
|
||||
setItems(findings.items);
|
||||
})
|
||||
.catch((requestError) => {
|
||||
if (!active) return;
|
||||
const message = errorMessage(requestError);
|
||||
if (message.includes('no tiene clasificación técnica')) setUnsupported(true);
|
||||
else setError(message);
|
||||
})
|
||||
.finally(() => active && setLoading(false));
|
||||
return () => { active = false; };
|
||||
}, [assetId]);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const needle = search.trim().toLocaleLowerCase();
|
||||
return selection?.items.filter((item) => !needle || [item.title, item.code, item.categoryName]
|
||||
.some((value) => value.toLocaleLowerCase().includes(needle))) ?? [];
|
||||
}, [selection, search]);
|
||||
if (!needle) return items;
|
||||
return items.filter((item) => [item.title, item.code, item.categoryName, item.legalBasis ?? '']
|
||||
.some((value) => value.toLocaleLowerCase().includes(needle)));
|
||||
}, [items, search]);
|
||||
|
||||
const toggle = (id: string) => setEnabled((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true); setError(''); setSuccess('');
|
||||
try {
|
||||
const saved = await replaceFindingCatalogAssetSelection(assetId, { enabledItemIds: [...enabled], reason });
|
||||
setSelection(saved);
|
||||
setEnabled(new Set(saved.items.filter((item) => item.enabled).map((item) => item.id)));
|
||||
setReason('');
|
||||
setSuccess('Subconjunto de hallazgos actualizado para este objeto.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
if (loading) return <div className="panel"><LoadingBlock label="Cargando hallazgos aplicables…" /></div>;
|
||||
if (!selection) return <Alert>{error || 'No se pudo cargar la configuración.'}</Alert>;
|
||||
|
||||
return <section className="panel asset-finding-catalog-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">HALLAZGOS APLICABLES</span><h2>{selection.asset.name}</h2><p className="section-copy">Base: {selection.asset.assetTypeName}. Las excepciones de esta pantalla afectan sólo a este objeto del Inventario.</p></div><span className="count-pill">{enabled.size} habilitados</span></div>
|
||||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
||||
{!selection.typeConfigured && <div className="temporal-notice"><Icon name="alert" /><p><strong>El tipo técnico todavía no tiene un catálogo restringido.</strong> Por compatibilidad, su base actual incluye todo el catálogo activo. Podés configurar primero el tipo general en “Catálogo de hallazgos”.</p></div>}
|
||||
<label className="search-field"><Icon name="search" /><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Buscar hallazgo aplicable…" /></label>
|
||||
<div className="finding-selection-list">{visible.map((item) => <label className={`finding-selection-row ${item.assetOverride !== null ? 'override' : ''}`} key={item.id}><input type="checkbox" disabled={!canManage} checked={enabled.has(item.id)} onChange={() => toggle(item.id)} /><span><strong>{item.title}</strong><small>{item.categoryName} · {item.code} · base del tipo: {item.typeDefaultEnabled ? 'sí' : 'no'}{item.assetOverride !== null ? ' · excepción de este objeto' : ''}{item.suggestedSeverity ? ` · gravedad sugerida ${item.suggestedSeverity}/10` : ''}</small></span></label>)}</div>
|
||||
{canManage && <><label className="field"><span>Motivo de la excepción</span><textarea rows={2} value={reason} onChange={(event) => setReason(event.target.value)} maxLength={2000} placeholder="Explicá por qué este objeto usa un subconjunto distinto…" /></label><div className="form-actions"><button type="button" className="button primary" disabled={saving || reason.trim().length < 5} onClick={save}><Icon name="check" />{saving ? 'Guardando…' : 'Guardar excepciones'}</button></div></>}
|
||||
if (loading) return <div className="panel"><LoadingBlock label="Cargando clasificación y Hallazgos aplicables…" /></div>;
|
||||
if (error) return <Alert>{error}</Alert>;
|
||||
if (unsupported || !technical) return <section className="panel asset-finding-catalog-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">HALLAZGOS APLICABLES</span><h2>Sin clasificación técnica</h2><p className="section-copy">Departamento, Área y Yacimiento son niveles estructurales. Los Hallazgos se registran sobre Instalaciones y Subinstalaciones clasificadas.</p></div></div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
return <div className="asset-tab-stack">
|
||||
<AssetTechnicalDataPanel assetId={assetId} canEdit={canManage} />
|
||||
<section className="panel asset-finding-catalog-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<span className="eyebrow">HALLAZGOS APLICABLES</span>
|
||||
<h2>{technical.family.name}</h2>
|
||||
<p className="section-copy">Se muestran únicamente los Hallazgos asociados a esta clasificación técnica. Éste es el mismo catálogo que consume la APK para este objeto.</p>
|
||||
</div>
|
||||
<span className="count-pill">{items.length} asociados</span>
|
||||
</div>
|
||||
|
||||
{canManage && <div className="temporal-notice">
|
||||
<Icon name="layers" />
|
||||
<p><strong>¿Falta un Hallazgo?</strong> Administrá la clasificación en Configuración de Inventarios y el cambio se aplicará a todos los objetos de este mismo tipo técnico.</p>
|
||||
<Link className="button secondary" to="/admin/asset-types">Configurar Inventarios</Link>
|
||||
</div>}
|
||||
|
||||
{items.length > 0 && <label className="search-field">
|
||||
<Icon name="search" />
|
||||
<input value={search} onChange={(event) => setSearch(event.target.value)} placeholder={`Buscar entre los ${items.length} Hallazgos asociados…`} />
|
||||
</label>}
|
||||
|
||||
{items.length === 0
|
||||
? <div className="inline-empty">Esta clasificación todavía no tiene Hallazgos asociados. Desde Configuración de Inventarios podés vincular los que correspondan.</div>
|
||||
: visible.length === 0
|
||||
? <div className="inline-empty">No hay Hallazgos asociados que coincidan con la búsqueda.</div>
|
||||
: <div className="finding-selection-list">{visible.map((item) => <div className="finding-selection-row" key={item.id}>
|
||||
<span className="asset-symbol"><Icon name="alert" size={15} /></span>
|
||||
<span>
|
||||
<strong>{item.title}</strong>
|
||||
<small>{item.categoryName} · {item.code}{item.suggestedSeverity ? ` · gravedad sugerida ${item.suggestedSeverity}/10` : ''}</small>
|
||||
{item.legalBasis && <small>{item.legalBasis}</small>}
|
||||
</span>
|
||||
</div>)}</div>}
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user