@@ -0,0 +1,247 @@
import { useEffect , useMemo , useState } from 'react' ;
import type { FormEvent } from 'react' ;
import { Link , useSearchParams } from 'react-router' ;
import { useAuth } from '../auth/AuthContext' ;
import { Alert , LoadingBlock , errorMessage } from '../components/Feedback' ;
import { Icon } from '../components/Icon' ;
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel' ;
import { assetStatusClass , assetStatusLabel } from '../features/assets/assetPresentation' ;
import {
listAreaCompanyRelations ,
listAssetTreeChildren ,
listInspectionVisits ,
updateAsset ,
} from '../lib/api' ;
import type { AreaCompanyRelation , AssetDetail , AssetListItem } from '../lib/api' ;
import {
listInspectionActsGlobalF4 ,
type InspectionActStatusF4 ,
} from '../lib/inspectionActF4Api' ;
import { listInspectionReportsF4 } from '../lib/reportWorkflowApi' ;
import './territorialInventory.css' ;
type TerritoryTab = 'summary' | 'history' ;
function normalized ( value : string ) {
return value
. normalize ( 'NFD' )
. replace ( /[\u0300-\u036f]/g , '' )
. toLowerCase ( )
. replace ( /[^a-z0-9]+/g , ' ' )
. trim ( ) ;
}
function typeTokens ( asset : AssetDetail ) {
return new Set ( ` ${ normalized ( asset . type . code ) } ${ normalized ( asset . type . name ) } ` . split ( ' ' ) . filter ( Boolean ) ) ;
}
export function isTerritorialInventoryAsset ( asset : AssetDetail ) {
const tokens = typeTokens ( asset ) ;
return tokens . has ( 'departamento' ) || tokens . has ( 'area' ) || tokens . has ( 'yacimiento' ) ;
}
function isOperationalArea ( asset : AssetDetail ) {
return typeTokens ( asset ) . has ( 'area' ) ;
}
function relationRoleLabel ( role : AreaCompanyRelation [ 'relationRole' ] ) {
if ( role === 'OPERATOR' ) return 'Operadora' ;
if ( role === 'TECHNICAL_OPERATOR' ) return 'Operadora técnica' ;
if ( role === 'CONCESSIONAIRE' ) return 'Concesionaria / titular' ;
if ( role === 'PERMIT_HOLDER' ) return 'Permisionaria' ;
if ( role === 'PARTICIPANT' ) return 'Participante' ;
return 'Vinculada' ;
}
export function TerritorialInventoryPage ( { initialAsset } : { initialAsset : AssetDetail } ) {
const [ params , setParams ] = useSearchParams ( ) ;
const requestedTab = params . get ( 'tab' ) as TerritoryTab | null ;
const tab : TerritoryTab = requestedTab === 'history' ? 'history' : 'summary' ;
const { hasPermission } = useAuth ( ) ;
const canEdit = hasPermission ( 'assets.update' ) ;
const canCreate = hasPermission ( 'assets.create' ) ;
const canReadHistory = hasPermission ( 'assets.read_history' ) ;
const canReadRelations = hasPermission ( 'asset_relations.read' ) ;
const canReadActs = hasPermission ( 'inspection_acts.read' ) ;
const canReadReports = hasPermission ( 'inspection_reports.read' ) ;
const canReadInspections = hasPermission ( 'inspections.read' ) ;
const [ asset , setAsset ] = useState ( initialAsset ) ;
const [ children , setChildren ] = useState < AssetListItem [ ] > ( [ ] ) ;
const [ childrenHasMore , setChildrenHasMore ] = useState ( false ) ;
const [ relations , setRelations ] = useState < AreaCompanyRelation [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState ( '' ) ;
const [ success , setSuccess ] = useState ( '' ) ;
const [ editing , setEditing ] = useState ( false ) ;
const [ saving , setSaving ] = useState ( false ) ;
const [ name , setName ] = useState ( initialAsset . name ) ;
const [ description , setDescription ] = useState ( initialAsset . description ? ? '' ) ;
const [ metrics , setMetrics ] = useState ( { activeInspections : 0 , acts : 0 , reports : 0 } ) ;
const area = isOperationalArea ( asset ) ;
const activeRelations = useMemo ( ( ) = > relations . filter ( ( item ) = > item . active ) , [ relations ] ) ;
const loadSummary = async ( ) = > {
setLoading ( true ) ;
setError ( '' ) ;
try {
const [ childPage , loadedRelations ] = await Promise . all ( [
listAssetTreeChildren ( { parentId : asset.id , limit : 100 } ) ,
area && canReadRelations
? listAreaCompanyRelations ( { areaId : asset.id , includeHistory : true } )
: Promise . resolve ( [ ] ) ,
] ) ;
setChildren ( childPage . data ) ;
setChildrenHasMore ( childPage . meta . hasMore ) ;
setRelations ( loadedRelations ) ;
} catch ( requestError ) {
setError ( errorMessage ( requestError ) ) ;
} finally {
setLoading ( false ) ;
}
} ;
useEffect ( ( ) = > { void loadSummary ( ) ; } , [ asset . id , area , canReadRelations ] ) ;
useEffect ( ( ) = > {
if ( ! area ) {
setMetrics ( { activeInspections : 0 , acts : 0 , reports : 0 } ) ;
return ;
}
let cancelled = false ;
const loadMetrics = async ( ) = > {
try {
const actStatuses : InspectionActStatusF4 [ ] = [ 'SEALED' , 'CLOSED' , 'RECTIFIED' ] ;
const [ planned , inProgress , actPages , reportPage ] = await Promise . all ( [
canReadInspections ? listInspectionVisits ( { areaId : asset.id , status : 'PLANNED' , pageSize : 1 } ) : Promise . resolve ( null ) ,
canReadInspections ? listInspectionVisits ( { areaId : asset.id , status : 'IN_PROGRESS' , pageSize : 1 } ) : Promise . resolve ( null ) ,
canReadActs
? Promise . all ( actStatuses . map ( ( status ) = > listInspectionActsGlobalF4 ( { areaId : asset.id , status , pageSize : 1 } ) ) )
: Promise . resolve ( [ ] ) ,
canReadReports ? listInspectionReportsF4 ( { areaId : asset.id , pageSize : 1 } ) : Promise . resolve ( null ) ,
] ) ;
if ( cancelled ) return ;
setMetrics ( {
activeInspections : ( planned ? . meta . total ? ? 0 ) + ( inProgress ? . meta . total ? ? 0 ) ,
acts : actPages.reduce ( ( total , page ) = > total + page . meta . total , 0 ) ,
reports : reportPage?.meta.total ? ? 0 ,
} ) ;
} catch {
if ( ! cancelled ) setMetrics ( { activeInspections : 0 , acts : 0 , reports : 0 } ) ;
}
} ;
void loadMetrics ( ) ;
return ( ) = > { cancelled = true ; } ;
} , [ asset . id , area , canReadActs , canReadReports , canReadInspections ] ) ;
const save = async ( event : FormEvent ) = > {
event . preventDefault ( ) ;
if ( ! canEdit || ! name . trim ( ) ) return ;
setSaving ( true ) ; setError ( '' ) ; setSuccess ( '' ) ;
try {
const saved = await updateAsset ( asset . id , {
name : name.trim ( ) ,
description : description.trim ( ) || null ,
} ) ;
setAsset ( saved ) ;
setName ( saved . name ) ;
setDescription ( saved . description ? ? '' ) ;
setEditing ( false ) ;
setSuccess ( 'Datos actualizados.' ) ;
} catch ( requestError ) {
setError ( errorMessage ( requestError ) ) ;
} finally {
setSaving ( false ) ;
}
} ;
const setTab = ( next : TerritoryTab ) = > {
const nextParams = new URLSearchParams ( params ) ;
next === 'summary' ? nextParams . delete ( 'tab' ) : nextParams . set ( 'tab' , next ) ;
nextParams . delete ( 'advanced' ) ;
setParams ( nextParams ) ;
} ;
return < section className = "narrow-section territory-page" >
< nav className = "breadcrumb territory-breadcrumb" aria-label = "Ruta territorial" >
< Link to = "/inventarios" > Inventarios < / Link > < span > › < / span >
< Link to = "/inventarios?section=territory" > Á reas y yacimientos < / Link > < span > › < / span > < strong > { asset . name } < / strong >
< / nav >
< header className = "territory-hero" >
< div >
< span className = "eyebrow" > { asset . type . name . toUpperCase ( ) } < / span >
< h1 > { asset . name } < / h1 >
< p > < strong > { asset . code } < / strong > < / p >
< / div >
< div className = "territory-hero-actions" >
< span className = { ` status-badge ${ assetStatusClass ( asset . informationStatus ) } ` } > { assetStatusLabel ( asset . informationStatus ) } < / span >
{ canEdit && tab === 'summary' && < button type = "button" className = "button secondary" onClick = { ( ) = > setEditing ( ( current ) = > ! current ) } > < Icon name = "edit" / > { editing ? 'Cancelar edición' : 'Editar' } < / button > }
{ canCreate && tab === 'summary' && < Link className = "button primary" to = { ` /inventarios/nuevo?parentId= ${ asset . id } ` } > < Icon name = "plus" / > Agregar elemento < / Link > }
< / div >
< / header >
< div className = "territory-purpose-note" > < Icon name = "map" / > < span > Este nivel organiza el territorio y la navegación . Los hallazgos se cargan sobre elementos inspeccionables , no sobre { asset . type . name . toLowerCase ( ) } s . < / span > < / div >
< nav className = "territory-tabs" aria-label = "Secciones territoriales" >
< button type = "button" className = { tab === 'summary' ? 'active' : '' } onClick = { ( ) = > setTab ( 'summary' ) } > Resumen < / button >
{ canReadHistory && < button type = "button" className = { tab === 'history' ? 'active' : '' } onClick = { ( ) = > setTab ( 'history' ) } > Historial < / button > }
< / nav >
{ error && < Alert > { error } < / Alert > } { success && < Alert type = "success" > { success } < / Alert > }
{ tab === 'summary' && < div className = "territory-stack" >
< article className = "panel territory-card" >
< div className = "territory-card-heading" >
< div > < span className = "eyebrow" > DATOS BÁSICOS < / span > < h2 > Identificación < / h2 > < / div >
< / div >
{ editing ? < form className = "territory-edit-form" onSubmit = { save } >
< label className = "field" > < span > Nombre < / span > < input value = { name } onChange = { ( event ) = > setName ( event . target . value ) } required maxLength = { 200 } / > < / label >
< label className = "field" > < span > Descripción < em > opcional < / em > < / span > < textarea value = { description } onChange = { ( event ) = > setDescription ( event . target . value ) } rows = { 3 } maxLength = { 4000 } / > < / label >
< div className = "form-actions" > < button type = "button" className = "button secondary" onClick = { ( ) = > { setEditing ( false ) ; setName ( asset . name ) ; setDescription ( asset . description ? ? '' ) ; } } > Cancelar < / button > < button className = "button primary" disabled = { saving } > < Icon name = "check" / > { saving ? 'Guardando…' : 'Guardar' } < / button > < / div >
< / form > : < div className = "territory-data-grid" >
< div > < small > Tipo < / small > < strong > { asset . type . name } < / strong > < / div >
< div > < small > Código DH < / small > < strong > { asset . code } < / strong > < / div >
{ asset . description && < div className = "wide" > < small > Descripción < / small > < strong > { asset . description } < / strong > < / div > }
< / div > }
< / article >
< article className = "panel territory-card" >
< div className = "territory-card-heading" >
< div > < span className = "eyebrow" > CONTENIDO < / span > < h2 > Registros dentro de { asset . name } < / h2 > < p > Acceso directo a los niveles que dependen de este registro . < / p > < / div >
< span className = "count-pill" > { children . length } { childrenHasMore ? '+' : '' } < / span >
< / div >
{ loading ? < LoadingBlock label = "Cargando contenido…" / > : children . length === 0 ? < div className = "inline-empty" > Todavía no hay registros dentro de { asset . name } . < / div > : < div className = "territory-child-list" >
{ children . map ( ( child ) = > < Link to = { ` /inventarios/ ${ child . id } ` } key = { child . id } className = "territory-child-row" >
< span className = "asset-symbol" > < Icon name = "layers" / > < / span >
< span > < strong > { child . name } < / strong > < small > { child . type . name } · { child . code } < / small > < / span >
< span className = { ` status-badge ${ assetStatusClass ( child . informationStatus ) } ` } > { assetStatusLabel ( child . informationStatus ) } < / span >
< Icon name = "chevron" size = { 16 } / >
< / Link > ) }
< / div > }
{ childrenHasMore && < div className = "territory-card-actions" > < Link className = "button secondary" to = { ` /inventarios?section=territory&parentId= ${ asset . id } ` } > Ver contenido completo < / Link > < / div > }
< / article >
{ area && canReadRelations && < article className = "panel territory-card" >
< div className = "territory-card-heading" > < div > < span className = "eyebrow" > OPERACIÓN < / span > < h2 > Empresas vinculadas < / h2 > < p > Quién opera o participa actualmente en esta á rea . < / p > < / div > < span className = "count-pill" > { activeRelations . length } < / span > < / div >
{ loading ? < LoadingBlock label = "Cargando empresas…" / > : activeRelations . length === 0 ? < div className = "inline-empty" > No hay empresas con vínculo vigente . < / div > : < div className = "territory-company-list" >
{ activeRelations . map ( ( relation ) = > < Link to = { ` /inventarios/ ${ relation . company . id } ` } key = { relation . id } className = "territory-company-row" > < span > < strong > { relation . company . name } < / strong > < small > { relationRoleLabel ( relation . relationRole ) } < / small > < / span > < Icon name = "chevron" size = { 16 } / > < / Link > ) }
< / div > }
< / article > }
{ area && ( canReadInspections || canReadActs || canReadReports ) && < article className = "panel territory-card" >
< div className = "territory-card-heading" > < div > < span className = "eyebrow" > ACTIVIDAD < / span > < h2 > Actividad del á rea < / h2 > < p > Resumen documental y operativo , sin mezclarlo con hallazgos de los elementos inspeccionados . < / p > < / div > < / div >
< div className = "territory-metrics" >
{ canReadInspections && < div > < small > Inspecciones activas < / small > < strong > { metrics . activeInspections } < / strong > < / div > }
{ canReadActs && < div > < small > Actas emitidas < / small > < strong > { metrics . acts } < / strong > < / div > }
{ canReadReports && < div > < small > Informes < / small > < strong > { metrics . reports } < / strong > < / div > }
< / div >
< / article > }
< div className = "territory-advanced-link" > < span > Los datos estructurales , procedencia , ubicación y opciones técnicas quedan fuera de la vista cotidiana . < / span > < Link to = { ` /inventarios/ ${ asset . id } ?advanced=1 ` } > Administración avanzada < / Link > < / div >
< / div > }
{ tab === 'history' && canReadHistory && < AssetHistoryPanel assetId = { asset . id } / > }
< / section > ;
}