diff --git a/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx b/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx new file mode 100644 index 0000000..af221b1 --- /dev/null +++ b/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx @@ -0,0 +1,205 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; +import { Icon } from '../components/Icon'; +import { getFindingCatalogAdmin, listAssetTypes } from '../lib/api'; +import type { AssetType, FindingAdminCatalog } from '../lib/api'; +import { listInventoryFamiliesAdmin } from '../lib/inventoryStructureApi'; +import type { InventoryFamily } from '../lib/inventoryStructureApi'; + +type CanonicalKind = 'EMPRESA' | 'DEPARTAMENTO' | 'AREA' | 'YACIMIENTO' | 'INSTALACION' | 'SUBINSTALACION'; + +type StructuralField = { + label: string; + detail: string; + required?: boolean; + relation?: boolean; +}; + +const LEVELS: Array<{ kind: CanonicalKind; label: string; description: string; fields: StructuralField[] }> = [ + { + kind: 'EMPRESA', + label: 'Empresa', + description: 'Maestro independiente. La Empresa se relaciona directamente con uno o más Yacimientos.', + fields: [{ label: 'Nombre', detail: 'Nombre de la Empresa / Operadora.', required: true }], + }, + { + kind: 'DEPARTAMENTO', + label: 'Departamento', + description: 'Raíz territorial. No depende de ningún otro nivel.', + fields: [{ label: 'Nombre', detail: 'Nombre del Departamento.', required: true }], + }, + { + kind: 'AREA', + label: 'Área', + description: 'El Área sólo tiene Nombre y pertenece obligatoriamente a un Departamento.', + fields: [ + { label: 'Departamento', detail: 'Relación obligatoria Departamento → Área.', required: true, relation: true }, + { label: 'Nombre', detail: 'Nombre del Área.', required: true }, + ], + }, + { + kind: 'YACIMIENTO', + label: 'Yacimiento', + description: 'El Yacimiento concentra el contexto operativo: Área, Tipo de concesión y Empresa relacionada.', + fields: [ + { label: 'Área', detail: 'Relación obligatoria Área → Yacimiento.', required: true, relation: true }, + { label: 'Tipo de concesión', detail: 'Explotación o Exploración, según la fuente.', required: true, relation: true }, + { label: 'Empresa relacionada', detail: 'Empresa / Operadora asociada directamente al Yacimiento.', required: true, relation: true }, + { label: 'Nombre', detail: 'Nombre del Yacimiento. Puede repetirse en otra Área.', required: true }, + ], + }, + { + kind: 'INSTALACION', + label: 'Instalación', + description: 'Cada Instalación pertenece a un Yacimiento y posee una clasificación técnica.', + fields: [ + { label: 'Yacimiento', detail: 'Relación obligatoria Yacimiento → Instalación.', required: true, relation: true }, + { label: 'Tipo de instalación', detail: 'Clasificación técnica tomada del modelo de Instalaciones.', required: true, relation: true }, + ], + }, + { + kind: 'SUBINSTALACION', + label: 'Subinstalación', + description: 'Cada Subinstalación pertenece a una Instalación y usa una clasificación compatible con ella.', + fields: [ + { label: 'Instalación', detail: 'Relación obligatoria Instalación → Subinstalación.', required: true, relation: true }, + { label: 'Tipo de subinstalación', detail: 'Clasificación técnica compatible con el Tipo de instalación padre.', required: true, relation: true }, + ], + }, +]; + +const EMPTY_CATALOG: FindingAdminCatalog = { categories: [], items: [] }; + +function canonicalType(types: AssetType[], kind: CanonicalKind): AssetType | null { + if (kind === 'EMPRESA') return types.find((type) => type.operationalRole === 'COMPANY' && type.isActive) ?? null; + if (kind === 'AREA') return types.find((type) => type.operationalRole === 'AREA' && type.isActive) ?? null; + return types.find((type) => type.code.toLowerCase() === kind.toLowerCase() && type.isActive) ?? null; +} + +export function AuthoritativeInventoryConfigPage() { + const [types, setTypes] = useState([]); + const [families, setFamilies] = useState([]); + const [catalog, setCatalog] = useState(EMPTY_CATALOG); + const [selectedKind, setSelectedKind] = useState('AREA'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + Promise.all([listAssetTypes(), listInventoryFamiliesAdmin(), getFindingCatalogAdmin()]) + .then(([loadedTypes, loadedFamilies, loadedCatalog]) => { + setTypes(loadedTypes); + setFamilies(loadedFamilies); + setCatalog(loadedCatalog); + }) + .catch((requestError) => setError(errorMessage(requestError))) + .finally(() => setLoading(false)); + }, []); + + const level = LEVELS.find((item) => item.kind === selectedKind) ?? LEVELS[0]; + const selectedType = canonicalType(types, selectedKind); + const commonAttributes = useMemo(() => { + const attributes = selectedType?.attributes.filter((attribute) => attribute.isActive) ?? []; + // "Tipo de instalación" is represented by inventoryFamilyId, not duplicated + // as a free-text value in the authoritative structural presentation. + return selectedKind === 'INSTALACION' + ? attributes.filter((attribute) => attribute.code !== 'tipo_instalacion') + : attributes; + }, [selectedKind, selectedType]); + const installationFamilies = families.filter((family) => family.level === 'INSTALLATION' && family.isActive !== false); + const subinstallationFamilies = families.filter((family) => family.level === 'SUBINSTALLATION' && family.isActive !== false); + const activeFindings = catalog.items.filter((item) => item.isActive).length; + + if (loading) return ; + + return
+
+
+ ADMINISTRACIÓN · MODELO AUTORITATIVO +

Configuración de Inventarios

+

Relaciones fijas cargadas desde los SQL definitivos. Los vínculos estructurales no son campos de texto editables.

+
+ Agregar registro +
+ {error && {error}} + +
+
+
+ ESTRUCTURA FÍSICA +

Jerarquía obligatoria

+

Empresa es un maestro independiente y se asocia al Yacimiento. El árbol físico queda separado y sin ambigüedades.

+
+
+
+
1Departamentoraíz
+
2ÁreaDepartamento
+
3YacimientoÁrea + Empresa + concesión
+
4InstalaciónYacimiento
+
5SubinstalaciónInstalación
+
+
+ +

Empresa: ya no pertenece al Área. La relación canónica es Yacimiento → Empresa relacionada.

+
+
+ +
+
+
+ INFORMACIÓN ESTRUCTURAL +

{level.label}

+

{level.description}

+
+ Modelo SQL +
+
+ {LEVELS.map((item) => )} +
+ +
+ {level.fields.map((field, index) =>
+ {index + 1} + + {field.label} + {field.detail} + + + {field.relation && Relación} + {field.required && Obligatorio} + +
)} + {commonAttributes.map((attribute, index) =>
+ {level.fields.length + index + 1} + + {attribute.name} + {attribute.code} · Campo común del nivel + + {attribute.isRequired && Obligatorio} +
)} +
+
+ +
+
+
MODELO TÉCNICO

Tipos de Instalación

{installationFamilies.length} clasificaciones exactas del SQL.

+
+ {installationFamilies.map((family) =>
{family.name}{family.findingCount ?? 0} Hallazgos asociados{family.code}
)} +
+
+
+
MODELO TÉCNICO

Tipos de Subinstalación

{subinstallationFamilies.length} clasificaciones, cada una vinculada a su Tipo de instalación.

+
+ {subinstallationFamilies.map((family) =>
{family.name}{family.parentFamilies.map((parent) => parent.name).join(' · ') || 'Sin padre'} · {family.findingCount ?? 0} Hallazgos
)} +
+
+
+ +
+
+
HALLAZGOS

Catálogo contextual

{activeFindings} Hallazgos cargados con sus relaciones exactas a Instalaciones y Subinstalaciones.

+ Abrir catálogo +
+
+
; +}