# Registrar ALOCACAO_WMS em Designações (dispatch) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Permitir que um gestor crie e dispare missões de Alocação WMS pelo módulo Designações, selecionando produtos (com filtro opcional "sem localização") e operadores, em modo livre.

**Architecture:** Reaproveita o pipeline existente de Designações (Strategy + Satélite `GestaoMissoes*` + template method `GestaoMissoesBase::processarEnvio`). Cria o Satélite faltante `GestaoMissoesAlocacaoWms` (espelho de `GestaoMissoesAjuste`), liga o dispatcher e reusa a infra de filtro client-side do wizard, com a fonte de "localização" vinda da tabela nova `wms_alocacoes`.

**Tech Stack:** PHP 8 (enums, ModelInteligencia V1, Sondas V2), PostgreSQL (`dev_gx_goldie`), JS vanilla no wizard de designação.

## Global Constraints

- Escrita sempre via model (`->save()`/`->update()`/`create()`), nunca SQL cru via `Connect` para writes. Reads pontuais via Sonda/Inteligencia.
- Resolução de Satélite é por convenção: arquivo em `sinais/<Estrela>/Laboratorios/<Nome>.php`, namespace `GalaxiaMissoes\Logistica\sinais\Designacoes\Laboratorios\<Nome>`.
- Fonte de "tem localização" = `wms_alocacoes` (status `confirmada`), nunca `produtos.codigo_enderecamento`.
- TDD: teste falhando primeiro, rodar e ver falhar, implementação mínima, rodar e ver passar, commit.
- Testes de integração rodam só contra `dev_gx_goldie`; bootstrap seta `$_SESSION['galaxiaUserInfo']` na raiz E em `$_SESSION['dashboard']`.
- Commits terminam com: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`

---

## File Structure

- **Create:** `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php` — Satélite que cria a missão + itens (espelho do Ajuste).
- **Create:** `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php` — testes (reflection).
- **Modify:** `componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php` — 3 allowlists `$satelitesDesignacao` (linhas 47, 57, 137).
- **Modify:** `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsAlocacaoSonda.php` — método `mapaLocalizacaoConfirmadaPorProduto()`.
- **Create:** `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php` — teste do método novo.
- **Modify:** `componentes/Missoes/Logistica/sinais/Designacoes/Strategies/Designacao/AlocacaoWmsStrategy.php` — `getPrimaryItems()` enriquecido.
- **Modify:** `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.php` — toggle "Sem localização" (linha ~897).
- **Modify:** `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.js` — branch de filtro (linha 177) + card de produto (linha 485).

---

## Task 1: Satélite GestaoMissoesAlocacaoWms

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php`
- Test: `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php`

**Interfaces:**
- Consumes: `GestaoMissoesBase` (abstract; `processarEnvio`, `getConfiguracoesAdicionais`, `extrairReferenciaId`, `getReferenciaTipo`), `TipoMissao::ALOCACAO_WMS`, `Produto` Inteligencia (`find()->fetch(true)`).
- Produces: classe `GestaoMissoesAlocacaoWms` com `getTipoMissao(): TipoMissao`, `buscarItens(array): array`, `getConfiguracoesAdicionais(array,array): array` (→ `['modo_alocacao'=>'livre']`), `extrairReferenciaId($,$): null`, `getReferenciaTipo(): string` (→ `'ALOCACAO_WMS'`). `buscarItens` retorna array de itens com chaves `produto_id,quantidade,referencia_tipo,referencia_id,dados_adicionais{produto_nome,codigo_produto,codigo_barras,importacao_id}`.

- [ ] **Step 1: Escrever o teste falhando**

Create `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php`:

