import { useEffect, useMemo, useState } from 'react'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { createInventoryFamily, createInventoryFamilyAttribute, getInventoryFamilyAttributes, listInventoryFamiliesAdmin, updateInventoryFamily, updateInventoryFamilyAttribute, } from '../lib/inventoryStructureApi'; import type { InventoryFamily, InventoryFamilyAttribute, InventoryFamilyAttributeDataType, } from '../lib/inventoryStructureApi'; type FamilyLevel = 'INSTALLATION' | 'SUBINSTALLATION'; const FIELD_TYPES: Array<{ value: InventoryFamilyAttributeDataType; label: string }> = [ { value: 'TEXT', label: 'Texto' }, { value: 'NUMBER', label: 'Número' }, { value: 'DATE', label: 'Fecha' }, { value: 'BOOLEAN', label: 'Sí / No' }, ]; function fieldTypeLabel(value: InventoryFamilyAttributeDataType) { return FIELD_TYPES.find((item) => item.value === value)?.label ?? (value === 'DATETIME' ? 'Fecha y hora' : value === 'SELECT' ? 'Lista' : value); } function fieldCode(name: string) { return name .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 100) || `campo_${Date.now()}`; } export function AuthoritativeInventoryConfigPage() { const { hasPermission } = useAuth(); const canManage = hasPermission('asset_types.manage'); const [families, setFamilies] = useState([]); const [level, setLevel] = useState('INSTALLATION'); const [selectedFamilyId, setSelectedFamilyId] = useState(''); const [attributes, setAttributes] = useState([]); const [newTypeName, setNewTypeName] = useState(''); const [newTypeParents, setNewTypeParents] = useState([]); const [newFieldName, setNewFieldName] = useState(''); const [newFieldType, setNewFieldType] = useState('TEXT'); const [newFieldRequired, setNewFieldRequired] = useState(false); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const refreshFamilies = async () => { const loaded = await listInventoryFamiliesAdmin(); setFamilies(loaded); return loaded; }; useEffect(() => { refreshFamilies() .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, []); const installationFamilies = useMemo( () => families.filter((family) => family.level === 'INSTALLATION'), [families], ); const visibleFamilies = useMemo( () => families.filter((family) => family.level === level), [families, level], ); const selectedFamily = families.find((family) => family.id === selectedFamilyId) ?? null; useEffect(() => { const first = visibleFamilies.find((family) => family.isActive !== false) ?? visibleFamilies[0] ?? null; setSelectedFamilyId((current) => visibleFamilies.some((family) => family.id === current) ? current : first?.id ?? ''); }, [level, visibleFamilies]); useEffect(() => { if (!selectedFamilyId) { setAttributes([]); return; } getInventoryFamilyAttributes(selectedFamilyId) .then((result) => setAttributes(result.items.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)))) .catch((requestError) => setError(errorMessage(requestError))); }, [selectedFamilyId]); const run = async (action: () => Promise, message: string) => { setSaving(true); setError(''); setSuccess(''); try { await action(); setSuccess(message); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const createType = async () => { if (!newTypeName.trim()) return; if (level === 'SUBINSTALLATION' && newTypeParents.length === 0) { setError('Elegí al menos un Tipo de Instalación que pueda contener esta Subinstalación.'); return; } await run(async () => { const created = await createInventoryFamily({ level, name: newTypeName.trim(), parentFamilyIds: level === 'SUBINSTALLATION' ? newTypeParents : undefined, }); await refreshFamilies(); setSelectedFamilyId(created.id); setNewTypeName(''); setNewTypeParents([]); }, `${level === 'INSTALLATION' ? 'Tipo de Instalación' : 'Tipo de Subinstalación'} creado.`); }; const toggleParent = (parentId: string) => { setNewTypeParents((current) => current.includes(parentId) ? current.filter((id) => id !== parentId) : [...current, parentId]); }; const toggleSelectedParent = async (parentId: string) => { if (!selectedFamily || selectedFamily.level !== 'SUBINSTALLATION') return; const next = selectedFamily.parentFamilyIds.includes(parentId) ? selectedFamily.parentFamilyIds.filter((id) => id !== parentId) : [...selectedFamily.parentFamilyIds, parentId]; if (next.length === 0) { setError('Una Subinstalación debe quedar habilitada dentro de al menos un Tipo de Instalación.'); return; } await run(async () => { await updateInventoryFamily(selectedFamily.id, { parentFamilyIds: next }); await refreshFamilies(); }, 'Tipos permitidos actualizados.'); }; const createField = async () => { if (!selectedFamily || !newFieldName.trim()) return; await run(async () => { await createInventoryFamilyAttribute(selectedFamily.id, { code: fieldCode(newFieldName), name: newFieldName.trim(), dataType: newFieldType, isRequired: newFieldRequired, sortOrder: attributes.length, }); const loaded = await getInventoryFamilyAttributes(selectedFamily.id); setAttributes(loaded.items.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))); setNewFieldName(''); setNewFieldType('TEXT'); setNewFieldRequired(false); }, 'Campo agregado.'); }; const toggleField = async (attribute: InventoryFamilyAttribute, key: 'isRequired' | 'isActive') => { if (!selectedFamily) return; await run(async () => { await updateInventoryFamilyAttribute(selectedFamily.id, attribute.id, { [key]: !attribute[key] }); const loaded = await getInventoryFamilyAttributes(selectedFamily.id); setAttributes(loaded.items.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))); }, key === 'isActive' ? 'Visibilidad del campo actualizada.' : 'Obligatoriedad actualizada.'); }; const toggleFamilyActive = async () => { if (!selectedFamily) return; await run(async () => { await updateInventoryFamily(selectedFamily.id, { isActive: selectedFamily.isActive === false }); await refreshFamilies(); }, selectedFamily.isActive === false ? 'Tipo activado.' : 'Tipo desactivado.'); }; if (loading) return ; return
ADMINISTRACIÓN · INVENTARIOS

