# Aprovação por switch genérica (strategiada) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) ou superpowers:executing-plans pra implementar task-a-task. Steps usam checkbox (`- [ ]`).

**Goal:** Levar um switch aprovado/reprovado por-item ao `designacao_painel/{id}` de qualquer missão logística, com o caso WMS strategiado no mesmo mecanismo (v1: WMS + Separação).

**Architecture:** Um frame genérico (Interface + Factory + value object) em `Constelacoes/.../MissaoLogistica/Strategies/Aprovacao/`, resolvido por `tipo_missao_id`. WMS embrulha o `WmsMovimentoSonda` existente; Separação opera sobre `missao_logistica_itens` (nova coluna `aprovacao_gestor`). Endpoints: Laboratório genérico (`listar/alternar/alternarTodos`) + satélite `MissoesAprovacao::aprovar` generalizado (snapshot guard + CAS + desfecho concluir/reabrir). Reset imediato reusa o `voltarItemPendente` já existente.

**Tech Stack:** PHP (Galaxia V2), PostgreSQL, ModelInteligencia/Sondas, views Metronic + JS pareado, i18n `show()`.

## Global Constraints

- **NÃO COMMITAR.** Restrição explícita do usuário: nada commitado sem pedido. A árvore fica suja; a execução por subagentes usa **diff por snapshot** (como na feature 15). **Pular todo passo "Commit"** — o deliverável de cada task é "código + testes verdes", não um commit.
- **Sem raw SQL para writes** — usar `->update()`/`->atualizar()`/`->create()` do model. (O legado `voltarItensParaPendente` usa raw SQL; código NOVO não.)
- **i18n**: toda string PT via `show('texto PT literal')`; harvest nas 3 línguas + conferir no browser.
- **V2**: Interface + Factory/Registry; classes de strategy em `Constelacoes/.../MissaoLogistica/Strategies/Aprovacao/`; HTML/JS no sinal.
- **IDs de tipo**: `TipoMissao::SEPARACAO->id() === 2`, `ALOCACAO_WMS->id() === 14`.
- **Coluna nova** `missao_logistica_itens.aprovacao_gestor VARCHAR(20) NULL` — sem `DEFAULT`, ortogonal ao `status`; `block_*` não se aplica (tabela já multi-tenant).
- **Testes**: integração CLI em `dev_gx_goldie`, `block=22`, sessão sintética (`$_SESSION['galaxiaUserInfo']` na raiz e em `['dashboard']`), cleanup em `finally`; `requer_aprovacao` é BOOLEAN (`true`, não `1`).
- **Reset imediato (D10)**: reusa `MissoesItensEdicao::voltarItemPendente` (botão "Reiniciar" já existente). NÃO criar `resetar`.
- **Não-regressão**: a suíte WMS existente deve seguir verde; missões sem strategy mantêm o comportamento atual (Factory retorna `null`).

---

### Task 1: Migration — coluna `aprovacao_gestor` em `missao_logistica_itens`

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Migrations/001_add_aprovacao_gestor_itens.sql`
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/tables.sql` (bloco `CREATE TABLE missao_logistica_itens`, ~linha 92, logo após `status VARCHAR(30) DEFAULT 'pendente',`)
- Test: `tests/integration/Aprovacao/ColunaAprovacaoItensTddTest.php`

**Interfaces:**
- Produces: coluna `missao_logistica_itens.aprovacao_gestor` (VARCHAR(20), nullable). `NULL`/`'pendente'` = ligado; `'rejeitado'` = desligado.

- [ ] **Step 1: Escrever a migration versionada (idempotente)**

`componentes/Constelacoes/Logistica/MissaoLogistica/Migrations/001_add_aprovacao_gestor_itens.sql`:
```sql
-- =====================================================
-- Migration 001 (MissaoLogistica): decisão do gestor por ITEM (aprovação por switch)
-- Feature: operacao-wms / aprovacao-generica-strategy (16)
-- =====================================================
-- NULL/'pendente' = ligado (vai pro destino)  | 'rejeitado' = desligado (rework no commit)
-- Ortogonal ao `status` operacional. Idempotente (IF NOT EXISTS). Postgres.
-- =====================================================
ALTER TABLE missao_logistica_itens
    ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL;
```

- [ ] **Step 2: Adicionar a coluna ao `tables.sql` (instalações novas)**

Em `tables.sql`, no `CREATE TABLE missao_logistica_itens (...)`, logo após a linha `status VARCHAR(30) DEFAULT 'pendente',`, inserir:
```sql
    aprovacao_gestor VARCHAR(20) NULL, -- decisão do gestor por switch (NULL/pendente=ligado, rejeitado=desligado)
```

- [ ] **Step 3: Escrever o teste que aplica + verifica a coluna**

`tests/integration/Aprovacao/ColunaAprovacaoItensTddTest.php`:
```php
<?php
/** Aplica (idempotente) e verifica a coluna missao_logistica_itens.aprovacao_gestor. */
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
require_once __DIR__ . '/../../../autoload.php';

use Galaxia\_Controladores\Connect;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT db\n"); exit(1); }
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou, $falhas; if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; } }

$pdo->exec("ALTER TABLE missao_logistica_itens ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL");

$col = $pdo->query("SELECT data_type, is_nullable FROM information_schema.columns
                    WHERE table_name='missao_logistica_itens' AND column_name='aprovacao_gestor'")->fetch(\PDO::FETCH_ASSOC);
check($col !== false, 'coluna aprovacao_gestor existe');
check(($col['data_type'] ?? '') === 'character varying', 'tipo é varchar');
check(($col['is_nullable'] ?? '') === 'YES', 'é nullable');

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

- [ ] **Step 4: Rodar o teste (aplica a migration em dev_gx_goldie e verifica)**

Run: `php tests/integration/Aprovacao/ColunaAprovacaoItensTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (3 PASS).

- [ ] **Step 5: (sem commit)** Deixar a árvore suja. Nota: produção (`gx_goldie`) precisa do mesmo ALTER no deploy.

---

### Task 2: Frame da strategy — Interface + Resultado + WMS strategy + Factory

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyInterface.php`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/WmsAprovacaoStrategy.php`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php`
- Test: `tests/integration/Aprovacao/WmsAprovacaoStrategyTddTest.php`

**Interfaces:**
- Produces:
  - `AprovacaoResultado` (readonly): `desfecho:string('concluir'|'reabrir')`, `aprovados:int`, `reprovados:int`, `mensagem:string`; fábricas estáticas `concluir(...)`/`reabrir(...)`.
  - `AprovacaoStrategyInterface`: `listarUnidades(object $missao):array`, `alternar(int $unidadeId,bool $ligado,object $missao):array`, `alternarTodos(int $missaoId,bool $ligado):array`, `contarPendentesNovos(int $missaoId,int $snapshotMaxId):int`, `commit(object $missao):AprovacaoResultado`.
  - `AprovacaoStrategyFactory::criar(object $missao):?AprovacaoStrategyInterface` (registry por `tipo_missao_id`; `null` se sem strategy).
  - `WmsAprovacaoStrategy` (embrulha `WmsMovimentoSonda` via class_exists).
- Consumes: `WmsMovimentoSonda` (sinal AlocacaoWms) — `listarParaRevisao`, `alternarAprovacao`, `alternarAprovacaoTodos`, `contarPendentesNovos`, `commitAprovacao`.

- [ ] **Step 1: Escrever o teste falhando (WMS strategy + factory resolvem tipo 14)**

`tests/integration/Aprovacao/WmsAprovacaoStrategyTddTest.php`:
```php
<?php
/** WmsAprovacaoStrategy embrulha o WmsMovimentoSonda; Factory resolve tipo 14. */
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', 'gestor_logistico' => 1];
$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\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\WmsAprovacaoStrategy;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT db\n"); exit(1); }
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou, $falhas; if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; } }