```php
<?php

/**
 * TddTest — GestaoMissoesAlocacaoWms (integration, dev_gx_goldie)
 *
 * O construtor de GestaoMissoesBase auto-dispara o fluxo (switch no $opcao).
 * Para isolar a lógica nova, usamos ReflectionClass::newInstanceWithoutConstructor()
 * e invocamos os métodos protegidos diretamente.
 *
 * Produtos block=22: 16221, 16219, 16222 (existentes).
 */

if (php_sapi_name() !== 'cli') {
    die("Apenas via CLI\n");
}

$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';

require_once __DIR__ . '/../../../../../../../autoload.php';

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}
$info                          = new stdClass();
$info->userData                = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData            = (object)['id' => 22];
$info->principalEmpresa        = (object)['empresa_id' => 3];
$info->sectorData              = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use GalaxiaMissoes\Logistica\sinais\Designacoes\Laboratorios\GestaoMissoesAlocacaoWms;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;

$passou = 0;
$falhas = 0;
function check(bool $cond, string $msg): void
{
    global $passou, $falhas;
    if ($cond) { $passou++; echo "[PASS] {$msg}\n"; }
    else        { $falhas++; echo "[FAIL] {$msg}\n"; }
}

// Helpers de reflection para invocar métodos protegidos sem rodar o construtor.
$ref = new \ReflectionClass(GestaoMissoesAlocacaoWms::class);
$obj = $ref->newInstanceWithoutConstructor();
$invoke = function (string $metodo, array $args) use ($ref, $obj) {
    $m = $ref->getMethod($metodo);
    $m->setAccessible(true);
    return $m->invoke($obj, ...$args);
};

echo "====================================================\n";
echo "INTEGRATION — GestaoMissoesAlocacaoWms (dev_gx_goldie)\n";
echo "====================================================\n\n";

// 1. getTipoMissao
check($invoke('getTipoMissao', []) === TipoMissao::ALOCACAO_WMS, 'getTipoMissao() === ALOCACAO_WMS');

// 2. getReferenciaTipo
check($invoke('getReferenciaTipo', []) === 'ALOCACAO_WMS', 'getReferenciaTipo() === ALOCACAO_WMS');

// 3. extrairReferenciaId
check($invoke('extrairReferenciaId', [[1, 2], []]) === null, 'extrairReferenciaId() === null');

// 4. getConfiguracoesAdicionais
$cfg = $invoke('getConfiguracoesAdicionais', [[], []]);
check(($cfg['modo_alocacao'] ?? null) === 'livre', 'getConfiguracoesAdicionais() => modo_alocacao=livre');

// 5. buscarItens com produtos reais
$itens = $invoke('buscarItens', [[16221, 16219]]);
check(count($itens) === 2, 'buscarItens([16221,16219]) retorna 2 itens');
$primeiro = $itens[0] ?? [];
check(($primeiro['produto_id'] ?? 0) === 16221, 'item[0].produto_id === 16221');
check(($primeiro['quantidade'] ?? 0) === 1, 'item[0].quantidade === 1');
check(($primeiro['referencia_tipo'] ?? '') === 'ALOCACAO_WMS', 'item[0].referencia_tipo === ALOCACAO_WMS');
check(isset($primeiro['dados_adicionais']['produto_nome']) && $primeiro['dados_adicionais']['produto_nome'] !== '', 'item[0].dados_adicionais.produto_nome populado');
check(array_key_exists('codigo_produto', $primeiro['dados_adicionais'] ?? []), 'item[0].dados_adicionais tem codigo_produto');
check(array_key_exists('importacao_id', $primeiro['dados_adicionais'] ?? []), 'item[0].dados_adicionais tem importacao_id');

// 6. buscarItens vazio
check($invoke('buscarItens', [[]]) === [], 'buscarItens([]) === []');

echo "\n====================================================\n";
echo "RESULTADO: PASS={$passou}  FAIL={$falhas}\n";
if ($falhas > 0) {
    exit(1);
}
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar o teste e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php 2>/dev/null`
Expected: erro fatal "Class GestaoMissoesAlocacaoWms not found" (a classe ainda não existe).

- [ ] **Step 3: Implementar o Satélite**

Create `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php`:

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Designacoes\Laboratorios;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;

/**
 * Gestão de Missões de Alocação WMS.
 *
 * Seleção primária por produto (sem romaneio). Cria a missão direto ao operador
 * (requer_aprovacao=0) em modo livre (configuracoes.modo_alocacao='livre') — o
 * operador decide onde alocar. Futuro: modo dirigido quando o WMS enviar a
 * localização obrigatória.
 *
 * Espelha GestaoMissoesAjuste. A persistência (missão + itens) é feita pelo
 * template method GestaoMissoesBase::processarEnvio via MissoesLogisticas 'criacao'.
 */
class GestaoMissoesAlocacaoWms extends GestaoMissoesBase
{
    protected $inputData = [];

    protected function processarEnvio(array $data)
    {
        // O form pode enviar 'produtos' em vez de 'itens_selecionados'.
        if (empty($data['itens_selecionados']) && !empty($data['produtos'])) {
            $data['itens_selecionados'] = $data['produtos'];
        }

        // Alocação WMS vai direto ao operador — sem gate de aprovação.
        $data['requer_aprovacao'] = '0';

        $this->inputData = $data;
        return parent::processarEnvio($data);
    }

