From f8e866a140c9607051f95d5da53fcf476cd2175f Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 22:41:32 -0300 Subject: [PATCH] F4: add inventory function change panel --- .../assets/InventoryFunctionPanel.tsx | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 web-v2/src/features/assets/InventoryFunctionPanel.tsx diff --git a/web-v2/src/features/assets/InventoryFunctionPanel.tsx b/web-v2/src/features/assets/InventoryFunctionPanel.tsx new file mode 100644 index 0000000..73a48af --- /dev/null +++ b/web-v2/src/features/assets/InventoryFunctionPanel.tsx @@ -0,0 +1,160 @@ +import { useEffect, 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 { SearchableSelect } from '../../components/SearchableSelect'; +import { formatDate } from '../../lib/format'; +import { + changeInventoryFunction, + getInventoryFunctionHistory, + listInventoryFunctions, +} from '../../lib/inventoryFunctionApi'; +import type { + InventoryFunction, + InventoryFunctionHistory, +} from '../../lib/inventoryFunctionApi'; + +function localDateTime(value: Date): string { + const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000); + return local.toISOString().slice(0, 16); +} + +export function InventoryFunctionPanel({ assetId }: { assetId: string }) { + const { hasPermission } = useAuth(); + const canChange = hasPermission('assets.update'); + const canManageCatalog = hasPermission('asset_types.manage'); + const [catalog, setCatalog] = useState([]); + const [history, setHistory] = useState(null); + const [functionId, setFunctionId] = useState(''); + const [effectiveAt, setEffectiveAt] = useState(''); + const [reason, setReason] = useState(''); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const load = () => { + setLoading(true); + setError(''); + Promise.all([ + listInventoryFunctions(), + getInventoryFunctionHistory(assetId), + ]) + .then(([loadedCatalog, loadedHistory]) => { + setCatalog(loadedCatalog); + setHistory(loadedHistory); + setFunctionId(loadedHistory.currentFunction?.functionId ?? ''); + }) + .catch((requestError) => setError(errorMessage(requestError))) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, [assetId]); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (!functionId || functionId === history?.currentFunction?.functionId) return; + setBusy(true); + setError(''); + setSuccess(''); + try { + const updated = await changeInventoryFunction(assetId, { + functionId, + effectiveAt: effectiveAt ? new Date(effectiveAt).toISOString() : undefined, + reason: reason.trim() || null, + }); + setHistory(updated); + setFunctionId(updated.currentFunction?.functionId ?? ''); + setEffectiveAt(''); + setReason(''); + setSuccess('Cambio de función registrado. La función anterior permanece en el historial del Inventario.'); + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setBusy(false); + } + }; + + if (loading) return
; + if (!history) return {error || 'No se pudo cargar la función operativa.'}; + + const current = history.currentFunction; + + return
+
+
+ FUNCIÓN OPERATIVA +

Función de la Estación / Subestación

+

La identidad del Inventario no cambia. Cada mutación de función conserva fecha, usuario y antecedente para la consulta histórica.

+
+ {canManageCatalog && Administrar catálogo} +
+ + {error && {error}} + {success && {success}} + +
+
+ Función vigente + {current?.functionName ?? 'Sin función asignada'} + {current ? `${current.functionCode} · desde ${formatDate(current.validFrom)}` : 'Debe seleccionarse una función del catálogo.'} +
+
+ Cambios registrados + {history.history.length} + Historial temporal del mismo Inventario. +
+
+ + {canChange &&
+
+

Cambiar función

+

Ejemplo: Bombeo Mecánico AIB → Bombeo Mecánico Rotaflex. No se crea una Estación nueva.

+
+
+ + +
+