108 lines
4.3 KiB
Markdown
108 lines
4.3 KiB
Markdown
# 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`
|
|
|
|
```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
|
|
<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. |