    protected function getTipoMissao(): TipoMissao
    {
        return TipoMissao::ALOCACAO_WMS;
    }

    protected function buscarItens(array $selecao): array
    {
        $ids = [];
        foreach ($selecao as $item) {
            $id = is_array($item) ? ($item['produto_id'] ?? $item['id'] ?? null) : $item;
            if ($id) $ids[] = (int)$id;
        }

        if (empty($ids)) return [];

        $produtosModel = new \GalaxiaConstelacoes\Produtos\Crud\Inteligencias\Produto();
        $idsStr = implode(',', $ids);
        $produtosDados = $produtosModel->find("id IN ($idsStr)")->fetch(true);

        $mapaProdutos = [];
        if ($produtosDados) {
            foreach ($produtosDados as $p) {
                $mapaProdutos[$p->id] = $p;
            }
        }

        $itens = [];
        foreach ($ids as $produtoId) {
            if (!isset($mapaProdutos[$produtoId])) continue;
            $prod = $mapaProdutos[$produtoId];

            $itens[] = [
                'produto_id'       => $produtoId,
                'quantidade'       => 1,
                'referencia_tipo'  => TipoMissao::ALOCACAO_WMS->value,
                'referencia_id'    => $produtoId,
                'dados_adicionais' => [
                    'produto_nome'   => $prod->descricao ?? 'Produto sem nome',
                    'codigo_produto' => $prod->codigo_produto ?? null,
                    'codigo_barras'  => $prod->codigo_barras ?? null,
                    // importacao_id = codigomd5 do ERP — usado pelo sync WMS/ERP
                    'importacao_id'  => $prod->importacao_id ?? null,
                ],
            ];
        }
        return $itens;
    }

    protected function getConfiguracoesAdicionais(array $data, array $selecionados): array
    {
        return ['modo_alocacao' => 'livre'];
    }

    // Alocação WMS não tem romaneio de referência.
    protected function extrairReferenciaId($selecionados, $itens)
    {
        return null;
    }

    protected function getReferenciaTipo(): string
    {
        return TipoMissao::ALOCACAO_WMS->value;
    }
}
```

- [ ] **Step 4: Rodar o teste e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php 2>/dev/null`
Expected: `RESULTADO: PASS=11  FAIL=0` / `TODOS OS TESTES PASSARAM`

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php \
        componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php
git commit -m "$(cat <<'EOF'
feat(designacoes): satélite GestaoMissoesAlocacaoWms (dispatch ALOCACAO_WMS)

Cria missão ALOCACAO_WMS por seleção de produtos, direto ao operador
(requer_aprovacao=0) em modo livre (configuracoes.modo_alocacao='livre').
Espelha GestaoMissoesAjuste; persistência via template method da base.

TDD: GestaoMissoesAlocacaoWmsTddTest (11/11 pass, reflection)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 2: Wiring do dispatcher (allowlists)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php:47,57,137`

**Interfaces:**
- Consumes: classe `GestaoMissoesAlocacaoWms` (Task 1), resolvida por convenção pelo framework.
- Produces: dispatcher reconhece `GestaoMissoesAlocacaoWms` para emitir sucesso (`limparSelecao`) e redirect para `designacoes_lista`.

Sem teste automatizado: o roteamento do framework não é testável em CLI (sem precedente no repo). Verificação por inspeção + smoke manual.

- [ ] **Step 1: Adicionar à allowlist da linha 47**

Em `Designacoes.php`, a linha 47 é:

```php
        $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'MissoesDias', 'MissoesOperadores'];
```

Trocar por:

```php
        $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'GestaoMissoesAlocacaoWms', 'MissoesDias', 'MissoesOperadores'];
```

- [ ] **Step 2: Adicionar à allowlist da linha 57**

A linha 57 é:

```php
        $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'MissoesDias'];
```

Trocar por:

```php
        $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'GestaoMissoesAlocacaoWms', 'MissoesDias'];
```

- [ ] **Step 3: Adicionar à allowlist da linha 137**

A linha 137 (dentro de `GalaxiaAutoLoad`) é idêntica à da linha 57:

```php
            $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'MissoesDias'];
```

Trocar por:

```php
            $satelitesDesignacao = ['GestaoMissoesAjuste', 'GestaoMissoesSeparacao', 'GestaoMissoesPesagem', 'GestaoMissoesCarregamento', 'GestaoMissoesAlocacaoWms', 'MissoesDias'];
```

- [ ] **Step 4: Verificar sintaxe**

Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php`
Expected: `No syntax errors detected`

