Files
mercadodevida/work/artifacts/F-130/architect.md
2026-08-21 18:08:14 +02:00

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:

  1. /inventory → fila de producto con caducidad o 2025-12-31
  2. Click sobre la celda de caducidad → no pasa nada (es solo <span>)
  3. 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):

  1. Estado: añadir editingExp: boolean, editExpValue: string a ProductRow.
  2. Carga inicial: cuando se hidrata la fila, editExpValue = row.expirationDate ?? '' (YYYY-MM-DD).
  3. UI: la celda ExpirationCell se 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
  4. Guardado: nueva función handleSaveExp(variantId, productId, value):
    • Llama productsApi.update(productId, { expirationDate: value || null })
    • Actualiza la fila con el nuevo valor
  5. 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

  1. Editar apps/admin/src/app/(dashboard)/inventory/page.tsx — añadir estado + handler + UI.
  2. cd apps/admin && npx tsc --noEmit.
  3. cd apps/admin && npm run build.
  4. Cerrar gates.