F4: add inventory function change panel
This commit is contained in:
@@ -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<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>;
|
||||
}
|
||||
Reference in New Issue
Block a user