- [ ] **Step 5: Confirmar que há exatamente 3 ocorrências de GestaoMissoesAlocacaoWms no arquivo**

Run: `grep -c "GestaoMissoesAlocacaoWms" componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php`
Expected: `3`

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php
git commit -m "$(cat <<'EOF'
feat(designacoes): registrar GestaoMissoesAlocacaoWms no dispatcher

Adiciona o satélite às 3 allowlists de enviarMissoes para emitir sucesso
e redirect ao criar missão de Alocação WMS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 3: Sonda — mapa de localização confirmada por produto

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsAlocacaoSonda.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php`

**Interfaces:**
- Consumes: `StatusAlocacao::CONFIRMADA` (já importado na Sonda), `$this->find()->order()->fetch(true)`.
- Produces: `WmsAlocacaoSonda::mapaLocalizacaoConfirmadaPorProduto(): array` — retorna `[produto_id => localizacao_codigo]` da alocação confirmada mais recente por produto no tenant atual (block via customerControl).

- [ ] **Step 1: Escrever o teste falhando**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php`:

```php
<?php

/**
 * TddTest — WmsAlocacaoSonda::mapaLocalizacaoConfirmadaPorProduto (integration, dev_gx_goldie)
 *
 * Insere alocação confirmada via Sonda (block 22, produto 16221), confirma que
 * o mapa retorna produto_id => localizacao_codigo. Cleanup no finally.
 */

if (php_sapi_name() !== 'cli') {
    die("Apenas via CLI\n");
}

$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';

require_once __DIR__ . '/../../../../../../../autoload.php';

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}
$info                          = new stdClass();
$info->userData                = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData            = (object)['id' => 22];
$info->principalEmpresa        = (object)['empresa_id' => 3];
$info->sectorData              = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsAlocacaoSonda;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT: não conectou em dev_gx_goldie\n");
    exit(1);
}

$passou = 0;
$falhas = 0;
function check(bool $cond, string $msg): void
{
    global $passou, $falhas;
    if ($cond) { $passou++; echo "[PASS] {$msg}\n"; }
    else        { $falhas++; echo "[FAIL] {$msg}\n"; }
}

$BLOCK      = 22;
$PRODUTO_ID = 16221;
$COD_LOC    = '__test_mapa_loc_A1';
$sonda      = new WmsAlocacaoSonda();

echo "====================================================\n";
echo "INTEGRATION — WmsAlocacaoSonda::mapaLocalizacaoConfirmadaPorProduto\n";
echo "====================================================\n\n";

try {
    // Limpeza prévia + inserir alocação confirmada via Sonda
    $pdo->exec("DELETE FROM wms_alocacoes WHERE localizacao_codigo = '{$COD_LOC}'");
    $aloc = $sonda->registrarAlocacao(
        999999,        // missaoId fictício (não há FK estrita para este teste de leitura)
        null,          // missaoItemId
        $PRODUTO_ID,
        $COD_LOC,
        null,          // wmsLocalizacaoId
        null,          // quantidade
        'manual',      // origemLeitura
        1              // operadorId
    );

    $mapa = $sonda->mapaLocalizacaoConfirmadaPorProduto();

    check(is_array($mapa), 'retorna array');
    check(($mapa[$PRODUTO_ID] ?? null) === $COD_LOC, 'mapa[16221] === código alocado');

} finally {
    $pdo->exec("DELETE FROM wms_alocacoes WHERE localizacao_codigo = '{$COD_LOC}'");
}

echo "\n====================================================\n";
echo "RESULTADO: PASS={$passou}  FAIL={$falhas}\n";
if ($falhas > 0) {
    exit(1);
}
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar o teste e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php 2>/dev/null`
Expected: FAIL — `mapaLocalizacaoConfirmadaPorProduto()` ainda não existe (erro/None). Pode aparecer erro fatal de método inexistente, dado o `__call` da Sonda retorna null → `is_array(null)` falso.

- [ ] **Step 3: Implementar o método na Sonda**

Em `WmsAlocacaoSonda.php`, adicionar o método logo após `listarPorMissaoProduto()` (antes de `marcarIntegracaoErp`):

