# 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`. 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` 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(null)` junto al state de marcas/categorías. - `` 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`.