$PRODUTO_ID = 15424; $ITEM_ID = 128483; $OPERADOR_ID = 1; $BLOCK = 22;
$COD = '__test_loc_wmsstrat';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc strat',1,{$BLOCK})");
$LOC_ID = (int)$pdo->query("SELECT id FROM wms_localizacoes WHERE codigo='{$COD}'")->fetchColumn();
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, configuracoes, block, data_cadastro)
            VALUES (14,'aguardando_aprovacao',true,1,'{\"operacao_tipo\":\"alocacao\"}',{$BLOCK},NOW())");
$MISSAO_ID = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} AND tipo_missao_id=14 ORDER BY id DESC LIMIT 1")->fetchColumn();
$missao = (object)['id' => $MISSAO_ID, 'tipo_missao_id' => 14, 'status' => 'aguardando_aprovacao', 'configuracoes' => '{"operacao_tipo":"alocacao"}'];

$sonda = new WmsMovimentoSonda();
try {
    $a = $sonda->registrarMovimento($MISSAO_ID, $ITEM_ID, $PRODUTO_ID, $COD, $LOC_ID, 1.0, 'manual', $OPERADOR_ID, OperacaoTipo::ALOCACAO, null, null, null, AprovacaoGestor::PENDENTE);
    $b = $sonda->registrarMovimento($MISSAO_ID, $ITEM_ID, $PRODUTO_ID, $COD, $LOC_ID, 2.0, 'manual', $OPERADOR_ID, OperacaoTipo::ALOCACAO, null, null, null, AprovacaoGestor::PENDENTE);

    $strategy = AprovacaoStrategyFactory::criar($missao);
    check($strategy instanceof WmsAprovacaoStrategy, 'Factory resolve WMS (tipo 14)');
    check(AprovacaoStrategyFactory::criar((object)['id'=>0,'tipo_missao_id'=>999,'status'=>'x']) === null, 'Factory retorna null p/ tipo sem strategy');

    $unidades = $strategy->listarUnidades($missao);
    check(count($unidades) === 2, 'listarUnidades traz os 2 movimentos pendentes');
    check(isset($unidades[0]['id'], $unidades[0]['ligado'], $unidades[0]['produto_nome']), 'shape de unidade WMS ok');

    check($strategy->contarPendentesNovos($MISSAO_ID, (int)$a->id) === 1, 'contarPendentesNovos delega à Sonda');

    $r = $strategy->alternar((int)$b->id, false, $missao);
    check($r['ok'] === true, 'alternar desliga o movimento B');
    $estB = $pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id=" . (int)$b->id)->fetchColumn();
    check($estB === 'rejeitado', 'B persistido como rejeitado');

    $res = $strategy->commit($missao);
    check($res->desfecho === 'concluir', 'commit WMS sempre conclui');
    check($res->aprovados === 1 && $res->reprovados === 1, 'commit conta 1 aprovado / 1 rejeitado');
    $stA = $pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id=" . (int)$a->id)->fetchColumn();
    $stB = $pdo->query("SELECT status FROM wms_movimentos WHERE id=" . (int)$b->id)->fetchColumn();
    check($stA === 'aprovado' && $stB === 'cancelada', 'A aprovado, B cancelado');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar o teste (deve falhar — classes inexistentes)**

Run: `php tests/integration/Aprovacao/WmsAprovacaoStrategyTddTest.php`
Expected: erro fatal "Class ... AprovacaoStrategyFactory not found".

- [ ] **Step 3: Criar `AprovacaoResultado.php`**

```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

/**
 * Desfecho de um commit de aprovação. desfecho='concluir' (conclui+envia) ou
 * 'reabrir' (volta a em_andamento p/ rework dos reprovados).
 */
final class AprovacaoResultado
{
    public function __construct(
        public readonly string $desfecho,   // 'concluir' | 'reabrir'
        public readonly int $aprovados,
        public readonly int $reprovados,
        public readonly string $mensagem
    ) {}

    public static function concluir(int $aprovados, int $reprovados, string $mensagem): self
    {
        return new self('concluir', $aprovados, $reprovados, $mensagem);
    }

    public static function reabrir(int $aprovados, int $reprovados, string $mensagem): self
    {
        return new self('reabrir', $aprovados, $reprovados, $mensagem);
    }
}
```

- [ ] **Step 4: Criar `AprovacaoStrategyInterface.php`**

```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

/**
 * Contrato da aprovação por switch, por tipo de missão. O switch é genérico
 * (ligado=vai pro destino, desligado=não vai); cada tipo decide o efeito no commit.
 */
interface AprovacaoStrategyInterface
{
    /**
     * Unidades aprováveis prontas pra render (consumido só quando o tipo renderiza
     * via AJAX — WMS). @return array<int,array<string,mixed>> cada uma com ['id','ligado',...].
     */
    public function listarUnidades(object $missao): array;

    /** @return array{ok:bool,id:int,ligado:bool} */
    public function alternar(int $unidadeId, bool $ligado, object $missao): array;

    /** @return array{ok:bool,afetados:int} */
    public function alternarTodos(int $missaoId, bool $ligado): array;

    /** Conta unidades novas (id > snapshot) que o gestor não viu (snapshot scoping). */
    public function contarPendentesNovos(int $missaoId, int $snapshotMaxId): int;

    /** Aplica a decisão por-unidade e devolve o desfecho (concluir/reabrir). */
    public function commit(object $missao): AprovacaoResultado;
}
```

- [ ] **Step 5: Criar `WmsAprovacaoStrategy.php`**

```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

use GalaxiaConstelacoes\Produtos\Crud\Inteligencias\Produto;

/**
 * WmsAprovacaoStrategy — unidade = wms_movimentos. Embrulha o WmsMovimentoSonda do
 * sinal AlocacaoWms via class_exists (o Constelacao não depende do sinal deployado),
 * mesmo padrão do antigo MissoesAprovacao::aprovarOperacaoWms.
 */
class WmsAprovacaoStrategy implements AprovacaoStrategyInterface
{
    private const SONDA = '\\GalaxiaMissoes\\Logistica\\sinais\\AlocacaoWms\\Sondas\\WmsMovimentoSonda';

    private function sonda(): object
    {
        $cls = self::SONDA;
        if (!class_exists($cls)) {
            throw new \RuntimeException('Componente de movimentos WMS indisponível.');
        }
        return new $cls();
    }

    public function listarUnidades(object $missao): array
    {
        $missaoId = (int)($missao->id ?? 0);
        $rows = $this->sonda()->listarParaRevisao($missaoId);

        $nomes = [];
        $produtoModel = new Produto();
        $out = [];
        foreach ($rows as $r) {
            $pid = (int)$r->produto_id;
            if (!isset($nomes[$pid])) {
                $prod = $produtoModel->findById($pid);
                if (is_array($prod)) { $prod = $prod[0] ?? null; }
                $nomes[$pid] = [
                    'nome'   => $prod->descricao ?? ('Produto #' . $pid),
                    'codigo' => $prod->codigo_produto ?? '',
                ];
            }
            $out[] = [
                'id'             => (int)$r->id,
                'operacao_tipo'  => $r->operacao_tipo ?? 'alocacao',
                'produto_nome'   => $nomes[$pid]['nome'],
                'produto_codigo' => $nomes[$pid]['codigo'],
                'localizacao'    => $r->localizacao_codigo ?? '',
                'origem'         => $r->localizacao_codigo_origem ?? null,
                'destino'        => $r->localizacao_codigo_destino ?? null,
                'quantidade'     => $r->quantidade,
                'confirmada_em'  => $r->confirmada_em ?? null,
                // ON = pendente (será enviado); OFF = rejeitado (descartado)
                'ligado'         => (($r->aprovacao_gestor ?? 'pendente') !== 'rejeitado'),
            ];
        }
        return $out;
    }

    public function alternar(int $unidadeId, bool $ligado, object $missao): array
    {
        $n = (int)$this->sonda()->alternarAprovacao($unidadeId, $ligado);
        return ['ok' => $n > 0, 'id' => $unidadeId, 'ligado' => $ligado];
    }

    public function alternarTodos(int $missaoId, bool $ligado): array
    {
        $n = (int)$this->sonda()->alternarAprovacaoTodos($missaoId, $ligado);
        return ['ok' => true, 'afetados' => $n];
    }

    public function contarPendentesNovos(int $missaoId, int $snapshotMaxId): int
    {
        return (int)$this->sonda()->contarPendentesNovos($missaoId, $snapshotMaxId);
    }

    public function commit(object $missao): AprovacaoResultado
    {
        $missaoId = (int)($missao->id ?? 0);
        $resumo = $this->sonda()->commitAprovacao($missaoId);
        $aprovados  = (int)($resumo['aprovados'] ?? 0);
        $rejeitados = (int)($resumo['rejeitados'] ?? 0);
        return AprovacaoResultado::concluir(
            $aprovados,
            $rejeitados,
            "Missão concluída! {$aprovados} movimento(s) enviado(s), {$rejeitados} descartado(s)."
        );
    }
}
```

- [ ] **Step 6: Criar `AprovacaoStrategyFactory.php` (registra só WMS por enquanto)**

```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;

/**
 * Registry tipo_missao_id → strategy de aprovação por switch. Espelha
 * OperacaoAdapterFactory. Tipo sem strategy → null (painel mantém comportamento atual).
 *
 * Adicionar um tipo = registrar a entrada aqui + criar a *AprovacaoStrategy.
 */
class AprovacaoStrategyFactory
{
    /** @var array<int, class-string<AprovacaoStrategyInterface>> */
    private static array $strategies = [
        14 => WmsAprovacaoStrategy::class, // TipoMissao::ALOCACAO_WMS
    ];

    public static function criar(object $missao): ?AprovacaoStrategyInterface
    {
        $tipoId = (int)($missao->tipo_missao_id ?? 0);
        $classe = self::$strategies[$tipoId] ?? null;
        return $classe ? new $classe() : null;
    }
}
```
> Nota: a entrada `14` espelha `TipoMissao::ALOCACAO_WMS->id()`. O `use TipoMissao` fica pronto p/ a Task 3 (que pode trocar as chaves literais por `TipoMissao::X->id()` se preferir — manter literais é aceitável e evita custo de chamada estática em array property).

- [ ] **Step 7: Rodar o teste (deve passar)**

Run: `php tests/integration/Aprovacao/WmsAprovacaoStrategyTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (9 PASS).

- [ ] **Step 8: `php -l` nos 4 arquivos novos.** (sem commit)

Run: `for f in AprovacaoResultado AprovacaoStrategyInterface WmsAprovacaoStrategy AprovacaoStrategyFactory; do php -l componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/$f.php; done`
Expected: `No syntax errors detected` em todos.

---

### Task 3: `SeparacaoAprovacaoStrategy` + registro na Factory

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php`
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php` (adicionar a entrada `2`)
- Test: `tests/integration/Aprovacao/SeparacaoAprovacaoStrategyTddTest.php`

**Interfaces:**
- Consumes: `MissaoLogisticaItem` (`find`, `atualizar(array,$terms,$params):?int`), `StatusItem`.
- Produces: `SeparacaoAprovacaoStrategy` implementando `AprovacaoStrategyInterface`; commit = todos-on→concluir, algum-off→reabrir (off itens→pendente, dados limpos).

- [ ] **Step 1: Escrever o teste falhando (commit nos 2 caminhos + factory tipo 2)**

`tests/integration/Aprovacao/SeparacaoAprovacaoStrategyTddTest.php`:
```php
<?php
/** SeparacaoAprovacaoStrategy: alternar persiste aprovacao_gestor; commit todos-on=concluir,
 *  algum-off=reabrir (off→pendente, dados limpos; on seguem concluido). Factory resolve tipo 2. */
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', 'gestor_logistico' => 1];
$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 GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\SeparacaoAprovacaoStrategy;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT db\n"); exit(1); }
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou, $falhas; if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; } }

