diff --git a/web-v2/src/pages/InventoryCreatePage.tsx b/web-v2/src/pages/InventoryCreatePage.tsx new file mode 100644 index 0000000..49c15ad --- /dev/null +++ b/web-v2/src/pages/InventoryCreatePage.tsx @@ -0,0 +1,269 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { FormEvent } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router'; +import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; +import { Icon } from '../components/Icon'; +import { getAsset } from '../lib/api'; +import { + createInventoryStructure, + getInventoryFamilyFindings, + getInventoryStructureOptions, + listInventoryStructureParents, +} from '../lib/inventoryStructureApi'; +import type { + InventoryFamily, + InventoryFamilyFindings, + InventoryStructureKind, + InventoryStructureOptions, + InventoryStructureParent, +} from '../lib/inventoryStructureApi'; + +const KINDS: Array<{ kind: InventoryStructureKind; label: string; help: string; step: number }> = [ + { kind: 'AREA', label: 'Área', help: 'Nivel territorial raíz.', step: 1 }, + { kind: 'YACIMIENTO', label: 'Yacimiento', help: 'Debe pertenecer a un Área.', step: 2 }, + { kind: 'INSTALACION', label: 'Instalación', help: 'Debe pertenecer a un Yacimiento.', step: 3 }, + { kind: 'SUBINSTALACION', label: 'Subinstalación', help: 'Debe pertenecer a una Instalación.', step: 4 }, +]; + +const childKindByParentType: Record = { + area: 'YACIMIENTO', + yacimiento: 'INSTALACION', + instalacion: 'SUBINSTALACION', +}; + +function kindLabel(kind: InventoryStructureKind): string { + return KINDS.find((item) => item.kind === kind)?.label ?? kind; +} + +export function InventoryCreatePage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const contextParentId = searchParams.get('parentId'); + const [options, setOptions] = useState(null); + const [kind, setKind] = useState('AREA'); + const [parents, setParents] = useState([]); + const [parentSearch, setParentSearch] = useState(''); + const [parentId, setParentId] = useState(''); + const [familyId, setFamilyId] = useState(''); + const [familyFindings, setFamilyFindings] = useState(null); + const [code, setCode] = useState(''); + const [name, setName] = useState(''); + const [commonName, setCommonName] = useState(''); + const [description, setDescription] = useState(''); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + getInventoryStructureOptions() + .then(async (loaded) => { + setOptions(loaded); + if (contextParentId) { + const parent = await getAsset(contextParentId); + const inferred = childKindByParentType[parent.type.code.toLowerCase()]; + if (inferred) { + setKind(inferred); + setParentId(parent.id); + } + } + }) + .catch((requestError) => setError(errorMessage(requestError))) + .finally(() => setLoading(false)); + }, [contextParentId]); + + useEffect(() => { + if (kind === 'AREA') { + setParents([]); + setParentId(''); + return; + } + const timer = window.setTimeout(() => { + listInventoryStructureParents(kind, parentSearch) + .then((loaded) => { + setParents(loaded); + if (contextParentId && loaded.some((item) => item.id === contextParentId)) { + setParentId(contextParentId); + } + }) + .catch((requestError) => setError(errorMessage(requestError))); + }, 180); + return () => window.clearTimeout(timer); + }, [kind, parentSearch, contextParentId]); + + const selectedParent = parents.find((item) => item.id === parentId) ?? null; + const families = useMemo(() => { + if (!options) return [] as InventoryFamily[]; + if (kind === 'INSTALACION') return options.installationFamilies; + if (kind === 'SUBINSTALACION') { + const parentFamilyId = selectedParent?.inventoryFamily?.id; + return parentFamilyId + ? options.subinstallationFamilies.filter((item) => item.parentFamilyId === parentFamilyId) + : []; + } + return []; + }, [options, kind, selectedParent]); + const selectedFamily = families.find((item) => item.id === familyId) ?? null; + + useEffect(() => { + if (!familyId) { + setFamilyFindings(null); + return; + } + getInventoryFamilyFindings(familyId) + .then(setFamilyFindings) + .catch((requestError) => setError(errorMessage(requestError))); + }, [familyId]); + + useEffect(() => { + if (kind !== 'INSTALACION' && kind !== 'SUBINSTALACION') setFamilyId(''); + if (kind === 'SUBINSTALACION' && familyId && !families.some((item) => item.id === familyId)) setFamilyId(''); + }, [kind, familyId, families]); + + const chooseKind = (next: InventoryStructureKind) => { + setKind(next); + setParentId(''); + setParentSearch(''); + setFamilyId(''); + setFamilyFindings(null); + setError(''); + }; + + const save = async (event: FormEvent) => { + event.preventDefault(); + const requiresParent = kind !== 'AREA'; + const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION'; + if (requiresParent && !parentId) { + setError(`Seleccioná el ${kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación'} padre.`); + return; + } + if (requiresFamily && !familyId) { + setError(`Seleccioná la familia de ${kindLabel(kind).toLowerCase()}.`); + return; + } + setSaving(true); + setError(''); + try { + const created = await createInventoryStructure({ + kind, + code: code.trim() || null, + name: name.trim(), + commonName: commonName.trim() || null, + parentId: parentId || null, + familyId: familyId || null, + description: description.trim() || null, + }); + navigate(`/inventarios/${created.id}`, { replace: true }); + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setSaving(false); + } + }; + + if (loading) return ; + + const parentLabel = kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación'; + const currentStep = KINDS.find((item) => item.kind === kind)?.step ?? 1; + + return
+ + +
+
+ INVENTARIO +

Agregar a la estructura

+

La estructura oficial es Área → Yacimiento → Instalación → Subinstalación. Elegí el nivel y el sistema te guía con los vínculos válidos.

+
+
+ + {error && {error}} + +
+
+

1. ¿Qué querés crear?

Sólo se pueden crear los cuatro niveles estructurales definidos para DH.

+
+ {KINDS.map((item) => )} +
+
+ +

Ruta: {KINDS.slice(0, currentStep).map((item) => item.label).join(' → ')}

+
+
+
+ +
+ {kind !== 'AREA' &&
+

2. Ubicación en la estructura

Primero elegí el {parentLabel} al que pertenece este registro.

+ + +
} + + {(kind === 'INSTALACION' || kind === 'SUBINSTALACION') &&
+

{kind === 'AREA' ? '2' : '3'}. Familia técnica

La familia no crea otro nivel. Sirve para aplicar exactamente los Hallazgos del Excel que corresponden.

+ {kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? La Instalación seleccionada todavía no tiene una familia técnica F3.1. Revisala antes de crear una Subinstalación. : } + + {selectedFamily &&
+ +
+ Hallazgos asociados automáticamente + {familyFindings ? `${familyFindings.count} controles del Excel para ${selectedFamily.name}` : 'Cargando catálogo asociado…'} + {familyFindings && familyFindings.items.length > 0 &&
    + {familyFindings.items.slice(0, 7).map((item) =>
  • {item.title}
  • )} + {familyFindings.items.length > 7 &&
  • + {familyFindings.items.length - 7} hallazgos más
  • } +
} +
+
} + + {selectedFamily && selectedFamily.informationLabels.length > 0 &&
+ +

Información técnica esperada: {selectedFamily.informationLabels.join(' · ')}

+
} +
} + +
+

{kind === 'AREA' ? '2' : kind === 'YACIMIENTO' ? '3' : '4'}. Identificación

Usá el nombre real de campo. El código DH puede generarse automáticamente.

+
+ + + +
+