161 lines
6.6 KiB
TypeScript
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>;
|
|
}
|