$pdo->exec("ALTER TABLE missao_logistica_itens ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL");
$BLOCK = 22;

function novaMissaoSeparacao(\PDO $pdo, int $block): int {
    $pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, block, data_cadastro)
                VALUES (2,'aguardando_aprovacao',true,1,{$block},NOW())");
    return (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$block} AND tipo_missao_id=2 ORDER BY id DESC LIMIT 1")->fetchColumn();
}
function novoItem(\PDO $pdo, int $missaoId, int $block, string $status='concluido'): int {
    $pdo->exec("INSERT INTO missao_logistica_itens (missao_logistica_id, status, quantidade, quantidade_bipada, dados_coletados, concluido_em, block)
                VALUES ({$missaoId},'{$status}',1,1,'{\"x\":1}',NOW(),{$block})");
    return (int)$pdo->query("SELECT id FROM missao_logistica_itens WHERE missao_logistica_id={$missaoId} ORDER BY id DESC LIMIT 1")->fetchColumn();
}

$strategy = new SeparacaoAprovacaoStrategy();

// --- Caminho A: todos ligados → concluir ---
$M1 = novaMissaoSeparacao($pdo, $BLOCK);
$i1 = novoItem($pdo, $M1, $BLOCK); $i2 = novoItem($pdo, $M1, $BLOCK);
$missao1 = (object)['id'=>$M1,'tipo_missao_id'=>2,'status'=>'aguardando_aprovacao'];
try {
    check(AprovacaoStrategyFactory::criar($missao1) instanceof SeparacaoAprovacaoStrategy, 'Factory resolve Separação (tipo 2)');
    $res = $strategy->commit($missao1);
    check($res->desfecho === 'concluir', 'todos ligados → concluir');
    check($res->aprovados === 2 && $res->reprovados === 0, 'conta 2 aprovados / 0 reprovados');
    $st1 = $pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$i1}")->fetchColumn();
    check($st1 === 'concluido', 'itens seguem concluido no concluir');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$M1}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$M1}");
}

// --- Caminho B: algum desligado → reabrir ---
$M2 = novaMissaoSeparacao($pdo, $BLOCK);
$j1 = novoItem($pdo, $M2, $BLOCK); $j2 = novoItem($pdo, $M2, $BLOCK);
$missao2 = (object)['id'=>$M2,'tipo_missao_id'=>2,'status'=>'aguardando_aprovacao'];
try {
    $r = $strategy->alternar($j2, false, $missao2);
    check($r['ok'] === true, 'alternar desliga o item j2');
    $est = $pdo->query("SELECT aprovacao_gestor FROM missao_logistica_itens WHERE id={$j2}")->fetchColumn();
    check($est === 'rejeitado', 'j2 persistido como rejeitado');

    check($strategy->contarPendentesNovos($M2, $j1) === 1, 'contarPendentesNovos detecta o item concluído novo');

    $res = $strategy->commit($missao2);
    check($res->desfecho === 'reabrir', 'algum desligado → reabrir');
    check($res->aprovados === 1 && $res->reprovados === 1, 'conta 1 aprovado / 1 reprovado');
    $stOff = $pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$j2}")->fetchColumn();
    $stOn  = $pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$j1}")->fetchColumn();
    check($stOff === 'pendente', 'item reprovado volta pra pendente');
    check($stOn === 'concluido', 'item aprovado segue concluido');
    $dadosOff = $pdo->query("SELECT dados_coletados FROM missao_logistica_itens WHERE id={$j2}")->fetchColumn();
    check($dadosOff === null, 'dados coletados do reprovado foram limpos');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$M2}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$M2}");
}

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

- [ ] **Step 2: Rodar o teste (deve falhar — classe inexistente)**

Run: `php tests/integration/Aprovacao/SeparacaoAprovacaoStrategyTddTest.php`
Expected: erro "Class ... SeparacaoAprovacaoStrategy not found".

- [ ] **Step 3: Criar `SeparacaoAprovacaoStrategy.php`**