```php
    /**
     * Mapa produto_id => localizacao_codigo da alocação CONFIRMADA mais recente,
     * para o tenant atual (block via customerControl).
     *
     * Usado pela tela de designação (AlocacaoWmsStrategy::getPrimaryItems) para
     * marcar quais produtos já têm localização (fonte: wms_alocacoes, não o
     * legado produtos.codigo_enderecamento).
     *
     * Ordena ASC por confirmada_em e sobrescreve no loop → o último valor por
     * produto é o mais recente.
     *
     * @return array<int,string>
     */
    public function mapaLocalizacaoConfirmadaPorProduto(): array
    {
        $rows = $this->find(
            'status = :s',
            's=' . StatusAlocacao::CONFIRMADA->value
        )->order('confirmada_em ASC')->fetch(true);

        $mapa = [];
        if ($rows) {
            foreach ($rows as $r) {
                $mapa[(int)$r->produto_id] = $r->localizacao_codigo;
            }
        }
        return $mapa;
    }
```

- [ ] **Step 4: Rodar o teste e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php 2>/dev/null`
Expected: `RESULTADO: PASS=2  FAIL=0` / `TODOS OS TESTES PASSARAM`

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsAlocacaoSonda.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsAlocacaoSondaMapaLocTddTest.php
git commit -m "$(cat <<'EOF'
feat(alocacao-wms): Sonda mapaLocalizacaoConfirmadaPorProduto

Mapa produto_id => localizacao da alocação confirmada mais recente por
tenant. Fonte do filtro "sem localização" na designação.

TDD: WmsAlocacaoSondaMapaLocTddTest (2/2 pass)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 4: getPrimaryItems enriquecido + filtro "Sem localização" no wizard

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/Strategies/Designacao/AlocacaoWmsStrategy.php:46-73`
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.php:897-912`
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.js:177,485`

**Interfaces:**
- Consumes: `WmsAlocacaoSonda::mapaLocalizacaoConfirmadaPorProduto()` (Task 3).
- Produces: cada item de `getPrimaryItems()` ganha `codigo_produto`, `codigo_barras`, `localizacao` (vazio = sem localização). O wizard renderiza Alocação WMS como card de produto e oferece toggle "Sem localização".

Sem teste automatizado (JS do wizard + view) — validação por inspeção + smoke manual.

- [ ] **Step 1: Enriquecer `getPrimaryItems` na strategy**

Em `AlocacaoWmsStrategy.php`, substituir o corpo de `getPrimaryItems()` (linhas 46-73) por:

```php
    public function getPrimaryItems(object $galaxiaRoute): array
    {
        $response = $galaxiaRoute
            ->setConstelacao('Produtos')
            ->setEstrela('Crud')
            ->setSatelite('Produtos')
            ->setOpcao('read')
            ->dadosG([
                'status' => 1,
                'columns_required' => 'id,descricao,codigo_produto,codigo_barras',
                'limit' => 5000
            ]);

        if (is_error($response) || empty($response['produtosData'])) {
            return [];
        }

        // Fonte de "tem localização" = wms_alocacoes (não codigo_enderecamento).
        $mapaLoc = (new \GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsAlocacaoSonda())
            ->mapaLocalizacaoConfirmadaPorProduto();

        $itens = [];
        foreach ($response['produtosData'] as $produto) {
            $pid = (int)$produto->id;
            $itens[] = (object)[
                'id'             => $produto->id,
                'descricao'      => $produto->descricao ?? '',
                'codigo_produto' => $produto->codigo_produto ?? '',
                'codigo_barras'  => $produto->codigo_barras ?? '',
                // vazio => "sem localização" (JS: !localizacao && !codigo_enderecamento => !localizacao)
                'localizacao'    => $mapaLoc[$pid] ?? '',
                'total_itens'    => 1,
            ];
        }

        return $itens;
    }
```

- [ ] **Step 2: Verificar sintaxe da strategy**

Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/Strategies/Designacao/AlocacaoWmsStrategy.php`
Expected: `No syntax errors detected`

- [ ] **Step 3: Renderizar o toggle "Sem localização" na view**

Em `designar_unificado.php`, o bloco atual (linhas 897-912) é:

```php
                <?php if ($tipo === 'ajuste'): ?>
                <div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.5rem 0;">
                    <button type="button" id="toggle-sem-barras" class="btn-action" title="Mostrar apenas produtos sem código de barras">
                        <i class="bi bi-upc-scan"></i> Sem barras
                    </button>
                    <button type="button" id="toggle-sem-cubagem" class="btn-action" title="Mostrar apenas produtos sem cubagem">
                        <i class="bi bi-box"></i> Sem cubagem
                    </button>
                    <button type="button" id="toggle-sem-peso" class="btn-action" title="Mostrar apenas produtos sem peso">
                        <i class="bi bi-speedometer2"></i> Sem peso
                    </button>
                    <button type="button" id="toggle-sem-end" class="btn-action" title="Mostrar apenas produtos sem endereço">
                        <i class="bi bi-geo-alt"></i> Sem endereço
                    </button>
                </div>
                <?php endif; ?>
