import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useSearchParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { hasAdministratorRole } from '../auth/adminAccess'; 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 { getAssetLineage, listAreaCompanyRelations, listAssetTreeChildren, listInspectionVisits, updateAsset, } from '../lib/api'; import type { AreaCompanyRelation, AssetDetail, AssetLineageItem, 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 tokensForType(type: { code: string; name: string }) { return new Set(`${normalized(type.code)} ${normalized(type.name)}`.split(' ').filter(Boolean)); } function typeTokens(asset: AssetDetail) { return tokensForType(asset.type); } 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 isYacimiento(asset: AssetDetail) { return typeTokens(asset).has('yacimiento'); } function lineageArea(lineage: AssetLineageItem[]) { return [...lineage].reverse().find((item) => tokensForType(item.type).has('area')) ?? null; } 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 { user, 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 canAdvanced = hasAdministratorRole(user); const [asset, setAsset] = useState(initialAsset); const [children, setChildren] = useState([]); const [childrenHasMore, setChildrenHasMore] = useState(false); const [lineage, setLineage] = useState([]); const [relations, setRelations] = useState([]); 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 yacimiento = isYacimiento(asset); const activeRelations = useMemo(() => relations.filter((item) => item.active), [relations]); const relationAreaContext = useMemo(() => { if (area) return { id: asset.id, code: asset.code, name: asset.name }; if (asset.operationalArea) return asset.operationalArea; const ancestor = lineageArea(lineage); return ancestor ? { id: ancestor.id, code: ancestor.code, name: ancestor.name } : null; }, [area, asset.id, asset.code, asset.name, asset.operationalArea, lineage]); const loadSummary = async () => { setLoading(true); setError(''); try { const loadedLineage = await getAssetLineage(asset.id); const ancestorArea = lineageArea(loadedLineage); const relationAreaId = area ? asset.id : asset.operationalArea?.id ?? ancestorArea?.id ?? null; const [childPage, loadedRelations] = await Promise.all([ listAssetTreeChildren({ parentId: asset.id, limit: 100 }), relationAreaId && canReadRelations ? listAreaCompanyRelations({ areaId: relationAreaId, includeHistory: true }) : Promise.resolve([]), ]); setLineage(loadedLineage); 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
{asset.type.name.toUpperCase()}

{asset.name}

{asset.code}

{assetStatusLabel(asset.informationStatus)} {canEdit && tab === 'summary' && } {canCreate && tab === 'summary' && Agregar elemento}
Este nivel organiza el territorio y la navegación. Los hallazgos se cargan sobre elementos inspeccionables, no sobre {asset.type.name.toLowerCase()}s.
{error && {error}}{success && {success}} {tab === 'summary' &&
DATOS BÁSICOS

Identificación

{editing ?