```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Inteligencias\MissaoLogisticaItem;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\StatusItem;

/**
 * SeparacaoAprovacaoStrategy — unidade = missao_logistica_itens com status='concluido'.
 * Switch (aprovacao_gestor): NULL/'pendente'=ligado, 'rejeitado'=desligado.
 *
 * commit: todos ligados → concluir (envio normal pela via canônica do satélite);
 * algum desligado → reabrir (reprovados voltam pra pendente/rework; nada é enviado).
 */
class SeparacaoAprovacaoStrategy implements AprovacaoStrategyInterface
{
    private const REJEITADO = 'rejeitado';
    private const PENDENTE  = 'pendente';

    private function model(): MissaoLogisticaItem
    {
        return new MissaoLogisticaItem();
    }

    private function concluidoValue(): string
    {
        return StatusItem::CONCLUIDO->value;
    }

    public function listarUnidades(object $missao): array
    {
        $missaoId = (int)($missao->id ?? 0);
        $rows = $this->model()->find(
            'missao_logistica_id = :m AND status = :s',
            "m={$missaoId}&s=" . $this->concluidoValue()
        )->fetch(true) ?: [];

        $out = [];
        foreach ($rows as $r) {
            $out[] = [
                'id'           => (int)$r->id,
                'ligado'       => (($r->aprovacao_gestor ?? self::PENDENTE) !== self::REJEITADO),
                'produto_nome' => $r->produto_nome ?? $r->descricao ?? ('Item #' . $r->id),
                'quantidade'   => $r->quantidade ?? null,
            ];
        }
        return $out;
    }

    public function alternar(int $unidadeId, bool $ligado, object $missao): array
    {
        $novo = $ligado ? self::PENDENTE : self::REJEITADO;
        $n = (int)$this->model()->atualizar(
            ['aprovacao_gestor' => $novo],
            'id = :id AND status = :s',
            "id={$unidadeId}&s=" . $this->concluidoValue()
        );
        return ['ok' => $n > 0, 'id' => $unidadeId, 'ligado' => $ligado];
    }

    public function alternarTodos(int $missaoId, bool $ligado): array
    {
        $novo = $ligado ? self::PENDENTE : self::REJEITADO;
        $n = (int)$this->model()->atualizar(
            ['aprovacao_gestor' => $novo],
            'missao_logistica_id = :m AND status = :s',
            "m={$missaoId}&s=" . $this->concluidoValue()
        );
        return ['ok' => true, 'afetados' => $n];
    }

    public function contarPendentesNovos(int $missaoId, int $snapshotMaxId): int
    {
        $rows = $this->model()->find(
            'missao_logistica_id = :m AND status = :s AND id > :mx',
            "m={$missaoId}&s=" . $this->concluidoValue() . "&mx={$snapshotMaxId}"
        )->fetch(true);
        return $rows ? count($rows) : 0;
    }

    public function commit(object $missao): AprovacaoResultado
    {
        $missaoId = (int)($missao->id ?? 0);
        $model = $this->model();
        $concluido = $this->concluidoValue();

        $offRows = $model->find(
            'missao_logistica_id = :m AND status = :s AND aprovacao_gestor = :a',
            "m={$missaoId}&s={$concluido}&a=" . self::REJEITADO
        )->fetch(true) ?: [];

        $onRows = $model->find(
            'missao_logistica_id = :m AND status = :s AND (aprovacao_gestor IS NULL OR aprovacao_gestor <> :a)',
            "m={$missaoId}&s={$concluido}&a=" . self::REJEITADO
        )->fetch(true) ?: [];

        $aprovados  = count($onRows);
        $reprovados = count($offRows);

        if ($reprovados === 0) {
            return AprovacaoResultado::concluir($aprovados, 0, show('Missão aprovada com sucesso!'));
        }

        // Reprovados voltam pro operador (rework). Espelha voltarItemPendente (limpa execução).
        foreach ($offRows as $r) {
            $obs = (string)($r->observacoes ?? '') . ' [Reprovado na aprovação da missão]';
            $model->atualizar([
                'status'            => StatusItem::PENDENTE->value,
                'quantidade_bipada' => 0,
                'dados_coletados'   => null,
                'concluido_em'      => null,
                'concluido_por'     => null,
                'iniciado_em'       => null,
                'iniciado_por'      => null,
                'tempo_execucao'    => null,
                'aprovacao_gestor'  => null,
                'observacoes'       => $obs,
            ], 'id = :id', 'id=' . (int)$r->id);
        }

        // Limpa a decisão dos que ficaram (voltam a default ON quando a missão reabrir).
        $model->atualizar(
            ['aprovacao_gestor' => null],
            'missao_logistica_id = :m AND status = :s',
            "m={$missaoId}&s={$concluido}"
        );

        return AprovacaoResultado::reabrir(
            $aprovados,
            $reprovados,
            "{$reprovados} item(ns) reprovado(s) voltaram para refazer. Missão reaberta."
        );
    }
}
```

- [ ] **Step 4: Registrar Separação na Factory**

Em `AprovacaoStrategyFactory.php`, na property `$strategies`, adicionar a entrada `2`:
```php
    private static array $strategies = [
        14 => WmsAprovacaoStrategy::class,        // TipoMissao::ALOCACAO_WMS
        2  => SeparacaoAprovacaoStrategy::class,  // TipoMissao::SEPARACAO
    ];
```

- [ ] **Step 5: Rodar o teste (deve passar)**

Run: `php tests/integration/Aprovacao/SeparacaoAprovacaoStrategyTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (11 PASS).

- [ ] **Step 6: `php -l` na strategy e na factory.** (sem commit)

Run: `php -l componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php && php -l componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php`
Expected: `No syntax errors detected`.

---

### Task 4: Laboratório genérico `AprovacaoMissaoLaboratorio` + aposentar `RevisarMovimentosLaboratorio`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/AprovacaoMissaoLaboratorio.php`
- Delete: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php`
- (verificar/atualizar) qualquer referência a `RevisarMovimentosLaboratorio` em testes WMS.
- Test: `tests/integration/Aprovacao/AprovacaoMissaoLaboratorioTddTest.php`

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory`, `MissaoLogistica`.
- Produces: rotas `/labs/apis/Logistica/Designacoes/AprovacaoMissaoLaboratorio/{listar|alternar|alternarTodos}`. `alternar` recebe `{missao_id, unidade_id, ligado}`; `alternarTodos` `{missao_id, ligado}`; `listar` `?missao_id=N`.

- [ ] **Step 1: Confirmar o namespace do sinal Designacoes**

Run: `grep -n "^namespace" componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php`
Expected: linha `namespace GalaxiaMissoes\Logistica\sinais\Designacoes;` (usar o mesmo prefixo + `\Laboratorios` no lab).

- [ ] **Step 2: Escrever o teste falhando (lab dispatcha p/ a strategy)**