```

Trocar o `<?php endif; ?>` final por um `elseif` que reusa o id `toggle-sem-end` (listener já existe no JS):

```php
                <?php elseif ($tipo === 'alocacao_wms'): ?>
                <div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.5rem 0;">
                    <button type="button" id="toggle-sem-end" class="btn-action" title="Mostrar apenas produtos sem localização">
                        <i class="bi bi-geo-alt"></i> Sem localização
                    </button>
                </div>
                <?php endif; ?>
```

- [ ] **Step 4: Verificar sintaxe da view**

Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.php`
Expected: `No syntax errors detected`

- [ ] **Step 5: Estender o branch de filtro de produtos no JS**

Em `designar_unificado.js`, a linha 177 é:

```javascript
                    if (this.tipo === 'ajuste') {
```

Trocar por:

```javascript
                    if (this.tipo === 'ajuste' || this.tipo === 'alocacao_wms') {
```

- [ ] **Step 6: Renderizar Alocação WMS como card de produto (não romaneio)**

Em `designar_unificado.js`, a linha 485 é:

```javascript
            const isRomaneio = State.tipo !== 'ajuste';
```

Trocar por:

```javascript
            const isRomaneio = State.tipo !== 'ajuste' && State.tipo !== 'alocacao_wms';
```

- [ ] **Step 7: Smoke manual (servidor)**

Pedir ao usuário (ou rodar) o wizard de designação no tipo "Alocação WMS" em `dev-genesis`:
1. A lista mostra produtos (cards com nome + código), não "Romaneio #".
2. Buscar por nome/código filtra a lista.
3. Clicar "Sem localização" reduz a lista (com `wms_alocacoes` vazio, todos permanecem; após alocar um produto, ele some do filtro).
4. Selecionar 2+ produtos + operador(es) → "Criar Missão de Alocação WMS" → mensagem de sucesso + redirect para a lista.
5. A missão criada aparece e é executável pela tela AlocacaoWms (modo livre).

- [ ] **Step 8: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Designacoes/Strategies/Designacao/AlocacaoWmsStrategy.php \
        componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.php \
        componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.js
git commit -m "$(cat <<'EOF'
feat(designacoes): filtro "sem localização" no dispatch de Alocação WMS

getPrimaryItems enriquece produtos com localizacao (fonte: wms_alocacoes),
codigo_produto e codigo_barras. Wizard renderiza Alocação WMS como card de
produto e oferece toggle "Sem localização" reusando a infra de filtro.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Self-Review

**Spec coverage:**
- Satélite `GestaoMissoesAlocacaoWms` → Task 1. ✔
- Wiring allowlists (47/57/137) → Task 2. ✔
- `modo_alocacao='livre'` → Task 1 (`getConfiguracoesAdicionais`). ✔
- `requer_aprovacao=0` → Task 1 (`processarEnvio`). ✔
- Todos os produtos ativos → Task 4 (`getPrimaryItems`, status=1). ✔
- Filtro "sem localização" fonte `wms_alocacoes` → Tasks 3+4. ✔
- Itens com `dados_adicionais` (produto_nome, codigo_produto, codigo_barras, importacao_id) → Task 1 (`buscarItens`). ✔
- Card de produto (não romaneio) → Task 4 (linha 485). ✔

**Placeholder scan:** Sem TBD/TODO; todo passo de código mostra o código completo. ✔

**Type consistency:** `mapaLocalizacaoConfirmadaPorProduto()` definido na Task 3 e consumido na Task 4 com o mesmo nome e retorno `[produto_id => localizacao_codigo]`. `buscarItens`/`getConfiguracoesAdicionais`/`getReferenciaTipo`/`extrairReferenciaId` batem com as assinaturas de `GestaoMissoesBase`. ✔

**Notas de risco conhecidas:**
- `this.tipo` no JS vem de `#designacao-tipo` (= `$tipo` do wizard, lowercase `alocacao_wms`). Confirmar no smoke (Step 7 da Task 4) que o valor é exatamente `alocacao_wms`.
- `Produto::find("id IN (...)")` segue o padrão do `GestaoMissoesAjuste`; se o Inteligencia injetar block, os produtos de teste (block 22) casam com a sessão de teste (block 22).
