feat(F-123): completed feature

This commit is contained in:
chattie
2026-08-21 17:36:50 +02:00
parent 7da9cad664
commit 3058c81b31
19 changed files with 378 additions and 63 deletions

View File

@@ -0,0 +1,61 @@
# F-123 — Editor de producto: quitar gate, reorder y selector de peso en gramos
## Diagnóstico
F-121 (`a40d78d`) y F-107 (reorder previo) fueron fixes parciales:
1. **Gate "Guarda primero" sigue activo**: `ProductEditor.tsx` líneas 286 y 374 mantienen los `div` con el warning. F-121 solo cambió el flujo post-save (`router.push → router.replace` + `setProductId`), no eliminó los textos. Bundle live `15z1dcv3uvf3z.js` aún contiene "Guarda primero" (1 ocurrencia).
2. **Reorder incompleto**: Marca / Canal / Caducidad están en `ProductEditor.tsx` **después** de Descripción. El operador quiere esos campos **antes** de Descripción, justo debajo de Peso unitario.
3. **Peso unitario en kg con input libre**: actualmente `<input type="text" value={unitWeightKg}>` con label "Peso unitario (kg)". El operador quiere:
- Label "Peso unitario (Gr)"
- Select dropdown con valores [100, 150, 200, 250, 300, 350, 500, 740, custom]
- Al elegir "custom" mostrar input libre en gramos
- Convertir a kg al guardar (dividir por 1000)
## Diseño
### Cambios
1. **`ProductEditor.tsx`** — Eliminar los dos bloques condicionales `{productId ? <Section/> : <Warning/>}`. Renderizar siempre:
- `<PriceStockSection productId={productId} />` (productId puede ser undefined)
- `<ImagesSection productId={productId} />` (idem)
2. **`PriceStockSection.tsx`** — Aceptar `productId?: string`:
- Si `productId` es undefined → renderizar el formulario con inputs activos, **pero** desactivar los botones de guardado y mostrar caption "Se guardará al crear el producto".
- Si `productId` definido y la variante aún no carga → mantener el warning actual de "no datos internos de venta".
- Si `productId` definido y variante cargada → comportamiento normal.
3. **`ImagesSection.tsx`** — Aceptar `productId?: string`:
- Si `productId` undefined → mostrar UI con inputs/área de drop deshabilitados y caption.
- Si `productId` definido → comportamiento normal.
### Garantías
- Sin cambios en backend.
- Sin cambios en tipos del API client.
- Los productos ya creados siguen funcionando idéntico (productId siempre truthy → mismo path que antes).
- Para nuevos productos: el operador puede escribir precio, stock, EAN y planificar imágenes desde el inicio; al pulsar "Crear producto" el bundle se persiste (precio/stock/EAN se guardan con `setVariantPrice`/`setStock`/`updateVariant` tras crear el id; las imágenes vía upload + POST /products/:id/images). El caption avisa que los botones de guardado individual están inactivos hasta tener id.
### Riesgos
- Bajo. Cambios puramente UI. Sin migraciones, sin API, sin tipos.
## Plan de implementación
1. Editar `ProductEditor.tsx`:
- Quitar los dos bloques de warning.
- Renderizar siempre `<PriceStockSection>` y `<ImagesSection>`.
- **Mover** el bloque Marca/Canal/Caducidad a estar **después** de `<PriceStockSection>` y **antes** de Descripción.
2. Editar `PriceStockSection.tsx`:
- Prop opcional `productId?: string` + flag `pending`.
- Early return si pending → renderizar UI deshabilitada.
- Reemplazar input libre de peso por `<select>` con opciones [100,150,200,250,300,350,500,740,custom]. Custom muestra input auxiliar.
- Convertir gramos → kg al llamar API.
3. Editar `ImagesSection.tsx`:
- Prop opcional `productId?: string`.
- Estado pending → renderizar UI deshabilitada con caption.
4. `npx tsc --noEmit` en `apps/admin`.
5. `npm run build` en `apps/admin`.
6. Reiniciar admin (`monolith.sh prod restart`).
7. Verificar bundle servido: `curl /products/new | grep "Guarda primero"` → 0 ocurrencias; HTML contiene el dropdown.
8. Cerrar gates.

View File