`tests/integration/Aprovacao/AprovacaoMissaoLaboratorioTddTest.php`:
```php
<?php
/** O lab genérico resolve a strategy pela missão e dispatcha listar/alternar/alternarTodos. */
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', 'gestor_logistico' => 1];
$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\Designacoes\Laboratorios\AprovacaoMissaoLaboratorio;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT db\n"); exit(1); }
$pdo->exec("ALTER TABLE missao_logistica_itens ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL");
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou, $falhas; if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; } }

$BLOCK = 22;
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, block, data_cadastro)
            VALUES (2,'aguardando_aprovacao',true,1,{$BLOCK},NOW())");
$M = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} AND tipo_missao_id=2 ORDER BY id DESC LIMIT 1")->fetchColumn();
$pdo->exec("INSERT INTO missao_logistica_itens (missao_logistica_id, status, quantidade, quantidade_bipada, concluido_em, block)
            VALUES ({$M},'concluido',1,1,NOW(),{$BLOCK})");
$IT = (int)$pdo->query("SELECT id FROM missao_logistica_itens WHERE missao_logistica_id={$M} ORDER BY id DESC LIMIT 1")->fetchColumn();

$galaxia = new class { public function __call($n,$a){ return $this; } };
try {
    $lab = new AprovacaoMissaoLaboratorio($galaxia, 'listar', ['missao_id' => $M]);
    $d = json_decode($lab->response, true);
    check(($d['ok'] ?? false) === true, 'listar ok');
    check(count($d['movimentos'] ?? []) === 1, 'listar traz 1 unidade');

    $lab2 = new AprovacaoMissaoLaboratorio($galaxia, 'alternar', ['missao_id' => $M, 'unidade_id' => $IT, 'ligado' => 0]);
    $d2 = json_decode($lab2->response, true);
    check(($d2['ok'] ?? false) === true, 'alternar ok');
    $est = $pdo->query("SELECT aprovacao_gestor FROM missao_logistica_itens WHERE id={$IT}")->fetchColumn();
    check($est === 'rejeitado', 'alternar persistiu rejeitado via strategy');

    $lab3 = new AprovacaoMissaoLaboratorio($galaxia, 'alternarTodos', ['missao_id' => $M, 'ligado' => 1]);
    $d3 = json_decode($lab3->response, true);
    check(($d3['ok'] ?? false) === true && ($d3['afetados'] ?? 0) === 1, 'alternarTodos religa 1');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$M}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$M}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 3: Rodar o teste (deve falhar — lab inexistente)**

Run: `php tests/integration/Aprovacao/AprovacaoMissaoLaboratorioTddTest.php`
Expected: "Class ... AprovacaoMissaoLaboratorio not found".

- [ ] **Step 4: Criar `AprovacaoMissaoLaboratorio.php`**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Designacoes\Laboratorios;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Inteligencias\MissaoLogistica;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory;

/**
 * AprovacaoMissaoLaboratorio — JSON leve da aprovação por switch (gestor), genérico
 * por tipo de missão (resolve a strategy pela missão). Substitui o
 * RevisarMovimentosLaboratorio (que era WMS-only).
 *
 * Rotas (labs/apis):
 *   GET  /labs/apis/Logistica/Designacoes/AprovacaoMissaoLaboratorio/listar?missao_id=N
 *   POST .../alternar       { missao_id, unidade_id, ligado }
 *   POST .../alternarTodos  { missao_id, ligado }
 */
class AprovacaoMissaoLaboratorio
{
    public $response;
    public $galaxia;
    public $sinal;

    public function __construct($galaxia, string $sinal, ?array $data = [])
    {
        $this->galaxia = $galaxia;
        $this->sinal   = $sinal;

        switch ($sinal) {
            case 'listar':        $this->response = $this->listar($data ?? []);        break;
            case 'alternar':      $this->response = $this->alternar($data ?? []);      break;
            case 'alternarTodos': $this->response = $this->alternarTodos($data ?? []); break;
            default:
                $this->response = json_encode(['ok' => false, 'erro' => 'Opção inválida']);
        }
    }

    private function buscarMissao(int $missaoId): ?object
    {
        if ($missaoId <= 0) {
            return null;
        }
        $m = (new MissaoLogistica())->find('id = :id', "id={$missaoId}")->fetch();
        if (is_array($m)) { $m = $m[0] ?? null; }
        return $m ?: null;
    }

    private function listar(array $data): string
    {
        $missaoId = (int)($data['missao_id'] ?? $_GET['missao_id'] ?? $_POST['missao_id'] ?? 0);
        $missao = $this->buscarMissao($missaoId);
        if (!$missao) {
            return json_encode(['ok' => false, 'erro' => 'Missão não informada']);
        }
        $strategy = AprovacaoStrategyFactory::criar($missao);
        if ($strategy === null) {
            return json_encode(['ok' => false, 'erro' => 'Missão sem aprovação por switch']);
        }
        return json_encode(['ok' => true, 'missao_id' => $missaoId, 'movimentos' => $strategy->listarUnidades($missao)]);
    }

    private function alternar(array $data): string
    {
        $missaoId  = (int)($data['missao_id'] ?? $_POST['missao_id'] ?? 0);
        $unidadeId = (int)($data['unidade_id'] ?? $_POST['unidade_id'] ?? 0);
        $ligadoRaw = $data['ligado'] ?? $_POST['ligado'] ?? null;
        $missao = $this->buscarMissao($missaoId);
        if (!$missao || $unidadeId <= 0 || $ligadoRaw === null) {
            return json_encode(['ok' => false, 'erro' => 'Parâmetros inválidos']);
        }
        $strategy = AprovacaoStrategyFactory::criar($missao);
        if ($strategy === null) {
            return json_encode(['ok' => false, 'erro' => 'Missão sem aprovação por switch']);
        }
        $ligado = filter_var($ligadoRaw, FILTER_VALIDATE_BOOLEAN);
        return json_encode($strategy->alternar($unidadeId, $ligado, $missao));
    }

    private function alternarTodos(array $data): string
    {
        $missaoId  = (int)($data['missao_id'] ?? $_POST['missao_id'] ?? 0);
        $ligadoRaw = $data['ligado'] ?? $_POST['ligado'] ?? null;
        $missao = $this->buscarMissao($missaoId);
        if (!$missao || $ligadoRaw === null) {
            return json_encode(['ok' => false, 'erro' => 'Parâmetros inválidos']);
        }
        $strategy = AprovacaoStrategyFactory::criar($missao);
        if ($strategy === null) {
            return json_encode(['ok' => false, 'erro' => 'Missão sem aprovação por switch']);
        }
        $ligado = filter_var($ligadoRaw, FILTER_VALIDATE_BOOLEAN);
        $r = $strategy->alternarTodos($missaoId, $ligado);
        $r['missao_id'] = $missaoId;
        $r['ligado']    = $ligado;
        return json_encode($r);
    }
}
```

- [ ] **Step 5: Verificar referências ao lab antigo e removê-lo**

Run: `grep -rln "RevisarMovimentosLaboratorio" componentes/ tests/`
- Se houver testes referenciando `RevisarMovimentosLaboratorio`, **deletá-los/atualizá-los** (a lógica agora é coberta por `WmsAprovacaoStrategyTddTest` + `AprovacaoMissaoLaboratorioTddTest`). O `wms_movimentos_secao.php` é repontado na Task 6 (não conta como bloqueio aqui).
- Deletar o arquivo:
  Run: `rm componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php`

- [ ] **Step 6: Rodar o teste (deve passar) + `php -l`**

Run: `php tests/integration/Aprovacao/AprovacaoMissaoLaboratorioTddTest.php && php -l componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/AprovacaoMissaoLaboratorio.php`
Expected: `TODOS OS TESTES PASSARAM` (5 PASS) + `No syntax errors detected`. (sem commit)

---

### Task 5: Generalizar `MissoesAprovacao::aprovar` (strategy-aware) + alias

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php`
  - construtor `match` (linha ~52-58)
  - `aprovar()` (linhas 61-108) → vira dispatcher; corpo antigo vira `aprovarBinario()`
  - remover `aprovarOperacaoWms()` (linhas 172-284); adicionar `aprovarComStrategy()`
- Test: generalizar `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/SnapshotScopingTddTest.php` (já passa via alias) + novo `tests/integration/Aprovacao/AprovarSeparacaoTddTest.php`

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory`, `AprovacaoResultado`, `MissaoLogistica` (CAS via `update`), `finalizarMissao`, `registrarExtrato`.
- Produces: opção `aprovar` (e aliases `aprovarMissao`, `aprovarOperacaoWms`) resolvendo strategy; desfecho concluir (CAS→CONCLUIDA + finalizarMissao) / reabrir (CAS→EM_ANDAMENTO + extrato RETOMADA); snapshot guard genérico.

- [ ] **Step 1: Escrever o teste falhando (aprovar separação: concluir e reabrir via satélite)**

