F4: add inventory function catalog admin
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||
import {
|
||||
createInventoryFunction,
|
||||
listInventoryFunctions,
|
||||
updateInventoryFunction,
|
||||
} from '../lib/inventoryFunctionApi';
|
||||
import type { InventoryFunction } from '../lib/inventoryFunctionApi';
|
||||
|
||||
export function InventoryFunctionsPage() {
|
||||
const [items, setItems] = useState<InventoryFunction[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [sortOrder, setSortOrder] = useState('0');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editDescription, setEditDescription] = useState('');
|
||||
const [editSortOrder, setEditSortOrder] = useState('0');
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
listInventoryFunctions(true)
|
||||
.then(setItems)
|
||||
.catch((requestError) => setError(errorMessage(requestError)))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const create = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!code.trim() || !name.trim()) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
await createInventoryFunction({
|
||||
code,
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
sortOrder: Number(sortOrder || 0),
|
||||
});
|
||||
setCode('');
|
||||
setName('');
|
||||
setDescription('');
|
||||
setSortOrder('0');
|
||||
setSuccess('Función incorporada al catálogo. Ya puede asignarse a Estaciones y Subestaciones.');
|
||||
load();
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const beginEdit = (item: InventoryFunction) => {
|
||||
setEditingId(item.id);
|
||||
setEditName(item.name);
|
||||
setEditDescription(item.description ?? '');
|
||||
setEditSortOrder(String(item.sortOrder));
|
||||
setError('');
|
||||
setSuccess('');
|
||||
};
|
||||
|
||||
const saveEdit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!editingId || !editName.trim()) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
await updateInventoryFunction(editingId, {
|
||||
name: editName,
|
||||
description: editDescription.trim() || null,
|
||||
sortOrder: Number(editSortOrder || 0),
|
||||
});
|
||||
setEditingId(null);
|
||||
setSuccess('Función actualizada. Los registros históricos conservan la referencia temporal correspondiente.');
|
||||
load();
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = async (item: InventoryFunction) => {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
await updateInventoryFunction(item.id, { isActive: !item.isActive });
|
||||
setSuccess(item.isActive
|
||||
? 'Función desactivada. No podrá elegirse en nuevos cambios, pero permanece en el historial.'
|
||||
: 'Función reactivada y disponible nuevamente para asignación.');
|
||||
load();
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <LoadingBlock label="Cargando catálogo de funciones…" />;
|
||||
|
||||
return <section className="narrow-section">
|
||||
<div className="page-heading">
|
||||
<div><span className="eyebrow">ADMINISTRACIÓN</span><h1>Catálogo de funciones</h1><p>Funciones operativas que pueden asumir las Estaciones y Subestaciones. Desactivar una opción nunca borra su uso histórico.</p></div>
|
||||
</div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{success && <Alert type="success">{success}</Alert>}
|
||||
|
||||
<form className="panel form-panel" onSubmit={create}>
|
||||
<div className="form-section">
|
||||
<div><h2>Nueva función</h2><p className="section-copy">Ejemplos: Bombeo Mecánico AIB, Bombeo Mecánico Rotaflex, PCP.</p></div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Código</span><input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} required maxLength={120} placeholder="BOMBEO_AIB" /></label>
|
||||
<label className="field"><span>Nombre</span><input value={name} onChange={(event) => setName(event.target.value)} required maxLength={240} placeholder="Bombeo Mecánico AIB" /></label>
|
||||
<label className="field"><span>Orden</span><input type="number" min={0} max={100000} value={sortOrder} onChange={(event) => setSortOrder(event.target.value)} /></label>
|
||||
</div>
|
||||
<label className="field"><span>Descripción <em>opcional</em></span><textarea rows={2} maxLength={4000} value={description} onChange={(event) => setDescription(event.target.value)} /></label>
|
||||
<div className="form-actions"><button className="button primary" disabled={busy}>{busy ? 'Guardando…' : 'Agregar función'}</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">CATÁLOGO</span><h2>Funciones disponibles</h2></div><span className="count-pill">{items.length}</span></div>
|
||||
{items.length === 0 ? <div className="inline-empty">Todavía no hay funciones configuradas.</div> : <div className="dossier-link-list">
|
||||
{items.map((item) => <div key={item.id}>
|
||||
<div>
|
||||
<strong>{item.name}</strong>
|
||||
<small>{item.code}{item.description ? ` · ${item.description}` : ''}</small>
|
||||
</div>
|
||||
<span>{item.isActive ? 'Activa' : 'Inactiva'} · orden {item.sortOrder}</span>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="button secondary" disabled={busy} onClick={() => beginEdit(item)}>Editar</button>
|
||||
<button type="button" className="button secondary" disabled={busy} onClick={() => toggleActive(item)}>{item.isActive ? 'Desactivar' : 'Reactivar'}</button>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>}
|
||||
</article>
|
||||
|
||||
{editingId && <form className="panel form-panel" onSubmit={saveEdit}>
|
||||
<div className="form-section">
|
||||
<div><h2>Editar función</h2><p className="section-copy">El código técnico permanece estable; podés corregir nombre, descripción y orden.</p></div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Nombre</span><input value={editName} onChange={(event) => setEditName(event.target.value)} required maxLength={240} /></label>
|
||||
<label className="field"><span>Orden</span><input type="number" min={0} max={100000} value={editSortOrder} onChange={(event) => setEditSortOrder(event.target.value)} /></label>
|
||||
</div>
|
||||
<label className="field"><span>Descripción</span><textarea rows={2} maxLength={4000} value={editDescription} onChange={(event) => setEditDescription(event.target.value)} /></label>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="button secondary" onClick={() => setEditingId(null)}>Cancelar</button>
|
||||
<button className="button primary" disabled={busy}>{busy ? 'Guardando…' : 'Guardar cambios'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>}
|
||||
</section>;
|
||||
}
|
||||
Reference in New Issue
Block a user