@@ -1,11 +1,18 @@
import './AssetEditorPage.css' ;
import { SearchableSelect } from '../components/SearchableSelect' ;
import { SearchableSelect } from '../components/SearchableSelect' ;
import { lazy , Suspense , useEffect , useMemo , useState } from 'react' ;
import { lazy , Suspense , useEffect , useMemo , useState } from 'react' ;
import type { FormEvent } from 'react' ;
import type { FormEvent , ReactNode } from 'react' ;
import { Link , useNavigate , useParams , useSearchParams } from 'react-router' ;
import { Link , useNavigate , useParams , useSearchParams } from 'react-router' ;
import { useAuth } from '../auth/AuthContext' ;
import { useAuth } from '../auth/AuthContext' ;
import { Alert , LoadingBlock , errorMessage } from '../components/Feedback' ;
import { Alert , LoadingBlock , errorMessage } from '../components/Feedback' ;
import { Icon } from '../components/Icon' ;
import { Icon } from '../components/Icon' ;
import { ASSET_OPERATIONAL_STATUSES , ASSET_STATUSES , assetOperationalStatusLabel , assetStatusClass , assetStatusLabel } from '../features/assets/assetPresentation' ;
import {
ASSET_OPERATIONAL_STATUSES ,
ASSET_STATUSES ,
assetOperationalStatusLabel ,
assetStatusClass ,
assetStatusLabel ,
} from '../features/assets/assetPresentation' ;
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel' ;
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel' ;
import { AssetDossierPanel } from '../features/assets/AssetDossierPanel' ;
import { AssetDossierPanel } from '../features/assets/AssetDossierPanel' ;
import { AssetMediaPanel } from '../features/assets/AssetMediaPanel' ;
import { AssetMediaPanel } from '../features/assets/AssetMediaPanel' ;
@@ -15,15 +22,31 @@ import { AssetRegistryPanel } from '../features/assets/AssetRegistryPanel';
import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel' ;
import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel' ;
import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel' ;
import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel' ;
import {
import {
createAsset , getAsset , getAssetLineage , listAssetParentOptions , listAssetTypes , listCompaniesForArea ,
createAsset ,
listOperationalAreas , updateAsset , updateAssetInformationStatus , updateAssetOperationalStatus ,
getAsset ,
getAssetLineage ,
listAssetParentOptions ,
listAssetTypes ,
listCompaniesForArea ,
listOperationalAreas ,
updateAsset ,
updateAssetInformationStatus ,
updateAssetOperationalStatus ,
} from '../lib/api' ;
} from '../lib/api' ;
import type {
import type {
AssetAttributeDefinition , AssetDetail , AssetInformationStatus , AssetOperationalStatus ,
AssetAttributeDefinition ,
AssetLineageItem , AssetListItem , AssetType , OperationalAssetSummary ,
AssetDetail ,
AssetInformationStatus ,
AssetOperationalStatus ,
AssetLineageItem ,
AssetListItem ,
AssetType ,
OperationalAssetSummary ,
} from '../lib/api' ;
} from '../lib/api' ;
const AssetGeometryEditor = lazy ( ( ) = > import ( '../features/map/AssetGeometryEditor' ) . then ( ( module ) = > ( { default : module . AssetGeometryEditor } ) ) ) ;
const AssetGeometryEditor = lazy ( ( ) = >
import ( '../features/map/AssetGeometryEditor' ) . then ( ( module ) = > ( { default : module . AssetGeometryEditor } ) ) ,
) ;
type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history' ;
type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history' ;
@@ -35,16 +58,26 @@ function localDateTime(value: unknown): string {
return local . toISOString ( ) . slice ( 0 , 16 ) ;
return local . toISOString ( ) . slice ( 0 , 16 ) ;
}
}
function normalizeAttributeValues ( definitions : AssetAttributeDefinition [ ] , values : Record < string , unknown > ) : Record < string , unknown > {
function normalizeAttributeValues (
definitions : AssetAttributeDefinition [ ] ,
values : Record < string , unknown > ,
) : Record < string , unknown > {
const result : Record < string , unknown > = { } ;
const result : Record < string , unknown > = { } ;
definitions . filter ( ( item ) = > item . isActive ) . forEach ( ( definition ) = > {
definitions . filter ( ( item ) = > item . isActive ) . forEach ( ( definition ) = > {
const raw = values [ definition . id ] ;
const raw = values [ definition . id ] ;
if ( definition . dataType === 'BOOLEAN' ) result [ definition . id ] = Boolean ( raw ) ;
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 ;
else if ( raw !== undefined && raw !== null && raw !== '' ) {
result [ definition . id ] = definition . dataType === 'NUMBER' ? Number ( raw ) : raw ;
}
} ) ;
} ) ;
return result ;
return result ;
}
}
function hasAttributeValue ( definition : AssetAttributeDefinition , value : unknown ) : boolean {
if ( definition . dataType === 'BOOLEAN' ) return value === true ;
return value !== undefined && value !== null && String ( value ) . trim ( ) !== '' ;
}
export function AssetEditorPage() {
export function AssetEditorPage() {
const { id } = useParams ( ) ;
const { id } = useParams ( ) ;
const editing = Boolean ( id ) ;
const editing = Boolean ( id ) ;
@@ -52,7 +85,11 @@ export function AssetEditorPage() {
const [ searchParams ] = useSearchParams ( ) ;
const [ searchParams ] = useSearchParams ( ) ;
const contextParentId = ! editing ? searchParams . get ( 'parentId' ) : null ;
const contextParentId = ! editing ? searchParams . get ( 'parentId' ) : null ;
const requestedTab = searchParams . get ( 'tab' ) as DetailTab | 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 tab : DetailTab = requestedTab
&& [ 'summary' , 'dossier' , 'findings' , 'location' , 'registry' , 'files' , 'history' ] . includes ( requestedTab )
? requestedTab
: 'summary' ;
const { hasPermission } = useAuth ( ) ;
const { hasPermission } = useAuth ( ) ;
const canEdit = editing ? hasPermission ( 'assets.update' ) : hasPermission ( 'assets.create' ) ;
const canEdit = editing ? hasPermission ( 'assets.update' ) : hasPermission ( 'assets.create' ) ;
const canCreate = hasPermission ( 'assets.create' ) ;
const canCreate = hasPermission ( 'assets.create' ) ;
@@ -104,10 +141,14 @@ export function AssetEditorPage() {
const [ success , setSuccess ] = useState ( '' ) ;
const [ success , setSuccess ] = useState ( '' ) ;
const [ historyRefreshKey , setHistoryRefreshKey ] = useState ( 0 ) ;
const [ historyRefreshKey , setHistoryRefreshKey ] = useState ( 0 ) ;
const canDirectContextEdit = ! editing || Boolean ( asset ? . dataOrigin === 'FIELD_SURVEY' && asset . informationStatus === 'DRAFT' ) ;
const canDirectContextEdit = ! editing
|| Boolean ( asset ? . dataOrigin === 'FIELD_SURVEY' && asset . informationStatus === 'DRAFT' ) ;
const selectedType = types . find ( ( type ) = > type . id === typeId ) ? ? null ;
const selectedType = types . find ( ( type ) = > type . id === typeId ) ? ? null ;
const definitions = useMemo ( ( ) = > selectedType ? . attributes . filter ( ( item ) = > item . isActive ) ? ? [ ] , [ selectedType ] ) ;
const definitions = useMemo (
( ) = > selectedType ? . attributes . filter ( ( item ) = > item . isActive ) ? ? [ ] ,
[ selectedType ] ,
) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
Promise . all ( [
Promise . all ( [
@@ -119,13 +160,26 @@ export function AssetEditorPage() {
setTypes ( loadedTypes ) ;
setTypes ( loadedTypes ) ;
setLineage ( loadedLineage ) ;
setLineage ( loadedLineage ) ;
if ( loadedAsset ) {
if ( loadedAsset ) {
setAsset ( loadedAsset ) ; setCode ( loadedAsset . code ) ; setName ( loadedAsset . name ) ; setCommonName ( loadedAsset . commonName ? ? '' ) ; setTypeId ( loadedAsset . type . id ) ;
setAsset ( loadedAsset ) ;
setParentId ( loadedAsset . parent ? . id ? ? '' ) ; setOperationalAreaId ( loadedAsset . operationalArea ? . id ? ? '' ) ;
setCode ( loadedAsset . code ) ;
setOperatorCompanyId ( loadedAsset . operatorCompany ? . id ? ? '' ) ; setDescription ( loadedAsset . description ? ? '' ) ;
setName ( loadedAsset . name ) ;
setStatus ( loadedAsset . informationStatus ) ; setOperationalStatus ( loadedAsset . operationalStatus ) ;
setCommonName ( loadedAsset . commonName ? ? '' ) ;
setAttributeValues ( Object . fromEntries ( loadedAsset . attributes . map ( ( attribute ) = > [ attribute . definitionId , attribute . dataType === 'DATETIME' ? localDateTime ( attribute . value ) : attribute . value ? ? '' ] ) ) ) ;
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 ) {
} else if ( ! contextParentId ) {
const first = loadedTypes . find ( ( type ) = > type . isActive && type . canBeRoot ) ? ? loadedTypes . find ( ( type ) = > type . isActive ) ;
const first = loadedTypes . find ( ( type ) = > type . isActive && type . canBeRoot )
? ? loadedTypes . find ( ( type ) = > type . isActive ) ;
if ( first ) setTypeId ( first . id ) ;
if ( first ) setTypeId ( first . id ) ;
}
}
} )
} )
@@ -135,38 +189,56 @@ export function AssetEditorPage() {
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( editing || ! contextParentId || types . length === 0 ) return ;
if ( editing || ! contextParentId || types . length === 0 ) return ;
Promise . all ( [ getAsset ( contextParentId ) , getAssetLineage ( contextParentId ) ] ) . then ( ( [ parent , parentLineage ] ) = > {
Promise . all ( [ getAsset ( contextParentId ) , getAssetLineage ( contextParentId ) ] )
setContextParent ( parent ) ;
. then ( ( [ parent , parentLineage ] ) = > {
setLineage ( parentLineage ) ;
setContextParent ( parent ) ;
const compatible = types . find ( ( type ) = > type . isActive && type . allowedParentTypes . some ( ( allowed ) = > allowed . id === parent . type . id ) ) ;
setLineage ( parentLineage ) ;
if ( compatible ) {
const compatible = types . find (
setTypeId ( compatible . id ) ; setParentId ( parent . id ) ;
( type ) = > type . isActive && type . allowedParentTypes . some ( ( allowed ) = > allowed . id === parent . type . id ) ,
const parentType = types . find ( ( type ) = > type . id === parent . type . id ) ;
) ;
if ( parentType ? . operationalRole === 'AREA' ) setOperationalAreaId ( parent . id ) ;
if ( compatible ) {
else if ( parent . operationalArea ) setOperationalAreaId ( parent . operationalArea . id ) ;
setTypeId ( compatible . id ) ;
}
setParentId ( parent . id ) ;
} ) . catch ( ( requestError ) = > setError ( errorMessage ( requestError ) ) ) ;
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 ] ) ;
} , [ editing , contextParentId , types ] ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( ! typeId ) { setParents ( [ ] ) ; return ; }
if ( ! typeId ) {
const timer = window . setTimeout ( ( ) = > listAssetParentOptions ( typeId , id , parentSearch ) . then ( setParents ) . catch ( ( requestError ) = > setError ( errorMessage ( requestError ) ) ) , 220 ) ;
setParents ( [ ] ) ;
return ;
}
const timer = window . setTimeout (
( ) = > listAssetParentOptions ( typeId , id , parentSearch )
. then ( setParents )
. catch ( ( requestError ) = > setError ( errorMessage ( requestError ) ) ) ,
220 ,
) ;
return ( ) = > window . clearTimeout ( timer ) ;
return ( ) = > window . clearTimeout ( timer ) ;
} , [ typeId , id , parentSearch ] ) ;
} , [ typeId , id , parentSearch ] ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( ! canReadRelations || selectedType ? . operationalRole !== 'GENERIC' || ! parentId ) {
if ( ! canReadRelations || selectedType ? . operationalRole !== 'GENERIC' || ! parentId ) {
setOperationalAreas ( [ ] ) ;
setOperationalAreas ( [ ] ) ;
if ( ! parentId ) { setOperationalAreaId ( '' ) ; if ( ! editing ) setOperatorCompanyId ( '' ) ; }
if ( ! parentId ) {
return ;
}
listOperationalAreas ( parentId ) . then ( ( loadedAreas ) = > {
setOperationalAreas ( loadedAreas ) ;
if ( operationalAreaId && ! loadedAreas . some ( ( area ) = > area . id === operationalAreaId ) ) {
setOperationalAreaId ( '' ) ;
setOperationalAreaId ( '' ) ;
if ( ! editing ) setOperatorCompanyId ( '' ) ;
if ( ! editing ) setOperatorCompanyId ( '' ) ;
}
}
} ) . catch ( ( requestError ) = > setError ( errorMessage ( requestError ) ) ) ;
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 ] ) ;
} , [ canReadRelations , selectedType ? . operationalRole , parentId , editing ] ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
@@ -174,39 +246,85 @@ export function AssetEditorPage() {
setOperationalCompanies ( [ ] ) ;
setOperationalCompanies ( [ ] ) ;
return ;
return ;
}
}
listCompaniesForArea ( operationalAreaId ) . then ( ( loadedCompanies ) = > {
listCompaniesForArea ( operationalAreaId )
setOperationalCompanies ( loadedCompanies ) ;
. then ( ( loadedCompanies ) = > {
setOperatorCompanyId ( ( current ) = > loadedCompanies . some ( ( company ) = > company . id === current )
setOperationalCompanies ( loadedCompanies ) ;
? current
setOperatorCompanyId ( ( current ) = > loadedCompanies . some ( ( company ) = > company . id === current )
: loadedCompanies.length === 1 ? ( loadedCompanies [ 0 ] ? . id ? ? '' ) : '' ) ;
? current
} ) . catch ( ( requestError ) = > {
: loadedCompanies.length === 1 ? ( loadedCompanies [ 0 ] ? . id ? ? '' ) : '' ) ;
setOperationalCompanies ( [ ] ) ;
} )
setOperatorCompanyId ( '' ) ;
. catch ( ( requestError ) = > {
setError ( errorMessage ( requestError ) ) ;
setOperationalCompanies ( [ ] ) ;
} ) ;
setOperatorCompanyId ( '' ) ;
setError ( errorMessage ( requestError ) ) ;
} ) ;
} , [ editing , canReadRelations , operationalAreaId , selectedType ? . operationalRole ] ) ;
} , [ editing , canReadRelations , operationalAreaId , selectedType ? . operationalRole ] ) ;
const changeType = ( nextTypeId : string ) = > { setTypeId ( nextTypeId ) ; setParentId ( '' ) ; setOperationalAreaId ( '' ) ; if ( ! editing ) setOperatorCompanyId ( '' ) ; setAttributeValues ( { } ) ; setParentSearch ( '' ) ; } ;
const changeType = ( nextTypeId : string ) = > {
const setAttribute = ( definitionId : string , value : unknown ) = > setAttributeValues ( ( current ) = > ( { . . . current , [ definitionId ] : value } ) ) ;
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 ) = > {
const save = async ( event : FormEvent ) = > {
event . preventDefault ( ) ; if ( ! selectedType ) return ;
event . preventDefault ( ) ;
setSaving ( true ) ; setError ( '' ) ; setSuccess ( '' ) ;
if ( ! selectedType ) return ;
setSaving ( true ) ;
setError ( '' ) ;
setSuccess ( '' ) ;
try {
try {
const attributes = normalizeAttributeValues ( definitions , attributeValues ) ;
const attributes = normalizeAttributeValues ( definitions , attributeValues ) ;
let saved : AssetDetail ;
let saved : AssetDetail ;
if ( editing && id ) {
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 } ) ;
if ( canEdit ) {
else if ( asset ) saved = asset ; else return ;
saved = await updateAsset ( id , {
if ( canChangeStatus && saved . informationStatus !== status ) saved = await updateAssetInformationStatus ( id , status ) ;
typeId : canDirectContextEdit ? typeId : undefined ,
if ( canChangeOperationalStatus && saved . operationalStatus !== operationalStatus ) saved = await updateAssetOperationalStatus ( id , operationalStatus ) ;
code ,
setAsset ( saved ) ; setSuccess ( 'Registro actualizado correctamente' ) ; setHistoryRefreshKey ( ( current ) = > current + 1 ) ;
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 {
} 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 } ) ;
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 } ) ;
navigate ( ` /inventarios/ ${ saved . id } ` , { replace : true } ) ;
}
}
} catch ( requestError ) { setError ( errorMessage ( requestError ) ) ; }
} catch ( requestError ) {
finally { setSaving ( false ) ; }
setError ( errorMessage ( requestError ) ) ;
} finally {
setSaving ( false ) ;
}
} ;
} ;
if ( loading ) return < LoadingBlock label = "Cargando registro…" / > ;
if ( loading ) return < LoadingBlock label = "Cargando registro…" / > ;
@@ -221,44 +339,621 @@ export function AssetEditorPage() {
{ key : 'history' , label : 'Historial' , show : editing && canReadHistory } ,
{ key : 'history' , label : 'Historial' , show : editing && canReadHistory } ,
] as const ;
] as const ;
const breadcrumbSection = asset ? . type . code === 'empresa' || contextParent ? . type . code === 'empresa' ? 'companies' : 'territory' ;
const breadcrumbSection = asset ? . type . code === 'empresa' || contextParent ? . type . code === 'empresa'
? 'companies'
: 'territory' ;
const structuralTypeCode = selectedType ? . code . trim ( ) . toLowerCase ( ) ? ? '' ;
const compactStructuralSummary = editing
&& ( structuralTypeCode === 'instalacion' || structuralTypeCode === 'subinstalacion' ) ;
const technicalFilledCount = definitions . filter (
( definition ) = > hasAttributeValue ( definition , attributeValues [ definition . id ] ) ,
) . length ;
const missingRequiredTechnical = definitions . some (
( definition ) = > definition . isRequired && ! hasAttributeValue ( definition , attributeValues [ definition . id ] ) ,
) ;
const handleContextChanged = ( saved : AssetDetail ) = > {
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 ) ;
} ;
const renderAttributes = ( ) : ReactNode = > {
if ( definitions . length === 0 ) {
return < div className = "inline-empty" > Este tipo no requiere datos técnicos adicionales . < / div > ;
}
return < 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 > ;
} ;
const renderSaveActions = ( ) = > 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 > ;
const renderCompactStructuralForm = ( ) = > < form className = "panel form-panel asset-compact-form" onSubmit = { save } >
< section className = "asset-compact-section" >
< div className = "asset-compact-section-heading" >
< div >
< h2 > Datos principales < / h2 >
< p className = "section-copy" > Lo que se usa todos los días para reconocer este registro . < / p >
< / div >
< / div >
< div className = "form-grid asset-compact-primary-grid" >
< 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.: planta vieja, celda principal…"
/ >
< / label >
< / div >
< div className = "asset-compact-meta" >
< span > < small > Código DH < / small > < strong > { code } < / strong > < / span >
< span > < small > Tipo < / small > < strong > { selectedType ? . name ? ? 'Sin tipo' } < / strong > < / span >
< / div >
< / section >
< section className = "asset-compact-section" >
< div className = "asset-compact-section-heading" >
< div >
< h2 > Ubicación actual < / h2 >
< p className = "section-copy" > Dónde está contenido el elemento dentro del Inventario . < / p >
< / div >
{ canReadHistory && id && < Link className = "button secondary asset-compact-context-button" to = { ` /inventarios/ ${ id } ?tab=history ` } >
< Icon name = "edit" / > Cambiar / ver historial
< / Link > }
< / div >
< div className = "asset-compact-context-grid" >
< div className = "asset-compact-context-item" >
< small > Registro padre < / small >
< strong > { asset ? . parent ? . name ? ? 'Sin padre' } < / strong >
{ asset ? . parent ? . code && < span > { asset . parent . code } < / span > }
< / div >
< div className = "asset-compact-context-item" >
< small > Á rea < / small >
< strong > { asset ? . operationalArea ? . name ? ? 'Sin área asignada' } < / strong >
{ asset ? . operationalArea ? . code && < span > { asset . operationalArea . code } < / span > }
< / div >
< / div >
< / section >
{ ( canChangeStatus || canChangeOperationalStatus ) && < details className = "asset-compact-details" >
< summary >
< span > < strong > Estado y opciones < / strong > < small > Calidad del dato y situación operativa < / small > < / span >
< span aria-hidden = "true" > ⌄ < / span >
< / summary >
< div className = "asset-compact-details-body" >
< 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 >
< / label > }
{ 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 >
< / label > }
< / div >
< / div >
< / details > }
< details className = "asset-compact-details" open = { missingRequiredTechnical || canDirectContextEdit } >
< summary >
< span >
< strong > Más datos < / strong >
< small >
{ description . trim ( ) ? 'Descripción cargada' : 'Sin descripción' }
{ ' · ' }
{ technicalFilledCount } / { definitions . length } datos técnicos cargados
< / small >
< / span >
< span aria-hidden = "true" > ⌄ < / span >
< / summary >
< div className = "asset-compact-details-body" >
< 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 >
{ canDirectContextEdit && < label className = "field" >
< span > Tipo de elemento < / span >
< SearchableSelect
value = { typeId }
onChange = { ( event ) = > changeType ( event . target . value ) }
disabled = { ! 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 >
{ canDirectContextEdit && < div className = "form-grid" >
< 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 }
placeholder = "Buscar planta, batería, estación…"
/ >
< SearchableSelect
value = { parentId }
onChange = { ( event ) = > {
setParentId ( event . target . value ) ;
setParentSearch ( '' ) ;
} }
disabled = { ! canEdit }
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 >
< / div >
{ selectedType ? . operationalRole === 'GENERIC' && canReadRelations && < label className = "field" >
< span > Á rea < / span >
< SearchableSelect
value = { operationalAreaId }
onChange = { ( event ) = > setOperationalAreaId ( event . target . value ) }
disabled = { ! canEdit }
>
< option value = "" > Sin asignación operativa < / option >
{ operationalAreas . map ( ( area ) = >
< option key = { area . id } value = { area . id } > { area . name } < / option > ) }
< / SearchableSelect >
< / 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 className = "asset-compact-technical" >
< div >
< h3 > Datos técnicos < / h3 >
< p className = "section-copy" > Sólo completá lo que corresponda para este tipo . < / p >
< / div >
{ renderAttributes ( ) }
< / div >
< / div >
< / details >
{ renderSaveActions ( ) }
< / form > ;
const renderFullForm = ( ) = > < 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 >
{ renderAttributes ( ) }
< / div >
{ renderSaveActions ( ) }
< / form > ;
return < section className = "narrow-section asset-detail-page" >
return < section className = "narrow-section asset-detail-page" >
< nav className = "breadcrumb asset-detail-breadcrumb" aria-label = "Ruta del inventario" >
< nav className = "breadcrumb asset-detail-breadcrumb" aria-label = "Ruta del inventario" >
< Link to = "/inventarios" > Inventarios < / Link > < span > › < / span >
< Link to = "/inventarios" > Inventarios < / Link > < span > › < / span >
< Link to = { breadcrumbSection === 'companies' ? '/inventarios?section=companies' : '/inventarios?section=territory' } > { breadcrumbSection === 'companies' ? 'Empresas' : 'Áreas y yacimientos' } < / Link >
< Link to = { breadcrumbSection === 'companies' ? '/inventarios?section=companies' : '/inventarios?section=territory' } >
{ breadcrumbSection === 'companies' ? 'Empresas' : 'Áreas y yacimientos' }
< / Link >
{ lineage . map ( ( item , index ) = > {
{ lineage . map ( ( item , index ) = > {
const isLast = index === lineage . length - 1 ;
const isLast = index === lineage . length - 1 ;
const showAsCurrent = editing ? isLast : false ;
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 > ;
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 > < / > }
{ ! editing && < > < span > › < / span > < strong > Nuevo registro < / strong > < / > }
< / nav >
< / 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 > }
< 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' ) && < >
{ ( ! 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 > }
{ contextParent && ! editing && < div className = "context-create-banner" >
< form className = "panel form-panel" onSubmit = { save } >
< Icon name = "layers" / >
< 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 > < strong > Alta contextual < / strong > < span > { contextParent . name } · { contextParent . code } < / span > < / div >
< Link to = { ` /inventarios/ ${ contextParent . id } ` } > Ver padre < / Link >
< / 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 >
{ compactStructuralSummary ? renderCompactStructuralForm ( ) : renderFullForm ( ) }
{ 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 > }
{ ! compactStructuralSummary && editing && id && asset && selectedType && canReadHistory
&& < AssetContextHistoryPanel
asset = { asset }
type = { selectedType }
canManage = { canManageContext }
onChanged = { handleContextChanged }
/ > }
< 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 >
{ editing && id && asset && canReadRelations && selectedType && selectedType . operationalRole !== 'GENERIC'
{ 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 > }
&& < AssetOperationalRelationsPanel assetId = { id } role = { selectedType . operationalRole } canManage = { canManageRelations } / > }
< / 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 === 'findings' && canReadFindingCatalog
{ editing && id && asset && tab === 'dossier' && canReadDossier && < AssetDossierPanel assetId = { id } / > }
&& < AssetFindingCatalogPanel assetId = { id } canManage = { canManageFindingCatalog } / > }
{ 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 === 'dossier' && canReadDossier
{ editing && id && asset && tab === 'files' && canReadMedia && < AssetMediaPanel assetId = { id } assetName = { asset . name } canManage = { canManageMedia } onChanged = { ( ) = > setHistoryRefreshKey ( ( current ) = > current + 1 ) } / > }
&& < AssetDossierPanel assetId = { id } / > }
{ editing && id && tab === 'history' && canReadHistory && < AssetHistoryPanel assetId = { id } refreshKey = { historyRefreshKey } / > }
{ 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 && < div className = "asset-tab-stack" >
{ compactStructuralSummary && asset && selectedType && < AssetContextHistoryPanel
asset = { asset }
type = { selectedType }
canManage = { canManageContext }
onChanged = { handleContextChanged }
/ > }
< AssetHistoryPanel assetId = { id } refreshKey = { historyRefreshKey } / >
< / div > }
< / section > ;
< / section > ;
}
}