`tests/integration/Aprovacao/AprovarSeparacaoTddTest.php`:
```php
<?php
/** MissoesAprovacao::aprovar p/ separação: todos-on conclui; algum-off reabre (em_andamento). */
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', 'gestor_logistico' => 1];
$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 GalaxiaConstelacoes\Logistica\MissaoLogistica\Satelites\MissoesAprovacao;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT db\n"); exit(1); }
$pdo->exec("ALTER TABLE missao_logistica_itens ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL");
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou, $falhas; if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; } }

$galaxia = new class {
    public function __call($name, $args) { return $this; }
    public function feito() { return ['ok' => true]; }
};
$BLOCK = 22;
function novaSep(\PDO $pdo, int $block): int {
    $pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, requer_conferencia, lider_id, block, data_cadastro)
                VALUES (2,'aguardando_aprovacao',true,true,1,{$block},NOW())");
    return (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$block} AND tipo_missao_id=2 ORDER BY id DESC LIMIT 1")->fetchColumn();
}
function item(\PDO $pdo, int $m, int $block): int {
    $pdo->exec("INSERT INTO missao_logistica_itens (missao_logistica_id, status, quantidade, quantidade_bipada, concluido_em, block)
                VALUES ({$m},'concluido',1,1,NOW(),{$block})");
    return (int)$pdo->query("SELECT id FROM missao_logistica_itens WHERE missao_logistica_id={$m} ORDER BY id DESC LIMIT 1")->fetchColumn();
}

// concluir
$M1 = novaSep($pdo, $BLOCK); item($pdo, $M1, $BLOCK); $maxA = item($pdo, $M1, $BLOCK);
try {
    new MissoesAprovacao($galaxia, 'aprovar', ['missao_id' => $M1, 'snapshot_max_id' => $maxA, 'usuario_id' => 1]);
    $st = $pdo->query("SELECT status FROM missao_logistica WHERE id={$M1}")->fetchColumn();
    check(in_array($st, ['concluida','concluido'], true), 'separação todos-on → missão concluída');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$M1}");
    $pdo->exec("DELETE FROM missao_logistica_extrato WHERE missao_logistica_id={$M1}");
    try { $pdo->exec("DELETE FROM missao_logistica WHERE missao_origem_id={$M1}"); } catch (\Throwable $e) {}
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$M1}");
}

// reabrir
$M2 = novaSep($pdo, $BLOCK); $on = item($pdo, $M2, $BLOCK); $off = item($pdo, $M2, $BLOCK);
$pdo->exec("UPDATE missao_logistica_itens SET aprovacao_gestor='rejeitado' WHERE id={$off}");
try {
    new MissoesAprovacao($galaxia, 'aprovar', ['missao_id' => $M2, 'snapshot_max_id' => $off, 'usuario_id' => 1]);
    $st = $pdo->query("SELECT status FROM missao_logistica WHERE id={$M2}")->fetchColumn();
    check($st === 'em_andamento', 'separação algum-off → missão reaberta (em_andamento)');
    $stOff = $pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$off}")->fetchColumn();
    $stOn  = $pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$on}")->fetchColumn();
    check($stOff === 'pendente', 'item reprovado voltou pra pendente');
    check($stOn === 'concluido', 'item aprovado segue concluido');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$M2}");
    $pdo->exec("DELETE FROM missao_logistica_extrato WHERE missao_logistica_id={$M2}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$M2}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 2: Rodar (deve falhar — `aprovar` ainda não é strategy-aware: separação cairia no binário e concluiria mesmo com item off)**

Run: `php tests/integration/Aprovacao/AprovarSeparacaoTddTest.php`
Expected: FALHA no caso "reabrir" (a missão concluiria em vez de reabrir).

- [ ] **Step 3: Atualizar o `match` do construtor**

Em `MissoesAprovacao.php`, trocar o bloco `match` (linhas ~52-58) por:
```php
        $this->response = match ($opcao) {
            'aprovarMissao', 'aprovar', 'aprovarOperacaoWms' => $this->aprovar($data),
            'aprovarSimples' => $this->aprovarSimples($data),
            'rejeitarItens', 'rejeitar' => $this->rejeitar($data),
            default => (new Message())->error('Opção inválida')->render()
        };
```

- [ ] **Step 4: Transformar `aprovar()` em dispatcher + extrair `aprovarBinario()`**

Substituir o método `aprovar()` atual (linhas 61-108) por estes dois:
```php
    private function aprovar(array $data)
    {
        $missaoId = $data['missao_id'] ?? null;
        $gestorId = $this->obterGestorId($data);

        if (!$missaoId) {
            return (new Message())->error('Missão não informada')->render();
        }
        $missao = $this->buscarMissao($missaoId);
        if (!$missao) {
            return (new Message())->error('Missão não encontrada')->render();
        }
        if ($missao->status !== StatusMissao::AGUARDANDO_APROVACAO->value) {
            return (new Message())->error('Missão não está aguardando aprovação')->render();
        }

        $strategy = \GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory::criar($missao);
        if ($strategy === null) {
            return $this->aprovarBinario($data, $missao, $gestorId);
        }
        return $this->aprovarComStrategy($data, $missao, $gestorId, $strategy);
    }

    /**
     * Aprovação binária legada (tipos sem strategy): aplica dados dos itens concluídos
     * ao produto e conclui. Mantido idêntico ao comportamento anterior.
     */
    private function aprovarBinario(array $data, object $missao, ?int $gestorId)
    {
        $missaoId = $missao->id;
        $itens = $this->buscarItensConcluidos($missaoId);

        if (empty($itens)) {
            $hasPendentes = (new MissaoLogisticaItem())
                ->find("missao_logistica_id = :mid AND status = 'pendente'", "mid={$missaoId}")
                ->fetch();
            if ($hasPendentes) {
                return (new Message())->error('A missão ainda possui itens pendentes e nenhum concluído para aprovação.')->render();
            }
            $atualizados = 0;
        } else {
            $atualizados = $this->aplicarDadosItens($itens);
        }

        $this->finalizarMissao($missaoId, $gestorId);

        $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_CONCLUIDA, null, [
            'aprovado_por' => $gestorId,
            'produtos_atualizados' => $atualizados,
        ]);

        $msgData = (new Message())->success("Missão aprovada com sucesso! {$atualizados} registros atualizados.")->render();

        return $this->galaxia->galaxiaResposta($msgData)
            ->removerElemento(".mission-row[data-id={$missaoId}]")
            ->feito();
    }
