Files
dh-inspeccion-v2/web-v2/src/features/assets/InventoryFunctionPanel.tsx
T

161 lines
6.6 KiB
TypeScript

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<InventoryFunction[]>([]);
const [history, setHistory] = useState<InventoryFunctionHistory | null>(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 <article className="panel"><LoadingBlock label="Cargando función operativa…" /></article>;
if (!history) return <Alert>{error || 'No se pudo cargar la función operativa.'}</Alert>;
const current = history.currentFunction;
return <article className="panel">
<div className="panel-heading">
<div>
<span className="eyebrow">FUNCIÓN OPERATIVA</span>
<h2>Función de la Estación / Subestación</h2>
<p className="section-copy">La identidad del Inventario no cambia. Cada mutación de función conserva fecha, usuario y antecedente para la consulta histórica.</p>
</div>
{canManageCatalog && <Link className="button secondary" to="/admin/inventory-functions">Administrar catálogo</Link>}
</div>
{error && <Alert>{error}</Alert>}
{success && <Alert type="success">{success}</Alert>}
<div className="finding-detail-summary-grid">
<div>
<small>Función vigente</small>
<strong>{current?.functionName ?? 'Sin función asignada'}</strong>
<span>{current ? `${current.functionCode} · desde ${formatDate(current.validFrom)}` : 'Debe seleccionarse una función del catálogo.'}</span>
</div>
<div>
<small>Cambios registrados</small>
<strong>{history.history.length}</strong>
<span>Historial temporal del mismo Inventario.</span>
</div>
</div>
{canChange && <form className="form-section" onSubmit={submit}>
<div>
<h3>Cambiar función</h3>
<p className="section-copy">Ejemplo: Bombeo Mecánico AIB Bombeo Mecánico Rotaflex. No se crea una Estación nueva.</p>
</div>
<div className="form-grid">
<label className="field">
<span>Nueva función</span>
<SearchableSelect value={functionId} onChange={(event) => setFunctionId(event.target.value)} required>
<option value="">Seleccionar función</option>
{catalog.map((item) => <option key={item.id} value={item.id}>{item.name} · {item.code}</option>)}
</SearchableSelect>
</label>
<label className="field">
<span>Fecha efectiva <em>opcional</em></span>
<input
type="datetime-local"
value={effectiveAt}
max={localDateTime(new Date())}
onChange={(event) => setEffectiveAt(event.target.value)}
/>
<small>Si queda vacía, se toma el momento actual.</small>
</label>
</div>
<label className="field">
<span>Motivo / observación <em>opcional</em></span>
<textarea rows={2} maxLength={4000} value={reason} onChange={(event) => setReason(event.target.value)} placeholder="Ej.: conversión del sistema de bombeo durante intervención de septiembre…" />
</label>
<div className="form-actions">
<button className="button primary" disabled={busy || !functionId || functionId === current?.functionId}>
{busy ? 'Registrando…' : 'Registrar cambio de función'}
</button>
</div>
</form>}
<div className="form-section">
<div><h3>Historial de funciones</h3><p className="section-copy">La función anterior nunca se sobrescribe ni se elimina.</p></div>
{history.history.length === 0
? <div className="inline-empty">Todavía no hay funciones registradas para este Inventario.</div>
: <div className="dossier-link-list">
{history.history.map((item) => <div key={item.id}>
<div>
<strong>{item.functionName}</strong>
<small>{item.functionCode}{item.reason ? ` · ${item.reason}` : ''}</small>
</div>
<span>{formatDate(item.validFrom)} {item.validUntil ? formatDate(item.validUntil) : 'actual'}{item.changedByUsername ? ` · ${item.changedByUsername}` : ''}</span>
</div>)}
</div>}
</div>
</article>;
}