265 lines
26 KiB
TypeScript
265 lines
26 KiB
TypeScript
import { SearchableSelect } from '../components/SearchableSelect';
|
||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
||
import type { FormEvent } from 'react';
|
||
import { Link, useNavigate, useParams, useSearchParams } from 'react-router';
|
||
import { useAuth } from '../auth/AuthContext';
|
||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||
import { Icon } from '../components/Icon';
|
||
import { ASSET_OPERATIONAL_STATUSES, ASSET_STATUSES, assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from '../features/assets/assetPresentation';
|
||
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel';
|
||
import { AssetDossierPanel } from '../features/assets/AssetDossierPanel';
|
||
import { AssetMediaPanel } from '../features/assets/AssetMediaPanel';
|
||
import { AssetProvenancePanel } from '../features/assets/AssetProvenancePanel';
|
||
import { AssetOperationalRelationsPanel } from '../features/assets/AssetOperationalRelationsPanel';
|
||
import { AssetRegistryPanel } from '../features/assets/AssetRegistryPanel';
|
||
import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel';
|
||
import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel';
|
||
import {
|
||
createAsset, getAsset, getAssetLineage, listAssetParentOptions, listAssetTypes, listCompaniesForArea,
|
||
listOperationalAreas, updateAsset, updateAssetInformationStatus, updateAssetOperationalStatus,
|
||
} from '../lib/api';
|
||
import type {
|
||
AssetAttributeDefinition, AssetDetail, AssetInformationStatus, AssetOperationalStatus,
|
||
AssetLineageItem, AssetListItem, AssetType, OperationalAssetSummary,
|
||
} from '../lib/api';
|
||
|
||
const AssetGeometryEditor = lazy(() => import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor })));
|
||
|
||
type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history';
|
||
|
||
function localDateTime(value: unknown): string {
|
||
if (typeof value !== 'string' || !value) return '';
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value.slice(0, 16);
|
||
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
|
||
return local.toISOString().slice(0, 16);
|
||
}
|
||
|
||
function normalizeAttributeValues(definitions: AssetAttributeDefinition[], values: Record<string, unknown>): Record<string, unknown> {
|
||
const result: Record<string, unknown> = {};
|
||
definitions.filter((item) => item.isActive).forEach((definition) => {
|
||
const raw = values[definition.id];
|
||
if (definition.dataType === 'BOOLEAN') result[definition.id] = Boolean(raw);
|
||
else if (raw !== undefined && raw !== null && raw !== '') result[definition.id] = definition.dataType === 'NUMBER' ? Number(raw) : raw;
|
||
});
|
||
return result;
|
||
}
|
||
|
||
export function AssetEditorPage() {
|
||
const { id } = useParams();
|
||
const editing = Boolean(id);
|
||
const navigate = useNavigate();
|
||
const [searchParams] = useSearchParams();
|
||
const contextParentId = !editing ? searchParams.get('parentId') : null;
|
||
const requestedTab = searchParams.get('tab') as DetailTab | null;
|
||
const tab: DetailTab = requestedTab && ['summary','dossier','findings','location','registry','files','history'].includes(requestedTab) ? requestedTab : 'summary';
|
||
const { hasPermission } = useAuth();
|
||
const canEdit = editing ? hasPermission('assets.update') : hasPermission('assets.create');
|
||
const canCreate = hasPermission('assets.create');
|
||
const canChangeStatus = hasPermission('assets.change_status');
|
||
const canChangeOperationalStatus = hasPermission('assets.change_operational_status');
|
||
const canEditGeometry = hasPermission('assets.update_geometry');
|
||
const canReadHistory = hasPermission('assets.read_history');
|
||
const canReadMedia = hasPermission('assets.read_media');
|
||
const canManageMedia = hasPermission('assets.manage_media');
|
||
const canReadProvenance = hasPermission('assets.read_provenance');
|
||
const canManageProvenance = hasPermission('assets.manage_provenance');
|
||
const canVerifyProvenance = hasPermission('assets.verify_provenance');
|
||
const canReadRelations = hasPermission('asset_relations.read');
|
||
const canManageRelations = hasPermission('asset_relations.manage');
|
||
const canManageContext = hasPermission('assets.manage_context');
|
||
const canReadFindingCatalog = hasPermission('finding_catalog.read');
|
||
const canManageFindingCatalog = hasPermission('finding_catalog.manage');
|
||
const canReadDossier = hasPermission('inspections.read')
|
||
&& hasPermission('inspection_acts.read')
|
||
&& hasPermission('inspection_findings.read')
|
||
&& hasPermission('inspection_evidence.read')
|
||
&& hasPermission('inspection_communications.read');
|
||
const canReadRegistry = hasPermission('asset_registry.read');
|
||
const canManageRegistry = hasPermission('asset_registry.manage');
|
||
const canSave = canEdit || (editing && (canChangeStatus || canChangeOperationalStatus));
|
||
|
||
const [types, setTypes] = useState<AssetType[]>([]);
|
||
const [asset, setAsset] = useState<AssetDetail | null>(null);
|
||
const [contextParent, setContextParent] = useState<AssetDetail | null>(null);
|
||
const [lineage, setLineage] = useState<AssetLineageItem[]>([]);
|
||
const [parents, setParents] = useState<AssetListItem[]>([]);
|
||
const [parentSearch, setParentSearch] = useState('');
|
||
const [code, setCode] = useState('');
|
||
const [name, setName] = useState('');
|
||
const [commonName, setCommonName] = useState('');
|
||
const [typeId, setTypeId] = useState('');
|
||
const [parentId, setParentId] = useState('');
|
||
const [operationalAreaId, setOperationalAreaId] = useState('');
|
||
const [operatorCompanyId, setOperatorCompanyId] = useState('');
|
||
const [operationalAreas, setOperationalAreas] = useState<OperationalAssetSummary[]>([]);
|
||
const [operationalCompanies, setOperationalCompanies] = useState<OperationalAssetSummary[]>([]);
|
||
const [description, setDescription] = useState('');
|
||
const [status, setStatus] = useState<AssetInformationStatus>('DRAFT');
|
||
const [operationalStatus, setOperationalStatus] = useState<AssetOperationalStatus>('UNKNOWN');
|
||
const [attributeValues, setAttributeValues] = useState<Record<string, unknown>>({});
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [success, setSuccess] = useState('');
|
||
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
|
||
|
||
const canDirectContextEdit = !editing || Boolean(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT');
|
||
|
||
const selectedType = types.find((type) => type.id === typeId) ?? null;
|
||
const definitions = useMemo(() => selectedType?.attributes.filter((item) => item.isActive) ?? [], [selectedType]);
|
||
|
||
useEffect(() => {
|
||
Promise.all([
|
||
listAssetTypes(),
|
||
id ? getAsset(id) : Promise.resolve(null),
|
||
id ? getAssetLineage(id) : Promise.resolve([] as AssetLineageItem[]),
|
||
])
|
||
.then(([loadedTypes, loadedAsset, loadedLineage]) => {
|
||
setTypes(loadedTypes);
|
||
setLineage(loadedLineage);
|
||
if (loadedAsset) {
|
||
setAsset(loadedAsset); setCode(loadedAsset.code); setName(loadedAsset.name); setCommonName(loadedAsset.commonName ?? ''); setTypeId(loadedAsset.type.id);
|
||
setParentId(loadedAsset.parent?.id ?? ''); setOperationalAreaId(loadedAsset.operationalArea?.id ?? '');
|
||
setOperatorCompanyId(loadedAsset.operatorCompany?.id ?? ''); setDescription(loadedAsset.description ?? '');
|
||
setStatus(loadedAsset.informationStatus); setOperationalStatus(loadedAsset.operationalStatus);
|
||
setAttributeValues(Object.fromEntries(loadedAsset.attributes.map((attribute) => [attribute.definitionId, attribute.dataType === 'DATETIME' ? localDateTime(attribute.value) : attribute.value ?? ''])));
|
||
} else if (!contextParentId) {
|
||
const first = loadedTypes.find((type) => type.isActive && type.canBeRoot) ?? loadedTypes.find((type) => type.isActive);
|
||
if (first) setTypeId(first.id);
|
||
}
|
||
})
|
||
.catch((requestError) => setError(errorMessage(requestError)))
|
||
.finally(() => setLoading(false));
|
||
}, [id]);
|
||
|
||
useEffect(() => {
|
||
if (editing || !contextParentId || types.length === 0) return;
|
||
Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)]).then(([parent, parentLineage]) => {
|
||
setContextParent(parent);
|
||
setLineage(parentLineage);
|
||
const compatible = types.find((type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id));
|
||
if (compatible) {
|
||
setTypeId(compatible.id); setParentId(parent.id);
|
||
const parentType = types.find((type) => type.id === parent.type.id);
|
||
if (parentType?.operationalRole === 'AREA') setOperationalAreaId(parent.id);
|
||
else if (parent.operationalArea) setOperationalAreaId(parent.operationalArea.id);
|
||
}
|
||
}).catch((requestError) => setError(errorMessage(requestError)));
|
||
}, [editing, contextParentId, types]);
|
||
|
||
useEffect(() => {
|
||
if (!typeId) { setParents([]); return; }
|
||
const timer = window.setTimeout(() => listAssetParentOptions(typeId, id, parentSearch).then(setParents).catch((requestError) => setError(errorMessage(requestError))), 220);
|
||
return () => window.clearTimeout(timer);
|
||
}, [typeId, id, parentSearch]);
|
||
|
||
useEffect(() => {
|
||
if (!canReadRelations || selectedType?.operationalRole !== 'GENERIC' || !parentId) {
|
||
setOperationalAreas([]);
|
||
if (!parentId) { setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); }
|
||
return;
|
||
}
|
||
listOperationalAreas(parentId).then((loadedAreas) => {
|
||
setOperationalAreas(loadedAreas);
|
||
if (operationalAreaId && !loadedAreas.some((area) => area.id === operationalAreaId)) {
|
||
setOperationalAreaId('');
|
||
if (!editing) setOperatorCompanyId('');
|
||
}
|
||
}).catch((requestError) => setError(errorMessage(requestError)));
|
||
}, [canReadRelations, selectedType?.operationalRole, parentId, editing]);
|
||
|
||
useEffect(() => {
|
||
if (editing || !canReadRelations || !operationalAreaId || selectedType?.operationalRole !== 'GENERIC') {
|
||
setOperationalCompanies([]);
|
||
return;
|
||
}
|
||
listCompaniesForArea(operationalAreaId).then((loadedCompanies) => {
|
||
setOperationalCompanies(loadedCompanies);
|
||
setOperatorCompanyId((current) => loadedCompanies.some((company) => company.id === current)
|
||
? current
|
||
: loadedCompanies.length === 1 ? (loadedCompanies[0]?.id ?? '') : '');
|
||
}).catch((requestError) => {
|
||
setOperationalCompanies([]);
|
||
setOperatorCompanyId('');
|
||
setError(errorMessage(requestError));
|
||
});
|
||
}, [editing, canReadRelations, operationalAreaId, selectedType?.operationalRole]);
|
||
|
||
const changeType = (nextTypeId: string) => { setTypeId(nextTypeId); setParentId(''); setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); setAttributeValues({}); setParentSearch(''); };
|
||
const setAttribute = (definitionId: string, value: unknown) => setAttributeValues((current) => ({ ...current, [definitionId]: value }));
|
||
|
||
const save = async (event: FormEvent) => {
|
||
event.preventDefault(); if (!selectedType) return;
|
||
setSaving(true); setError(''); setSuccess('');
|
||
try {
|
||
const attributes = normalizeAttributeValues(definitions, attributeValues);
|
||
let saved: AssetDetail;
|
||
if (editing && id) {
|
||
if (canEdit) saved = await updateAsset(id, { typeId: canDirectContextEdit ? typeId : undefined, code, name, commonName: commonName.trim() || null, parentId: canDirectContextEdit ? parentId || null : undefined, operationalAreaId: canDirectContextEdit ? operationalAreaId || null : undefined, description: description.trim() || null, attributes });
|
||
else if (asset) saved = asset; else return;
|
||
if (canChangeStatus && saved.informationStatus !== status) saved = await updateAssetInformationStatus(id, status);
|
||
if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) saved = await updateAssetOperationalStatus(id, operationalStatus);
|
||
setAsset(saved); setSuccess('Registro actualizado correctamente'); setHistoryRefreshKey((current) => current + 1);
|
||
} else {
|
||
saved = await createAsset({ code, name, commonName: commonName.trim() || null, typeId, parentId: parentId || null, operationalAreaId: operationalAreaId || null, operatorCompanyId: operatorCompanyId || null, description: description.trim() || null, informationStatus: canChangeStatus ? status : 'DRAFT', attributes });
|
||
navigate(`/inventarios/${saved.id}`, { replace: true });
|
||
}
|
||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||
finally { setSaving(false); }
|
||
};
|
||
|
||
if (loading) return <LoadingBlock label="Cargando registro…" />;
|
||
|
||
const detailTabs = [
|
||
{ key: 'summary', label: 'Resumen', show: true },
|
||
{ key: 'dossier', label: 'Expediente', show: editing && canReadDossier },
|
||
{ key: 'findings', label: 'Hallazgos aplicables', show: editing && canReadFindingCatalog },
|
||
{ key: 'location', label: 'Ubicación', show: editing },
|
||
{ key: 'registry', label: 'Documentos y registro', show: editing && (canReadRegistry || canReadProvenance) },
|
||
{ key: 'files', label: 'Archivos', show: editing && canReadMedia },
|
||
{ key: 'history', label: 'Historial', show: editing && canReadHistory },
|
||
] as const;
|
||
|
||
const breadcrumbSection = asset?.type.code === 'empresa' || contextParent?.type.code === 'empresa' ? 'companies' : 'territory';
|
||
return <section className="narrow-section asset-detail-page">
|
||
<nav className="breadcrumb asset-detail-breadcrumb" aria-label="Ruta del inventario">
|
||
<Link to="/inventarios">Inventarios</Link><span>›</span>
|
||
<Link to={breadcrumbSection === 'companies' ? '/inventarios?section=companies' : '/inventarios?section=territory'}>{breadcrumbSection === 'companies' ? 'Empresas' : 'Áreas y yacimientos'}</Link>
|
||
{lineage.map((item, index) => {
|
||
const isLast = index === lineage.length - 1;
|
||
const showAsCurrent = editing ? isLast : false;
|
||
return <span className="asset-detail-crumb-part" key={item.id}><span>›</span>{showAsCurrent ? <strong>{item.name}</strong> : <Link to={`/inventarios?section=${breadcrumbSection}&parentId=${item.id}`}>{item.name}</Link>}</span>;
|
||
})}
|
||
{!editing && <><span>›</span><strong>Nuevo registro</strong></>}
|
||
</nav>
|
||
<div className="page-heading asset-editor-heading"><div><span className="eyebrow">INVENTARIO</span><h1>{editing ? asset?.name ?? 'Registro' : contextParent ? `Agregar en ${contextParent.name}` : 'Nuevo registro'}</h1><p>{editing ? `${asset?.type.name} · ${asset?.code}` : contextParent ? `El sistema heredará el contexto disponible de ${contextParent.code}.` : 'Creá una entidad con identidad, jerarquía y atributos propios.'}</p></div><div className="asset-heading-actions">{editing && asset && <div className="heading-statuses"><span className={`status-badge ${assetStatusClass(asset.informationStatus)}`}>{assetStatusLabel(asset.informationStatus)}</span><small>{assetOperationalStatusLabel(asset.operationalStatus)}</small></div>}{editing && id && canCreate && <Link className="button primary" to={`/inventarios/nuevo?parentId=${id}`}><Icon name="plus" />Agregar registro aquí</Link>}</div></div>
|
||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
||
|
||
{editing && <nav className="asset-detail-tabs">{detailTabs.filter((item) => item.show).map((item) => <Link key={item.key} className={tab === item.key ? 'active' : ''} to={`/inventarios/${id}${item.key === 'summary' ? '' : `?tab=${item.key}`}`}>{item.label}</Link>)}</nav>}
|
||
|
||
{(!editing || tab === 'summary') && <>
|
||
{contextParent && !editing && <div className="context-create-banner"><Icon name="layers" /><div><strong>Alta contextual</strong><span>{contextParent.name} · {contextParent.code}</span></div><Link to={`/inventarios/${contextParent.id}`}>Ver padre</Link></div>}
|
||
<form className="panel form-panel" onSubmit={save}>
|
||
<div className="form-section"><div><h2>Identificación</h2><p className="section-copy">Conservá el nombre técnico y, cuando exista, agregá el nombre habitual usado en campo.</p></div><div className="form-grid"><label className="field"><span>Código DH</span><input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} disabled={!canEdit} required maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" /></label><label className="field"><span>Nombre técnico</span><input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required maxLength={200} /></label><label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} disabled={!canEdit} maxLength={200} placeholder="Ej.: tanque grande, ET vieja, batería norte…" /><small>También se usa en las búsquedas del Inventario.</small></label></div><label className="field"><span>Descripción <em>opcional</em></span><textarea value={description} onChange={(event) => setDescription(event.target.value)} disabled={!canEdit} maxLength={4000} rows={2} /></label></div>
|
||
|
||
<div className="form-section"><div><h2>Ubicación en la estructura</h2><p className="section-copy">Elegí qué es y dónde está contenido. La ubicación física es independiente de la Operadora del Área. Los registros ya consolidados cambian de ubicación desde el bloque histórico inferior.</p></div><div className="form-grid"><label className="field"><span>Tipo de elemento</span><SearchableSelect value={typeId} onChange={(event) => changeType(event.target.value)} disabled={(editing && !(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT')) || !canEdit} required><option value="">Seleccionar…</option>{types.filter((type) => type.isActive || type.id === typeId).map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label><div className="field parent-picker"><span>Registro padre {!selectedType?.canBeRoot && <em>obligatorio</em>}</span><input className="parent-search" value={parentSearch} onChange={(event) => setParentSearch(event.target.value)} disabled={!canEdit || !canDirectContextEdit} placeholder="Buscar planta, batería, estación…" /><SearchableSelect value={parentId} onChange={(event) => { setParentId(event.target.value); setParentSearch(''); }} disabled={!canEdit || !canDirectContextEdit} required={!selectedType?.canBeRoot}><option value="">{selectedType?.canBeRoot ? 'Sin padre · registro raíz' : 'Seleccionar registro padre…'}</option>{parents.map((parent) => <option key={parent.id} value={parent.id}>{parent.name} · {parent.code} ({parent.type.name})</option>)}</SearchableSelect><small>Escribí para buscar entre los registros compatibles.</small></div></div><div className="form-grid">{canChangeStatus && <label className="field"><span>Estado del dato</span><SearchableSelect value={status} onChange={(event) => setStatus(event.target.value as AssetInformationStatus)} disabled={!canEdit && !canChangeStatus}>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect><small>Calidad y validación del registro.</small></label>}{editing && canChangeOperationalStatus && <label className="field"><span>Estado operativo</span><SearchableSelect value={operationalStatus} onChange={(event) => setOperationalStatus(event.target.value as AssetOperationalStatus)}>{ASSET_OPERATIONAL_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect><small>Situación física u operativa del elemento.</small></label>}</div></div>
|
||
|
||
{selectedType?.operationalRole === 'GENERIC' && canReadRelations && <div className="form-section"><div><h2>{editing ? 'Área física' : 'Área y operadora al alta'}</h2><p className="section-copy">{editing ? 'El Inventario pertenece físicamente al Área. La Operadora vigente se administra en las relaciones temporales del Área y no se reescribe dentro del Inventario.' : 'Al crear el registro, la Operadora activa del Área queda guardada únicamente como snapshot histórico de alta.'}</p></div><div className="form-grid"><label className="field"><span>Área</span><SearchableSelect value={operationalAreaId} onChange={(event) => { setOperationalAreaId(event.target.value); if (!editing) setOperatorCompanyId(''); }} disabled={!canEdit || !canDirectContextEdit}><option value="">Sin asignación operativa</option>{operationalAreas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}</SearchableSelect>{editing && <small>La Operadora se resuelve por la relación Área↔Empresa vigente.</small>}</label>{!editing && <label className="field"><span>Operadora {operationalAreaId && <em>obligatoria</em>}</span><SearchableSelect value={operatorCompanyId} onChange={(event) => setOperatorCompanyId(event.target.value)} disabled={!canEdit || !operationalAreaId} required={Boolean(operationalAreaId)}><option value="">{operationalAreaId ? 'Seleccionar operadora…' : 'Primero seleccioná un área'}</option>{operationalCompanies.map((company) => <option key={company.id} value={company.id}>{company.name}</option>)}</SearchableSelect></label>}</div>{!editing && operationalAreaId && operatorCompanyId && <div className="temporal-notice"><Icon name="check" /><p><strong>Contexto de alta confirmado.</strong> La Operadora seleccionada se conservará como referencia histórica; futuros cambios se harán en la relación temporal del Área.</p></div>}{editing && asset?.operatorCompany && <div className="temporal-notice"><Icon name="layers" /><p><strong>Snapshot histórico de alta:</strong> {asset.operatorCompany.name}. No se modifica desde este registro.</p></div>}</div>}
|
||
|
||
<div className="form-section"><div><h2>Datos técnicos</h2><p className="section-copy">Campos definidos para el tipo seleccionado.</p></div>{definitions.length === 0 ? <div className="inline-empty">Este tipo no requiere datos técnicos adicionales.</div> : <div className="dynamic-attributes">{definitions.map((definition) => { const value = attributeValues[definition.id]; const label = <span>{definition.name}{definition.unit ? ` (${definition.unit})` : ''}{definition.isRequired ? <em>obligatorio</em> : <em>opcional</em>}</span>; if (definition.dataType === 'BOOLEAN') return <label className="check-row attribute-check" key={definition.id}><input type="checkbox" checked={Boolean(value)} onChange={(event) => setAttribute(definition.id, event.target.checked)} disabled={!canEdit} /><span><strong>{definition.name}</strong><small>{definition.code}</small></span></label>; if (definition.dataType === 'SELECT') return <label className="field" key={definition.id}>{label}<SearchableSelect value={String(value ?? '')} onChange={(event) => setAttribute(definition.id, event.target.value)} disabled={!canEdit} required={definition.isRequired}><option value="">Seleccionar…</option>{definition.options?.map((option) => <option key={option} value={option}>{option}</option>)}</SearchableSelect></label>; const inputType = definition.dataType === 'NUMBER' ? 'number' : definition.dataType === 'DATE' ? 'date' : definition.dataType === 'DATETIME' ? 'datetime-local' : 'text'; return <label className="field" key={definition.id}>{label}<input type={inputType} value={String(value ?? '')} onChange={(event) => setAttribute(definition.id, event.target.value)} disabled={!canEdit} required={definition.isRequired} step={definition.dataType === 'NUMBER' ? 'any' : undefined} maxLength={definition.dataType === 'TEXT' ? 4000 : undefined} /></label>; })}</div>}</div>
|
||
{canSave && <div className="form-actions"><Link className="button secondary" to="/inventarios">Cancelar</Link><button className="button primary" disabled={saving}><Icon name="check" />{saving ? 'Guardando…' : editing && !canEdit ? 'Actualizar estado' : editing ? 'Guardar cambios' : 'Crear registro'}</button></div>}
|
||
</form>
|
||
{editing && id && asset && selectedType && canReadHistory && <AssetContextHistoryPanel asset={asset} type={selectedType} canManage={canManageContext} onChanged={(saved) => { setAsset(saved); setParentId(saved.parent?.id ?? ''); setOperationalAreaId(saved.operationalArea?.id ?? ''); setOperatorCompanyId(saved.operatorCompany?.id ?? ''); setLineage([]); getAssetLineage(saved.id).then(setLineage).catch(() => undefined); setHistoryRefreshKey((current) => current + 1); }} />}
|
||
{editing && id && asset && canReadRelations && selectedType && selectedType.operationalRole !== 'GENERIC' && <AssetOperationalRelationsPanel assetId={id} role={selectedType.operationalRole} canManage={canManageRelations} />}
|
||
</>}
|
||
|
||
{editing && id && asset && tab === 'findings' && canReadFindingCatalog && <AssetFindingCatalogPanel assetId={id} canManage={canManageFindingCatalog} />}
|
||
{editing && id && asset && tab === 'dossier' && canReadDossier && <AssetDossierPanel assetId={id} />}
|
||
{editing && id && asset && tab === 'location' && <Suspense fallback={<div className="panel"><LoadingBlock label="Cargando ubicación…" /></div>}><AssetGeometryEditor assetId={id} assetName={asset.name} canEdit={canEditGeometry} onChanged={() => setHistoryRefreshKey((current) => current + 1)} /></Suspense>}
|
||
{editing && id && asset && tab === 'registry' && <div className="asset-tab-stack">{canReadRegistry && selectedType && <AssetRegistryPanel assetId={id} assetName={asset.name} role={selectedType.operationalRole} canManage={canManageRegistry} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}{canReadProvenance && <AssetProvenancePanel assetId={id} canManage={canManageProvenance} canVerify={canVerifyProvenance} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}</div>}
|
||
{editing && id && asset && tab === 'files' && canReadMedia && <AssetMediaPanel assetId={id} assetName={asset.name} canManage={canManageMedia} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}
|
||
{editing && id && tab === 'history' && canReadHistory && <AssetHistoryPanel assetId={id} refreshKey={historyRefreshKey} />}
|
||
</section>;
|
||
}
|