# 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`.