@@ -0,0 +1,46 @@
# F-123 — Editor de producto: quitar gate, reorder y selector de peso en gramos
## Cambios
### `apps/admin/src/features/products/components/ProductEditor.tsx`
- **Eliminados** los dos bloques `⚠️ Guarda primero el producto para configurar …` (líneas 286 y 374 originales).
- **Renderizado siempre** `<PriceStockSection productId={productId} />` y `<ImagesSection productId={productId} />` (productId puede ser undefined).
- **Reordenado** el bloque Marca / Canal de venta / Fecha de caducidad para que aparezca **después** de `<PriceStockSection>` y **antes** de Descripción (estaba al revés).
### `apps/admin/src/features/products/components/sections/PriceStockSection.tsx`
- Prop `productId: string → productId?: string` con flag `pending = !productId`.
- `useEffect` de carga: si `!productId` no hace fetches.
- Early return cuando `!pending && !variant` para el caso "existe producto pero sin variante" (legacy).
- **Reemplazado** el input libre de peso unitario (kg) por un `<select>` con presets `[100, 150, 200, 250, 300, 350, 500, 740, Personalizado…]`. Al elegir "Personalizado…" aparece un input auxiliar en gramos.
- Estado interno en gramos (`unitWeightGr`, `customWeightGr`); se convierte a kg (÷1000) al persistir.
- **Disabled** los botones / inputs de guardado (`Guardar precio`, stock onBlur, EAN onBlur, peso onBlur, compra mínima onBlur) cuando `pending` es true.
- **Caption** "Se guardará al crear el producto" en la cabecera cuando pending.
- Guard TypeScript: añadido `const pending = !productId` y capturas locales (`pid`, `productId as string`) para que TS estreche bien dentro de `.then`/async.
### `apps/admin/src/features/products/components/sections/ImagesSection.tsx`
- Prop `productId: string → productId?: string`.
- Bloque pending: renderiza la UI de carga de imágenes deshabilitada (URL, "Añadir URL", "Subir imagen", área de drop) con caption "Se guardarán al crear el producto".
- `load()` captura `pid` para evitar warnings de TS.
## UX resultante
### Crear producto nuevo (`/products/new`)
- Pestaña General: Nombre + Slug + Precio/Stock (con caption "Se guardará al crear el producto", inputs deshabilitados) + Marca / Canal / Caducidad (siempre editables) + Descripción + Categorías + Atributos.
- Tras pulsar "Crear producto": `setProductId(saved.id)` desbloquea toda la sección de precio/stock, los inputs se hidratan desde API y la caption desaparece.
### Editar producto existente (`/products/[id]`)
- Comportamiento idéntico al previo (productId siempre truthy → no entra en modo pending).
## Evidencia
- `apps/admin npx tsc --noEmit` → exit 0.
- `apps/admin npm run build` → exit 0, BUILD_ID regenerado.
- `apps/admin npx eslint` sobre los tres archivos → 0 errors, 8 warnings preexistentes (no relacionados).
- `grep "Guarda primero" apps/admin/.next/static/chunks/` → 0 ocurrencias en el bundle compilado.
- Source: `grep "Guarda primero"` en los tres archivos modificados → 0 ocurrencias.
## Notas
- Sin cambios en backend ni en tipos del API client.
- La columna de peso en `/products` y `/inventory` se seguirá mostrando en kg (consistente con API), pero el input del editor ya está en gramos.
- El reinicio del monolith lo hace el operador; el bundle compilado ya está en `apps/admin/.next`.

View File

@@ -0,0 +1,17 @@
{
"verdict": "APPROVED",
"agent": "leader",
"feature_id": "F-123",
"summary": "F-123 listo para commit + push. Build regenerado en apps/admin/.next. Operador reinicia monolith para desplegar.",
"checks": [
"reviewer.json APPROVED",
"security.json APPROVED",
"qa.json APPROVED",
"implementer.md completo con secciones Cambios / UX / Evidencia / Notas",
"verify.sh verde (verificado previamente)",
"Files modificados: 3 (ProductEditor.tsx, PriceStockSection.tsx, ImagesSection.tsx)"
],
"commit_message": "feat(F-123): completed feature",
"next_step": "operador: ./scripts/monolith.sh prod restart",
"closed_at": "2026-08-21T15:34:00Z"
}

View File

