4.3 KiB
4.3 KiB
F-130 — Caducidad editable inline en /inventory
Diagnóstico
En /admin/inventory, la columna "Caducidad" muestra la fecha (<ExpirationCell>) pero NO es editable inline. El operador tiene que abrir el editor del producto (/products/:id) → tab General → campo "Fecha de caducidad" → guardar → volver al inventario.
Reproducción:
/inventory→ fila de producto con caducidad—o2025-12-31- Click sobre la celda de caducidad → no pasa nada (es solo
<span>) - Para cambiarla, hay que ir a
/products/<uuid>y editar el producto completo
El backend ya soporta expirationDate en PATCH /products/:id (catalog.routes.ts:96 — z.iso.date().optional().nullable()). El cliente productsApi.update(id, { expirationDate }) está disponible.
Diseño
Añadir edición inline para expirationDate en /inventory siguiendo el patrón de SKU/EAN/Stock (F-075):
- Estado: añadir
editingExp: boolean,editExpValue: stringaProductRow. - Carga inicial: cuando se hidrata la fila,
editExpValue = row.expirationDate ?? ''(YYYY-MM-DD). - UI: la celda
ExpirationCellse convierte en un componente controlado:- Modo lectura:
ExpirationCell dateIso={row.expirationDate}(actual) - Modo edición:
<input type="date" />con autofocus, blur/Enter para guardar, Escape para salir
- Modo lectura:
- Guardado: nueva función
handleSaveExp(variantId, productId, value):- Llama
productsApi.update(productId, { expirationDate: value || null }) - Actualiza la fila con el nuevo valor
- Llama
- Empty = clear: si el operador borra la fecha, se envía
null(caducidad sin valor).
Cambios
apps/admin/src/app/(dashboard)/inventory/page.tsx
interface ProductRow {
…
editingEan: boolean;
editEanValue: string;
savingEan: boolean;
+ editingExp: boolean;
+ editExpValue: string;
+ savingExp: boolean;
}
// Inicialización de fila
expirationDate: product.expirationDate ?? null,
loading: true,
editing: false,
editValue: '',
saving: false,
msg: '',
editingEan: false,
editEanValue: '',
savingEan: false,
+ editingExp: false,
+ editExpValue: product.expirationDate?.slice(0, 10) ?? '',
+ savingExp: false,
});
// Nueva función
const handleSaveExp = async (variantId: string, productId: string, value: string) => {
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingExp: true } : r));
try {
const updated = await productsApi.update(productId, { expirationDate: value || null });
setRows(prev => prev.map(r => r.variant.id === variantId
? { ...r, expirationDate: updated.expirationDate ?? null, editExpValue: updated.expirationDate?.slice(0, 10) ?? '', editingExp: false, savingExp: false }
: r));
} catch {
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingExp: false } : r));
}
};
// Celda modificada
<td className="px-4 py-3">
{row.editingExp ? (
<input
autoFocus
type="date"
value={row.editExpValue}
onChange={e => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editExpValue: e.target.value } : r))}
onBlur={() => handleSaveExp(row.variant.id, row.productId, row.editExpValue)}
onKeyDown={e => {
if (e.key === 'Enter') handleSaveExp(row.variant.id, row.productId, row.editExpValue);
if (e.key === 'Escape') setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingExp: false } : r));
}}
disabled={row.savingExp}
className="px-2 py-1 border border-[#2D6A4F] rounded text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
/>
) : (
<button
onClick={() => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingExp: true } : r))}
title="Clic para editar caducidad"
className="cursor-text text-left"
>
<ExpirationCell dateIso={row.expirationDate} />
</button>
)}
</td>
Riesgo
Bajo. Sin cambios en backend. El campo expirationDate ya estaba validado por z.iso.date() y acepta formato YYYY-MM-DD (lo que produce <input type="date">).
Plan
- Editar
apps/admin/src/app/(dashboard)/inventory/page.tsx— añadir estado + handler + UI. cd apps/admin && npx tsc --noEmit.cd apps/admin && npm run build.- Cerrar gates.