diff --git a/web-v2/src/pages/AssetTypesPage.tsx b/web-v2/src/pages/AssetTypesPage.tsx index 276b180..b22c5ae 100644 --- a/web-v2/src/pages/AssetTypesPage.tsx +++ b/web-v2/src/pages/AssetTypesPage.tsx @@ -7,6 +7,7 @@ import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { createAssetAttribute, + getFindingCatalogAdmin, listAssetTypes, updateAssetAttribute, } from '../lib/api'; @@ -14,10 +15,12 @@ import type { AssetAttributeDataType, AssetAttributeDefinition, AssetType, + FindingAdminCatalog, } from '../lib/api'; import { createInventoryFamily, listInventoryFamiliesAdmin, + replaceInventoryFamilyFindings, updateInventoryFamily, } from '../lib/inventoryStructureApi'; import type { InventoryFamily } from '../lib/inventoryStructureApi'; @@ -31,8 +34,11 @@ const ATTRIBUTE_TYPES: Array<{ value: AssetAttributeDataType; label: string }> = { value: 'SELECT', label: 'Lista de opciones' }, ]; +const EMPTY_FINDING_CATALOG: FindingAdminCatalog = { categories: [], items: [] }; + type CanonicalKind = 'EMPRESA' | 'DEPARTAMENTO' | 'AREA' | 'YACIMIENTO' | 'INSTALACION' | 'SUBINSTALACION'; type FamilyEditor = InventoryFamily | 'new' | null; +type FamilyFindingMode = 'ASSOCIATED' | 'ALL'; const LEVELS: Array<{ kind: CanonicalKind; label: string; description: string }> = [ { kind: 'EMPRESA', label: 'Empresa', description: 'Maestro independiente. Se vincula temporalmente a un Área.' }, @@ -64,7 +70,9 @@ export function AssetTypesPage() { const canManage = hasPermission('asset_types.manage'); const [types, setTypes] = useState([]); const [families, setFamilies] = useState([]); + const [findingCatalog, setFindingCatalog] = useState(EMPTY_FINDING_CATALOG); const [selectedKind, setSelectedKind] = useState('INSTALACION'); + const [selectedInstallationFamilyId, setSelectedInstallationFamilyId] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); @@ -75,6 +83,9 @@ export function AssetTypesPage() { const [familyName, setFamilyName] = useState(''); const [familyParentId, setFamilyParentId] = useState(''); const [familyActive, setFamilyActive] = useState(true); + const [familyFindingIds, setFamilyFindingIds] = useState>(new Set()); + const [familyFindingMode, setFamilyFindingMode] = useState('ASSOCIATED'); + const [familyFindingSearch, setFamilyFindingSearch] = useState(''); const [attributeEditor, setAttributeEditor] = useState(null); const [attributeCode, setAttributeCode] = useState(''); @@ -87,12 +98,14 @@ export function AssetTypesPage() { const [attributeOrder, setAttributeOrder] = useState(0); const load = async () => { - const [loadedTypes, loadedFamilies] = await Promise.all([ + const [loadedTypes, loadedFamilies, loadedFindingCatalog] = await Promise.all([ listAssetTypes(), listInventoryFamiliesAdmin(), + getFindingCatalogAdmin(), ]); setTypes(loadedTypes); setFamilies(loadedFamilies); + setFindingCatalog(loadedFindingCatalog); }; useEffect(() => { @@ -102,17 +115,47 @@ export function AssetTypesPage() { 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 visibleSubinstallationFamilies = useMemo(() => subinstallationFamilies.filter((family) => + family.isActive !== false && (!selectedInstallationFamilyId || family.parentFamilyId === selectedInstallationFamilyId), + ), [subinstallationFamilies, selectedInstallationFamilyId]); + + const activeFindingCategoryIds = useMemo(() => new Set( + findingCatalog.categories.filter((category) => category.isActive).map((category) => category.id), + ), [findingCatalog.categories]); + const findingCategoryNames = useMemo(() => new Map( + findingCatalog.categories.map((category) => [category.id, category.name]), + ), [findingCatalog.categories]); + const visibleFamilyFindings = useMemo(() => { + const needle = familyFindingSearch.trim().toLocaleLowerCase('es-AR'); + return findingCatalog.items.filter((item) => { + if (!item.isActive || !activeFindingCategoryIds.has(item.categoryId)) return false; + if (familyFindingMode === 'ASSOCIATED' && !familyFindingIds.has(item.id)) return false; + if (!needle) return true; + return [item.title, item.code, findingCategoryNames.get(item.categoryId) ?? ''] + .some((value) => value.toLocaleLowerCase('es-AR').includes(needle)); + }); + }, [findingCatalog.items, activeFindingCategoryIds, familyFindingMode, familyFindingIds, familyFindingSearch, findingCategoryNames]); const openNewFamily = (level: 'INSTALLATION' | 'SUBINSTALLATION') => { - setFamilyEditor('new'); setFamilyLevel(level); setFamilyName(''); setFamilyParentId(''); setFamilyActive(true); + setFamilyEditor('new'); setFamilyLevel(level); setFamilyName(''); + setFamilyParentId(level === 'SUBINSTALLATION' ? selectedInstallationFamilyId : ''); + setFamilyActive(true); setFamilyFindingIds(new Set()); setFamilyFindingMode('ASSOCIATED'); setFamilyFindingSearch(''); setError(''); setSuccess(''); }; const openFamily = (family: InventoryFamily) => { + if (family.level === 'INSTALLATION') setSelectedInstallationFamilyId(family.id); setFamilyEditor(family); setFamilyLevel(family.level); setFamilyName(family.name); setFamilyParentId(family.parentFamilyId ?? ''); setFamilyActive(family.isActive !== false); + setFamilyFindingIds(new Set(family.findingItemIds ?? [])); setFamilyFindingMode('ASSOCIATED'); setFamilyFindingSearch(''); setError(''); setSuccess(''); }; + const toggleFamilyFinding = (id: string) => setFamilyFindingIds((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + const saveFamily = async (event: FormEvent) => { event.preventDefault(); setSaving(true); setError(''); setSuccess(''); try { @@ -129,7 +172,11 @@ export function AssetTypesPage() { parentFamilyId: familyLevel === 'SUBINSTALLATION' ? familyParentId : null, isActive: familyActive, }); - setSuccess('Clasificación actualizada.'); + await replaceInventoryFamilyFindings(familyEditor.id, { + itemIds: [...familyFindingIds], + reason: 'Actualización desde Configuración de Inventarios', + }); + setSuccess('Clasificación y Hallazgos asociados actualizados.'); } await load(); setFamilyEditor(null); @@ -213,13 +260,14 @@ export function AssetTypesPage() {
-
CLASIFICACIONES

Tipos de Instalación

Definen el rubro técnico y los Hallazgos aplicables.

{canManage && }
-
{installationFamilies.filter((family) => family.isActive !== false).map((family) => )}
+
CLASIFICACIONES

Tipos de Instalación

Tocá una Instalación para filtrar sus Subinstalaciones y editar sus Hallazgos.

{canManage && }
+
{installationFamilies.filter((family) => family.isActive !== false).map((family) => )}
-
CLASIFICACIONES

Tipos de Subinstalación

Cada tipo pertenece a un tipo de Instalación y tiene sus propios Hallazgos.

{canManage && }
-
{subinstallationFamilies.filter((family) => family.isActive !== false).map((family) => )}
+
CLASIFICACIONES

Tipos de Subinstalación

Mostrá todas o sólo las que pertenecen a una Instalación.

{canManage && }
+ + {visibleSubinstallationFamilies.length === 0 ?
{selectedInstallationFamilyId ? 'Esta Instalación todavía no tiene tipos de Subinstalación asociados.' : 'No hay tipos de Subinstalación configurados.'}
:
{visibleSubinstallationFamilies.map((family) => )}
}
@@ -230,9 +278,9 @@ export function AssetTypesPage() { {!selectedType ? Este nivel todavía no tiene un tipo maestro activo. : selectedType.attributes.length === 0 ?
No hay columnas adicionales configuradas para {LEVELS.find((item) => item.kind === selectedKind)?.label}.
:
{selectedType.attributes.map((attribute) => )}
} -
HALLAZGOS

Relación por clasificación

Abrí una clasificación arriba para ver cuántos Hallazgos tiene y entrar directamente a sus Hallazgos asociados. OTROS permanece disponible en la APK.

Abrir Catálogo de hallazgos
+
HALLAZGOS

Relación por clasificación

Los Hallazgos se pueden revisar y editar directamente dentro de cada tipo. El Catálogo completo sigue disponible para una administración masiva.

Abrir Catálogo de hallazgos
- {familyEditor &&
CLASIFICACIÓN DE INVENTARIO

{familyEditor === 'new' ? 'Nuevo tipo' : 'Editar tipo'}

{familyEditor === 'new' && }{familyLevel === 'SUBINSTALLATION' && }{familyEditor !== 'new' && <>

{familyEditor.findingCount ?? 0} Hallazgos asociados. Revisalos o modificá la asociación desde el Catálogo.

Ver Hallazgos asociados }
} + {familyEditor &&
CLASIFICACIÓN DE INVENTARIO

{familyEditor === 'new' ? 'Nuevo tipo' : 'Editar tipo'}

{familyEditor === 'new' && }{familyLevel === 'SUBINSTALLATION' && }{familyEditor !== 'new' && <>

{familyFindingIds.size} Hallazgos asociados. Se guardan junto con esta clasificación.

{visibleFamilyFindings.length === 0 ?
{familyFindingMode === 'ASSOCIATED' ? 'No hay Hallazgos asociados. Tocá “Agregar o quitar” para vincularlos.' : 'No hay Hallazgos que coincidan con la búsqueda.'}
:
{visibleFamilyFindings.map((item) => familyFindingMode === 'ALL' ? :
{item.title}{findingCategoryNames.get(item.categoryId) ?? 'Catálogo'} · {item.code}
)}
}Abrir en Catálogo completo }
} {attributeEditor && selectedType &&
COLUMNA DE {LEVELS.find((item) => item.kind === selectedKind)?.label.toUpperCase()}

{attributeEditor === 'new' ? 'Nueva columna' : 'Editar columna'}

{attributeType === 'SELECT' &&