70 lines
5.4 KiB
Markdown
70 lines
5.4 KiB
Markdown
# F-137 — Implementer notes: Unify price/stock/EAN/meta save into "Guardar cambios"
|
||
|
||
## Cambios
|
||
|
||
### `apps/admin/src/features/products/components/sections/PriceStockSection.tsx`
|
||
|
||
- Convertido de function declaration a `forwardRef<PriceStockHandle, { productId?: string }>`. El nuevo `PriceStockHandle` expone un único método `saveAll(): Promise<{price, stock, ean, meta}>`.
|
||
- Eliminados los `useState` `savingPrice`, `savingStock`, `savingEan`, `savingProductMeta`. Los mensajes (`priceMsg`, `stockMsg`, `eanMsg`, `metaMsg`) ahora solo se setean en caso de error (sin flashes de éxito tipo `✓ Guardado`).
|
||
- Las funciones `savePrice` / `saveStock` / `saveEan` / `saveProductMeta` ahora devuelven `Promise<boolean>` y propagan el mensaje de error en el state msg correspondiente. Reutilizan los mismos endpoints HTTP que antes — sin cambios de backend.
|
||
- Nuevo helper `ensureVariant()`: si la sección aún no tiene `variant` en su state local (caso típico del flujo de creación, donde `productId` se setea pero el `useEffect` de carga aún no corrió), hace un `productsApi.getVariants(productId)` y guarda el primer item. Idempotente.
|
||
- `useImperativeHandle` con deps array = state values (gross, net, cost, offer, vatRate, stock, ean, unitWeightGr, minPurchaseQty, taxRates). Esto garantiza que el handle siempre vea los valores más recientes del formulario.
|
||
- `saveAll()` ejecuta los 4 saves con `Promise.all` y devuelve el mapa de éxito. No lanza excepción: cada grupo se maneja con su propio try/catch interno.
|
||
- **Eliminado el botón "Guardar precio"** (líneas que iban a ser 392–399 del original).
|
||
- **Eliminados `onBlur={saveX}` y `onKeyDown={Enter→saveX}`** en los inputs de stock, EAN, peso unitario y compra mínima. Los inputs ya no persisten automáticamente al perder el foco.
|
||
- **Eliminados `disabled={savingX}`** en los inputs correspondientes (ya no hay estado de guardado).
|
||
- Cambiado el render de los mensajes para que solo muestren texto rojo (sin el `text-green-600` para `✓`).
|
||
- Sin cambios en la lógica de cálculo IVA/PVP/Neto (las funciones `onGrossChange`, `onNetChange`, `onVatChange` siguen iguales).
|
||
- Sin cambios en la carga inicial de datos (el `useEffect` que llama `productsApi.get` / `getVariants` / `pricingApi.getVariantPrice` / `inventoryApi.getAvailability` sigue intacto).
|
||
|
||
### `apps/admin/src/features/products/components/ProductEditor.tsx`
|
||
|
||
- Añadido import del tipo: `import { PriceStockSection, type PriceStockHandle } from './sections/PriceStockSection';`.
|
||
- Añadido `priceStockRef = useRef<PriceStockHandle>(null)` junto al state de marcas/categorías.
|
||
- `<PriceStockSection ref={priceStockRef} productId={productId} />` en la pestaña General.
|
||
- `handleSave`:
|
||
- Después del bloque de generación SEO, llama `priceStockRef.current?.saveAll()` dentro de su propio try/catch.
|
||
- Si `saveAll` reporta fallos parciales (algún grupo `ok: false`), se calcula la lista de grupos fallidos y se muestra un mensaje tipo `Producto guardado. Revisa precio, EAN.` mapeando cada clave a su etiqueta en español.
|
||
- Si todo va bien, mensaje estándar (`¡Producto creado!` / `Cambios guardados`).
|
||
- Si `saveAll` lanza una excepción (caso muy improbable, sería bug), se setea `error`.
|
||
- Refactor menor: el `setSuccess` ya no se setea en la rama de generación de IA si el producto tenía contenido incompleto (porque ahora se sobreescribe con el resultado del `saveAll`). El mensaje final siempre refleja el estado combinado.
|
||
|
||
## UX
|
||
|
||
Antes:
|
||
- Precio → botón "Guardar precio"
|
||
- Stock, EAN, peso, compra mínima → auto-save en blur o Enter (con ✓ verde durante 3s)
|
||
|
||
Ahora:
|
||
- Todos los campos persisten al pulsar "Guardar cambios" en la cabecera del editor.
|
||
- Los inputs son "tontos": solo mantienen estado local; no hacen red al perder foco.
|
||
- Si el guardado central falla parcialmente, se muestra qué grupo falló (`precio`, `stock`, `EAN`, `peso/compra mínima`) en el banner verde de éxito (con coletilla "Revisa X").
|
||
- Si todo va bien, banner verde estándar.
|
||
|
||
## Archivos modificados
|
||
|
||
- `project/apps/admin/src/features/products/components/sections/PriceStockSection.tsx`
|
||
- `project/apps/admin/src/features/products/components/ProductEditor.tsx`
|
||
|
||
## Evidencia
|
||
|
||
```
|
||
$ cd project/apps/admin && npx tsc --noEmit
|
||
(exit 0)
|
||
|
||
$ npx eslint src/features/products/components/sections/PriceStockSection.tsx \
|
||
src/features/products/components/ProductEditor.tsx
|
||
✖ 7 problems (0 errors, 7 warnings)
|
||
- 6 warnings pre-existentes sobre `as any` en ProductEditor.tsx
|
||
- 1 warning pre-existente sobre unused eslint-disable
|
||
|
||
$ npm run build
|
||
✓ Compiled successfully
|
||
```
|
||
|
||
## Notas
|
||
|
||
- En el flujo de creación, `saveAll` debe ejecutarse después de que `productId` esté seteado (porque `PriceStockSection` necesita un productId para resolver la variante). Como `handleSave` hace `setProductId(saved.id)` justo después de `productsApi.create`, y `saveAll` se invoca en el siguiente tick de microtask, el ref está disponible. El helper `ensureVariant` cubre el caso race donde el `useEffect` de carga aún no haya disparado.
|
||
- No hay tests automatizados para esta vista. La verificación end-to-end requiere navegador: crear un producto nuevo con PVP + stock + EAN + peso, pulsar "Crear producto", recargar `/products/:id`, comprobar que los valores se mantienen.
|
||
- El build regenerado queda en `apps/admin/.next/`. El operador debe reiniciar el monolito (`./scripts/monolith.sh prod restart`) para que el bundle actualizado se sirva en `:3004`.
|