# F-130 — Caducidad editable inline en /inventory ## Diagnóstico En `/admin/inventory`, la columna "Caducidad" muestra la fecha (``) 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 ``) 3. Para cambiarla, hay que ir a `/products/` 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: `` 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` ```diff 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 {row.editingExp ? ( 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" /> ) : ( )} ``` ## 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 ``). ## 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.