Tipos y campos

Configuración simple para la puesta a punto. Los usuarios de campo no ven esta pantalla.

{error && {error}} {success && {success}}
ESTRUCTURA FIJA

Departamento → Área → Yacimiento → Instalación → Subinstalación

Un Área puede estar vinculada a varias Empresas. Cada Yacimiento elige una sola Empresa operadora de las vinculadas a su Área.

Campos básicos del sistema: nombre, código, ubicación jerárquica, estado y GPS. No hace falta configurarlos acá.

TIPOS

{level === 'INSTALLATION' ? 'Instalaciones' : 'Subinstalaciones'}

Seleccioná un tipo para administrar sus campos.

{visibleFamilies.length === 0 &&

Todavía no hay tipos configurados.

} {visibleFamilies.map((family) => )}
{canManage &&

+ Nuevo tipo

{level === 'SUBINSTALLATION' &&
Puede estar dentro de
{installationFamilies.filter((family) => family.isActive !== false).map((family) => )}
}
}
{!selectedFamily ?
Seleccioná un tipo para ver sus campos.
: <>
{selectedFamily.level === 'INSTALLATION' ? 'INSTALACIÓN' : 'SUBINSTALACIÓN'}

{selectedFamily.name}

Campos sencillos que se muestran al cargar este tipo.

{canManage && }
{selectedFamily.level === 'SUBINSTALLATION' &&
Puede estar dentro de:
{installationFamilies.filter((family) => family.isActive !== false).map((family) => )}
}
{attributes.length === 0 &&

Este tipo no tiene campos específicos. Puede usarse sólo con los datos básicos.

} {attributes.map((attribute) =>
{attribute.sortOrder + 1} {attribute.name} {fieldTypeLabel(attribute.dataType)}{attribute.isRequired ? ' · Obligatorio' : ' · Opcional'}{attribute.isActive ? '' : ' · Oculto'} {canManage && }
)}
{canManage && selectedFamily.isActive !== false &&

+ Agregar campo

Teléfono, email o identificadores simples se cargan como Texto. GPS es un dato básico del registro y no se configura como campo.

} }
; }