# Plan de Ejecución — Módulo WMS

> **Documento vivo.** Refleja el estado real del módulo WMS en `aconta` (frontend) y `api-a-conta` (backend). Se actualiza al cerrar cada bloque o iteración.

## Índice

- [Repositorios y convenciones](#repositorios-y-convenciones)
- [Estrategia de ramas git](#estrategia-de-ramas-git)
- [Estado actual del módulo](#estado-actual-del-módulo)
- [Histórico — Fases 1–10 (cerradas 2026-05-14)](#histórico--fases-110-cerradas-2026-05-14)
- [Iteración 1 — Mejoras WMS (cerrada 2026-05-15)](#iteración-1--mejoras-wms-cerrada-2026-05-15)
- [Iteración 2 — Modelo espacial (en curso, arrancada 2026-05-15)](#iteración-2--modelo-espacial-en-curso-arrancada-2026-05-15)
- [Mejoras Layout 2D (en curso, arrancada 2026-05-22)](#mejoras-layout-2d-en-curso-arrancada-2026-05-22)
- [Apéndice A — Estructura del módulo](#apéndice-a--estructura-del-módulo)
- [Apéndice B — Riesgos y mitigaciones](#apéndice-b--riesgos-y-mitigaciones)
- [Apéndice C — Comandos útiles](#apéndice-c--comandos-útiles)

---

## Repositorios y convenciones

**Repositorios** (Windows, XAMPP):
- Backend: `C:\xampp\htdocs\api-a-conta` — Laravel 9.19, PHP 8, MySQL, Sanctum
- Frontend: `C:\xampp\htdocs\aconta` — Nuxt 3, Vue 3, TypeScript, Bootstrap 5, vue-konva

**Convenciones obligatorias:**
- Clases prefijo **`Wms`**, tablas prefijo **`wms_`**, rutas en camelCase (`/wmsRacks`).
- Backend: `app/Http/Controllers/Wms/`, `app/Models/Wms/`, `app/Services/Wms/`.
- **Validación inline** con `$request->validate([...])` — NO usar FormRequest.
- **Soft-delete por columna `estado` TINYINT (1/0)** — NO usar trait `SoftDeletes`.
- Transacciones: `DB::transaction(fn () => ...)`.
- Multi-tenant: filtrar por `$request->user()->par_empresa_id`.
- Migraciones: PK `id` BIGINT UNSIGNED, timestamps, charset `utf8mb4`.
- Frontend: file-based routing en `pages/pos/wms/...`, `definePageMeta({ layout: 'admin', page: 'X', modulo: 'WMS' })`, llamadas con `const { $api } = useNuxtApp()`.
- Componentes con canvas envueltos en `<ClientOnly>` y registrados vía plugin `.client.ts`.

**Reglas WMS:**
1. WMS **nunca** escribe directo en `pos_bodega_articulos.stock`. Siempre vía `pos_inventarios`.
2. Suma `wms_ubicacion_stock.stock_unidades` por (bodega, artículo) DEBE igualar `pos_bodega_articulos.stock`. Validar con `php artisan wms:reconcile`.
3. Estrategias slotting: FIFO (`fecha_ingreso`), FEFO (`fecha_vencimiento`), ABC (clase A/B/C), MIXTA (suma ponderada).

---

## Estrategia de ramas git

```
main
 └── develop
      └── feature/wms                         (integradora del módulo)
           ├── feature/wms-NN-...             (fases originales 1–10, cerradas)
           ├── feature/wms-it1-NN-...         (iteración 1, cerrada)
           └── feature/wms-it2-NN-...         (iteración 2, en curso)
                └── feature/wms-it2           (integradora de la iteración)
```

**Flujo por bloque/fase** (en ambos repos):

```bash
git checkout feature/wms-it2 && git pull       # rama integradora actual
git checkout -b feature/wms-it2-NN-nombre
# ... trabajo ...
git add -A && git commit -m "wms(it2-bloqueNN): descripción"
git checkout feature/wms-it2 && git merge --no-ff feature/wms-it2-NN-nombre
```

Cuando la iteración cierra: merge `feature/wms-it2` → `feature/wms` con `--no-ff`.

**Convención de commits:** `wms(<contexto>): <descripción>`
- Fases originales: `wms(faseNN): …`
- Iteración 1: `wms(it1-bloqueNN): …`
- Iteración 2: `wms(it2-bloqueNN): …`

---

## Estado actual del módulo

| Hito | Estado | Fecha |
|---|---|---|
| Fases 1–10 — Fundación del módulo | ✅ Cerradas | 2026-05-14 |
| Iteración 1 — Mejoras WMS | ✅ Cerrada | 2026-05-15 |
| Iteración 2 — Modelo espacial | ⛔ Pausada tras bloque 01 | 2026-05-15 |
| **Refactor — Bloque A (Reset BD)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 1 (Backend editor CAD)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 2 (Limpieza frontend + esqueleto editor)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 3a (Arrastre + edición de aristas + pan/zoom)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 3b (Aberturas + elementos arquitectónicos)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 4a (Racks)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 4b (Zonas operativas + snapshot endpoint)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 5a (Niveles + ubicaciones autom.)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 5b (Tareas + movimientos + integración POS↔WMS)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque B paso 5c (Rack lifecycle + cascade)** | ✅ Cerrado | 2026-05-16 |
| **Refactor — Bloque C (Investigación slotting↔contabilidad)** | ✅ Cerrado | 2026-05-16 |
| **Pendientes + rescatables del módulo previo** | 📄 [Doc dedicado](PENDIENTES_Y_RESCATABLES.md) | 2026-05-16 |
| **Rescate R-1 (Reportes ocupación/rotación/vencimientos)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-2 (Vista de elevación del rack)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-3 (Integración POS↔WMS verificada)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-4 (Ajustes manuales + Reconcile)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-5 (Listado de racks + CRUD niveles)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-6 (ScannerQR + flujo móvil)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-7 (Conteo cíclico)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-8 (Pisos + slotting + autoconfig)** | ✅ Cerrado | 2026-05-16 |
| **Rescate R-9 (PUT snapshot + Panels)** | ✅ Cerrado | 2026-05-16 |
| **Mejoras Layout 2D (L-1, L-5, L-7, L-8, L-9, L-10, L-11, L-12, L-13, L-14, L-15, L-16, L-17, L-18, L-19, L-20, L-21, L-22, L-23, L-24, L-25, L-26, L-27, L-28, L-29, L-30, L-31, L-32, L-33, L-34)** | 🔄 En curso | 2026-05-22 |
| **Asignación / Slotting (algoritmo + UI unificada + 3 estados + ocupación por volumen)** | ✅ Cerrada | 2026-05-23 |

> ⚠️ **La iteración 2 se canceló tras el bloque 01** (ver sección "Iteración 2"). Se reorientó la estrategia hacia un reset de BD WMS (solo artículos sobreviven) y un refactor del editor 2D a estilo AutoCAD (dibujo por vértices con acotación manual). Los bloques 02–06 de it2 quedan cancelados; el bloque 01 cerrado queda en historial pero su tabla `wms_habitaciones` fue dropeada en el reset y recreada con otro schema.

**Migraciones WMS vigentes:** 20 nuevas (`2026_05_16_000001` → `000020`) tras el reset del bloque A. Las 23 anteriores fueron eliminadas del repo.

**Flujo de desarrollo vigente** (decisión 2026-05-15): Claude Design es **opcional** por bloque. El patrón visual ya está consolidado tras fases 1–10 + it1; se codea UI directo aplicando los tokens en `aconta/design/aconta-design-tokens.json`. El registro en este documento al cerrar bloque **sigue siendo obligatorio**.

---

## Histórico — Fases 1–10 (cerradas 2026-05-14)

Plan original de 14 migraciones, 12 modelos, 13 controllers, 5 servicios y ~10 páginas. Detalle ya consolidado en código; la BD y los archivos del repo son la fuente de verdad.

| Fase | Rama | Entrega |
|---|---|---|
| 1 — Fundación BD + modelos | `feature/wms-01-foundation` | 14 migraciones (`2026_05_14_*`), 12 modelos Eloquent, seeder mínimo |
| 2 — Empaques | `feature/wms-02-empaques` | CRUD `wms_articulo_empaques` con dimensiones, peso bruto/neto y orientaciones |
| 3 — Racks heterogéneos | `feature/wms-03-racks` | Racks con niveles variables, `WmsRackController`, `WmsRackNivelController` |
| 4 — Migración legacy | `feature/wms-04-migracion` | Comandos `wms:migrate-ubicaciones` y `wms:reconcile` |
| 5 — Slotting + ABC | `feature/wms-05-slotting-abc` | `WmsSlottingService`, `WmsClasificacionAbcService`, `RecalcularAbcJob` semanal |
| 6 — Canvas 2D top-down | `feature/wms-06-canvas-topdown` | Editor visual con Konva (`LayoutTopDown`, `ZonaPolygon`, `RackShape`, `ToolbarLayout`) |
| 7 — Canvas rack lateral | `feature/wms-07-canvas-rack` | `RackElevation` con render de niveles, divisiones y vista lateral |
| 8 — Modo asistido | `feature/wms-08-modo-asistido` | Heurística "Sugerir layout óptimo" con propuestas aceptables individualmente |
| 9 — Integración POS↔WMS | `feature/wms-09-integracion-movimientos` | Observer `PosInventarioObserver`, `WmsTareaController`, `WmsMovimientoController` |
| 10 — Reportes | `feature/wms-10-reportes` | Reportes de ocupación, rotación y vencimientos (`/pos/wms/reportes/*`) |

---

## Iteración 1 — Mejoras WMS (cerrada 2026-05-15)

Refinamiento sobre las 10 fases. 8 bloques temáticos + 2 revisiones sobre el bloque 06.

### Bloques cerrados

| Bloque | Rama | Backend | Frontend | Descripción |
|---|---|---|---|---|
| 01 | `feature/wms-it1-01-aside-y-dashboard` | — | `ab407bb` | Iconos descriptivos en aside (desktop + mobile), dashboard `/pos/wms` con grid 5×2 compacto |
| 02 | `feature/wms-it1-02-empaques-orientacion` | `c66af3d` | `ee55ca1` | Columna `orientacion_descripcion` en `wms_articulo_empaques`, UI con preset condicional |
| 03 | `feature/wms-it1-03-pisos` | `65ea97a` | `61191f4` | Tabla `wms_pisos`, FK `wms_piso_id` en racks y zonas con backfill, CRUD y selector de piso |
| 04 | `feature/wms-it1-04-rack-divisiones-subdivisiones` | `a619f7d` | `b737d8c` | Tabla `wms_rack_nivel_subdivisiones`, columna `numero_subdivision` en `wms_ubicaciones`, UI en `RackForm` y `RackElevation` |
| 05 | `feature/wms-it1-05-layout-interacciones` | — | `8bba0d4` | Ctrl+wheel zoom, pan con click medio, menú contextual por rack, validación AABB con rollback |
| 06 | `feature/wms-it1-06-zonas-y-racks-desde-layout` | — | `dfa9962` | Crear racks y zonas desde el toolbar del layout |
| 06b | `feature/wms-it1-06b-zona-rectangulo` | — | `823b108` | Modal con `pos_x/pos_y/ancho_m/alto_m` en vez de dibujo de polígono |
| 06c | `feature/wms-it1-06c-zonas-arrastrables` | `d575ba6` | `3cbbc42` | Zonas como bloques rectangulares draggable, backfill desde AABB |
| 07 | `feature/wms-it1-07-ayudas-y-tooltips` | — | `b98927d` | `<details>` colapsable de ayuda en 6 páginas operativas |
| 08 | `feature/wms-it1-08-slotting-aplicado` | `7112ca1` | `6e20ad8` | Columna `auto_sugerir` en `wms_regla_slottings`, CRUD reglas, botón "Sugerir ubicación" |

### Migraciones aplicadas (`2026_05_15_*`)

1. `000001_add_orientacion_descripcion_to_wms_articulo_empaques`
2. `000002_create_wms_pisos_table`
3. `000003_add_wms_piso_id_to_wms_racks_and_wms_zonas` (con backfill en transacción)
4. `000004_alter_wms_ubicaciones_add_numero_subdivision`
5. `000005_create_wms_rack_nivel_subdivisiones_table`
6. `000006_add_rect_columns_to_wms_zonas` (con backfill desde AABB)
7. `000007_add_auto_sugerir_to_wms_regla_slottings`

Todas idempotentes, encima de las 14 originales. Ninguna migración antigua fue tocada.

### Diferido a iteración 2

- **Modelo espacial completo**: habitaciones, paredes, escaleras.
- **Agrupación de zonas** (zona padre/hijo).
- Ajustes UX que surjan al probar lo entregado.

### Estado git al cierre

Ambos repos en `feature/wms` con merges `--no-ff` desde las sub-ramas de bloque. Sin push, sin merge a develop/main. Backend +8 commits, frontend +18 commits sobre `origin/feature/wms` antes del push de cierre.

---

## Iteración 2 — Modelo espacial (cancelada 2026-05-16)

Solo cerró el bloque 01 (habitaciones rectangulares, rama `feature/wms-it2-01-habitaciones`). Los bloques 02–06 quedaron cancelados porque el refactor reemplazó el modelo: el reset de BD dropeó `wms_habitaciones` y el nuevo editor CAD usa polígonos de vértices acotados en vez de habitaciones rectangulares. El bloque 01 queda en historial git pero su tabla no existe en el esquema vigente.

---

## Refactor — Reset BD + Layout 2D CAD (arrancado 2026-05-16)

> Las 7 decisiones de arranque del refactor quedaron cerradas el 2026-05-16. Detalle de cada bloque a continuación.

### Bloque A — Reset de BD (cerrado 2026-05-16)

**Rama:** `feature/wms-refactor` desde `feature/wms`.

**Backup previo:** `backup_wms_pre_refactor_20260516.sql` (43 KB) — dump de las 15 tablas `wms_*` + `pos_bodega_articulo_ubicaciones` antes del reset.

**Borrados** (`git rm`):
- 23 migraciones WMS (`2026_05_14_000001` → `2026_05_14_000014` y `2026_05_15_000001` → `2026_05_15_000008`).
- 1 migración legacy (`2025_12_23_161448_create_pos_bodega_articulo_ubicaciones_table.php`).
- 2 comandos artisan (`wms:migrate-ubicaciones`, `wms:reconcile`).
- 12 modelos Eloquent (BodegaLayout, Zona, Rack, RackNivel, RackNivelSubdivision, Ubicacion, UbicacionStock, Movimiento, Tarea, TareaLinea, Habitacion, Piso).
- 11 controllers (BodegaLayout, Zona, Rack, RackNivel, UbicacionStock, Movimiento, Tarea, Piso, Habitacion, Slotting, Reporte).
- 4 services (CapacidadService, LayoutService, MovimientoService, SlottingService).
- 3 seeders (WmsSeeder, WmsDatosPruebaSeeder, WmsMenusSeeder).

**Limpieza colateral:**
- `PosInventarioObserver` reducido a no-op (será reescrito en bloque B).
- `routes/api.php` solo conserva rutas de `wmsArticuloEmpaques` y `wmsReglaSlottings`.
- `DatabaseSeeder` ya no llama a seeders WMS.
- `WmsReglaSlotting` perdió el método `layout()` (apuntaba a clase borrada).

**Conservados** (las 3 supervivientes del módulo):
- Modelos: `WmsArticuloEmpaque`, `WmsClasificacionAbc`, `WmsReglaSlotting`.
- Controllers: `WmsArticuloEmpaqueController`, `WmsReglaSlottingController`.
- Service: `WmsClasificacionAbcService` (más `app/Jobs/Wms/RecalcularAbcJob.php`).

**Nuevas migraciones (`2026_05_16_*`, 20 archivos):**

Conservadas (recreadas con schema final):
1. `000001_create_wms_articulo_empaques_table` (con `orientacion_descripcion`)
2. `000002_create_wms_clasificacion_abcs_table`
3. `000003_create_wms_regla_slottings_table` (con `auto_sugerir`)

Alters POS (conservan columnas pero sin FK a tablas dropeadas):
4. `000004_alter_pos_inventarios_add_wms_movimiento_id` (nullable, sin FK)
5. `000005_alter_pos_articulos_add_requiere_gestion_wms`

Geometría base del editor CAD:
6. `000006_create_wms_bodega_layouts_table` (esquema mínimo: 1 por bodega)
7. `000007_create_wms_pisos_table`
8. `000008_create_wms_vertices_table` (NUEVA — vértices compartibles)
9. `000009_create_wms_aristas_table` (NUEVA — con `grosor_m` y `subtipo_pared`)
10. `000010_create_wms_habitaciones_table` (polígono)
11. `000011_create_wms_habitacion_vertices_table` (pivot habitación↔vértice)

Primitivas arquitectónicas (todo lo que hay en un piso de casa o bodega):
12. `000012_create_wms_aberturas_table` (NUEVA — puerta/ventana/vano/portón como sub-segmentos de aristas)
13. `000013_create_wms_elementos_arquitectonicos_table` (NUEVA — columna/escalera/muelle/otro)
14. `000014_create_wms_zonas_table` (recreada, polígono lógico)
15. `000015_create_wms_anotaciones_table` (NUEVA — texto libre sobre el plano)

Operativas (igual schema que antes, fueron dropeadas y recreadas):
16. `000016_create_wms_racks_table` (con piso/habitación nullable)
17. `000017_create_wms_rack_niveles_table`
18. `000018_create_wms_ubicaciones_table` (con `numero_subdivision` incluido desde el create)
19. `000019_create_wms_ubicacion_stocks_table` (con columnas generadas y unique multi-key)
20. `000020_create_wms_rack_nivel_subdivisiones_table`

**Verificaciones hechas:**
- `php artisan migrate:fresh --force` corre limpio, 59 migraciones en `DONE` (POS + WMS + módulos restantes).
- 18 tablas `wms_*` creadas en MySQL: `wms_aberturas`, `wms_anotaciones`, `wms_aristas`, `wms_articulo_empaques`, `wms_bodega_layouts`, `wms_clasificacion_abcs`, `wms_elementos_arquitectonicos`, `wms_habitacion_vertices`, `wms_habitaciones`, `wms_pisos`, `wms_rack_nivel_subdivisiones`, `wms_rack_niveles`, `wms_racks`, `wms_regla_slottings`, `wms_ubicacion_stocks`, `wms_ubicaciones`, `wms_vertices`, `wms_zonas`.
- 0 referencias colgantes a clases borradas en `app/`.

### Bloque B paso 1 — Backend del editor CAD (cerrado 2026-05-16)

**Alcance:** modelos Eloquent + controllers + rutas para las 9 entidades geométricas/arquitectónicas. Sin UI todavía — eso es paso 2.

**Modelos creados** (9 archivos en `app/Models/Wms/`):
- `WmsBodegaLayout`, `WmsPiso`, `WmsVertice`, `WmsArista`, `WmsHabitacion`, `WmsAbertura`, `WmsElementoArquitectonico`, `WmsZona`, `WmsAnotacion`.
- Patrón: `protected $guarded = ['id']`, casts numéricos a `decimal:3`, relaciones Eloquent (belongsTo / hasMany / belongsToMany).
- `WmsHabitacion ↔ WmsVertice` con pivot `wms_habitacion_vertices` y `withPivot('orden')->orderBy('pivot_orden')` para conservar el orden del polígono.

**Controllers creados** (9 archivos en `app/Http/Controllers/Wms/`):
- Cada uno con `index`, `store`, `show`, `update`, `destroy`.
- Validación inline con `$request->validate([...])` (sin FormRequest, según convención WMS).
- Soft-delete vía `estado = 0` para layouts, pisos, habitaciones, aberturas, elementos, zonas, anotaciones.
- Hard-delete con validación de referencias para vértices y aristas (no tienen columna `estado`).
- `WmsBodegaLayoutController::index` aplica filtro multi-tenant por `par_empresa_id` vía `pos_bodegas`.
- `WmsAberturaController` valida que `posicion_m_desde_inicio + ancho_m` no exceda `distancia_real_m` de la arista.
- `WmsAristaController` limpia `grosor_m`/`subtipo_pared` si `tipo != 'pared'`.
- `WmsHabitacionController::store` sincroniza el pivot de vértices ordenados dentro de `DB::transaction`.

**Rutas registradas** (`routes/api.php`):
- 9 `Route::apiResource` → 45 endpoints (`GET /wmsBodegaLayouts`, `POST /wmsVertices`, etc.).

**Smoke test** (tinker, rollback al final):
- Layout + piso + 4 vértices + 4 aristas (rectángulo 10m × 5m con paredes exteriores de 0.15m).
- 1 habitación con los 4 vértices ordenados.
- 1 puerta (sobre arista 1, 4m + 0.9m, abre der), 1 ventana (sobre arista 3, 3m + 1.5m, alto 1.2m, h 1.0m).
- 1 columna, 1 zona almacenamiento, 1 anotación.
- Eager-load completo: `WmsBodegaLayout::with(['pisos', 'vertices', 'aristas.aberturas', 'habitaciones.vertices', 'elementos', 'zonas', 'anotaciones'])` devuelve el grafo correcto.

### Bloque B paso 2 — Limpieza frontend + esqueleto editor CAD (cerrado 2026-05-16)

**Rama frontend:** `feature/wms-refactor` (en `aconta/`) desde `feature/wms`.

**Limpieza** — borrados del repo `aconta/`:
- Páginas WMS rotas: `pos/wms/layout/index.vue`, `pos/wms/layout/[bodegaId].vue`, `pos/wms/zonas/index.vue`, `pos/wms/pisos/index.vue`, todo `pos/wms/racks/`, todo `pos/wms/operaciones/`, todo `pos/wms/reportes/`.
- Componentes WMS obsoletos: todo `components/wms/canvas/` (LayoutTopDown, RackElevation, RackShape, ZonaPolygon, HabitacionRect, ToolbarLayout, PanelArticulosDisponibles, PanelOcupacion, RackPropuesto, GridBackground), `components/wms/forms/RackForm.vue`, `components/wms/operaciones/ScannerQR.vue`.
- Composable colgante: `composables/useWmsCanvas.ts` (lo usaba el editor viejo).

**Adaptaciones**:
- `pages/pos/wms/index.vue` (dashboard) — tiles reducidas a 3 (Empaques, Reglas slotting, Editor 2D), stats consultan solo endpoints supervivientes, alerta visible sobre refactor en curso.
- `components/aside/menuOptions.js` — submenú WMS reducido de 13 a 4 entradas (Inicio, Empaques, Reglas slotting, Editor 2D).
- Páginas que sobreviven sin cambios: `pos/wms/empaques/index.vue`, `pos/wms/reglas-slotting.vue`.

**Esqueleto del editor CAD** — archivos nuevos:
- `composables/useCadSnap.ts` — funciones puras de snap: a vértice (radio 12 px), a midpoint de aristas, y ortogonal (múltiplos de 45° desde el ancla). Aplica prioridad: vértice > midpoint > ortho.
- `composables/useCadDraw.ts` — state machine de modos (`select`, `draw-polygon`, `edit-vertex`, `acotar`) + ref de vértices del polígono en construcción + flag ortho.
- `components/wms/canvas/cad/Vertex.vue` — círculo Konva con hover y drag opcional, color dinámico según `snapped`.
- `components/wms/canvas/cad/Edge.vue` — segmento Konva con label de acotación clickeable. El stroke se ensancha proporcional a `grosor_m` cuando `tipo='pared'`.
- `components/wms/canvas/cad/Polygon.vue` — polígono cerrado con fill semitransparente y dash cuando no está seleccionado. Vértices ordenados por `pivot.orden`.

**Páginas nuevas**:
- `pages/pos/wms/layout/index.vue` — lista de bodegas con badge de estado (con/sin layout) y botón para crear/abrir.
- `pages/pos/wms/layout/[bodegaId].vue` — editor MVP con:
  - Toolbar: modos `select` / `draw-polygon`, toggle `ortho`, mensajes contextuales.
  - Canvas Konva 1200×700 con grid de fondo (25 px).
  - Render de habitaciones existentes (polígonos), aristas con label de acotación, vértices.
  - Modo `draw-polygon`: clicks agregan vértices con snap; Enter o doble-click cierran el polígono y persiste todo (POST de vértices → POST de aristas con `distancia_real_m` calculada desde la escala → POST de habitación con `vertice_ids` ordenados, todo orquestado desde el frontend).
  - Click sobre label de arista abre prompt SweetAlert2 para editar la distancia real.
  - Atajos teclado: Esc cancela, P entra a draw-polygon, O toggle ortho, Z deshace último vértice, Enter cierra polígono, Del elimina habitación seleccionada.

**Lo que NO está en el MVP (entra en B paso 3):**
- Aberturas (puertas/ventanas/vanos) sobre paredes.
- Elementos arquitectónicos (columnas/escaleras/muelles).
- Edición de paredes con grosor (toolbar de tipo `pared` y subtipo).
- Arrastrar vértices existentes para deformar polígonos.
- Snap perpendicular visible.
- Pan/zoom (Ctrl+wheel).
- Múltiples pisos en el mismo editor.
- Zonas y racks dentro del editor.

**Verificación:**
- Sin `npm run dev` corrido en esta sesión — el usuario debe probar el editor en navegador antes de dar por validado el MVP. Casos a verificar:
  1. Abrir `/pos/wms/layout` con sesión activa → ver bodegas listadas.
  2. Crear layout en una bodega sin layout → redirige a `/pos/wms/layout/{bodegaId}`.
  3. Click "Dibujar habitación" → modo activo, cursor preview aparece.
  4. Click 4 veces en el canvas + Enter → habitación creada y persistida.
  5. Click sobre label de distancia → editar y guardar.
  6. Seleccionar habitación + Del → desaparece.

**Próximo paso (Bloque B paso 3):** completar el editor con aberturas, elementos arquitectónicos, edición de paredes con grosor, arrastre de vértices, pan/zoom.

### Bloque B paso 3a — Arrastre + edición de aristas + pan/zoom (cerrado 2026-05-16)

Cambios solo en `pages/pos/wms/layout/[bodegaId].vue` (sin tocar composables ni componentes Konva — los hooks de `Vertex.vue` y `Edge.vue` ya soportaban estos casos).

**Arrastre de vértices** (modo `edit-vertex`, atajo `V`):
- Botón nuevo en toolbar (icono `bi-arrows-move`).
- Vertex.vue se vuelve `draggable` solo en este modo.
- Handler `onVerticeDragEnd`: aplica snap a otros vértices al soltar (excluye el propio), update optimista local, `PUT /wmsVertices/{id}` con las nuevas coords, refresca aristas/habitaciones para mantener consistencia. Rollback con `cargar()` si falla.
- Las distancias reales de las aristas conectadas NO se modifican (decisión D5 del refactor).

**Selección de arista + panel lateral**:
- En modo `select`, click sobre body de arista → selecciona y abre panel flotante (top-right del canvas, ancho 280 px).
- Panel muestra: tipo (`borde_habitacion`/`pared`/`guia`), distancia real (m), y si `tipo='pared'` también grosor (cm) y subtipo (`interior`/`exterior`/`porton`).
- Cada cambio dispara `PUT /wmsAristas/{id}` con el campo modificado; update optimista local. Backend ya limpia grosor/subtipo cuando tipo deja de ser pared.

**Pan/zoom**:
- `Ctrl+wheel` zoom (factor 1.1, rango 0.2–5×, centrado en el cursor con corrección de translate).
- Click medio + drag → pan del stage (`isPanning` flag, clientX/Y diff).
- Indicador "%" en toolbar, botón "Reset vista" (atajo `R`).
- `getPointerPos` usa `getRelativePointerPosition()` para que los snaps trabajen en coords del mundo, no de pantalla.

**UX detalles**:
- Atajos teclado ignoran cuando el foco está en input/textarea/select del panel.
- Esc deselecciona arista y habitación además de cancelar el draft de polígono.

**Próximo paso (Bloque B paso 3b):** aberturas (puertas/ventanas/vanos/portones) sobre paredes + elementos arquitectónicos (columnas/escaleras/muelles).

### Bloque B paso 3b — Aberturas + elementos arquitectónicos (cerrado 2026-05-16)

**Archivos nuevos:**
- `components/wms/canvas/cad/AberturaMark.vue` — rectángulo Konva posicionado sobre una pared usando la geometría de la arista padre. Calcula `pxPerM = lengthPx / distancia_real_m` para escalar `ancho_m` y `posicion_m_desde_inicio` al canvas. `offsetY = grosorPx/2` lo centra sobre la línea de la pared. Rotación = atan2(dy,dx). Colores por tipo: puerta blanca/azul, ventana celeste/azul, vano blanco/gris, portón naranja.
- `components/wms/canvas/cad/Elemento.vue` — rectángulo Konva con `offsetX/Y` centrados en `(x_canvas, y_canvas)`, rotación arbitraria, letra de tipo (C/E/M/?) superpuesta. Colores por tipo: columna azul, escalera amarillo, muelle rojo, otro gris.

**Cambios en `composables/useCadDraw.ts`:**
- `CadMode` extendido con `add-opening` y `add-element`.
- Nuevo tipo `ElementoTipo = 'columna' | 'escalera' | 'muelle' | 'otro'`.
- Refs nuevas: `elementoTipo` (default `columna`).
- Función nueva: `setElementoTipo(t)`.

**Cambios en `pages/pos/wms/layout/[bodegaId].vue`:**
- Carga inicial extendida: trae `wmsElementosArquitectonicos?wms_bodega_layout_id=` y extrae `aberturas` desde el include de `wmsAristas` (el backend ya las trae con `with('aberturas')` en index).
- Toolbar nuevo: botón "Abertura" (icono door-open, atajo `A`), dropdown "Elemento" (icono bricks, atajo `E`) con opciones columna/escalera/muelle/otro.
- Render en stage:
  - `aberturasConArista` (computed) cruza `aberturas` con sus `aristas` y vértices, alimenta a `<CadAberturaMark>`.
  - `<CadElemento>` itera sobre `elementos`, le pasa `escala_px_por_m` del layout.
- Handlers de creación:
  - `crearAbertura(arista)` — solo si `arista.tipo === 'pared'`. Abre modal SweetAlert2 HTML custom con select (tipo), input ancho_m, posición desde inicio, alto_m, sentido apertura. Valida que `pos + ancho ≤ distancia_real_m`. POST `/wmsAberturas`.
  - `crearElemento(pos)` — POST `/wmsElementosArquitectonicos` con tipo (de `elementoTipo`), x_canvas/y_canvas (del cursor), ancho_m/largo_m/rotacion_grados del modal.
- Handlers de selección:
  - `onAristaClick(a)` — en `select` abre panel; en `add-opening` invoca `crearAbertura`.
  - `selectAbertura(ab)` — modal info + opción eliminar (DELETE).
  - `selectElemento(el)` — modal info + opción eliminar.
- Status bar muestra mensajes contextuales por modo (incluyendo `add-opening`, `add-element`).
- Stats actualizados: vértices · aristas · habitaciones · aberturas · elementos.
- Ayuda actualizada con secciones de Aberturas y Elementos.

**Lo que NO está cubierto en B3b** (siguen pendientes para B paso 4 o futuras):
- Racks, niveles, ubicaciones (el backend las creó en migraciones; falta UI).
- Zonas operativas (recepción/picking/despacho) dentro del editor.
- Pisos múltiples en el mismo editor (filtro o switcher).
- Snapshot endpoint (`GET/PUT /wmsBodegaLayouts/{id}/dibujo`) para guardar/restaurar todo el dibujo en una transacción.
- Edición avanzada de aberturas (cambiar tipo, posición, sentido sin borrar y recrear).
- Snap perpendicular visible al cursor.

**Próximo paso (Bloque B paso 4):** racks dentro del editor (anclados a habitación o flotantes), zonas operativas, snapshot endpoint para guardar todo el dibujo en una operación.

### Bloque B paso 4a — Racks (cerrado 2026-05-16)

**Backend nuevo:**
- 5 modelos Eloquent en `app/Models/Wms/`: `WmsRack`, `WmsRackNivel`, `WmsUbicacion`, `WmsUbicacionStock`, `WmsRackNivelSubdivision`. Schema sin cambios (las tablas estaban desde Bloque A).
- `WmsRackNivel` relaciona con `WmsRack`, `WmsUbicacion`, `WmsRackNivelSubdivision`.
- `WmsUbicacionStock` excluye `lote_key` y `venc_key` del `$guarded` porque son columnas STORED generadas por MySQL.
- `app/Http/Controllers/Wms/WmsRackController.php` — CRUD inline + filtros por layout/piso/habitación/zona en index. Valida unicidad `(wms_bodega_layout_id, codigo)` en store/update con mensaje específico. Soft-delete con `estado=0`.
- Ruta `Route::apiResource('/wmsRacks', WmsRackController::class)` agregada a `routes/api.php`.
- Smoke test tinker: layout firstOrCreate + crear rack + eager-load `with(['layout', 'niveles'])` OK.

**Frontend nuevo:**
- `components/wms/canvas/cad/Rack.vue` — Konva `v-group` rotado por `rotacion_grados`. Rectángulo central con `largo_m × ancho_m` escalado por `escala_px_por_m`. Líneas internas verticales separan niveles (si `niveles_count > 1`). Triángulo en un extremo marca el frente. Código del rack como label encima. Colores por tipo: selectivo azul claro, drive-in verde, push-back amarillo, cantilever rojo, mezzanine violeta.
- `CadMode` ampliado con `'add-rack'` en `composables/useCadDraw.ts`.

**Cambios en el editor (`pages/pos/wms/layout/[bodegaId].vue`):**
- `racks` ref + carga en `cargar()` y `cargarHabitacionesYAristas()` desde `wmsRacks?wms_bodega_layout_id=`.
- `<CadRack>` itera sobre `racks`, recibe `escalaPxPorM` del layout y `highlighted` cuando coincide `selectedRackId`.
- Botón nuevo en toolbar (icono `bi-server`) para entrar a modo `add-rack`.
- Status bar contextual `'Rack: click en el canvas donde querés ubicarlo...'`.
- Stats actualizadas con `{{ racks.length }}rk`.
- `onStageClick` en modo `add-rack`: obtiene pos del cursor → `crearRack(pos)`.
- `crearRack(pos)` — modal SweetAlert2 HTML custom con campos: código (req), tipo (selectivo/drive_in/push_back/cantilever/mezzanine), largo/ancho/alto (m), niveles, rotación (°), material (acero/madera/aluminio/plástico). POST `/wmsRacks` con `wms_bodega_layout_id` + pos_x/pos_y + payload.
- `selectRack(r)` — modal info (tipo, dimensiones, material, niveles, capacidad) con opción Eliminar (DELETE).
- Ayuda actualizada con sección de Racks.

**Pendiente para B paso 4b:**
- Zonas operativas como overlays editables (rectángulo o polígono).
- Snapshot endpoint `GET/PUT /wmsBodegaLayouts/{id}/dibujo` para serializar todo el dibujo en una transacción y permitir guardar/restaurar atómicamente.
- Niveles de rack y subdivisiones desde la UI (hoy `niveles_count` se almacena pero no se crean los registros de `wms_rack_niveles`/`wms_ubicaciones`).

### Bloque B paso 4b — Zonas operativas + snapshot endpoint (cerrado 2026-05-16)

**Backend:**
- Migration `2026_05_16_000021_add_geometry_to_wms_zonas` añade `pos_x`, `pos_y`, `ancho_m` (default 1), `largo_m` (default 1), `rotacion_grados` a la tabla. Las zonas existentes quedan con valores por defecto (rect 1×1 en el origen).
- `WmsZona` model + `WmsZonaController` actualizados con los nuevos campos en validate y casts.
- Endpoint nuevo `GET /wmsBodegaLayouts/{id}/dibujo` (`WmsBodegaLayoutController::dibujo`): devuelve `{ layout, bodega, pisos, vertices, aristas (con aberturas), habitaciones (con vertices+pivot), zonas, elementos, anotaciones, racks }` en una sola request transaccional de lectura. Reemplaza 5-7 requests paralelas del editor.

**Frontend:**
- `components/wms/canvas/cad/Zona.vue` — Konva `v-group` rotado. `v-rect` semitransparente con borde punteado por defecto (`dash:[6,3]`), color por tipo (verde recepción, azul almacenamiento, amarillo picking, rojo despacho, violeta staging, naranja devolución, gris otra). Nombre + label de tipo dentro del rect.
- `useCadDraw.ts` extendido: `CadMode += 'add-zone'`, tipo `ZonaTipo`, ref `zonaTipo` (default `almacenamiento`), función `setZonaTipo`.
- Editor (`pages/pos/wms/layout/[bodegaId].vue`):
  - `zonas` ref + `selectedZonaId` ref.
  - Render `<CadZona>` antes que los racks (zonas debajo, racks encima).
  - Dropdown nuevo en toolbar (icono `bi-bounding-box`) con los 7 tipos de zona.
  - Status bar contextual + stats con `zn`.
  - `iniciarAddZona(tipo)` setea `zonaTipo` y entra a modo `add-zone`.
  - `onStageClick` en `add-zone` → `crearZona(pos)` que abre modal Swal con nombre/ancho/largo/rotación/prioridad → POST `/wmsZonas`.
  - `selectZona(z)` → modal info con opción Eliminar (DELETE).
  - `cargar()` refactorizado: llama a `cargarSnapshot(layoutId)` que hace 1 request a `wmsBodegaLayouts/{id}/dibujo` y desestructura las 7 colecciones + layout/bodega. `cargarHabitacionesYAristas()` conserva el nombre pero delega al snapshot completo (compat con llamadas previas).
- Ayuda actualizada con sección de Zonas operativas.

**Beneficio del snapshot endpoint**: editor abierto pasa de hacer 5-7 requests paralelas (vertices, aristas, habitaciones, elementos, racks, layouts...) a 1 sola request al cargar y a cada refresco después de una mutación. Latencia visible más baja.

**Próximo paso (Bloque B paso 5):** generar `wms_rack_niveles` + `wms_ubicaciones` automáticamente al crear un rack con `niveles_count > 0`, integrar con flag `requiere_gestion_wms` de artículos POS para que las ventas/compras generen movimientos WMS, restaurar `PosInventarioObserver` con la nueva lógica (pendiente desde Bloque A).

### Bloque B paso 5a — Niveles + ubicaciones automáticos al crear rack (cerrado 2026-05-16)

**Backend:**
- `WmsRackController::store` ahora envuelve la creación en `DB::transaction` y dispara `generarNivelesYUbicaciones($rack)` si `niveles_count > 0`.
- `generarNivelesYUbicaciones` crea N `wms_rack_niveles` con:
  - `numero` 1..N,
  - `altura_desde_piso_cm` incremental (`(i-1) * altoPorNivelCm`),
  - `alto_nivel_cm = floor(alto_m * 100 / niveles_count)`,
  - `largo_cm = round(largo_m * 100)`, `ancho_cm = round(ancho_m * 100)`,
  - `capacidad_kg = capacidad_total_kg / niveles_count` (default 500 kg si no se especifica),
  - `divisiones = 1`, `tipo_almacen = 'caja'`.
- Por cada nivel, crea 1 `wms_ubicaciones` con código `{codigo_rack}-N{i}` (ej. `R-01-N1`), `division=1`, `numero_subdivision=1`, `prioridad=100`.
- Endpoint snapshot (`/wmsBodegaLayouts/{id}/dibujo`) incluye los niveles y sus ubicaciones via eager-load `with(['niveles' => fn estado=1 orderBy numero, 'niveles.ubicaciones' => fn estado=1])`.
- store devuelve el rack con `load('niveles.ubicaciones')`.
- Smoke test pasó con `niveles_count=3 + alto_m=2.4 + capacidad_total_kg=900`: generó 3 niveles de 240×60×80 cm cap 300 kg + 3 ubicaciones `R-XXX-N{1,2,3}`. Rollback OK.

**Frontend:**
- `pages/pos/wms/layout/[bodegaId].vue` actualiza `selectRack(r)`:
  - Calcula `totalUbicaciones` sumando `niveles[].ubicaciones`.
  - Muestra tabla con cada nivel (número, dim cm, capacidad kg, lista de códigos de ubicación).
  - Si el rack no tiene niveles cargados, mensaje "Sin niveles cargados".
- El modal usa `width: 520px` para acomodar la tabla.

**Limitaciones pendientes (B-5b):**
- Update de un rack no regenera niveles si cambia `niveles_count`. Soft-delete del rack no marca niveles ni ubicaciones (quedan vivos en BD; podrían colisionar al recrear con mismo código por el unique `(pos_bodega_id, codigo)` de ubicaciones).
- Sin divisiones ni subdivisiones por nivel desde la UI (siempre 1×1). Para racks "drive-in" o "push-back" con múltiples posiciones por nivel hay que extender el flujo.
- `PosInventarioObserver` sigue en no-op desde Bloque A. La integración real con ventas/compras POS queda para B paso 5b.

### Bloque B paso 5b — Tareas, movimientos e integración POS↔WMS (cerrado 2026-05-16)

**Decisiones de diseño confirmadas al arrancar:**
1. `wms_movimientos` recreada como audit log inmutable (origen/destino, costo, lote, ref_tipo + ref_id).
2. Split picking permitido: una venta puede tomar de N ubicaciones según estrategia, hasta cubrir cantidad.
3. Flujo explícito con `WmsTarea` (no observer automático): cada venta/compra crea tarea pendiente; un operario la completa línea por línea.
4. API `wmsMovimientos` read-only (GET index/show + GET kardex), sin POST/PUT/DELETE.
5. Compras: simétrico — auto-creación de WmsTarea put_list, operario confirma ubicación.

**Migraciones nuevas (3):**
- `2026_05_16_000022_create_wms_movimientos_table`: id, tipo (enum picking/put_away/transferencia/ajuste), pos_articulo_id, pos_bodega_id, wms_ubicacion_origen_id (nullable), wms_ubicacion_destino_id (nullable), unidades, costo_unitario, lote, fecha_vencimiento, ref_tipo, ref_id, user_id, observacion, created_at. Sin `updated_at` (audit log inmutable).
- `2026_05_16_000023_create_wms_tareas_table`: id, pos_bodega_id, par_empresa_id, par_sucursal_id, tipo (enum pick_list/put_list/conteo/transferencia), referencia_tipo, referencia_id, prioridad, estado_tarea (enum pendiente/en_proceso/completada/cancelada), asignada_a_user_id, iniciada_at, completada_at, observacion, estado, timestamps.
- `2026_05_16_000024_create_wms_tarea_lineas_table`: id, wms_tarea_id (cascade), pos_articulo_id, wms_ubicacion_sugerida_id, wms_ubicacion_real_id, unidades_solicitadas, unidades_realizadas, secuencia, estado_linea (enum pendiente/parcial/completada/fallida), lote, fecha_vencimiento, observacion, wms_movimiento_id, estado, timestamps.

**Modelos:**
- `WmsMovimiento` — relaciones articulo, bodega, ubicacionOrigen, ubicacionDestino. `public $timestamps = false` (sólo `created_at`). Inmutable conceptualmente.
- `WmsTarea` — hasMany `lineas` (ordenadas por secuencia), belongsTo bodega.
- `WmsTareaLinea` — relaciones tarea, articulo, ubicacionSugerida, ubicacionReal, movimiento.

**Service nuevo `app/Services/Wms/WmsSlottingService.php`:**
- `sugerirPicking(articuloId, bodegaId, cantidad)`: lee `wms_regla_slottings.estrategia`, ordena `wms_ubicacion_stocks` disponibles (`stock - reservado > 0`) según FIFO/FEFO/LIFO/ABC/MIXTA, itera tomando lo necesario. Devuelve array de items `{wms_ubicacion_id, unidades, lote, fecha_vencimiento, costo_unitario, wms_ubicacion_stock_id}`. Si el stock total no alcanza, agrega un item residual con `wms_ubicacion_id=null` y observación "Stock insuficiente".
- `sugerirPutaway(articuloId, bodegaId, cantidad)`: heurística — primero consolidar con ubicación que ya tenga el artículo, después primera ubicación libre en zona recepción/almacenamiento, después cualquier ubicación no bloqueada. Devuelve 1 item.
- Soporta las 5 estrategias FIFO, FEFO, LIFO, ABC, MIXTA (MIXTA con implementación pragmática: si peso_fefo > peso_fifo, comportamiento FEFO; sino FIFO).

**Controllers nuevos:**
- `WmsMovimientoController` (read-only):
  - `index` con filtros: `pos_bodega_id`, `pos_articulo_id`, `tipo`, `wms_ubicacion_id` (origen O destino), `fecha_desde`, `fecha_hasta`, `ref_tipo`, `ref_id`, `limit` (default 200, max 1000). Multi-tenant por `par_empresa_id`.
  - `show($id)` con eager-load de articulo, bodega, ubicaciones.
  - `kardex(Request)` — recibe `pos_articulo_id`, `pos_bodega_id`, `desde`, `hasta`; devuelve `{movimientos[], totales: {entradas, salidas, neto}}`.
- `WmsTareaController` (full + acciones):
  - `index` con filtros estado/tipo, ordenado por prioridad DESC + id ASC.
  - `show($id)` con eager-load completo.
  - `store(Request)` — crea tarea + N líneas en una transacción (para creación manual; el observer también la usa indirectamente).
  - `update(Request, $id)` — solo prioridad, asignada_a_user_id, observacion.
  - `destroy($id)` — cancela si no está completada (estado=0, estado_tarea='cancelada').
  - `iniciar(Request, $id)` — pendiente → en_proceso, asigna user_id, registra iniciada_at.
  - `completarLinea(Request, $id, $lineaId)` — **el corazón**: dentro de DB::transaction descuenta/agrega stock según tipo (pick_list / put_list), crea WmsMovimiento, marca línea completada o parcial, cierra tarea si todas las líneas listas. Promedio ponderado del costo al consolidar stock en put-away.

**Observer restaurado (`app/Observers/PosInventarioObserver.php`):**
- Reemplaza el no-op del Bloque A. Sigue las mismas precondiciones (movimiento=1/2, requiere_gestion_wms, layout activo).
- Ventas (movimiento=2) → llama `WmsSlottingService::sugerirPicking` y crea `WmsTarea` pick_list con N líneas (split picking). Cada línea ya viene con `wms_ubicacion_sugerida_id`, `lote`, `fecha_vencimiento`. Línea residual con stock_insuficiente queda con `wms_ubicacion_sugerida_id=null` y observación específica.
- Compras (movimiento=1) → `sugerirPutaway` + `WmsTarea` put_list con 1 línea.
- Importante: NO descuenta stock automáticamente. Eso ocurre cuando el operario completa la línea desde la UI.

**Rutas API nuevas:**
- `GET /wmsMovimientos/kardex` (declarada antes para no chocar con `/wmsMovimientos/{id}`).
- `GET /wmsMovimientos` y `/wmsMovimientos/{id}`.
- `apiResource /wmsTareas` con custom: `POST /wmsTareas/{id}/iniciar`, `POST /wmsTareas/{id}/lineas/{lineaId}/completar`.

**Frontend:**
- `pages/pos/wms/operaciones/tareas.vue` — listado con filtros (estado, tipo), card por tarea con tabla de líneas. Acciones: "Iniciar tarea", "Cancelar", "Completar" por línea. El modal de completar línea acepta código de ubicación (verificado contra las ubicaciones de la tarea), unidades, costo y lote. Pre-fill con valores de la línea sugerida.
- Dashboard `pages/pos/wms/index.vue` agrega tile **Tareas** + stat **Tareas pendientes**.
- Menú aside agrega entrada `Wms-Tareas`.

**Smoke test E2E pasado:**
1. Crear rack con 2 niveles → 2 ubicaciones generadas.
2. Stock inicial: 5 en N1 (fecha_ingreso vieja), 3 en N2 (fecha_ingreso reciente).
3. `slotting->sugerirPicking(7)` → devuelve 2 items: `{N1: 5, N2: 2}` (FIFO).
4. Crear `WmsTarea pick_list` con esas 2 líneas vía `store`.
5. Completar ambas líneas vía `completarLinea`.
6. Resultado: stock final N1=0, N2=1 ✓, tarea estado=completada ✓, ambas líneas completada ✓, 2 movimientos generados con `ref_tipo=wms_tarea` ✓.

### Bloque B paso 5c — Rack lifecycle + cascade (cerrado 2026-05-16)

**`WmsRackController::update`:**
- Si `niveles_count` cambia, valida que no haya stock activo en las ubicaciones del rack (`wms_ubicacion_stocks.stock_unidades > 0`). Si hay stock, devuelve 422 con mensaje específico.
- Si no hay stock, dentro de `DB::transaction`: borra (hard) las `wms_rack_nivel_subdivisiones`, `wms_ubicaciones`, y `wms_rack_niveles` actuales del rack, luego regenera con la nueva `niveles_count` vía `generarNivelesYUbicaciones`.
- Devuelve el rack con `load('niveles.ubicaciones')`.

**`WmsRackController::destroy` (soft delete cascade):**
- Marca `estado=0` en todas las `wms_ubicaciones` cuyos `wms_rack_nivel_id` pertenecen al rack.
- Marca `estado=0` en todas las `wms_rack_niveles` del rack.
- Marca `estado=0` en el rack.
- Todo en `DB::transaction`. Mensaje: "Rack desactivado (niveles y ubicaciones también)".

### Bloque C — Investigación slotting↔contabilidad (cerrado 2026-05-16)

Documento nuevo en `docs/wms/RELACION_SLOTTING_CONTABILIDAD.md`. Cubre:
- Matriz método contable (FIFO/LIFO/PMP/ID específica) × estrategia slotting (FIFO/FEFO/LIFO/ABC/MIXTA) con marcadores OK / OK-con-reconciliación / NO.
- Recomendación de default por método contable (incluida pseudo-función `metodoContable → estrategia` para autoconfiguración futura).
- Análisis del esquema `wms_ubicacion_stocks` — confirma que las columnas (lote, fecha_vencimiento, fecha_ingreso, costo_unitario) soportan los 4 métodos.
- Flujo operativo actual paso a paso (compra → put_list, venta → pick_list, convergencia de stocks).
- Lista de 5 invariantes que el sistema debe respetar.
- Lista de cambios futuros (validación cruzada de lote para ID específica, autoconfiguración al crear `wms_regla_slottings`, reporte costo-contable-vs-físico).

---

## Rescate — 9 sesiones (R-1 a R-9, cerradas 2026-05-16)

Sub-ramas `feature/wms-rescate-RN` mergeadas `--no-ff` a `feature/wms-refactor` en ambos repos. Plan original en [PENDIENTES_Y_RESCATABLES.md](PENDIENTES_Y_RESCATABLES.md). Resumen de lo entregado:

| R | Tema | Backend | Frontend |
|---|---|---|---|
| R-1 | Reportes ocupación/rotación/vencimientos | `WmsReporteController` (3 endpoints `/wmsReportes/*`), seeder + 4 menús | 3 páginas en `pages/pos/wms/reportes/{ocupacion,rotacion,vencimientos}.vue` |
| R-2 | Vista lateral del rack | `WmsRackController::show` enriquecido + nuevo `GET /wmsRacks/{id}/stocks` | `RackElevation.vue` restaurado + `pages/pos/wms/racks/[id]/elevacion.vue` simplificada + link "Vista lateral" en modal del editor |
| R-3 | Integración POS↔WMS | Comando `wms:verificar-integracion` (diagnóstico 7-checks + simulacro opcional) | — (los controllers POS ya generan `pos_inventarios` con `wms_movimiento_id=null` correctamente, observer dispara) |
| R-4 | Ajustes manuales + Reconcile | `WmsUbicacionStockController` (CRUD con `WmsMovimiento tipo=ajuste` por delta) + `wms:reconcile` solo-reporte (sin `--fix`) | `pages/pos/wms/operaciones/ajustes-stock.vue` con tabla + modales crear/editar/eliminar |
| R-5 | Listado racks + CRUD niveles | `WmsRackNivelController` (CRUD soft-delete + recalcula `niveles_count`) + `apiResource /wmsRackNiveles` | `pages/pos/wms/racks/index.vue` con tabla + botones "Vista lateral" y "Niveles" (modal con tabla editable) |
| R-6 | ScannerQR | — | `ScannerQR.vue` restaurado + overlay sobre el modal Swal de completar línea en `tareas.vue` (botón QR junto al input "Ubicación real") |
| R-7 | Conteo cíclico | `WmsTareaController::completarLinea` extiende switch a `tipo='conteo'` (delta vs esperado → `WmsMovimiento tipo=ajuste` con signo según diferencia) | `pages/pos/wms/operaciones/conteo.vue` lista tareas conteo + modal crear (genera 1 línea por stock>0) + modal contar (Δ visual) |
| R-8 | Pisos + slotting + autoconfig | `WmsSlottingController` con `sugerencia` (modo picking/putaway) y `recalcularAbc` + autoconfig en `WmsReglaSlottingController::store` (lee `par_empresas.metodo_inventario_id` + % artículos con vencimiento → FIFO/FEFO/ABC) | `pages/pos/wms/pisos/index.vue` restaurada tal cual |
| R-9 | PUT snapshot + paneles | `PUT /wmsBodegaLayouts/{id}/dibujo` con diff/upsert/delete transaccional sobre vértices, aristas, aberturas, habitaciones (+ pivot ordenado), zonas, elementos, anotaciones; bump `version` al cerrar | `PanelOcupacion.vue` y `PanelArticulosDisponibles.vue` restaurados; PanelOcupacion montado en la vista de elevación reemplazando la card hand-rolled |

### Migraciones nuevas

Ninguna. Todo el rescate trabajó sobre el set 20 migraciones del Bloque A (`2026_05_16_000001`–`000020`) + las 3 operativas (`000022`–`000024`) del bloque B-5b.

### Smoke tests pasados

- R-1: 3 endpoints `/wmsReportes/*` devuelven HTTP 200 sobre BD vacía.
- R-2: `GET /wmsRacks/{id}/stocks` HTTP 200.
- R-3: `wms:verificar-integracion` reporta "Estado integración: OK" con 8 checks de esquema/observer.
- R-4: `wms:reconcile` reporta "Sin diferencias entre WMS y POS"; HTTP `GET /wmsUbicacionStocks` 200.
- R-5: `GET /wmsRackNiveles` HTTP 200.
- R-8: smoke parcial — `wmsSlotting/sugerencia` no ejecutable con 0 artículos en BD; lint y bootstrap del controller OK.
- R-9: `PUT /wmsBodegaLayouts/{id}/dibujo` con payload vacío sobre layout existente HTTP 200, version bumpeada (rollback).

### Lo que NO se hizo

- Validación cruzada de lote para método contable "ID específica" (1.2 del doc PENDIENTES) — queda fuera del scope del rescate.
- Reporte costo contable vs físico (1.4 del doc) — queda fuera del scope.
- Drag-drop de artículos al canvas desde `PanelArticulosDisponibles` — el componente queda disponible pero sin montar; el flujo operativo es vía tareas WMS (R-7) + ajustes (R-4).
- Validación en navegador — el usuario no lo probó antes del rescate y todas las verificaciones quedaron en lint + smoke vía tinker.

### Estado git al cierre

Backend `api-a-conta` y frontend `aconta` ambos en rama `feature/wms-refactor` con 9 merges `--no-ff` desde sub-ramas `feature/wms-rescate-R1`..`R9`. Sin push. Sin merge a `feature/wms` o `develop`/`main`.

---

## Mejoras Layout 2D (en curso, arrancada 2026-05-22)

Sub-ramas `feature/wms-layout-mejoras-NN-...` mergeadas `--no-ff` a la integradora `feature/wms-layout-mejoras` en ambos repos. La integradora a su vez se fast-forward a `feature/wms` al cierre de cada bloque (sin push).

| Bloque | Rama (backend / frontend) | Backend | Frontend | Descripción |
|---|---|---|---|---|
| L-1 | `feature/wms-layout-mejoras` | `962d6c0` | — | `WmsZonaController` valida que los 4 corners del AABB de la zona (con su rotación) caigan dentro del polígono de la habitación enlazada (o de alguna habitación del layout si la zona es independiente). Falla con 422 si se sale. |
| L-5 | `feature/wms-layout-mejoras` | `ce9057f` | — | Endpoint `POST /wmsAristas/{id}/split` divide una arista en dos creando un vértice intermedio (recibe `x_canvas`/`y_canvas` o `t` 0..1). Reutilizable desde el editor para insertar vértices sin redibujar. |
| L-7 | `feature/wms-layout-mejoras` | `c5612ec` | — | Migración `2026_05_22_000001_alter_wms_elementos_arquitectonicos_add_piso_destino.php` agrega `wms_piso_destino_id` (nullable, FK a `wms_pisos`, nullOnDelete). `WmsElementoArquitectonicoController` valida que el destino exista y pertenezca al mismo layout cuando el tipo es `escalera`; limpia el campo si el tipo cambia a otro. |
| L-8 | `feature/wms-layout-mejoras-01-rack-drag` | — | `b946c4f` | `CadRack.vue` soporta `draggable`/hover/eventos drag-start/drag-end (mismo patrón que `CadZona`). El editor habilita el drag en `mode='select'`, persiste con `PUT /wmsRacks/{id}` (optimista, rollback en error) y muestra tooltip de hover con código + dimensiones + tipo + niveles + rotación. Sin validación AABB: los racks pueden vivir en el hall (fuera de habitaciones). |
| L-9 | `feature/wms-layout-mejoras-02-zonas-poligono` | `6492fe5` | `61eb6c9` | Zonas dejan de ser AABB y pasan a polígono de vértices (mismo patrón que habitaciones). Migraciones `2026_05_22_000002` (pivot `wms_zona_vertices`), `000003` (backfill desde AABB) y `000004` (drop `pos_x`/`pos_y`/`ancho_m`/`largo_m`/`rotacion_grados` de `wms_zonas`). Modelo + controller con `vertice_ids` y validación poligonal contra habitaciones. `WmsBodegaLayoutController` (`dibujo` + `snapshotPut`) actualizado con sync de pivot zona-vértices. `CadZona.vue` renderiza polígono. Editor: nuevo modo `draw-zone` (reemplaza `add-zone`), `crearZonaPoligonal` detecta habitación contenedora y la enlaza, y al cerrar una habitación con tipo de zona, la zona resultante comparte vértices con la habitación. Seeder demo actualizado para poblar el pivot. |
| L-10 | `feature/wms-layout-mejoras-03-pisos-ui` | — | `16b39e8` | UI de pisos en el toolbar del editor: el grupo siempre visible (no más `v-if=pisos.length>1`) con tres botones junto al selector — **+** crear piso (modal con número, nombre, altura), **✏** renombrar el piso activo, **🗑** eliminar con confirmación que avisa cuántas habitaciones/racks/zonas/elementos quedarían huérfanos. Computed `pisoActivo`. Backend de pisos sin cambios (ya tenía CRUD completo). Ayuda colapsable actualizada. |
| L-11 | `feature/wms-layout-mejoras-04-seeder-minimo` | `58c13ab` | — | Reset BD demo. Nuevo `WmsSeedMinimoSeeder` (1 bodega + 15 artículos + empaques + pivotes) reemplaza a `WmsDemoCompletoSeeder`, `PosArticulosDemoSeeder` y `PosArticulosWmsDemoSeeder` (los tres eliminados). `DatabaseSeeder` llama solo `ParIndEconomicos`, `WmsMenus` y `WmsSeedMinimo`. El usuario modela toda la geometría (pisos, habitaciones, zonas, racks) desde el editor. `migrate:fresh --seed` corrido en local: 1 bodega, 15 artículos, 15 empaques, 15 pivotes, 0 pisos/zonas/racks. |
| L-12 | `feature/wms-layout-mejoras-05-piso-default-al-crear-layout` | `f2de5d9` | `24bf628` | Toda bodega arranca con al menos un piso. `WmsBodegaLayoutController::store` ahora envuelve el insert en `DB::transaction` y crea automáticamente `WmsPiso { numero=1, nombre='Piso 1', altura=0 }`. El contorno geométrico **no** se autogenera (puede no ser rectangular); el usuario lo dibuja con la herramienta Habitación. Onboarding nuevo en el editor: card flotante centrada sobre el canvas cuando el piso activo no tiene vértices/habitaciones, con CTA "Dibujar contorno" que entra a modo `draw-polygon`. Solo aparece en `mode='select'` para no estorbar durante edición. |
| L-13 | `feature/wms-layout-mejoras-06-contorno-piso` | `d647ac6` | `40bcdf9` | **Distinción conceptual contorno vs habitación**: el piso tiene su propio contorno (paredes exteriores) y *contiene* habitaciones como subdivisiones internas. Migración `2026_05_22_000005` crea pivot `wms_piso_vertices` (mismo patrón que habitaciones/zonas). `WmsPiso::vertices` ahora belongsToMany ordenado; controller acepta `vertice_ids` opcional. `WmsBodegaLayoutController::dibujo` + `snapshotPut` agregan sync de pisos. Cleanup de vértices protege también `wms_piso_vertices`. Frontend: nuevo componente `CadFloorContour.vue` (stroke 4px azul oscuro = pared exterior), modo `draw-floor` en `useCadDraw`, botón "Contorno" en toolbar (atajo C) antes de "Habitación", `terminarContornoPiso` confirma reemplazo si ya existe contorno. Onboarding redirigido al nuevo modo (en vez de `draw-polygon`) y ayuda colapsable diferencia ambos conceptos. |
| L-14 | `feature/wms-layout-mejoras-07-habitacion-zonas` | — | `5199ee9` | **Habitación 1↔N zonas**: una habitación es subdivisión física del piso y puede contener 0, 1 o N zonas operativas (ej: hall central con zona escalera + zona almacenamiento a piso). Quitado el acoplamiento visual: habitaciones renderizan su color propio (sin heredar color de zona), TODAS las zonas se renderizan como overlay encima (antes solo las independientes). Modal de habitación ya no pide tipo de zona, y `terminarPoligono` no crea zona automática. Tooltip de hover muestra cantidad de zonas contenidas; panel de selección lista las zonas con color y tipo, o sugiere dibujar una si no hay. Helper `zonasDeHabitacion(id)`. Ayuda colapsable reescrita para separar Contorno / Habitación / Zonas. |
| L-15 | `feature/wms-layout-mejoras-08-resize-arista-y-drag-poligonos` | — | `3b5ed80` | **Reescalado de arista por distancia** + **drag de habitación/zona como bloque**. (1) Editar `distancia_real_m` de una arista ahora reescala geométricamente: el vértice B se mueve a lo largo de la dirección A→B para que la nueva distancia coincida con el dibujo. Función `reescalarAristaPorDistancia` PUT-ea vértice + arista y refresca snapshot; conectada desde el panel lateral (`actualizarArista`) y el modal de `editarDistancia`. Aviso del panel y ayuda colapsable explican que las aristas vecinas no recalculan. (2) `CadPolygon.vue` y `CadZona.vue` aceptan prop `draggable`; `v-group` emite `drag-end` con `dx`/`dy` del offset y resetea posición. Helper común `moverVerticesEnBloque(vertices, dx, dy)` hace PUT en paralelo a cada vértice + refresca snapshot. Mover una habitación arrastra también las zonas que comparten vértices (esperado: los vértices son compartidos por diseño). Ambos shapes son draggable solo en `mode='select'`. |
| L-16 | `feature/wms-layout-mejoras-09-crear-por-medidas` | — | `bd65fcb` | **Crear contorno / habitación / zona como rectángulo por medidas**. Botón nuevo "Por medidas" (icono `bi-rulers`, atajo `M`) abre un modal único: select tipo + ancho + largo + nombre (habitación/zona) + tipo zona + prioridad (zona). Helper `crearRectangulo(opts)` centra el rectángulo en el viewport actual (usa `stage.scaleX`/`x` para coords del mundo), POST-ea los 4 vértices, crea aristas según tipo (pared exterior 0.15m para contorno, borde_habitacion para habitación, ninguna para zona), y asocia/POST-ea la entidad. Zona detecta habitación contenedora del centro y la enlaza automáticamente. Confirma reemplazo si el contorno ya existe. Fix de paso: `terminarContornoPiso` (dibujo manual del L-13) también crea ahora 4+ aristas pared/exterior 0.15m por cada lado (antes solo asociaba vértices). El dibujo manual sigue disponible en paralelo. |
| L-17 | `feature/wms-layout-mejoras-10-cleanup-vertices` | `f7e8d31` | `b21e79b` | **Cleanup de vértices/aristas huérfanos al eliminar estructuras**. Antes los `destroy` de habitación/zona/piso eran solo soft-delete y dejaban vértices, aristas y pivots vivos; el `destroy` de vértice rechazaba si tenía aristas y no chequeaba pivots. Nuevo helper `WmsVertice::cleanupHuerfanos(array $ids)` borra los vértices de la lista que ya no estén en ningún pivot, junto con sus aristas y aberturas. Los controllers de habitación/zona/piso ahora capturan los vértices con `.reorder().pluck()`, detach el pivot, soft-delete la entidad y llaman al cleanup. `WmsAristaController::destroy` borra aberturas + arista + limpia los 2 vértices si quedan huérfanos. `WmsVerticeController::destroy` acepta `?cascade=1`: sin cascade falla 422 con detalle de refs; con cascade borra aberturas/aristas/3 pivots/vértice en una transacción. Frontend pasa `?cascade=1` desde el menú contextual de vértice. Smoke test tinker: 4 vértices + 4 aristas + 1 habitación → destroy → delta 0 en BD. |
| L-18 | `feature/wms-L18-conectar-vertices` | `dae3c39` | `efa96f3` | **Conectar vértices manualmente con una arista**. Útil para reconectar vértices que quedaron sueltos tras eliminar otro vértice/arista en cascada. Nuevo modo `connect-vertices` en `useCadDraw`, botón "Conectar" en toolbar (icono `bi-share`, atajo `K`). UX: click sobre primer vértice → queda resaltado en amarillo; click sobre segundo → POST `/wmsAristas` con tipo `borde_habitacion` y `distancia_real_m` calculada por escala. Se encadena automáticamente (el segundo vértice queda como nuevo "primero" para seguir conectando). Esc cancela y vacía el estado. `Vertex.vue` ahora emite `{id, x, y}` en click y acepta prop `highlighted`. Backend: `WmsAristaController::store` rechaza 422 si ya existe arista para el par `(vertice_a_id, vertice_b_id)` en cualquier orden. |
| L-19 | `feature/wms-L19-paredes-habitacion` | `8d8583e` | `cf43be9` | **Las aristas de habitación son paredes**. Se elimina el concepto separado `borde_habitacion`: los 3 flujos que crean aristas de habitación (dibujo manual `terminarPoligono`, rectángulo por medidas `crearRectangulo` con tipo=habitacion, modo conectar vértices) ahora producen `pared` con `subtipo_pared='interior'` y `grosor_m=0.10`. Contorno del piso sigue siendo `pared exterior 0.15`. Migración de datos `2026_05_22_000006_migrate_borde_habitacion_a_pared_interior.php` promueve las aristas legacy en BD. El enum del schema mantiene `borde_habitacion` para compat (no se rompe nada si quedaba algún uso externo). Ayuda colapsable actualizada. |
| L-20 | `feature/wms-L20-puerta-drag-vertices-ocultos` | `559caed` | `92a6cf7` | **Puertas/aberturas arrastrables sobre la pared** + **vértices ocultos por default**. (1) `CadAberturaMark.vue` ahora acepta `draggable` y usa `dragBoundFunc` para constrain el movimiento a la línea de la arista padre (proyección sobre va→vb con clamp para que la abertura quepa); `onAberturaDragEnd` calcula nueva `posicion_m_desde_inicio` por (t / pxPerM) y PUT-ea optimista con rollback. (2) Computed `verticesAMostrar` filtra: en modos draw/edit/add/connect muestra todos los vértices visibles del piso; en otros modos solo los de la entidad seleccionada (habitación, zona, **piso**). `CadFloorContour.vue` ahora es clickable (emite `click` con el piso) y resalta cuando está seleccionado; nuevo ref `selectedPisoId` + handler `selectPisoContorno`. `clearSelection` limpia el piso; `selectedElement` lo incluye; panel lateral muestra info del piso; `eliminarSeleccionado` soporta tipo `piso`. Ayuda colapsable actualizada. |
| L-21 | `feature/wms-L21-snap-habitacion-y-labels` | `9d2ee9a` | `b30998a` | **Magnetismo de habitación al contorno** + **labels de aristas reposicionadas perpendicular**. (1) `Edge.vue` ahora rota la label paralela a la arista y la offsetea perpendicularmente (medio grosor + 6px margen), eligiendo siempre el lado consistente vía vector normal `(-dy, dx) / len`. Si el ángulo dejaría el texto patas arriba, se suma 180° para que se lea siempre de izquierda a derecha. Resultado: las medidas dejan de superponerse sobre el stroke. (2) `snapearAlContornoPiso(habVertices, dx, dy)` se ejecuta en `onHabitacionDragEnd`: para cada vértice de la habitación con su delta tentativo, busca el snap más cercano dentro de 14px contra (a) cualquier vértice del contorno y (b) cualquier arista del contorno (proyección perpendicular con clamp `t ∈ [0,1]`). Si encuentra candidato, corrige `dx/dy` con `(ox, oy)` para que el vértice de la habitación caiga exactamente sobre el vértice/borde del piso. La habitación NO se deforma — se traslada entera con el delta ajustado. |
| L-22 | `feature/wms-L22-snap-zonas` | `0b12181` | `3b47599` | **Magnetismo de zonas**. Refactor del helper de snap a `snapearADestinos(verticesARastrear, dx, dy, destinos)` genérico que acepta una lista arbitraria de polígonos destino y auto-excluye vértices propios para evitar auto-snap (caso típico: zona que comparte vértices con su habitación contenedora). `onHabitacionDragEnd` sigue snapeando solo al contorno del piso. `onZonaDragEnd` ahora snapea contra `[contorno del piso] + [todas las habitaciones del piso activo]`, así una zona se puede pegar al hall, a las paredes externas o a las paredes internas de cualquier habitación con un solo arrastre. |
| L-23 | `feature/wms-L23-piso-recreable` | `51de2bf` | `3506302` | **Permitir recrear un piso con el mismo número tras eliminarlo**. Bug: `WmsPisoController::destroy` hacía soft-delete (`estado=0`) y el unique `(wms_bodega_layout_id, numero)` bloqueaba recrear con el mismo número. Fix: el `destroy` ahora valida que no haya habitaciones/racks/zonas/elementos activos en el piso (devuelve 422 con detalle si los hay), suelta `wms_piso_id` en cualquier registro (incluso soft-deleted) que aún apunte al piso, y hace **hard-delete** del registro + cleanup de vértices del contorno. El `store` además filtra el check de duplicado por `estado=1`. Migración `2026_05_22_000007_cleanup_pisos_soft_deleted_legacy.php` purga pisos zombi previos (estado=0) que ya estaban bloqueando. Frontend (`eliminarPisoActivo`): si hay contenido activo, ahora muestra modal informativo y bloquea; si está vacío, advierte que se elimina por completo y que se puede recrear con el mismo número. |
| L-24 | `feature/wms-L24-editar-medidas` | — | `d8f6314` | **Editar medidas (ancho × largo) de piso, habitación y zona**. Botón nuevo "Medidas" (icono `bi-rulers`) en el panel lateral cuando hay un polígono seleccionado de tipo `habitacion`/`zona`/`piso`. Modal pide ancho y largo en metros (pre-fill con AABB actual). Helper `aabbDelPoligono(vertices, escala)` calcula bounding box. `editarMedidasSeleccionado` aplica factor `fx = nuevoAncho/anchoActual`, `fy = nuevoLargo/largoActual` a cada vértice, escalando respecto a la esquina superior izquierda del AABB (preserva la forma del polígono, no la deforma de manera no uniforme). Persiste con PUT paralelo a `/wmsVertices/{id}` y, además, recalcula `distancia_real_m` de las aristas cuyos dos extremos pertenecen al polígono, para mantener coherencia con la geometría visual. |
| L-25 | `feature/wms-L25-fix-drag-puerta-zoom` | — | `f038b9b` | **Fix drag de puerta/abertura con zoom ≠ 100%**. Bug: `CadAberturaMark.vue` (L-20) usaba `dragBoundFunc` para constrain la puerta a la pared, pero Konva pasa `pos` en coords absolutas del stage (post-transform), mientras que el código proyectaba contra `va.x_canvas` que está en coords world. Con zoom != 1 el cálculo se desfasaba por el factor de escala y la puerta "saltaba" verticalmente al soltarla; al recargar la BD tenía la posición correcta (porque el dragend recalculaba con `node.x()/y()`, que sí están en espacio padre). Fix: eliminar `dragBoundFunc` y usar `@dragmove` + `node.x()/y()`, que siempre están en coords del padre (la layer sin transform = world), funciona con cualquier zoom. Helper `constrainAtLine(node)` reusado entre `onDragMove` y `onDragEnd`. |
| L-26 | `feature/wms-L26-fix-zonas-medidas` | `daca64d` | `ca026a4` | **Fix crear zonas con medidas (zonas en el hall + detección por 4 vértices)**. Bug 1: `crearRectangulo` para zona detectaba la habitación contenedora pasando solo el **centro** del rectángulo a `habitacionContenedoraDePuntos`. Si el centro caía dentro de una habitación pero las esquinas no → backend rechazaba. Fix: ahora pasa los 4 corners del rectángulo (todos deben estar dentro). Bug 2: `WmsZonaController` exigía que la zona estuviera dentro de **alguna habitación** si el layout tenía habitaciones, lo que impedía crear zonas en el hall (fuera de habitaciones pero dentro del contorno del piso). Fix: la validación ahora es jerárquica — si hay `wms_habitacion_id` valida contra esa habitación; si no, valida contra el contorno del piso (si está definido). Permite zonas en el hall. `crearZonaPoligonal` también se relajó: no bloquea en cliente, deja que el backend devuelva mensaje específico si la zona se sale del contorno. |
| L-27 | `feature/wms-L27-labels-no-superpuestos` | — | `555c091` | **Labels de piso/habitación/zona no se superponen cuando uno está dentro de otro**. Tres cambios coordinados: (1) `CadFloorContour` mueve el label del piso de la esquina superior izquierda interior a **arriba-fuera del contorno** (`bbox.minY - 16`, clamped a ≥ 2 para no salir del canvas), así no compite con labels de habitaciones que también usan TL interior. (2) Todos los labels (`FloorContour`, `CadPolygon`, `CadZona` — nombre + tipo + métricas) ahora tienen **halo blanco** (`shadowColor: '#FFFFFF', shadowBlur: 3-4, shadowOpacity: 0.9-0.95`), lo que mantiene la legibilidad cuando un label se superpone con otro shape o label. (3) Opacidades de habitación y zona subidas a 0.75-0.95 (antes 0.55) para evitar que se vean apagados; al estar el polígono seleccionado (`highlighted`) el label de habitación va a opacity 1. |
| L-28 | `feature/wms-L28-labels-exteriores` | `3cd67e1` | `58aa1aa` | **Medidas de aristas hacia el exterior del polígono + nombres en posiciones distintas por tipo**. (1) `Edge.vue` acepta `referencePoint` (centroide del polígono contenedor). Computa `normalSign = dot((ref-mid), normal) > 0 ? -1 : 1` y orienta el offset perpendicular hacia el lado OPUESTO al centroide → la medida queda siempre fuera del polígono (habitación o contorno del piso). Si no hay `referencePoint`, mantiene el comportamiento anterior. (2) Editor: `aristasConVertices` enriquece cada arista con `_referencePoint`, buscando el primer polígono cuyos vértices contengan los extremos de la arista como consecutivos (prioriza habitaciones, fallback al contorno del piso). (3) `CadZona.vue` mueve nombre + tipo + métricas de la esquina superior **izquierda** a la **derecha** (TR del bbox), con `align: 'right'` y `offsetX` calculado por longitud. Esto da una matriz visual clara: piso = arriba-fuera, habitación = centroide, zona = TR. Cuando se anidan no se apilan en el mismo punto. |
| L-29 | `feature/wms-L29-piso-aislado` | — | `2f75a41` | **Fix: al crear un 2do piso se veían los vértices y aristas del piso 1 (efecto "copia")**. Causa: `verticesVisibles` y `aristasVisibles` filtraban con `perteneceAlPiso(item)`, que consideraba "huérfanos" a los items con `wms_piso_id == null` y los mostraba en TODOS los pisos. Pero los vértices y aristas no tienen `wms_piso_id` propio: pertenecen al piso vía los pivots (`wms_piso_vertices`, `wms_habitacion_vertices`, `wms_zona_vertices`). Fix: nuevo computed `verticeIdsDelPisoActivo` que arma el set de IDs de vértices del piso activo agregando los del contorno + todas las habitaciones + todas las zonas del piso. `verticesVisibles` filtra por ese set; `aristasVisibles` exige que **ambos** extremos estén en el set. Resultado: piso 2 arranca en blanco, sin geometría heredada del piso 1. |
| L-30 | (solo frontend) | — | `37a755a` | **Fix: aberturas (puertas/ventanas) se duplicaban entre pisos** + **filtro estricto sin huérfanos**. (1) `aberturasConArista` usaba el map global `aristas.value` en vez del filtrado por piso (`aristasVisibles` del L-29). Si la arista no pertenece al piso activo, ahora la abertura se descarta. (2) `perteneceAlPiso` cambia de comportamiento permisivo (mostrar huérfanos `wms_piso_id == null` en todos los pisos) a **estricto**: si hay piso activo, exige `wms_piso_id` no nulo y matching exacto. El frontend siempre setea `wms_piso_id` al crear; un null hoy indica dato legacy/corrupto. Resultado: al cambiar de piso, el canvas muestra solo elementos del piso activo. |
| L-31 | `feature/wms-L31-elemento-drag` | — | `a9fa49a` | **Elementos arquitectónicos (escalera/columna/etc.) arrastrables; click corto navega**. `CadElemento.vue` refactor: `v-group` con `x/y` en el config (en vez del v-rect interno) y `rotation` aplicada al grupo, los hijos quedan en `(0, 0)` relativo. Esto permite `draggable: true` directo en el grupo. Emite `drag-end` con `x_canvas/y_canvas` nuevos. Editor: `:draggable="mode === 'select'"` + handler `onElementoDragEnd` (PUT optimista con rollback). Konva NO dispara el evento `click` cuando hubo drag con movimiento, así que la navegación por click-corto a `wms_piso_destino_id` (escalera) sigue funcionando: click breve = navegar; arrastrar = mover. |
| L-32 | `feature/wms-L32-rack-ocupacion-volumen` | — | `ff782af` | **% de ocupación del rack ahora por volumen (cm³) en vez de peso (kg)**. Conceptualmente, el espacio físico que un empaque ocupa dentro del nivel es lo que define cuánto cabe — no su masa. `PanelOcupacion.vue` refactor a interfaz genérica: recibe `ocupadoPorNivel` + `capacidadPorNivel` + `unidad` + `unidadLabel`, sin asumir que es peso. `elevacion.vue` calcula `volumenPorArticulo` (`largo_cm × ancho_cm × alto_cm` del empaque primario), `capacidadVolumenPorNivel` (`largo_cm × ancho_cm × alto_nivel_cm` del `wms_rack_niveles`) y `ocupacionVolumenPorNivel` (Σ `stock_unidades × volumen_empaque`). Pasa `unidad='cm³'`, `unidadLabel='volumen'`. El cálculo de `pesoPorNivel` se conserva (lo usa la tabla de stocks para mostrar peso por línea) y se sigue pasando a `RackElevation` por compat. |
| L-33 | `feature/wms-L33-rack-ocupacion-subdivision` | — | `ae73838` | **Ocupación por subdivisión (además del agregado por nivel)**. `elevacion.vue` agrega dos computeds: `capacidadVolumenPorSubdivision` (`alto × ancho × profundidad` del `wms_rack_nivel_subdivisiones`) y `ocupacionVolumenPorSubdivision` (suma de volumen de stocks cuyas ubicaciones están enlazadas a esa subdivisión vía `wms_ubicacion_id`). Los pasa a `PanelOcupacion` junto con `subdivisionesPorNivel`. `PanelOcupacion.vue` ahora renderea cada nivel con un toggle (`bi-caret-right/down`); al expandir muestra cada subdivisión con código `D{division}-S{numero}`, su capacidad, ocupación, % y barra propia. Auto-expande niveles que tienen al menos una subdivisión ≥ 95%. El total por nivel sigue agregando todo (visible siempre). Útil para detectar saturaciones internas que el promedio del nivel ocultaría. |
| L-34 | `feature/wms-L34-rack-grafico-subdivisiones` | — | `4d04b2d` | **Render gráfico del rack muestra el % de cada subdivisión**. `RackElevation.vue` ahora acepta `ocupadoPorSubdivision` + `capacidadPorSubdivision` y, cuando un nivel tiene subdivisiones explícitas, dibuja una grilla de **celdas** (división × subdivisión) cada una con su propio sombreado horizontal proporcional al `%` (rojo si ≥95%, azul translúcido si no) + label con `{pct}%` en la esquina TR de la celda. Borde fino entre celdas. Si el nivel **no** tiene subdivisiones, mantiene el comportamiento previo (barra horizontal del nivel completo). El `%` global del nivel (arriba-derecha) sigue mostrándose como agregado. `elevacion.vue` pasa las dos props nuevas. |

### Próximo

- Aún no definido. Posibles candidatos abiertos: recalcular distancia real de aristas vecinas al reescalar (consistencia automática), validación de habitación dentro del contorno del piso (con feedback visual), validación de zona/rack dentro del contorno cuando no hay habitación, indicador visual de escalera→piso destino, sync automático de `wms_piso_id` al crear/mover racks/zonas/elementos.

---

## Asignación / Slotting (cerrada 2026-05-23)

Iteración integral del módulo de slotting: algoritmo + UI unificada + 3 estados de stock + reporte de ocupación por volumen + sweep de lenguaje. Rama: `feature/wms` directo (sin sub-ramas porque el alcance se fue ampliando durante la sesión).

### Backend (api-a-conta)

| Hash | Cambio |
|---|---|
| `6b28abe` | **wms(slotting)** — Reescritura del servicio con score de cercanía (piso + distancia euclidiana a zona picking + nivel + flag `es_picking`) y ajuste por clase ABC (A acerca a picking; C invierte hacia el fondo). `sugerirPicking` acepta override de estrategia; MIXTA combina los 4 pesos normalizados. `WmsClasificacionAbcService::calcular` marca como C los artículos del catálogo sin historial de ventas (cobertura 100%). Nueva columna `dias_ventana_abc` en `wms_regla_slottings` (default 90) configurable por bodega; el job y el endpoint manual la respetan. Endpoints nuevos bajo `/wmsSlotting`: `estadoBodega`, `articulos`, `stockSinAsignar`, `resumenEstados`, `aceptar` (crea WmsTarea pendiente con validación de disponibilidad, bloquea sobreasignación con 422). `sugerencia` enriquecida con contexto del artículo (`stock_pos`/`asignado`/`en_proceso`/`sin_asignar`) y descripción legible de la ubicación. Migration `2026_05_22_000008_add_dias_ventana_abc_to_wms_regla_slottings`. Nuevo seeder `WmsSeedSlottingPruebaSeeder` con 10 lotes de vencimiento variado (vencidos, críticos, alerta, normales), tareas put_list pendientes para demo del estado "en proceso", sincronización pos↔wms y 269 ventas históricas que disparan ABC. |
| `9f89721` | **wms(reportes)** — Ocupación calculada por volumen (`largo × ancho × alto_nivel` para capacidad, `Σ stock × volumen_empaque` para uso) en cm³, en vez de peso en kg. Eliminado el método `costoContableVsFisico` (~100 líneas) y su ruta. |

### Frontend (aconta)

| Hash | Cambio |
|---|---|
| `994634b` | **wms(asignacion)** — Página única `/pos/wms/asignacion-ubicaciones` con tabs **Reglas por bodega** y **Sugerir**. Reemplaza `reglas-slotting.vue` y `probar-slotting.vue` (ambas eliminadas). Tab Reglas: CRUD con suma normalizada en vivo + "Restaurar defaults" en MIXTA + onboarding cuando ninguna bodega tiene regla. Tab Sugerir: selector de bodega con chips de salud + botón "Recalcular ABC", buscador de artículos con autocompletado y badge ABC, info de empaque junto a cantidad, override de estrategia, toggle "Comparar las 5 estrategias" (paralelo), widget de 3 cards (Sin asignar / En proceso / Asignado), listado de stock sin asignar con botón "Asignar", validación de sobreasignación (frontend + backend), modal de confirmación antes de crear tarea, visualizador SVG con plano del piso + rack lateral con nivel/división resaltados. Navegación cruzada entre tabs (botones "Sugerir" en card de regla y "Editar regla" en estado de salud). Menú lateral y dashboard WMS actualizados. |
| `4d0215a` | **wms(operaciones)** — Vista lateral del rack: el botón "Asignar" ahora crea `WmsTarea put_list` pendiente en vez de escribir directo en `wms_ubicacion_stocks`. Misma vista incluye widget de 3 cards y tabla de stock sin asignar. Fix en ajustes-stock: `cargarArticulos` ya no llama a endpoint inexistente; usa `/wmsSlotting/articulos`. Página de tareas: lenguaje amigable (`PICK`/`PUT` → `DESPACHO`/`GUARDADO`, `TRANSF` → `TRASLADO`, filtros y headers de tabla con español neutro). |
| `da66ad1` | **wms(reportes)** — Reporte de ocupación en cm³ con conversión automática a dm³/m³ según escala. % aparece **arriba de la barra** (ApexCharts `dataLabels.offsetY: -22`), color por umbral (verde <60, amarillo 60-85, rojo ≥85). Excel exportador actualizado. Páginas eliminadas: `/pos/wms/pisos` (CRUD standalone — el editor 2D ya cubre el caso) y `/pos/wms/reportes/costo-contable-vs-fisico`. Lenguaje amigable en rotación. |

### Comportamiento clave del flujo

- **3 estados de stock por artículo+bodega**: `asignado` (`Σ wms_ubicacion_stocks`), `en_proceso` (tareas `put_list` pendientes/`en_proceso`) y `sin_asignar` (= `stock_pos − asignado − en_proceso`).
- **Las dos vías de asignar stock convergen al flujo de tareas**: el modal de Racks ya no salta a `wms_ubicacion_stocks` directo; ahora genera `WmsTarea` igual que el módulo Asignación. Evita la desincronia que mostraba `wms:reconcile`.
- **Sobreasignación bloqueada**: `aceptar` valida que la cantidad pedida no exceda lo disponible según modo (`sin_asignar` para guardado, `asignado` para despacho). Devuelve 422 con detalle de los 4 estados.
- **Validación de stock POS sincronizado**: el seeder de prueba mantiene la invariante `Σ wms_ubicacion_stocks = pos_bodega_articulos.stock`, y agrega 30u "en proceso" + 163u "sin asignar" para que ambos estados se vean en la UI.

---

## Apéndice A — Estructura del módulo

> Snapshot del estado del repo al cierre del rescate R-1..R-9 (2026-05-16).

**Backend** `api-a-conta`:

```
app/
  Http/Controllers/Wms/                   (18 controllers)
    WmsAberturaController.php             ← refactor B
    WmsAnotacionController.php            ← refactor B
    WmsAristaController.php               ← refactor B
    WmsArticuloEmpaqueController.php
    WmsBodegaLayoutController.php         (+ dibujo GET/PUT — B4b/R9)
    WmsElementoArquitectonicoController.php  ← refactor B
    WmsHabitacionController.php           ← refactor B (poligono de vertices)
    WmsMovimientoController.php
    WmsPisoController.php
    WmsRackController.php                 (+ stocks endpoint — R2)
    WmsRackNivelController.php            ← rescate R-5
    WmsReglaSlottingController.php        (+ autoconfig estrategia — R8)
    WmsReporteController.php              ← rescate R-1
    WmsSlottingController.php             ← rescate R-8
    WmsTareaController.php                (+ tipo conteo — R7)
    WmsUbicacionStockController.php       ← rescate R-4
    WmsVerticeController.php              ← refactor B
    WmsZonaController.php                 ← refactor B (poligono / rect)
  Models/Wms/                             (20 modelos)
  Services/Wms/
    WmsClasificacionAbcService.php
    WmsSlottingService.php                ← B-5b
  Observers/PosInventarioObserver.php     (genera WmsTarea desde pos_inventarios)
  Jobs/Wms/RecalcularAbcJob.php
  Console/Commands/Wms/
    ReconcileStockCommand.php             ← rescate R-4 (solo-reporte)
    VerificarIntegracionCommand.php       ← rescate R-3 (diagnostico)
database/
  migrations/
    2026_05_16_000001..000020             ← reset bloque A
    2026_05_16_000021                     ← B-4b (geometria zonas)
    2026_05_16_000022..000024             ← B-5b (movimientos, tareas, lineas)
  seeders/
    WmsMenusSeeder.php                    (entradas aside)
    PosArticulosDemoSeeder.php
    PosArticulosWmsDemoSeeder.php         ← 15 articulos con requiere_gestion_wms=1
docs/wms/
  README.md
  PROXIMOS_PASOS.md                       ← punto de entrada actual
  PLAN_EJECUCION.md                       ← este documento
  PENDIENTES_Y_RESCATABLES.md             (referencia historica R-1..R-9)
  RELACION_SLOTTING_CONTABILIDAD.md
```

**Frontend** `aconta`:

```
pages/pos/wms/
  index.vue                          (dashboard)
  layout/index.vue                   (lista bodegas con/sin layout)
  layout/[bodegaId].vue              (editor 2D CAD)
  racks/index.vue                    ← rescate R-5
  racks/[id]/elevacion.vue           ← rescate R-2 + R-2b (drag-drop, sugerencias, delete)
  empaques/index.vue
  pisos/index.vue                    ← rescate R-8
  operaciones/tareas.vue             (+ scanner QR — R-6)
  operaciones/ajustes-stock.vue      ← rescate R-4
  operaciones/conteo.vue             ← rescate R-7
  reportes/{ocupacion,rotacion,vencimientos}.vue   ← rescate R-1
  reglas-slotting.vue
components/wms/
  canvas/
    RackElevation.vue                ← rescate R-2
    PanelOcupacion.vue               ← rescate R-9
    PanelArticulosDisponibles.vue    ← rescate R-9 (montado en R-2b)
    cad/
      AberturaMark.vue
      Edge.vue
      Elemento.vue
      Polygon.vue
      Rack.vue
      Vertex.vue
      Zona.vue
  operaciones/
    ScannerQR.vue                    ← rescate R-6
composables/
  useCadDraw.ts                      (state machine modos del editor)
  useCadSnap.ts                      (snap a vertice/midpoint/ortho)
plugins/
  vue-konva.client.ts
design/
  aconta-design-tokens.json
utils/
  wmsReporteExporter.js              (Excel para reportes R-1)
```

---

## Apéndice B — Riesgos y mitigaciones

1. **SSR rompe Konva** — plugin `.client.ts` + `<ClientOnly>` siempre. Probar `nuxt build && nuxt start` ante cualquier cambio del canvas.
2. **Stock desincronizado** — `wms:reconcile` en cron diario; alertar si diff > 0.
3. **Performance canvas con 500+ racks** — `Layer.listening(false)` en el fondo, batch draws, `node.cache()` en componentes estáticos.
4. **Multi-tenant** — todas las consultas filtran por `par_empresa_id` del usuario autenticado.
5. **AABB front↔back desincronizado** — el helper de validación AABB se mantiene compartido entre PHP y TS (introducido en it1-05). Cualquier cambio en uno requiere actualizar el otro.

---

## Apéndice C — Comandos útiles

```powershell
# Backend
cd C:\xampp\htdocs\api-a-conta
php artisan migrate
php artisan migrate:rollback --step=1
php artisan migrate:fresh --force                          # reset BD (cuidado en prod)
php artisan wms:reconcile                                   # diff stock POS vs WMS (solo reporta)
php artisan wms:reconcile --bodega=5                        # mismo, filtrando bodega
php artisan wms:verificar-integracion                       # 8 checks observer POS-WMS
php artisan wms:verificar-integracion --bodega=5 --simular=venta --articulo=10 --cantidad=2
php artisan db:seed --class=PosArticulosWmsDemoSeeder       # 15 articulos demo WMS
php artisan tinker

# Frontend
cd C:\xampp\htdocs\aconta
npm run dev
npm run build && npm run preview

# Git por sesion/bloque (en ambos repos)
git checkout feature/wms-refactor && git pull
git checkout -b feature/wms-{rescate-RN | nombre}
# ... trabajo ...
git add -A && git commit -m "wms({rescate-RN | contexto}): descripcion"
git checkout feature/wms-it2 && git merge --no-ff feature/wms-it2-NN-nombre
```
