From 1b769a0ff4f7c8b2094b8d31e36a016e9b81b5ec Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 22:11:28 -0300 Subject: [PATCH] feat(web): simplify canonical inventory details --- .../src/pages/SimpleInventoryDetailPage.tsx | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 web-v2/src/pages/SimpleInventoryDetailPage.tsx diff --git a/web-v2/src/pages/SimpleInventoryDetailPage.tsx b/web-v2/src/pages/SimpleInventoryDetailPage.tsx new file mode 100644 index 0000000..2617513 --- /dev/null +++ b/web-v2/src/pages/SimpleInventoryDetailPage.tsx @@ -0,0 +1,350 @@ +import { lazy, Suspense, 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 { AssetDossierPanel } from '../features/assets/AssetDossierPanel'; +import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel'; +import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel'; +import { AssetMediaPanel } from '../features/assets/AssetMediaPanel'; +import { AssetTechnicalDataPanel } from '../features/assets/AssetTechnicalDataPanel'; +import { + ASSET_OPERATIONAL_STATUSES, + ASSET_STATUSES, + assetOperationalStatusLabel, + assetStatusClass, + assetStatusLabel, +} from '../features/assets/assetPresentation'; +import { + getAssetLineage, + listAssetTreeChildren, + updateAsset, + updateAssetInformationStatus, + updateAssetOperationalStatus, +} from '../lib/api'; +import type { + AssetDetail, + AssetInformationStatus, + AssetLineageItem, + AssetListItem, + AssetOperationalStatus, +} from '../lib/api'; +import { getInventoryTechnicalValues } from '../lib/inventoryStructureApi'; +import type { InventoryTechnicalValues } from '../lib/inventoryStructureApi'; +import './SimpleInventoryDetailPage.css'; + +const AssetGeometryEditor = lazy(() => + import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor })), +); + +export type SimpleInventoryKind = + | 'DEPARTAMENTO' + | 'AREA' + | 'YACIMIENTO' + | 'INSTALACION' + | 'SUBINSTALACION'; + +type SimpleTab = 'summary' | 'activity' | 'findings' | 'location' | 'photos' | 'history'; + +function normalized(value: string | null | undefined): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .toLowerCase() + .replace(/[_\s]+/g, '-'); +} + +export function simpleInventoryKind(asset: AssetDetail): SimpleInventoryKind | null { + const code = normalized(asset.type.code); + const name = normalized(asset.type.name); + const values = new Set([code, name]); + if (values.has('departamento')) return 'DEPARTAMENTO'; + if (values.has('area')) return 'AREA'; + if (values.has('yacimiento')) return 'YACIMIENTO'; + if (values.has('subinstalacion')) return 'SUBINSTALACION'; + if ( + values.has('instalacion') + || values.has('instalacion-de-superficie') + || values.has('instalacion-superficie') + ) return 'INSTALACION'; + return null; +} + +export function isSimpleInventoryAsset(asset: AssetDetail): boolean { + return simpleInventoryKind(asset) !== null; +} + +const labels: Record = { + DEPARTAMENTO: 'Departamento', + AREA: 'Área', + YACIMIENTO: 'Yacimiento', + INSTALACION: 'Instalación', + SUBINSTALACION: 'Subinstalación', +}; + +const childLabels: Partial> = { + DEPARTAMENTO: 'Área', + AREA: 'Yacimiento', + YACIMIENTO: 'Instalación', + INSTALACION: 'Subinstalación', +}; + +function lineageLabel(item: AssetLineageItem): string { + return item.commonName ? `${item.name} · ${item.commonName}` : item.name; +} + +export function SimpleInventoryDetailPage({ initialAsset }: { initialAsset: AssetDetail }) { + const [params, setParams] = useSearchParams(); + const { user, hasPermission } = useAuth(); + const kind = simpleInventoryKind(initialAsset); + const [asset, setAsset] = useState(initialAsset); + const [lineage, setLineage] = useState([]); + const [children, setChildren] = useState([]); + const [childrenHasMore, setChildrenHasMore] = useState(false); + const [technical, setTechnical] = useState(null); + const [loadingRelated, setLoadingRelated] = useState(true); + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [name, setName] = useState(initialAsset.name); + const [commonName, setCommonName] = useState(initialAsset.commonName ?? ''); + const [description, setDescription] = useState(initialAsset.description ?? ''); + const [status, setStatus] = useState(initialAsset.informationStatus); + const [operationalStatus, setOperationalStatus] = useState(initialAsset.operationalStatus); + const [historyRefreshKey, setHistoryRefreshKey] = useState(0); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const canEdit = hasPermission('assets.update'); + const canCreate = hasPermission('assets.create'); + const canChangeStatus = hasPermission('assets.change_status'); + const canChangeOperationalStatus = hasPermission('assets.change_operational_status'); + const canReadHistory = hasPermission('assets.read_history'); + const canReadMedia = hasPermission('assets.read_media'); + const canManageMedia = hasPermission('assets.manage_media'); + const canEditGeometry = hasPermission('assets.update_geometry'); + 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 canAdvanced = hasAdministratorRole(user); + + const inspectable = kind === 'YACIMIENTO' || kind === 'INSTALACION' || kind === 'SUBINSTALACION'; + const technicalLevel = kind === 'INSTALACION' || kind === 'SUBINSTALACION'; + const hasChildren = kind !== 'SUBINSTALACION'; + const requestedTab = params.get('tab') as SimpleTab | null; + const availableTabs = useMemo(() => new Set([ + 'summary', + ...(inspectable && canReadDossier ? ['activity' as const] : []), + ...(technicalLevel && canReadFindingCatalog ? ['findings' as const] : []), + 'location', + ...(canReadMedia ? ['photos' as const] : []), + ...(canReadHistory ? ['history' as const] : []), + ]), [inspectable, technicalLevel, canReadDossier, canReadFindingCatalog, canReadMedia, canReadHistory]); + const tab: SimpleTab = requestedTab && availableTabs.has(requestedTab) ? requestedTab : 'summary'; + + const loadRelated = async () => { + setLoadingRelated(true); + try { + const [loadedLineage, childPage, loadedTechnical] = await Promise.all([ + getAssetLineage(asset.id), + hasChildren + ? listAssetTreeChildren({ parentId: asset.id, limit: 100 }) + : Promise.resolve({ data: [] as AssetListItem[], meta: { count: 0, hasMore: false } }), + technicalLevel + ? getInventoryTechnicalValues(asset.id).catch(() => null) + : Promise.resolve(null), + ]); + setLineage(loadedLineage); + setChildren(childPage.data); + setChildrenHasMore(childPage.meta.hasMore); + setTechnical(loadedTechnical); + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setLoadingRelated(false); + } + }; + + useEffect(() => { void loadRelated(); }, [asset.id]); + + if (!kind) return Este registro no pertenece al modelo simple de Inventarios.; + + const save = async (event: FormEvent) => { + event.preventDefault(); + if (!name.trim()) return; + setSaving(true); + setError(''); + setSuccess(''); + try { + let saved = asset; + if (canEdit) { + saved = await updateAsset(asset.id, { + name: name.trim(), + commonName: commonName.trim() || null, + description: description.trim() || null, + }); + } + if (canChangeStatus && saved.informationStatus !== status) { + saved = await updateAssetInformationStatus(asset.id, status); + } + if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) { + saved = await updateAssetOperationalStatus(asset.id, operationalStatus); + } + setAsset(saved); + setName(saved.name); + setCommonName(saved.commonName ?? ''); + setDescription(saved.description ?? ''); + setStatus(saved.informationStatus); + setOperationalStatus(saved.operationalStatus); + setEditing(false); + setHistoryRefreshKey((current) => current + 1); + setSuccess('Registro actualizado.'); + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setSaving(false); + } + }; + + const cancelEdit = () => { + setEditing(false); + setName(asset.name); + setCommonName(asset.commonName ?? ''); + setDescription(asset.description ?? ''); + setStatus(asset.informationStatus); + setOperationalStatus(asset.operationalStatus); + }; + + const setTab = (next: SimpleTab) => { + const nextParams = new URLSearchParams(params); + next === 'summary' ? nextParams.delete('tab') : nextParams.set('tab', next); + nextParams.delete('advanced'); + setParams(nextParams); + }; + + const childLabel = childLabels[kind]; + const parentLabel = kind === 'AREA' + ? 'Departamento' + : kind === 'YACIMIENTO' + ? 'Área' + : kind === 'INSTALACION' + ? 'Yacimiento' + : kind === 'SUBINSTALACION' + ? 'Instalación' + : null; + + return
+ + +
+
+ {labels[kind].toUpperCase()} +

{asset.name}

+
{asset.code}{asset.commonName && {asset.commonName}}
+
+
+ {assetStatusLabel(asset.informationStatus)} + {tab === 'summary' && (canEdit || canChangeStatus || canChangeOperationalStatus) && } + {tab === 'summary' && canCreate && childLabel && Agregar {childLabel.toLowerCase()}} +
+
+ +
+ + {inspectable + ? 'Ficha operativa: identidad, ubicación y actividad de inspección. La información administrativa avanzada queda fuera de la vista cotidiana.' + : 'Este nivel organiza la estructura del Inventario. La vista cotidiana muestra sólo los datos necesarios para ubicar y navegar.'} +
+ + + + {error && {error}} + {success && {success}} + + {tab === 'summary' &&
+
+
DATOS PRINCIPALES

Identificación

+ {editing ?
+
+ + +
+
+ Más datos +