diff --git a/web-v2/src/pages/AssetTypesPage.tsx b/web-v2/src/pages/AssetTypesPage.tsx index 38d9055..a7ebf83 100644 --- a/web-v2/src/pages/AssetTypesPage.tsx +++ b/web-v2/src/pages/AssetTypesPage.tsx @@ -1,32 +1,26 @@ import { SearchableSelect } from '../components/SearchableSelect'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; +import { Link } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { - bootstrapMasterDefaults, createAssetAttribute, - createAssetType, - enrichMasterDefaults, - getMasterEnrichmentStatus, listAssetTypes, updateAssetAttribute, - updateAssetType, } from '../lib/api'; import type { AssetAttributeDataType, AssetAttributeDefinition, AssetType, - AssetTypeOperationalRole, - MasterEnrichmentStatus, } from '../lib/api'; - -const OPERATIONAL_ROLES: Array<{ value: AssetTypeOperationalRole; label: string; help: string }> = [ - { value: 'GENERIC', label: 'Elemento operativo / genérico', help: 'Instalaciones, estaciones, equipos y demás elementos administrables.' }, - { value: 'AREA', label: 'Área', help: 'Representa el ámbito territorial de operación.' }, - { value: 'COMPANY', label: 'Organización', help: 'Empresa, UTE u otra organización vinculable a áreas.' }, -]; +import { + createInventoryFamily, + listInventoryFamiliesAdmin, + updateInventoryFamily, +} from '../lib/inventoryStructureApi'; +import type { InventoryFamily } from '../lib/inventoryStructureApi'; const ATTRIBUTE_TYPES: Array<{ value: AssetAttributeDataType; label: string }> = [ { value: 'TEXT', label: 'Texto' }, @@ -37,27 +31,50 @@ const ATTRIBUTE_TYPES: Array<{ value: AssetAttributeDataType; label: string }> = { value: 'SELECT', label: 'Lista de opciones' }, ]; +type CanonicalKind = 'EMPRESA' | 'AREA' | 'YACIMIENTO' | 'INSTALACION' | 'SUBINSTALACION'; +type FamilyEditor = InventoryFamily | 'new' | null; + +const LEVELS: Array<{ kind: CanonicalKind; label: string; description: string }> = [ + { kind: 'EMPRESA', label: 'Empresa', description: 'Maestro independiente. Se vincula temporalmente a un Área.' }, + { kind: 'AREA', label: 'Área', description: 'Raíz territorial de la estructura física.' }, + { kind: 'YACIMIENTO', label: 'Yacimiento', description: 'Pertenece a un Área; su nombre puede repetirse en otra Área.' }, + { kind: 'INSTALACION', label: 'Instalación', description: 'Instancia física dentro de un Yacimiento y con clasificación técnica.' }, + { kind: 'SUBINSTALACION', label: 'Subinstalación', description: 'Instancia física dentro de una Instalación y con clasificación técnica.' }, +]; + +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; + const code = kind.toLowerCase(); + return types.find((type) => type.code.toLowerCase() === code && type.isActive) ?? null; +} + function attributeTypeLabel(value: AssetAttributeDataType) { return ATTRIBUTE_TYPES.find((item) => item.value === value)?.label ?? value; } -function typeCodeFromName(value: string) { - return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 80); +function attributeCodeFromName(value: string) { + return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim() + .replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 80); } export function AssetTypesPage() { const { hasPermission } = useAuth(); const canManage = hasPermission('asset_types.manage'); const [types, setTypes] = useState([]); - const [selectedId, setSelectedId] = useState(null); - const [creating, setCreating] = useState(false); - const [code, setCode] = useState(''); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [canBeRoot, setCanBeRoot] = useState(false); - const [isActive, setIsActive] = useState(true); - const [operationalRole, setOperationalRole] = useState('GENERIC'); - const [parentTypeIds, setParentTypeIds] = useState([]); + const [families, setFamilies] = useState([]); + const [selectedKind, setSelectedKind] = useState('INSTALACION'); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const [familyEditor, setFamilyEditor] = useState(null); + const [familyLevel, setFamilyLevel] = useState<'INSTALLATION' | 'SUBINSTALLATION'>('INSTALLATION'); + const [familyName, setFamilyName] = useState(''); + const [familyParentId, setFamilyParentId] = useState(''); + const [familyActive, setFamilyActive] = useState(true); + const [attributeEditor, setAttributeEditor] = useState(null); const [attributeCode, setAttributeCode] = useState(''); const [attributeName, setAttributeName] = useState(''); @@ -67,166 +84,154 @@ export function AssetTypesPage() { const [attributeUnit, setAttributeUnit] = useState(''); const [attributeOptions, setAttributeOptions] = useState(''); const [attributeOrder, setAttributeOrder] = useState(0); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - const [enrichment, setEnrichment] = useState(null); - const selected = types.find((type) => type.id === selectedId) ?? null; - - const selectType = (type: AssetType) => { - setCreating(false); setSelectedId(type.id); setCode(type.code); setName(type.name); - setDescription(type.description); setCanBeRoot(type.canBeRoot); setIsActive(type.isActive); - setOperationalRole(type.operationalRole); - setParentTypeIds(type.allowedParentTypes.map((parent) => parent.id)); - setAttributeEditor(null); setError(''); setSuccess(''); - }; - - const startCreate = () => { - setCreating(true); setSelectedId(null); setCode(''); setName(''); setDescription(''); - setCanBeRoot(false); setIsActive(true); setOperationalRole('GENERIC'); setParentTypeIds([]); - setAttributeEditor(null); setError(''); setSuccess(''); - }; - - const load = async (preferId?: string) => { - const loaded = await listAssetTypes(); - setTypes(loaded); - if (loaded.length > 0) { - try { setEnrichment(await getMasterEnrichmentStatus()); } catch { setEnrichment(null); } - } else { - setEnrichment(null); - } - const next = loaded.find((type) => type.id === preferId) ?? loaded[0]; - if (next) selectType(next); + const load = async () => { + const [loadedTypes, loadedFamilies] = await Promise.all([ + listAssetTypes(), + listInventoryFamiliesAdmin(), + ]); + setTypes(loadedTypes); + setFamilies(loadedFamilies); }; useEffect(() => { load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, []); - const toggleParent = (id: string) => setParentTypeIds((current) => - current.includes(id) ? current.filter((value) => value !== id) : [...current, id], - ); + const selectedType = canonicalType(types, selectedKind); + const installationFamilies = useMemo(() => families.filter((item) => item.level === 'INSTALLATION'), [families]); + const subinstallationFamilies = useMemo(() => families.filter((item) => item.level === 'SUBINSTALLATION'), [families]); - const installDefaultMaster = async () => { - const confirmed = window.confirm( - '¿Instalar la configuración inicial de Hidrocarburos?\n\nSe crearán tipos, jerarquías y atributos base. No se crearán empresas, áreas ni registros reales.', - ); - if (!confirmed) return; - setSaving(true); setError(''); setSuccess(''); - try { - const result = await bootstrapMasterDefaults(); - setTypes(result.data); - const next = result.data.find((type) => type.code === 'area') ?? result.data[0]; - if (next) selectType(next); - setSuccess(`Configuración inicial instalada: ${result.createdTypeCount} tipos, ${result.createdAttributeCount} atributos y ${result.createdParentRuleCount} reglas de jerarquía.`); - } catch (requestError) { setError(errorMessage(requestError)); } - finally { setSaving(false); } + const openNewFamily = (level: 'INSTALLATION' | 'SUBINSTALLATION') => { + setFamilyEditor('new'); setFamilyLevel(level); setFamilyName(''); setFamilyParentId(''); setFamilyActive(true); + setError(''); setSuccess(''); + }; + const openFamily = (family: InventoryFamily) => { + setFamilyEditor(family); setFamilyLevel(family.level); setFamilyName(family.name); + setFamilyParentId(family.parentFamilyId ?? ''); setFamilyActive(family.isActive !== false); + setError(''); setSuccess(''); }; - const enrichTechnicalCatalog = async () => { - const confirmed = window.confirm( - '¿Completar el catálogo técnico de Hidrocarburos?\n\nSólo se agregarán tipos, atributos y relaciones de jerarquía que falten. No se modificarán tipos existentes ni se crearán registros reales.', - ); - if (!confirmed) return; - setSaving(true); setError(''); setSuccess(''); - try { - const result = await enrichMasterDefaults(); - setTypes(result.data); - setEnrichment(await getMasterEnrichmentStatus()); - const next = result.data.find((type) => type.id === selectedId) ?? result.data.find((type) => type.code === 'area') ?? result.data[0]; - if (next) selectType(next); - setSuccess(`Catálogo técnico completado: ${result.createdTypeCount} tipos, ${result.createdAttributeCount} atributos y ${result.createdParentRuleCount} reglas nuevas.`); - } catch (requestError) { setError(errorMessage(requestError)); } - finally { setSaving(false); } - }; - - const saveType = async (event: FormEvent) => { + const saveFamily = async (event: FormEvent) => { event.preventDefault(); setSaving(true); setError(''); setSuccess(''); try { - const saved = creating - ? await createAssetType({ code, name, description, canBeRoot, operationalRole, allowedParentTypeIds: parentTypeIds }) - : await updateAssetType(selected!.id, { name, description, canBeRoot, isActive, operationalRole, allowedParentTypeIds: parentTypeIds }); - await load(saved.id); - setSuccess(creating ? 'Tipo de elemento creado correctamente' : 'Tipo de elemento actualizado'); - setCreating(false); + if (familyEditor === 'new') { + await createInventoryFamily({ + level: familyLevel, + name: familyName.trim(), + parentFamilyId: familyLevel === 'SUBINSTALLATION' ? familyParentId : null, + }); + setSuccess(`${familyLevel === 'INSTALLATION' ? 'Tipo de Instalación' : 'Tipo de Subinstalación'} creado.`); + } else if (familyEditor) { + await updateInventoryFamily(familyEditor.id, { + name: familyName.trim(), + parentFamilyId: familyLevel === 'SUBINSTALLATION' ? familyParentId : null, + isActive: familyActive, + }); + setSuccess('Clasificación actualizada.'); + } + await load(); + setFamilyEditor(null); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const openAttribute = (attribute: AssetAttributeDefinition | 'new') => { - setAttributeEditor(attribute); + setAttributeEditor(attribute); setError(''); setSuccess(''); if (attribute === 'new') { - setAttributeCode(''); setAttributeName(''); setAttributeType('TEXT'); - setAttributeRequired(false); setAttributeActive(true); setAttributeUnit(''); - setAttributeOptions(''); setAttributeOrder(selected?.attributes.length ?? 0); + setAttributeCode(''); setAttributeName(''); setAttributeType('TEXT'); setAttributeRequired(false); + setAttributeActive(true); setAttributeUnit(''); setAttributeOptions(''); + setAttributeOrder(selectedType?.attributes.length ?? 0); } else { - setAttributeCode(attribute.code); setAttributeName(attribute.name); - setAttributeType(attribute.dataType); setAttributeRequired(attribute.isRequired); - setAttributeActive(attribute.isActive); setAttributeUnit(attribute.unit ?? ''); - setAttributeOptions(attribute.options?.join('\n') ?? ''); setAttributeOrder(attribute.sortOrder); + setAttributeCode(attribute.code); setAttributeName(attribute.name); setAttributeType(attribute.dataType); + setAttributeRequired(attribute.isRequired); setAttributeActive(attribute.isActive); + setAttributeUnit(attribute.unit ?? ''); setAttributeOptions(attribute.options?.join('\n') ?? ''); + setAttributeOrder(attribute.sortOrder); } - setError(''); setSuccess(''); }; const saveAttribute = async (event: FormEvent) => { event.preventDefault(); - if (!selected || !attributeEditor) return; + if (!selectedType || !attributeEditor) return; setSaving(true); setError(''); setSuccess(''); const options = attributeOptions.split(/\n|,/).map((item) => item.trim()).filter(Boolean); try { - const saved = attributeEditor === 'new' - ? await createAssetAttribute(selected.id, { - code: attributeCode, name: attributeName, dataType: attributeType, - isRequired: attributeRequired, unit: attributeUnit || null, - ...(attributeType === 'SELECT' ? { options } : {}), sortOrder: attributeOrder, - }) - : await updateAssetAttribute(selected.id, attributeEditor.id, { - name: attributeName, dataType: attributeType, - isRequired: attributeRequired, isActive: attributeActive, - unit: attributeUnit || null, - options: attributeType === 'SELECT' ? options : null, - sortOrder: attributeOrder, - }); - await load(saved.id); - setSuccess(attributeEditor === 'new' ? 'Atributo agregado correctamente' : 'Atributo actualizado'); + if (attributeEditor === 'new') { + await createAssetAttribute(selectedType.id, { + code: attributeCode, + name: attributeName, + dataType: attributeType, + isRequired: attributeRequired, + unit: attributeUnit || null, + ...(attributeType === 'SELECT' ? { options } : {}), + sortOrder: attributeOrder, + }); + setSuccess(`Nueva columna agregada a ${LEVELS.find((item) => item.kind === selectedKind)?.label}.`); + } else { + await updateAssetAttribute(selectedType.id, attributeEditor.id, { + name: attributeName, + dataType: attributeType, + isRequired: attributeRequired, + isActive: attributeActive, + unit: attributeUnit || null, + options: attributeType === 'SELECT' ? options : null, + sortOrder: attributeOrder, + }); + setSuccess('Columna actualizada.'); + } + await load(); setAttributeEditor(null); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; - if (loading) return ; + if (loading) return ; - return
-
ADMINISTRACIÓN

Configuración de Inventarios

Definí qué clases de elementos pueden formar parte de los inventarios y qué información necesita cada una. Las reglas técnicas quedan en configuración avanzada.

{canManage && }
- {error && {error}}{success && {success}} - {types.length > 0 && enrichment && !enrichment.complete &&
CATÁLOGO TÉCNICO

Completar nomenclatura de inspección

La estructura base ya existe. Esta mejora agrega únicamente las familias técnicas que faltan: plantas, baterías, sistemas y equipos específicos, manteniendo un solo tipo Pozo con método/función configurable.

Es una ampliación no destructiva.No reemplaza configuraciones existentes ni crea operadoras, áreas o registros reales.
Pendiente
{enrichment.missingTypeCodes.length} tipos técnicos{enrichment.missingAttributeCount} atributos{enrichment.missingParentRuleCount} reglas de jerarquía
{canManage && enrichment.canApply ?
: {enrichment.reason ?? 'No se puede aplicar automáticamente sobre esta configuración.'}}
} - {types.length === 0 && !creating ?
CONFIGURACIÓN INICIAL

Preparar inventarios de Hidrocarburos

La configuración de inventarios está vacía. Podés instalar una estructura inicial segura con niveles territoriales, instalaciones, sistemas y familias técnicas de inspección.

No carga datos reales automáticamente.Las operadoras, áreas y registros concretos se cargarán después con fuente y vigencia.
Incluye
{['Área','Organización','Yacimiento / Locación','Planta / Batería / Estación','Sistemas técnicos','Pozo con método configurable','Tanques, bombas y otros equipos','Ducto / Cañería'].map((label) => {label})}
{canManage ?
: Necesitás permiso para administrar tipos de inventario y ejecutar la configuración inicial.}
:
- - -
-
-
{creating ? 'NUEVO TIPO' : selected?.isActive ? 'TIPO DISPONIBLE' : 'TIPO NO DISPONIBLE'}

{creating ? 'Crear tipo de elemento' : name}

{!creating && {isActive ? 'Disponible' : 'No disponible'}}
- -