```

- [ ] **Step 5: Remover `aprovarOperacaoWms()` e adicionar `aprovarComStrategy()`**

Apagar o método `aprovarOperacaoWms()` inteiro (docblock linhas 172-182 + corpo 183-284) e colocar no lugar:
```php
    /**
     * Aprovação por switch, dirigida pela strategy do tipo (WMS, separação, …).
     * Parte transversal aqui (snapshot guard, CAS anti double-submit, extrato, desfecho);
     * a decisão por-unidade fica na strategy.
     */
    private function aprovarComStrategy(array $data, object $missao, ?int $gestorId, $strategy)
    {
        $missaoId = (int)$missao->id;

        // Snapshot scoping genérico: bloqueia se chegaram unidades novas (id > snapshot)
        // desde que o gestor abriu a revisão. snapshot_max_id ausente (-1) desabilita o guard.
        $snapshotMaxId = isset($data['snapshot_max_id']) ? (int)$data['snapshot_max_id'] : -1;
        if ($snapshotMaxId >= 0) {
            try {
                $novos = $strategy->contarPendentesNovos($missaoId, $snapshotMaxId);
            } catch (\Throwable $e) {
                error_log("[aprovar] checagem de novos falhou missao={$missaoId}: " . $e->getMessage());
                $novos = 0;
            }
            if ($novos > 0) {
                $msgErro = (new Message())->error(
                    "{$novos} novo(s) item(ns) chegaram desde que você abriu a revisão. Recarregue antes de aprovar."
                )->render();
                return $this->galaxia->galaxiaResposta($msgErro)
                    ->chamarFuncao('rmwRecarregar', ['missao_id' => $missaoId])
                    ->feito();
            }
        }

        try {
            $resultado = $strategy->commit($missao);
        } catch (\Throwable $e) {
            error_log("[aprovar] commit falhou missao={$missaoId}: " . $e->getMessage());
            return (new Message())->error('Falha ao processar. Nada foi concluído; tente novamente.')->render();
        }

        if ($resultado->desfecho === 'concluir') {
            $claim = (int)(new MissaoLogistica())->update(
                [
                    'status'         => StatusMissao::CONCLUIDA->value,
                    'finalizado_em'  => date('Y-m-d H:i:s'),
                    'finalizada_por' => $gestorId,
                ],
                "id = :id AND status = :st",
                "id={$missaoId}&st=" . StatusMissao::AGUARDANDO_APROVACAO->value
            );
            if ($claim === 0) {
                return (new Message())->success('Missão já foi concluída.')->render();
            }
            try {
                $this->finalizarMissao($missaoId, $gestorId, $missao, true);
            } catch (\Throwable $e) {
                error_log("[aprovar] pós-conclusão falhou missao={$missaoId}: " . $e->getMessage());
            }
            $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_CONCLUIDA, null, [
                'aprovado_por'   => $gestorId,
                'tipo_aprovacao' => 'strategy',
                'aprovados'      => $resultado->aprovados,
                'reprovados'     => $resultado->reprovados,
            ]);
            $msg = (new Message())->success($resultado->mensagem)->render();
            return $this->galaxia->galaxiaResposta($msg)
                ->chamarFuncao('recarregarPainelMissao')
                ->feito();
        }

        // reabrir (rework parcial): CAS aguardando → em_andamento
        $claim = (int)(new MissaoLogistica())->update(
            [
                'status'       => StatusMissao::EM_ANDAMENTO->value,
                'concluido_em' => null,
            ],
            "id = :id AND status = :st",
            "id={$missaoId}&st=" . StatusMissao::AGUARDANDO_APROVACAO->value
        );
        if ($claim === 0) {
            return (new Message())->success('Missão já foi atualizada.')->render();
        }
        $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_RETOMADA, null, [
            'reaberta_por' => $gestorId,
            'tipo'         => 'aprovacao_parcial',
            'reprovados'   => $resultado->reprovados,
        ]);
        $msg = (new Message())->success($resultado->mensagem)->render();
        return $this->galaxia->galaxiaResposta($msg)
            ->chamarFuncao('recarregarPainelMissao')
            ->feito();
    }
```

- [ ] **Step 6: Rodar o novo teste + os testes WMS (regressão via alias)**

Run:
```
php tests/integration/Aprovacao/AprovarSeparacaoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/SnapshotScopingTddTest.php
```
Expected: ambos `TODOS OS TESTES PASSARAM` (o SnapshotScoping chama `aprovarOperacaoWms`, agora alias de `aprovar` → resolve `WmsAprovacaoStrategy`; comportamento idêntico).

- [ ] **Step 7: `php -l` + rodar a suíte WMS completa (não-regressão).** (sem commit)

Run: `php -l componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php`
Run: rodar todos os `*TddTest.php` em `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/` (ver Task 8) — devem seguir verdes.

---

### Task 6: Frontend — repontar a seção WMS pros endpoints genéricos

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php`

**Interfaces:**
- Consumes: lab `AprovacaoMissaoLaboratorio` (Task 4), opção `aprovar` (Task 5).

- [ ] **Step 1: Trocar a URL base do lab**

Em `wms_movimentos_secao.php`, linha 56:
```php
    var base = '/labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/';
```
por:
```php
    var base = '/labs/apis/Logistica/Designacoes/AprovacaoMissaoLaboratorio/';
```

- [ ] **Step 2: Mandar `missao_id` + `unidade_id` no `alternar`**

Em `wms_movimentos_secao.php`, no corpo do `fetch(base + 'alternar', ...)` (linha ~108), trocar:
```php
            body: JSON.stringify({ movimento_id: sw.dataset.id, ligado: ligado ? 1 : 0 })
```
por:
```php
            body: JSON.stringify({ missao_id: missaoId, unidade_id: sw.dataset.id, ligado: ligado ? 1 : 0 })
```

- [ ] **Step 3: Trocar a opção do botão "Aprovar missão" pra `aprovar` + reload pós-conclusão**

Linha 45, trocar `->setOpcao('aprovarOperacaoWms')` por `->setOpcao('aprovar')`.
No fim do IIFE, logo antes de `window.rmwRecarregar = ...` (linha ~181), adicionar:
```php
    window.recarregarPainelMissao = function () { location.reload(); };
```

- [ ] **Step 4: `php -l` + smoke**

Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php`
Expected: `No syntax errors detected`.
Smoke (browser, manual pelo usuário): abrir `designacao_painel/{id}` de uma missão WMS em `aguardando_aprovacao`; alternar switches; "Aprovar missão" conclui e o painel recarrega. (sem commit)

---

### Task 7: Frontend — coluna de switch na separação + rodapé "Aprovar missão" + remover binário

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php`

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory` (flag de exibição), lab `AprovacaoMissaoLaboratorio`, opção `aprovar`.
- Vars já em escopo: `$missao`, `$itens`, `$isGestorLogistico`, `$status`, `$item`, `$itemStatus`, `$gridColumns`/`$colsArr`, `$galaxia`.

- [ ] **Step 1: Computar as flags da aprovação por switch (após o `$isWms`, linha 49)**

Inserir logo após a linha 49 (`$isWms = ...;`):
```php

// === Aprovação por switch (strategiada) — só p/ tipos não-WMS com strategy registrada ===
$statusMissaoPainel = (string)($missao->status ?? '');
$aprovacaoStrategy = \GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory::criar($missao);
$temSwitchSeparacao = !$isWms
    && $aprovacaoStrategy !== null
    && in_array($statusMissaoPainel, ['aguardando_aprovacao', 'concluida', 'cancelada'], true);
$switchEditavel = ($statusMissaoPainel === 'aguardando_aprovacao');
```

- [ ] **Step 2: Acrescentar a coluna de switch ao grid**

No bloco do grid (linhas 65-71), trocar:
```php
$colsArr[] = '100px';
$colsArr[] = '90px';
$gridColumns = implode(' ', $colsArr);
```
por:
```php
$colsArr[] = '100px'; // Conf/Total
if ($temSwitchSeparacao) { $colsArr[] = '70px'; } // Aprovar (switch)
$colsArr[] = '90px'; // Ações
$gridColumns = implode(' ', $colsArr);
```

- [ ] **Step 3: Acrescentar o header da coluna**

No `table-header` (linhas 867-878), trocar:
```php
                    <div>Conf / Total</div>
                    <div>Ações</div>
```
por:
```php
                    <div>Conf / Total</div>
                    <?php if ($temSwitchSeparacao): ?><div class="text-center"><?= show('Aprovar') ?></div><?php endif; ?>
                    <div>Ações</div>
```

- [ ] **Step 4: Acrescentar a célula de switch em cada linha (logo antes de `<div class="item-actions">`, linha 1228)**

Imediatamente antes de `<div class="item-actions">` (linha 1228), inserir:
```php
                            <?php if ($temSwitchSeparacao): ?>
                            <div class="item-aprovacao text-center">
                                <?php if ($itemStatus === 'concluido'): ?>
                                    <?php $itemLigado = (($item->aprovacao_gestor ?? 'pendente') !== 'rejeitado'); ?>
                                    <div class="form-check form-switch d-inline-block">
                                        <input class="form-check-input sep-switch" type="checkbox"
                                               data-id="<?= (int)$item->id ?>" <?= $itemLigado ? 'checked' : '' ?> <?= $switchEditavel ? '' : 'disabled' ?>>
                                    </div>
                                <?php endif; ?>
                            </div>
                            <?php endif; ?>

```

- [ ] **Step 5: Remover o botão binário "Rejeitar" da separação (linhas 243-245)**

Trocar:
```php
                        <button class="btn-admin btn-admin-danger" onclick="acaoRejeitarItens()">
                            <i class="bi bi-x-circle"></i> Rejeitar
                        </button>
```
por:
```php
                        <?php // "Rejeitar"/"Aprovar" binários substituídos pelo switch por-item + "Aprovar missão" (feature 16). ?>