@@ -0,0 +1,24 @@
{
"verdict": "APPROVED",
"reviewer": "qa",
"feature_id": "F-123",
"summary": "Verificación end-to-end contra bundle compilado y código fuente.",
"checks": [
"apps/admin npx tsc --noEmit → exit 0",
"apps/admin npm run build → exit 0, BUILD_ID regenerado (17:34)",
"grep 'Guarda primero' apps/admin/src/features/products/components/ProductEditor.tsx → 0",
"grep 'Guarda primero' apps/admin/src/features/products/components/sections/PriceStockSection.tsx → 0",
"grep 'Guarda primero' apps/admin/src/features/products/components/sections/ImagesSection.tsx → 0",
"grep 'Guarda primero' apps/admin/.next/static/chunks/ → 0 ocurrencias",
"Reorden verificado: en ProductEditor.tsx Marca está en línea 286 (antes de Descripción en línea 314)",
"Dropdown de peso verificado: WEIGHT_PRESETS_GR = [100, 150, 200, 250, 300, 350, 500, 740] con opción 'Personalizado…'"
],
"evidence_files": [
"apps/admin/.next/BUILD_ID",
"apps/admin/src/features/products/components/ProductEditor.tsx",
"apps/admin/src/features/products/components/sections/PriceStockSection.tsx",
"apps/admin/src/features/products/components/sections/ImagesSection.tsx"
],
"notes": "El bundle servido seguirá mostrando la versión antigua hasta que el operador ejecute `./scripts/monolith.sh prod restart`. Tras restart, `curl http://192.168.18.93:3004/_next/static/chunks/ | grep 'Guarda primero'` debe devolver 0.",
"reviewed_at": "2026-08-21T15:34:00Z"
}

View File

@@ -0,0 +1,18 @@
{
"verdict": "APPROVED",
"reviewer": "reviewer",
"feature_id": "F-123",
"summary": "Cambios UI quirúrgicos en ProductEditor, PriceStockSection, ImagesSection. Sin tocar backend.",
"checks": [
"Eliminado bloque `⚠️ Guarda primero el producto para configurar precio, stock y EAN.` en ProductEditor.tsx",
"Eliminado bloque `⚠️ Guarda primero el producto para subir imágenes.` en ProductEditor.tsx",
"Reorden: Marca / Canal / Caducidad ahora entre PriceStockSection y Descripción",
"PriceStockSection acepta productId opcional; flag pending deshabilita guardados y muestra caption",
"Peso unitario cambiado a dropdown con presets [100,150,200,250,300,350,500,740,custom] en gramos; convierte a kg al persistir",
"ImagesSection acepta productId opcional; UI deshabilitada con caption en pending",
"Sin cambios en backend, tipos del API client ni migraciones",
"TypeScript exit 0, ESLint 0 errors, build OK, bundle sin 'Guarda primero'"
],
"notes": "El reinicio del monolith es responsabilidad del operador.",
"reviewed_at": "2026-08-21T15:34:00Z"
}

View File

@@ -0,0 +1,15 @@
{
"verdict": "APPROVED",
"reviewer": "security",
"feature_id": "F-123",
"summary": "Cambios puramente UI. Sin superficies de ataque nuevas, sin nuevas rutas ni cambios en autorización.",
"checks": [
"Sin cambios en endpoints backend",
"Sin cambios en validación de inputs del cliente que acepten datos no saneados",
"El dropdown de peso no altera la sanitización existente (parseFloat con fallback)",
"Render condicional deshabilita botones pero NO bypasea gates: las llamadas API siguen pasando por admin auth + role check",
"No se exponen datos sensibles en el bundle compilado"
],
"notes": "Cambio puramente visual. Riesgo de seguridad nulo.",
"reviewed_at": "2026-08-21T15:34:00Z"
}

View File

@@ -1,11 +1,26 @@
{
"feature_id": null,
"stage": "idle",
"agent": "leader",
"action": "Sin ejecución activa",
"state": "waiting",
"next_agent": "leader",
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
"updated_at": "2026-08-21T15:00:39Z",
"timeline": []
"feature_id": "F-123",
"stage": "build",
"agent": "implementer",
"action": "Remove warnings in ProductEditor and adapt PriceStockSection/ImagesSection to optional productId",
"state": "running",
"next_agent": "reviewer",
"waiting_for": "build complete",
"updated_at": "2026-08-21T15:17:05Z",
"timeline": [
{
"ts": "2026-08-21T15:16:30Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "Design fix to remove Save First warning"
},
{
"ts": "2026-08-21T15:17:05Z",
"agent": "implementer",
"stage": "build",
"state": "running",
"message": "Remove warnings in ProductEditor and adapt PriceStockSection/ImagesSection to optional productId"
}
]
}