```
> O reset por-item continua disponível: o botão "Reiniciar" (`voltarItemPendente`) já existe na coluna de Ações de cada item (linhas 1251-1265). Opcional: trocar o `title` e o `<span class="mobile-label">Reiniciar</span>` por "Refazer este item".

- [ ] **Step 6: Adicionar o rodapé "Aprovar missão" + o `<script>` da separação (após o footer "Fim da lista de itens", linha 1308)**

Logo após `</div>` que fecha o footer "Fim da lista de itens" (linha 1308), e antes do `</div>` que fecha `.items-list` (linha 1309), inserir:
```php
            <?php if ($temSwitchSeparacao && $switchEditavel): ?>
            <div class="sep-aprovacao-footer" style="display:flex; align-items:center; gap:.5rem; padding:1rem 1.5rem; border-top:1px solid var(--admin-border); background: rgba(0,0,0,0.2);">
                <button type="button" class="btn-admin btn-sm btn-admin-secondary" id="sep-all-on"><?= show('Confirmar todos') ?></button>
                <button type="button" class="btn-admin btn-sm btn-admin-secondary" id="sep-all-off"><?= show('Descartar todos') ?></button>
                <span class="text-muted" id="sep-count"></span>
                <button type="button" class="btn-admin btn-admin-success" id="sep-aprovar" style="margin-left:auto;"
                    <?= $galaxia->setConstelacao('Logistica')->setEstrela('MissaoLogistica')->setSatelite('MissoesAprovacao')->setOpcao('aprovar')->acaoElemento('missao_id=' . $missao->id . '&snapshot_max_id={snapshot_max_id}'); ?>>
                    <i class="bi bi-check-circle-fill"></i> <?= show('Aprovar missão') ?> <span id="sep-aprovar-n"></span>
                </button>
            </div>
            <?php endif; ?>
```

E, logo antes do fechamento `</div> <!-- Closing .admin-container -->` (linha 1315), inserir o script (guardado):
```php
<?php if ($temSwitchSeparacao): ?>
<script>
(function () {
    var missaoId = <?= (int)$missao->id ?>;
    var editavel = <?= $switchEditavel ? 'true' : 'false' ?>;
    var base = '/labs/apis/Logistica/Designacoes/AprovacaoMissaoLaboratorio/';
    var btnAprovar = document.getElementById('sep-aprovar');
    function switches() { return document.querySelectorAll('input.sep-switch'); }
    function atualizarContagem() {
        var all = switches(), on = 0;
        all.forEach(function (s) { if (s.checked) on++; });
        var c = document.getElementById('sep-count'); if (c) { c.textContent = on + '/' + all.length; }
        var n = document.getElementById('sep-aprovar-n'); if (n) { n.textContent = '(' + on + ')'; }
    }
    function atualizarSnapshot() {
        if (!btnAprovar) { return; }
        if (!btnAprovar.dataset.acaoTemplate) { btnAprovar.dataset.acaoTemplate = btnAprovar.getAttribute('data-acao-dados') || ''; }
        var maxId = 0; switches().forEach(function (s) { var id = +s.dataset.id; if (id > maxId) { maxId = id; } });
        var tpl = btnAprovar.dataset.acaoTemplate;
        if (tpl) { btnAprovar.setAttribute('data-acao-dados', tpl.replace(/\{snapshot_max_id\}|%7Bsnapshot_max_id%7D/g, maxId)); }
    }
    var pendingWrites = 0;
    function atualizarBotao() {
        if (!btnAprovar) { return; }
        btnAprovar.disabled = pendingWrites > 0;
        btnAprovar.title = pendingWrites > 0 ? '<?= show('Salvando…') ?>' : '';
    }
    function alternar(id, ligado) {
        pendingWrites++; atualizarBotao();
        fetch(base + 'alternar', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ missao_id: missaoId, unidade_id: id, ligado: ligado ? 1 : 0 })
        })
        .then(function (r) { if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); })
        .then(function (j) { if (!j || j.ok === false) { location.reload(); } })
        .catch(function () { location.reload(); })
        .finally(function () { pendingWrites--; atualizarBotao(); });
    }
    function setAll(estado) {
        switches().forEach(function (s) { s.checked = estado; });
        atualizarContagem();
        pendingWrites++; atualizarBotao();
        fetch(base + 'alternarTodos', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ missao_id: missaoId, ligado: estado ? 1 : 0 })
        })
        .then(function (r) { if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); })
        .then(function (j) { if (!j || j.ok === false) { location.reload(); } })
        .catch(function () { location.reload(); })
        .finally(function () { pendingWrites--; atualizarBotao(); });
    }
    if (editavel) {
        switches().forEach(function (s) {
            s.addEventListener('change', function () { atualizarContagem(); alternar(this.dataset.id, this.checked); });
        });
        var on = document.getElementById('sep-all-on'); if (on) { on.addEventListener('click', function () { setAll(true); }); }
        var off = document.getElementById('sep-all-off'); if (off) { off.addEventListener('click', function () { setAll(false); }); }
    }
    window.rmwRecarregar = function () { location.reload(); };
    window.recarregarPainelMissao = function () { location.reload(); };
    atualizarSnapshot();
    atualizarContagem();
})();
</script>
<?php endif; ?>
```

- [ ] **Step 7: `php -l` + smoke**

Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php`
Expected: `No syntax errors detected`.
Smoke (browser, manual): missão de **separação** em `aguardando_aprovacao` mostra a coluna de switch (só nos itens concluídos) + rodapé "Aprovar missão"; desligar um item + Aprovar → missão reabre (em_andamento) e o item volta pra pendente; todos ligados + Aprovar → conclui. Missão **não-WMS sem strategy** (ex.: pesagem) e missão **WMS** seguem como antes. (sem commit)

---

### Task 8: i18n + regressão final

**Files:**
- Modify: `linguagens/pt-br.json`, `linguagens/en.json`, `linguagens/es.json` (harvest)

- [ ] **Step 1: Harvest das strings novas**

Strings adicionadas via `show()`: `Aprovar`, `Aprovar missão`, `Confirmar todos`, `Descartar todos`, `Missão aprovada com sucesso!`, `Salvando…`. Rodar o harvest do projeto (mesmo procedimento das features 14/15) e preencher EN/ES das chaves novas (md5 do texto PT). Conferir no browser que não há chave crua.

- [ ] **Step 2: Rodar TODA a suíte WMS + os testes novos (não-regressão)**

Run:
```
for t in $(find componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes -name '*TddTest.php'); do echo "== $t =="; php "$t" || echo "FALHOU: $t"; done
for t in $(find tests/integration/Aprovacao -name '*TddTest.php'); do echo "== $t =="; php "$t" || echo "FALHOU: $t"; done
```
Expected: todos `TODOS OS TESTES PASSARAM`, nenhum "FALHOU".

- [ ] **Step 3: (sem commit)** Reportar o resumo (suítes verdes, arquivos `php -l` limpos, strings i18n). A árvore fica suja, aguardando o usuário pedir o commit.

---

## Self-Review (do plano vs spec)

- **Cobertura**: D1/D2 frame+strategies (T2/T3) · D3 escopo WMS+Separação (T2/T3) · D4 commit separação reabrir (T3/T5) · D5 commit WMS (T2/T5) · D6 coluna `aprovacao_gestor` (T1) · D7 2 sites de render (T6/T7) · D8 binários removidos (T7) · D9 lab genérico + `aprovar` (T4/T5) · D10 reset reusa `voltarItemPendente` (T7 step 5 nota). ✔
- **Tipos**: `AprovacaoResultado(desfecho,aprovados,reprovados,mensagem)` consistente em T2/T3/T5. Interface idêntica entre T2 (def) e T3/T5 (uso). `criar(object):?Interface` consistente em T2/T4/T5/T7. ✔
- **Placeholders**: nenhum TODO/“similar a”; todo passo de código traz o código. ✔
- **Não-commit**: todos os passos "Commit" substituídos por "sem commit". ✔
