# Aprovação por switch genérica (strategiada) — Plano de Implementação

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

**Goal:** Dar a qualquer missão logística um switch aprovado/reprovado por unidade no `designacao_painel`, com o efeito decidido por uma strategy por tipo (WMS = enviar/descartar; Separação = rework dos reprovados).

**Architecture:** Interface `AprovacaoStrategy` + `AprovacaoStrategyFactory` (registry por `tipo_missao_id`) em `Constelacoes/.../Strategies/Aprovacao/`. WMS vira `WmsAprovacaoStrategy` (absorve `RevisarMovimentosLaboratorio`); Separação vira `SeparacaoAprovacaoStrategy` (commit = rework dos itens reprovados). Lab genérico `RevisarAprovacaoLaboratorio` delega à strategy. Orquestrador novo `MissoesAprovacao::aprovarRevisao` (CAS+conclui ou reabre). Painel: WMS mantém a seção; Separação ganha switch na célula de Ações + rodapé. JS único `AprovacaoSwitches` (2 modos).

**Tech Stack:** PHP 8.1 (enums, named args, readonly), Galaxia V2 (ModelInteligencia, GalaxiaRoute, acaoElemento), PostgreSQL, JS vanilla, i18n `show()`.

## Global Constraints

- **NÃO COMMITAR.** Restrição explícita do usuário ("nada commitado"). Cada task termina em **verificação** (`php -l` + testes), nunca em `git commit`. Trabalho fica no working tree.
- **Sem raw SQL para writes de app.** Usar `->update()`/`->make()->save()` da Inteligência (o `update()` aceita `null` → grava `NULL`). Raw SQL só na migration (DDL) e em fixtures de teste.
- **Não-regressão WMS:** a suíte WMS existente (13 testes) tem de seguir verde. `aprovarOperacaoWms` permanece no dispatcher como **alias** → `aprovarRevisao`.
- **`aprovarMissao` já existe** no dispatcher (→ `aprovar()`, fluxo AJUSTE_DADOS) — o método genérico novo chama-se **`aprovarRevisao`** (não reusar `aprovarMissao`).
- **Separação = só rework** (decisão do usuário): o botão do painel devolve os reprovados; a conclusão/envio com tudo ligado **continua no fluxo atual** (lista + modal de faltas). A strategy de separação **nunca conclui** (`concluir=false`).
- **Tipos com strategy (v1):** `SEPARACAO` (id 2) e `ALOCACAO_WMS` (id 14). Tipos sem strategy → painel inalterado.
- **i18n:** toda string PT nova via `show('texto literal')` (key = md5 do texto). Harvest + conferir no browser.
- **Tenant:** integração roda em `dev_gx_goldie`, block=22, com `$_SESSION['galaxiaUserInfo']` na raiz e em `$_SESSION['dashboard']`.

## File Structure

**Criar:**
- `componentes/Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php` — enum compartilhado (aprovado/pendente/rejeitado).
- `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategy.php` — interface.
- `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php` — DTO do commit.
- `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/WmsAprovacaoStrategy.php`
- `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php`
- `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php`
- `componentes/Constelacoes/Logistica/MissaoLogistica/migrations/2026_06_26_add_aprovacao_gestor_itens.sql`
- `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/RevisarAprovacaoLaboratorio.php`
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/WmsAprovacaoStrategyTddTest.php`
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/SeparacaoAprovacaoStrategyTddTest.php`
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/AprovacaoStrategyFactoryTddTest.php`
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/AprovarRevisaoSeparacaoTddTest.php`

**Modificar:**
- `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php` — add `aprovarRevisao`; `aprovarOperacaoWms` vira alias; dispatch.
- `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.js` — módulo `AprovacaoSwitches` + switch no load-more.
- `componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php` — usar lab/ação genéricos + `AprovacaoSwitches.init`.
- `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php` — `$temAprovacao`, switch na célula de Ações, rodapé, ocultar "Rejeitar".

**Remover:**
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php` — lógica migrada para `WmsAprovacaoStrategy` (após confirmar zero refs).

---

### Task 1: Fundação — migration, enum compartilhado, DTO e interface

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/migrations/2026_06_26_add_aprovacao_gestor_itens.sql`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php`
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategy.php`

**Interfaces:**
- Consumes: nada (fundação).
- Produces: `AprovacaoGestor` (enum string aprovado/pendente/rejeitado); `AprovacaoResultado(bool $concluir, bool $reabrir, string $mensagem, int $aprovados=0, int $rejeitados=0)`; `AprovacaoStrategy::{listar(int):array, alternar(int,bool):int, alternarTodos(int,bool):int, contarPendentesNovos(int,int):int, commit(int,int):AprovacaoResultado}`; coluna `missao_logistica_itens.aprovacao_gestor VARCHAR(20) DEFAULT 'aprovado'`.

- [ ] **Step 1: Criar a migration**

`componentes/Constelacoes/Logistica/MissaoLogistica/migrations/2026_06_26_add_aprovacao_gestor_itens.sql`:
```sql
-- Migration: add aprovacao_gestor to missao_logistica_itens
-- Gate de aprovação do gestor por item (switch aprovado/reprovado no painel).
-- DEFAULT 'aprovado' => itens existentes mantêm o comportamento de envio.
BEGIN;
ALTER TABLE missao_logistica_itens
    ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT 'aprovado';
COMMIT;
```

- [ ] **Step 2: Aplicar a migration no banco de testes (dev_gx_goldie)**

Run (a partir da raiz do repo):
```bash
php -r '$f=getcwd()."/autoload.php"; require $f; use Galaxia\_Controladores\Connect; $pdo=(new Connect)->getInstance(null,null,"dev_gx_goldie"); $pdo->exec("ALTER TABLE missao_logistica_itens ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT \x27aprovado\x27"); echo "ok\n";'
```
Expected: `ok` (idempotente; rodar 2x não falha).
Verificar a coluna:
```bash
php -r '$f=getcwd()."/autoload.php"; require $f; use Galaxia\_Controladores\Connect; $pdo=(new Connect)->getInstance(null,null,"dev_gx_goldie"); var_dump($pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name=\x27missao_logistica_itens\x27 AND column_name=\x27aprovacao_gestor\x27")->fetchColumn());'
```
Expected: `string(16) "aprovacao_gestor"`.
> Nota: aplicar também em `gx_goldie` (DB de app) antes de usar em produção; aqui só dev para os testes.

- [ ] **Step 3: Criar o enum compartilhado**

`componentes/Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums;

/**
 * AprovacaoGestor — gate de aprovação do gestor por unidade aprovável (item/movimento).
 *
 * aprovado  : liberado (default — linha antiga / sem revisão). Switch ON.
 * pendente  : retido, aguardando revisão (usado pelo WMS). Switch ON.
 * rejeitado : gestor desligou o switch. Switch OFF.
 *
 * Enum compartilhado entre tipos de missão. O sinal AlocacaoWms mantém o seu
 * próprio enum de mesmos valores (não-regressão dos testes WMS).
 */
enum AprovacaoGestor: string
{
    case APROVADO  = 'aprovado';
    case PENDENTE  = 'pendente';
    case REJEITADO = 'rejeitado';

    public function label(): string
    {
        return match ($this) {
            self::APROVADO  => 'Aprovado',
            self::PENDENTE  => 'Pendente',
            self::REJEITADO => 'Rejeitado',
        };
    }
}
```

- [ ] **Step 4: Criar o DTO `AprovacaoResultado`**

`componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

/**
 * Resultado do commit de uma AprovacaoStrategy.
 *
 * concluir   : o orquestrador deve concluir a missão (CAS → CONCLUIDA + finalizar). WMS=true.
 * reabrir    : o orquestrador deve reabrir a missão (→ EM_ANDAMENTO). Separação(rework)=true.
 * mensagem   : mensagem pronta para o gestor.
 * aprovados  : nº unidades aprovadas/enviadas (informativo/extrato).
 * rejeitados : nº unidades rejeitadas/devolvidas (informativo/extrato).
 */
final class AprovacaoResultado
{
    public function __construct(
        public bool $concluir,
        public bool $reabrir,
        public string $mensagem,
        public int $aprovados = 0,
        public int $rejeitados = 0
    ) {}
}
```

- [ ] **Step 5: Criar a interface `AprovacaoStrategy`**

`componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategy.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

/**
 * Contrato de aprovação por unidade (switch) de uma missão logística.
 * Cada tipo de missão implementa a sua: o switch é genérico, o efeito é strategiado.
 */
interface AprovacaoStrategy
{
    /** Unidades aprováveis p/ a UI: [['id'=>int,'ligado'=>bool, ...colunas], ...]. */
    public function listar(int $missaoId): array;

    /** Alterna UMA unidade (ligado=aprovado/pendente; desligado=rejeitado). Retorna linhas afetadas. */
    public function alternar(int $unidadeId, bool $ligado): int;

    /** Alterna TODAS as unidades da missão. Retorna linhas afetadas. */
    public function alternarTodos(int $missaoId, bool $ligado): int;

    /** Snapshot scoping: nº de unidades novas com id > snapshotMaxId (0 onde não se aplica). */
    public function contarPendentesNovos(int $missaoId, int $snapshotMaxId): int;

    /** Aplica a decisão ao "Aprovar/Revisar missão" e devolve o que o orquestrador deve fazer. */
    public function commit(int $missaoId, int $gestorId): AprovacaoResultado;
}
```

- [ ] **Step 6: Verificar**

Run:
```bash
php -l componentes/Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php
php -l componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php
php -l componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategy.php
```
Expected: `No syntax errors detected` nos 3. Migration aplicada e coluna confirmada (Step 2).

---

### Task 2: `WmsAprovacaoStrategy` (absorve `RevisarMovimentosLaboratorio`)

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/WmsAprovacaoStrategy.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/WmsAprovacaoStrategyTddTest.php`

**Interfaces:**
- Consumes: `AprovacaoStrategy`, `AprovacaoResultado` (Task 1); `WmsMovimentoSonda` (sinal, via FQCN+class_exists): `listarParaRevisao`, `alternarAprovacao`, `alternarAprovacaoTodos`, `contarPendentesNovos`, `commitAprovacao(): ['aprovados'=>int,'rejeitados'=>int]`; `Produto::findById`.
- Produces: `WmsAprovacaoStrategy` (usada pela Factory na Task 4, pelo Lab na Task 5, pelo orquestrador na Task 6). `commit` retorna `concluir=true`, mensagem `"Missão concluída! N movimento(s) enviado(s), M descartado(s)."`.

- [ ] **Step 1: Escrever o teste (integração, dev_gx_goldie)**

`componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/WmsAprovacaoStrategyTddTest.php`:
```php
<?php
/** WmsAprovacaoStrategy: listar/alternar/commit espelham o fluxo WMS por movimento. */
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 as WmsAprovacao;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;
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',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} ORDER BY id DESC LIMIT 1")->fetchColumn();

$sonda = new WmsMovimentoSonda();
$strategy = new WmsAprovacaoStrategy();
try {
    $a = $sonda->registrarMovimento($MISSAO_ID, $ITEM_ID, $PRODUTO_ID, $COD, $LOC_ID, 1.0, 'manual', $OPERADOR_ID, OperacaoTipo::ALOCACAO, null, null, null, WmsAprovacao::PENDENTE);
    $b = $sonda->registrarMovimento($MISSAO_ID, $ITEM_ID, $PRODUTO_ID, $COD, $LOC_ID, 2.0, 'manual', $OPERADOR_ID, OperacaoTipo::ALOCACAO, null, null, null, WmsAprovacao::PENDENTE);
    $IDA = (int)$a->id; $IDB = (int)$b->id;

    $lista = $strategy->listar($MISSAO_ID);
    check(count($lista) === 2, 'listar() devolve os 2 movimentos');
    check($lista[0]['ligado'] === true && $lista[1]['ligado'] === true, 'ambos ligados (pendente) por padrão');

    check($strategy->alternar($IDB, false) === 1, 'alternar(B,false) afeta 1 linha');
    $listaB = $strategy->listar($MISSAO_ID);
    $ligadoB = null; foreach ($listaB as $u) { if ($u['id'] === $IDB) { $ligadoB = $u['ligado']; } }
    check($ligadoB === false, 'B aparece desligado após alternar');

    check($strategy->contarPendentesNovos($MISSAO_ID, $IDA) === 1, 'contarPendentesNovos detecta o B novo');

    $res = $strategy->commit($MISSAO_ID, 1);
    check($res->concluir === true && $res->reabrir === false, 'commit WMS: concluir=true, reabrir=false');
    check($res->aprovados === 1 && $res->rejeitados === 1, 'commit WMS: 1 aprovado, 1 rejeitado');
    $apA = $pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id={$IDA}")->fetchColumn();
    $stB = $pdo->query("SELECT status FROM wms_movimentos WHERE id={$IDB}")->fetchColumn();
    check($apA === 'aprovado', 'A → aprovado');
    check($stB === 'cancelada', 'B → cancelada (descartado)');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica_itens 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 e ver falhar**

Run:
```bash
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/WmsAprovacaoStrategyTddTest.php
```
Expected: erro fatal "Class ... WmsAprovacaoStrategy not found".

- [ ] **Step 3: Implementar `WmsAprovacaoStrategy`**

`componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/WmsAprovacaoStrategy.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

use GalaxiaConstelacoes\Produtos\Crud\Inteligencias\Produto;

/**
 * WmsAprovacaoStrategy — aprovação por movimento das operações WMS.
 * Unidade = wms_movimentos. Absorve a lógica do antigo RevisarMovimentosLaboratorio.
 * Depende do sinal AlocacaoWms (WmsMovimentoSonda) via class_exists — o core não
 * exige o sinal deployado (mesmo padrão dos *HookStrategy / aprovarOperacaoWms).
 */
final class WmsAprovacaoStrategy implements AprovacaoStrategy
{
    private const SONDA = '\GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda';

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

    public function listar(int $missaoId): array
    {
        $rows = $this->sonda()->listarParaRevisao($missaoId);

        $nomes = [];
        $produtoModel = new Produto();
        $unidades = [];
        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 ?? '',
                ];
            }
            $unidades[] = [
                'id'             => (int)$r->id,
                '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,
                'ligado'         => (($r->aprovacao_gestor ?? 'pendente') !== 'rejeitado'),
            ];
        }
        return $unidades;
    }

    public function alternar(int $unidadeId, bool $ligado): int
    {
        return (int)$this->sonda()->alternarAprovacao($unidadeId, $ligado);
    }

    public function alternarTodos(int $missaoId, bool $ligado): int
    {
        return (int)$this->sonda()->alternarAprovacaoTodos($missaoId, $ligado);
    }

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

    public function commit(int $missaoId, int $gestorId): AprovacaoResultado
    {
        $resumo = $this->sonda()->commitAprovacao($missaoId);
        $aprovados  = (int)($resumo['aprovados'] ?? 0);
        $rejeitados = (int)($resumo['rejeitados'] ?? 0);

        return new AprovacaoResultado(
            concluir: true,
            reabrir: false,
            mensagem: "Missão concluída! {$aprovados} movimento(s) enviado(s), {$rejeitados} descartado(s).",
            aprovados: $aprovados,
            rejeitados: $rejeitados
        );
    }
}
```

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

Run:
```bash
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/WmsAprovacaoStrategyTddTest.php
```
Expected: `TODOS OS TESTES PASSARAM` (PASS=8 FAIL=0).

- [ ] **Step 5: Lint**

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

---

### Task 3: `SeparacaoAprovacaoStrategy` (commit = rework dos reprovados)

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/SeparacaoAprovacaoStrategyTddTest.php`

**Interfaces:**
- Consumes: `AprovacaoStrategy`, `AprovacaoResultado` (Task 1); `MissaoLogisticaItem` (Inteligência) `find()->order()->fetch(true)` + `update(array,$terms,$params):?int`; enums `AprovacaoGestor`, `StatusItem`.
- Produces: `SeparacaoAprovacaoStrategy`. `commit`: sem reprovados → `concluir=false,reabrir=false`; com N reprovados → reabre os itens (status=pendente, limpa coleta, reseta intenção) e retorna `concluir=false,reabrir=true,rejeitados=N`.

- [ ] **Step 1: Escrever o teste (integração)**

`componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/SeparacaoAprovacaoStrategyTddTest.php`:
```php
<?php
/** SeparacaoAprovacaoStrategy: switch por item; commit = rework dos reprovados (sem envio). */
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\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"; } }

$BLOCK = 22; $PRODUTO_ID = 15424;
$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())");
$MISSAO_ID = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();
// 2 itens concluídos (aprovacao_gestor default 'aprovado')
for ($i = 0; $i < 2; $i++) {
    $pdo->exec("INSERT INTO missao_logistica_itens (missao_logistica_id, produto_id, status, quantidade, concluido_em, concluido_por, dados_coletados, block)
                VALUES ({$MISSAO_ID},{$PRODUTO_ID},'concluido',1,NOW(),1,'{\"x\":1}',{$BLOCK})");
}
$ids = $pdo->query("SELECT id FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID} ORDER BY id ASC")->fetchAll(\PDO::FETCH_COLUMN);
$ID1 = (int)$ids[0]; $ID2 = (int)$ids[1];

$strategy = new SeparacaoAprovacaoStrategy();
try {
    $lista = $strategy->listar($MISSAO_ID);
    check(count($lista) === 2, 'listar() devolve os 2 itens concluídos');
    check($lista[0]['ligado'] === true && $lista[1]['ligado'] === true, 'ambos ligados (aprovado) por padrão');

    check($strategy->contarPendentesNovos($MISSAO_ID, 0) === 0, 'contarPendentesNovos = 0 (no-op na separação)');

    // Nada reprovado → commit não conclui nem reabre
    $res0 = $strategy->commit($MISSAO_ID, 1);
    check($res0->concluir === false && $res0->reabrir === false, 'commit sem reprovados: concluir=false, reabrir=false');
    check($pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$ID1}")->fetchColumn() === 'concluido', 'item1 segue concluído');

    // Reprova o item2 e comita → rework só do item2
    check($strategy->alternar($ID2, false) === 1, 'alternar(item2,false) afeta 1 linha');
    check($pdo->query("SELECT aprovacao_gestor FROM missao_logistica_itens WHERE id={$ID2}")->fetchColumn() === 'rejeitado', 'item2 → rejeitado');

    $res = $strategy->commit($MISSAO_ID, 1);
    check($res->concluir === false && $res->reabrir === true, 'commit com reprovado: concluir=false, reabrir=true');
    check($res->rejeitados === 1, 'commit reporta 1 devolvido');
    check($pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$ID2}")->fetchColumn() === 'pendente', 'item2 → pendente (rework)');
    check($pdo->query("SELECT aprovacao_gestor FROM missao_logistica_itens WHERE id={$ID2}")->fetchColumn() === 'aprovado', 'item2 intenção resetada p/ aprovado');
    check($pdo->query("SELECT concluido_em FROM missao_logistica_itens WHERE id={$ID2}")->fetchColumn() === null, 'item2 concluido_em limpo (NULL via update)');
    check($pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$ID1}")->fetchColumn() === 'concluido', 'item1 (aprovado) segue concluído');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```
> Nota TDD: se o INSERT de `missao_logistica_itens` falhar por coluna NOT NULL ausente, adicionar a coluna ao INSERT (o schema é `tables.sql`). Os campos usados aqui cobrem o caso comum.

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/SeparacaoAprovacaoStrategyTddTest.php`
Expected: fatal "Class ... SeparacaoAprovacaoStrategy not found".

- [ ] **Step 3: Implementar `SeparacaoAprovacaoStrategy`**

`componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

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

/**
 * SeparacaoAprovacaoStrategy — aprovação por item da separação.
 * Unidade = missao_logistica_itens (status 'concluido'). Switch ON = aprovado.
 *
 * commit = REWORK (decisão do usuário): itens reprovados (switch OFF) voltam pra
 * 'pendente' (limpa coleta) e a missão reabre; NADA é enviado. A conclusão/envio
 * (tudo aprovado) segue pelo fluxo de aprovação atual (lista + modal de faltas) —
 * por isso esta strategy nunca conclui (concluir=false).
 */
final class SeparacaoAprovacaoStrategy implements AprovacaoStrategy
{
    public function listar(int $missaoId): array
    {
        $rows = (new MissaoLogisticaItem())
            ->find("missao_logistica_id = :m AND status = 'concluido'", "m={$missaoId}")
            ->order('id ASC')->fetch(true) ?: [];

        $unidades = [];
        foreach ($rows as $r) {
            $unidades[] = [
                'id'             => (int)$r->id,
                'produto_nome'   => $r->produto_nome ?? $r->descricao ?? ('Item #' . (int)$r->id),
                'produto_codigo' => $r->codigo_produto ?? '',
                'quantidade'     => $r->quantidade ?? null,
                'confirmada_em'  => $r->concluido_em ?? null,
                'ligado'         => (($r->aprovacao_gestor ?? 'aprovado') !== AprovacaoGestor::REJEITADO->value),
            ];
        }
        return $unidades;
    }

    public function alternar(int $unidadeId, bool $ligado): int
    {
        $novo = $ligado ? AprovacaoGestor::APROVADO->value : AprovacaoGestor::REJEITADO->value;
        return (int)(new MissaoLogisticaItem())->update(
            ['aprovacao_gestor' => $novo],
            "id = :id AND status = 'concluido'",
            "id={$unidadeId}"
        );
    }

    public function alternarTodos(int $missaoId, bool $ligado): int
    {
        $novo = $ligado ? AprovacaoGestor::APROVADO->value : AprovacaoGestor::REJEITADO->value;
        return (int)(new MissaoLogisticaItem())->update(
            ['aprovacao_gestor' => $novo],
            "missao_logistica_id = :m AND status = 'concluido'",
            "m={$missaoId}"
        );
    }

    public function contarPendentesNovos(int $missaoId, int $snapshotMaxId): int
    {
        // Itens não chegam durante a aprovação da separação → snapshot é no-op.
        return 0;
    }

    public function commit(int $missaoId, int $gestorId): AprovacaoResultado
    {
        $rejeitado = AprovacaoGestor::REJEITADO->value;

        $reprovados = (new MissaoLogisticaItem())->find(
            "missao_logistica_id = :m AND status = 'concluido' AND aprovacao_gestor = :r",
            "m={$missaoId}&r={$rejeitado}"
        )->fetch(true) ?: [];
        $n = count($reprovados);

        if ($n === 0) {
            return new AprovacaoResultado(
                concluir: false,
                reabrir: false,
                mensagem: 'Nenhum item reprovado. Use a aprovação normal para concluir a missão.'
            );
        }

        // Rework: reprovados → pendente (limpa coleta) e reseta a intenção p/ aprovado.
        (new MissaoLogisticaItem())->update(
            [
                'status'           => StatusItem::PENDENTE->value,
                'concluido_em'     => null,
                'concluido_por'    => null,
                'dados_coletados'  => null,
                'observacoes'      => 'REVISÃO: item devolvido pelo gestor para refazer',
                'aprovacao_gestor' => AprovacaoGestor::APROVADO->value,
            ],
            "missao_logistica_id = :m AND status = 'concluido' AND aprovacao_gestor = :r",
            "m={$missaoId}&r={$rejeitado}"
        );

        return new AprovacaoResultado(
            concluir: false,
            reabrir: true,
            mensagem: "{$n} item(ns) devolvido(s) para refazer. Missão reaberta.",
            rejeitados: $n
        );
    }
}
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/SeparacaoAprovacaoStrategyTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (PASS=11 FAIL=0).

- [ ] **Step 5: Lint**

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

---

### Task 4: `AprovacaoStrategyFactory`

**Files:**
- Create: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/AprovacaoStrategyFactoryTddTest.php`

**Interfaces:**
- Consumes: `WmsAprovacaoStrategy` (Task 2), `SeparacaoAprovacaoStrategy` (Task 3).
- Produces: `AprovacaoStrategyFactory::fromMissao(object):?AprovacaoStrategy`, `fromTipoId(int):?AprovacaoStrategy`, `registrar(int,string):void`. Registry: 2→Separacao, 14→Wms; outros→null.

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

`componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/AprovacaoStrategyFactoryTddTest.php`:
```php
<?php
/** AprovacaoStrategyFactory resolve a strategy por tipo de missão. */
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
require_once __DIR__ . '/../../../../../../../autoload.php';

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\WmsAprovacaoStrategy;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\SeparacaoAprovacaoStrategy;

$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"; } }

check(AprovacaoStrategyFactory::fromTipoId(14) instanceof WmsAprovacaoStrategy, 'tipo 14 → WmsAprovacaoStrategy');
check(AprovacaoStrategyFactory::fromTipoId(2) instanceof SeparacaoAprovacaoStrategy, 'tipo 2 → SeparacaoAprovacaoStrategy');
check(AprovacaoStrategyFactory::fromTipoId(8) === null, 'tipo sem strategy (8/conferência) → null');

$mWms = (object)['tipo_missao_id' => 14];
$mSep = (object)['tipo_missao_id' => 2];
$mOther = (object)['tipo_missao_id' => 3];
check(AprovacaoStrategyFactory::fromMissao($mWms) instanceof WmsAprovacaoStrategy, 'fromMissao(14) → Wms');
check(AprovacaoStrategyFactory::fromMissao($mSep) instanceof SeparacaoAprovacaoStrategy, 'fromMissao(2) → Separacao');
check(AprovacaoStrategyFactory::fromMissao($mOther) === null, 'fromMissao(3/carregamento) → null');

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

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/AprovacaoStrategyFactoryTddTest.php`
Expected: fatal "Class ... AprovacaoStrategyFactory not found".

- [ ] **Step 3: Implementar a factory**

`componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php`:
```php
<?php

namespace GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao;

/**
 * Resolve a AprovacaoStrategy por tipo de missão (registry + extensão via registrar()).
 * Tipo sem strategy → null (o painel fica como hoje; back-compat).
 *
 * IDs (ver Enums/TipoMissao::id()): SEPARACAO=2, ALOCACAO_WMS=14.
 */
final class AprovacaoStrategyFactory
{
    /** @var array<int,class-string<AprovacaoStrategy>> tipoMissaoId => strategy */
    private static array $registry = [
        2  => SeparacaoAprovacaoStrategy::class,  // SEPARACAO
        14 => WmsAprovacaoStrategy::class,        // ALOCACAO_WMS
    ];

    public static function registrar(int $tipoMissaoId, string $cls): void
    {
        self::$registry[$tipoMissaoId] = $cls;
    }

    public static function fromTipoId(int $tipoMissaoId): ?AprovacaoStrategy
    {
        $cls = self::$registry[$tipoMissaoId] ?? null;
        if (!$cls || !class_exists($cls)) {
            return null;
        }
        return new $cls();
    }

    public static function fromMissao(object $missao): ?AprovacaoStrategy
    {
        return self::fromTipoId((int)($missao->tipo_missao_id ?? 0));
    }
}
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/AprovacaoStrategyFactoryTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (PASS=6 FAIL=0).

- [ ] **Step 5: Lint**

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

---

### Task 5: Lab genérico `RevisarAprovacaoLaboratorio` (+ aposentar o WMS)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/RevisarAprovacaoLaboratorio.php`
- Remove: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php` (após confirmar zero refs)

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory::fromMissao` (Task 4); `MissaoLogistica::findById` (volta `[obj]` → desembrulhar).
- Produces: rotas `GET/POST /labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/{listar|alternar|alternarTodos}`. `listar?missao_id=N` → `{ok, unidades:[...]}`. `alternar` POST `{missao_id, unidade_id, ligado}`. `alternarTodos` POST `{missao_id, ligado}`.

- [ ] **Step 1: Confirmar refs do lab WMS antigo**

Run:
```bash
grep -rn "RevisarMovimentosLaboratorio" componentes/ tests/ webhooks/ crons/ 2>/dev/null
```
Expected após o repoint da Task 7: só a definição do arquivo (que será removido). Anotar quaisquer outras refs para ajuste.

- [ ] **Step 2: Implementar o lab genérico**

`componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/RevisarAprovacaoLaboratorio.php`:
```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Designacoes\Laboratorios;

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

/**
 * RevisarAprovacaoLaboratorio — backend genérico da revisão por unidade (switch).
 * Resolve a AprovacaoStrategy por missão e delega (a unidade é opaca: item/movimento).
 *
 * Rotas (labs/apis):
 *   GET  /labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/listar?missao_id=N
 *   POST /labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/alternar       { missao_id, unidade_id, ligado }
 *   POST /labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/alternarTodos  { missao_id, ligado }
 */
class RevisarAprovacaoLaboratorio
{
    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 strategyDe(int $missaoId): ?AprovacaoStrategy
    {
        if ($missaoId <= 0) { return null; }
        $missao = (new MissaoLogistica())->findById($missaoId);
        if (is_array($missao)) { $missao = $missao[0] ?? null; }
        if (!$missao) { return null; }
        return AprovacaoStrategyFactory::fromMissao($missao);
    }

    private function listar(array $data): string
    {
        $missaoId = (int)($data['missao_id'] ?? $_GET['missao_id'] ?? $_POST['missao_id'] ?? 0);
        $strategy = $this->strategyDe($missaoId);
        if (!$strategy) { return json_encode(['ok' => false, 'erro' => 'Missão sem aprovação por unidade']); }
        return json_encode(['ok' => true, 'missao_id' => $missaoId, 'unidades' => $strategy->listar($missaoId)]);
    }

    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;
        if ($unidadeId <= 0 || $ligadoRaw === null) {
            return json_encode(['ok' => false, 'erro' => 'Parâmetros inválidos']);
        }
        $strategy = $this->strategyDe($missaoId);
        if (!$strategy) { return json_encode(['ok' => false, 'erro' => 'Missão sem aprovação por unidade']); }
        $ligado = filter_var($ligadoRaw, FILTER_VALIDATE_BOOLEAN);
        $n = $strategy->alternar($unidadeId, $ligado);
        return json_encode(['ok' => $n > 0, 'unidade_id' => $unidadeId, 'ligado' => $ligado]);
    }

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

- [ ] **Step 3: Lint**

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

- [ ] **Step 4: Remover o lab WMS antigo (só se Step 1 + Task 7 confirmarem zero refs vivas)**

Run:
```bash
rm componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php
grep -rn "RevisarMovimentosLaboratorio" componentes/ tests/ webhooks/ crons/ 2>/dev/null || echo "sem refs"
```
Expected: `sem refs`. (Se aparecer ref viva, NÃO remover; ajustar a ref antes.)

---

### Task 6: Orquestrador `aprovarRevisao` + alias `aprovarOperacaoWms`

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/AprovarRevisaoSeparacaoTddTest.php`

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory::fromMissao`, `AprovacaoResultado` (Tasks 1/4); helpers existentes `buscarMissao`, `obterGestorId`, `finalizarMissao`, `registrarExtrato`; `MissaoLogistica::update/make->save`.
- Produces: dispatch `'aprovarRevisao'`; método `aprovarRevisao(array)`; `aprovarOperacaoWms` vira alias. Respostas: snapshot-block → `chamarFuncao('aprRecarregar')`; sucesso → `chamarFuncao('aprPosAprovar')`.

- [ ] **Step 1: Adicionar `aprovarRevisao` ao dispatch**

Em `MissoesAprovacao.php`, no `match` do construtor (linhas ~52-58), inserir a linha do `aprovarRevisao` antes do `aprovarOperacaoWms`:

Trocar:
```php
            'rejeitarItens', 'rejeitar' => $this->rejeitar($data),
            'aprovarOperacaoWms' => $this->aprovarOperacaoWms($data),
```
por:
```php
            'rejeitarItens', 'rejeitar' => $this->rejeitar($data),
            'aprovarRevisao' => $this->aprovarRevisao($data),
            'aprovarOperacaoWms' => $this->aprovarOperacaoWms($data),
```

- [ ] **Step 2: Adicionar o `use` da factory**

Em `MissoesAprovacao.php`, junto aos demais `use` do topo (após o `use ...Strategies\AjusteDadosHookStrategy;` da linha ~16), adicionar:
```php
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory;
```

- [ ] **Step 3: Substituir o corpo de `aprovarOperacaoWms` por um alias e adicionar `aprovarRevisao`**

Substituir TODO o método `aprovarOperacaoWms` (atual linhas ~183-284, do docblock `/** Aprovação por movimento...` até o `}` que fecha o método) por:
```php
    /**
     * Alias retrocompatível: WMS continua chamando 'aprovarOperacaoWms', mas o fluxo
     * agora é genérico (a factory resolve a WmsAprovacaoStrategy pelo tipo).
     */
    private function aprovarOperacaoWms(array $data)
    {
        return $this->aprovarRevisao($data);
    }

    /**
     * Aprovação por unidade (switch) genérica e strategiada.
     * Resolve a strategy do tipo da missão, aplica snapshot scoping e o commit;
     * conclui (CAS + finalizar) ou reabre a missão conforme o resultado.
     */
    private function aprovarRevisao(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 = AprovacaoStrategyFactory::fromMissao($missao);
        if (!$strategy) {
            return (new Message())->error('Esta missão não suporta aprovação por unidade.')->render();
        }

        // Snapshot scoping: bloqueia se chegaram unidades novas (id > snapshot) desde o load.
        // snapshot_max_id ausente (-1) desabilita o guard. (no-op nas strategies que retornam 0)
        $snapshotMaxId = isset($data['snapshot_max_id']) ? (int)$data['snapshot_max_id'] : -1;
        if ($snapshotMaxId >= 0) {
            try {
                $novos = $strategy->contarPendentesNovos((int)$missaoId, $snapshotMaxId);
            } catch (\Throwable $e) {
                error_log("[aprovarRevisao] checagem de novos falhou m={$missaoId}: " . $e->getMessage());
                $novos = 0;
            }
            if ($novos > 0) {
                $msgErro = (new Message())->error(
                    "{$novos} nova(s) unidade(s) chegaram desde que você abriu a revisão. Recarregue antes de aprovar."
                )->render();
                return $this->galaxia->galaxiaResposta($msgErro)
                    ->chamarFuncao('aprRecarregar', ['missao_id' => (int)$missaoId])
                    ->feito();
            }
        }

        try {
            $res = $strategy->commit((int)$missaoId, (int)$gestorId);
        } catch (\Throwable $e) {
            error_log("[aprovarRevisao] commit falhou m={$missaoId}: " . $e->getMessage());
            return (new Message())->error('Falha ao processar a revisão. Nada foi concluído; tente novamente.')->render();
        }

        if ($res->concluir) {
            // CAS atômico AGUARDANDO_APROVACAO → CONCLUIDA (anti double-submit; o commit é idempotente).
            $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=" . (int)$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("[aprovarRevisao] pós-conclusão m={$missaoId}: " . $e->getMessage());
            }
            $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_CONCLUIDA, null, [
                'aprovado_por'   => $gestorId,
                'tipo_aprovacao' => 'revisao_por_unidade',
                'aprovados'      => $res->aprovados,
                'rejeitados'     => $res->rejeitados,
            ]);
            return $this->galaxia->galaxiaResposta((new Message())->success($res->mensagem)->render())
                ->chamarFuncao('aprPosAprovar', ['missao_id' => (int)$missaoId, 'concluida' => true])
                ->feito();
        }

        if ($res->reabrir) {
            (new MissaoLogistica())->make([
                'id'           => $missaoId,
                'status'       => StatusMissao::EM_ANDAMENTO->value,
                'concluido_em' => null,
            ])->save();
            $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_RETOMADA, null, [
                'gestor_id'        => $gestorId,
                'tipo'             => 'revisao_por_unidade',
                'itens_devolvidos' => $res->rejeitados,
            ]);
            return $this->galaxia->galaxiaResposta((new Message())->success($res->mensagem)->render())
                ->chamarFuncao('aprPosAprovar', ['missao_id' => (int)$missaoId, 'reaberta' => true])
                ->feito();
        }

        // Sem efeito (ex.: separação sem reprovados).
        return (new Message())->warning($res->mensagem)->render();
    }
```

- [ ] **Step 4: Rodar a regressão WMS (alias)**

Run (todos os testes WMS já existentes — devem seguir verdes):
```bash
for t in componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/SnapshotScopingTddTest.php \
         componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/AprovacaoRobustezTddTest.php \
         componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/ForcarConclusaoWmsGuardTddTest.php; do
  echo "== $t =="; php "$t" || echo "FALHOU: $t"; done
```
Expected: `TODOS OS TESTES PASSARAM` em cada um (ajustar caminhos se os arquivos estiverem em subpastas diferentes; usar `find ... -name '*TddTest.php'` para localizar).

- [ ] **Step 5: Escrever o teste do orquestrador para separação**

`componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/AprovarRevisaoSeparacaoTddTest.php`:
```php
<?php
/** aprovarRevisao numa SEPARAÇÃO: reprovar item → reabre a missão (rework), sem envio. */
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); }
$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; $PRODUTO_ID = 15424;
$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())");
$MISSAO_ID = (int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();
for ($i = 0; $i < 2; $i++) {
    $pdo->exec("INSERT INTO missao_logistica_itens (missao_logistica_id, produto_id, status, quantidade, concluido_em, concluido_por, block)
                VALUES ({$MISSAO_ID},{$PRODUTO_ID},'concluido',1,NOW(),1,{$BLOCK})");
}
$ids = $pdo->query("SELECT id FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID} ORDER BY id ASC")->fetchAll(\PDO::FETCH_COLUMN);
$ID2 = (int)$ids[1];

try {
    // Reprova o item2 (switch off)
    $pdo->exec("UPDATE missao_logistica_itens SET aprovacao_gestor='rejeitado' WHERE id={$ID2}");

    new MissoesAprovacao($galaxia, 'aprovarRevisao', ['missao_id' => $MISSAO_ID, 'usuario_id' => 1]);

    $st = $pdo->query("SELECT status FROM missao_logistica WHERE id={$MISSAO_ID}")->fetchColumn();
    check($st === 'em_andamento', 'missão reaberta (em_andamento), não concluída');
    check($pdo->query("SELECT status FROM missao_logistica_itens WHERE id={$ID2}")->fetchColumn() === 'pendente', 'item reprovado voltou pra pendente');
    $extrato = (int)$pdo->query("SELECT COUNT(*) FROM missao_logistica_extrato WHERE missao_logistica_id={$MISSAO_ID} AND acao='MISSAO_RETOMADA'")->fetchColumn();
    check($extrato >= 1, 'extrato MISSAO_RETOMADA registrado');
    $concl = (int)$pdo->query("SELECT COUNT(*) FROM missao_logistica_extrato WHERE missao_logistica_id={$MISSAO_ID} AND acao='MISSAO_CONCLUIDA'")->fetchColumn();
    check($concl === 0, 'nenhum extrato de conclusão (nada enviado)');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_extrato WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```
> Nota: confirmar o nome real da coluna de ação no `missao_logistica_extrato` (`acao`) e o valor (`MISSAO_RETOMADA`/`MISSAO_CONCLUIDA`) via `AcaoExtrato`; ajustar o filtro do COUNT se o enum gravar outro literal.

- [ ] **Step 6: Rodar o teste do orquestrador e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Satelites/AprovarRevisaoSeparacaoTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (PASS=4 FAIL=0).

- [ ] **Step 7: Lint**

Run: `php -l componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php`
Expected: `No syntax errors detected`.

---

### Task 7: Módulo JS `AprovacaoSwitches` + refatorar a seção WMS

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.js` (adicionar o módulo no início do arquivo, fora de qualquer IIFE existente — em escopo global)
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php`

**Interfaces:**
- Consumes: lab `RevisarAprovacaoLaboratorio` (Task 5); ação `aprovarRevisao` (Task 6).
- Produces: `window.AprovacaoSwitches.init(opts)` (modos `'secao'` e `'coluna'`); hooks globais `window.aprRecarregar`, `window.aprPosAprovar`. Switch CSS class única `apr-switch` (`data-id`). Resposta do lab lida pela chave `unidades`.

- [ ] **Step 1: Adicionar o módulo `AprovacaoSwitches` ao topo de `designacao_painel.js`**

No início de `designacao_painel.js` (antes do código existente), inserir:
```javascript
/* ===== AprovacaoSwitches: controlador único dos switches de aprovação por unidade =====
 * modo 'secao'  : renderiza a própria tabela via listar (WMS, sem tabela de itens nativa).
 * modo 'coluna' : linhas já vêm do servidor (separação); só faz wiring via delegação.
 */
window.AprovacaoSwitches = (function () {
    function esc(v) {
        return String(v === null || v === undefined ? '' : v).replace(/[&<>"']/g, function (c) {
            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
        });
    }

    function init(opts) {
        var mode     = opts.mode;
        var missaoId = opts.missaoId;
        var editavel = !!opts.editavel;
        var base     = opts.base;
        var root     = document.getElementById(opts.rootId);
        if (!root) { return; }
        var btnEnviar = opts.btnEnviarId ? document.getElementById(opts.btnEnviarId) : null;
        var countEl   = opts.countId    ? document.getElementById(opts.countId)    : null;
        var enviarNEl = opts.enviarNId  ? document.getElementById(opts.enviarNId)  : null;
        var i18n      = opts.i18n || {};

        var pendingWrites = 0;
        var carregado = (mode === 'coluna'); // coluna: linhas já prontas

        function switches() { return root.querySelectorAll('input.apr-switch'); }

        function atualizarBotao() {
            if (!btnEnviar) { return; }
            if (!carregado) { btnEnviar.disabled = true; btnEnviar.title = i18n.carregando || ''; }
            else if (pendingWrites > 0) { btnEnviar.disabled = true; btnEnviar.title = i18n.salvando || ''; }
            else { btnEnviar.disabled = false; btnEnviar.title = ''; }
        }

        function atualizarContagem() {
            var all = switches();
            var ligados = 0;
            all.forEach(function (s) { if (s.checked) { ligados++; } });
            if (countEl) { countEl.textContent = ligados + '/' + all.length; }
            if (enviarNEl) {
                // secao (WMS): nº enviados = ligados; coluna (separação): nº reprovados = desligados
                var n = (mode === 'coluna') ? (all.length - ligados) : ligados;
                enviarNEl.textContent = '(' + n + ')';
            }
        }

        function atualizarSnapshot() {
            if (!btnEnviar) { return; }
            if (!btnEnviar.dataset.acaoTemplate) {
                btnEnviar.dataset.acaoTemplate = btnEnviar.getAttribute('data-acao-dados') || '';
            }
            var tpl = btnEnviar.dataset.acaoTemplate;
            if (!tpl) { return; }
            var maxId = 0;
            switches().forEach(function (s) { var v = +s.dataset.id; if (v > maxId) { maxId = v; } });
            btnEnviar.setAttribute('data-acao-dados', tpl.replace(/\{snapshot_max_id\}|%7Bsnapshot_max_id%7D/g, maxId));
        }

        function resync() { if (mode === 'secao') { carregar(); } else { window.location.reload(); } }

        function postJson(url, body) {
            pendingWrites++; atualizarBotao();
            fetch(base + url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            })
            .then(function (r) { if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); })
            .then(function (j) { if (!j || j.ok === false) { resync(); } })
            .catch(function () { resync(); })
            .finally(function () { pendingWrites--; atualizarBotao(); });
        }

        function alternar(sw) {
            postJson('alternar', { missao_id: missaoId, unidade_id: sw.dataset.id, ligado: sw.checked ? 1 : 0 });
        }

        function setAll(estado) {
            switches().forEach(function (s) { s.checked = estado; });
            atualizarContagem();
            postJson('alternarTodos', { missao_id: missaoId, ligado: estado ? 1 : 0 });
        }

        // --- modo 'secao' (WMS): render via listar ---
        function endereco(m) {
            if (m.origem && m.destino) { return m.origem + ' → ' + m.destino; }
            return m.localizacao || '—';
        }
        function render(unidades) {
            atualizarSnapshot();
            if (!unidades.length) {
                root.innerHTML = '<tr><td colspan="5" class="text-center text-muted">' + esc(i18n.vazio || '') + '</td></tr>';
                atualizarContagem(); return;
            }
            root.innerHTML = unidades.map(function (m) {
                var prod = esc(m.produto_nome) + (m.produto_codigo ? ' (' + esc(m.produto_codigo) + ')' : '');
                var qtd = (m.quantidade === null || m.quantidade === undefined) ? '—' : esc(m.quantidade);
                var dis = editavel ? '' : ' disabled';
                return '<tr data-id="' + esc(m.id) + '">' +
                    '<td>' + prod + '</td>' +
                    '<td>' + esc(endereco(m)) + '</td>' +
                    '<td class="text-end">' + qtd + '</td>' +
                    '<td class="text-muted small">' + esc(m.confirmada_em || '') + '</td>' +
                    '<td class="text-center"><div class="form-check form-switch d-inline-block">' +
                    '<input class="form-check-input apr-switch" type="checkbox" data-id="' + esc(m.id) + '"' + (m.ligado ? ' checked' : '') + dis + '></div></td>' +
                    '</tr>';
            }).join('');
            atualizarContagem();
        }
        function carregar() {
            fetch(base + 'listar?missao_id=' + missaoId)
                .then(function (r) { if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); })
                .then(function (d) { render((d && d.unidades) || []); carregado = true; atualizarBotao(); })
                .catch(function () { root.innerHTML = '<tr><td colspan="5" class="text-center text-danger">' + esc(i18n.erro || '') + '</td></tr>'; });
        }

        // --- wiring por delegação (cobre linhas do load-more no modo coluna) ---
        if (editavel) {
            root.addEventListener('change', function (e) {
                var sw = e.target;
                if (sw && sw.classList && sw.classList.contains('apr-switch')) {
                    atualizarContagem();
                    alternar(sw);
                }
            });
            if (opts.allOnId)  { var aon  = document.getElementById(opts.allOnId);  if (aon)  { aon.addEventListener('click',  function () { setAll(true);  }); } }
            if (opts.allOffId) { var aoff = document.getElementById(opts.allOffId); if (aoff) { aoff.addEventListener('click', function () { setAll(false); }); } }
        }

        window.aprRecarregar = function () { resync(); };
        window.aprPosAprovar = function () { window.location.reload(); };

        if (mode === 'secao') { carregar(); }
        else { atualizarSnapshot(); atualizarContagem(); }
        atualizarBotao();
    }

    return { init: init };
})();
```

- [ ] **Step 2: Refatorar `wms_movimentos_secao.php` para usar o módulo + lab/ação genéricos**

Substituir TODO o conteúdo de `componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php` por:
```php
<?php
/**
 * Seção de movimentos WMS para o painel da missão (designacao_painel).
 * Switches de aprovação por movimento — agora sobre o lab/ação GENÉRICOS
 * (RevisarAprovacaoLaboratorio + aprovarRevisao), via AprovacaoSwitches (modo 'secao').
 *
 * Lê do escopo do include: $missao (->id, ->status) e $galaxia (GalaxiaRoute).
 * editavel = true só em aguardando_aprovacao.
 */
$missaoId = (int)($missao->id ?? 0);
$statusMissao = (string)($missao->status ?? '');
$editavel = ($statusMissao === 'aguardando_aprovacao');
?>
<div class="wms-mov-secao" data-missao="<?= $missaoId ?>">
    <div style="display:flex; align-items:center; gap:.5rem; margin-bottom:.75rem;">
        <h3 style="margin:0; font-size:1rem; color:var(--admin-text, #fff);"><i class="bi bi-box-seam"></i> <?= show('Movimentos') ?></h3>
        <?php if ($editavel): ?>
        <button type="button" class="btn btn-sm btn-light" id="apr-all-on"><?= show('Confirmar todos') ?></button>
        <button type="button" class="btn btn-sm btn-light" id="apr-all-off"><?= show('Descartar todos') ?></button>
        <?php endif; ?>
        <span class="ms-auto text-muted" id="apr-count"></span>
    </div>

    <div class="table-responsive" style="max-height:60vh; overflow:auto;">
        <table class="table table-sm align-middle">
            <thead>
                <tr>
                    <th><?= show('Produto') ?></th>
                    <th><?= show('Endereço') ?></th>
                    <th class="text-end"><?= show('Qtd') ?></th>
                    <th><?= show('Quando') ?></th>
                    <th class="text-center"><?= show('Aprovar') ?></th>
                </tr>
            </thead>
            <tbody id="apr-tbody">
                <tr><td colspan="5" class="text-center text-muted"><?= show('Carregando…') ?></td></tr>
            </tbody>
        </table>
    </div>

    <?php if ($editavel): ?>
    <div style="display:flex; justify-content:flex-end; gap:.5rem; margin-top:1rem;">
        <button type="button" class="btn btn-success" id="apr-enviar"
                <?= $galaxia->setConstelacao('Logistica')->setEstrela('MissaoLogistica')->setSatelite('MissoesAprovacao')->setOpcao('aprovarRevisao')->acaoElemento('missao_id=' . $missaoId . '&snapshot_max_id={snapshot_max_id}'); ?>>
            <?= show('Aprovar missão') ?> <span id="apr-enviar-n"></span>
        </button>
    </div>
    <?php endif; ?>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    if (!window.AprovacaoSwitches) { return; }
    AprovacaoSwitches.init({
        mode: 'secao',
        missaoId: <?= $missaoId ?>,
        editavel: <?= $editavel ? 'true' : 'false' ?>,
        base: '/labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/',
        rootId: 'apr-tbody',
        btnEnviarId: 'apr-enviar',
        countId: 'apr-count',
        enviarNId: 'apr-enviar-n',
        allOnId: 'apr-all-on',
        allOffId: 'apr-all-off',
        i18n: {
            carregando: '<?= show('Carregando…') ?>',
            salvando: '<?= show('Salvando…') ?>',
            vazio: '<?= show('Nenhum movimento.') ?>',
            erro: '<?= show('Erro ao carregar.') ?>'
        }
    });
});
</script>
```

- [ ] **Step 3: Lint**

Run:
```bash
php -l componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php
node --check componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.js
```
Expected: `No syntax errors detected` (PHP) e nenhum erro do `node --check` (se `node` indisponível, validar o trecho manualmente / via lint do editor).

- [ ] **Step 4: Smoke manual (anotar p/ o usuário)**

Abrir uma missão WMS em `aguardando_aprovacao` no `designacao_painel/{ID}`: a seção carrega os movimentos, switches alternam, "Confirmar/Descartar todos" funciona, "Aprovar missão" conclui e a página recarrega. (Smoke é do usuário; aqui só registrar o roteiro.)

---

### Task 8: Superfície da separação no painel (switch na célula de Ações + rodapé)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php`
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.js` (renderer `carregarMaisItens`)

**Interfaces:**
- Consumes: `AprovacaoStrategyFactory::fromMissao` (Task 4); `AprovacaoSwitches.init` modo `'coluna'` (Task 7); lab/ação genéricos.
- Produces: switch `apr-switch` por item concluído na célula `.item-actions`; rodapé com `apr-all-on/off`, `apr-count`, `apr-enviar` (ação `aprovarRevisao`); flag global `window.__aprColuna`.

- [ ] **Step 1: Calcular `$temAprovacao` / `$temAprovacaoColuna` no painel**

Em `designacao_painel.php`, logo após a linha 49 (`$isWms = ...`), inserir:
```php
$temAprovacao = \GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\Aprovacao\AprovacaoStrategyFactory::fromMissao($missao) !== null;
// Switch por item (na célula de Ações) só p/ missões NÃO-WMS com strategy, em aprovação.
$temAprovacaoColuna = $temAprovacao && !$isWms && ((string)($missao->status ?? '') === 'aguardando_aprovacao');
```

- [ ] **Step 2: Ocultar o botão "Rejeitar" do header p/ tipos com strategy**

Em `designacao_painel.php`, envolver o botão "Rejeitar" (linhas 243-245) com um guard `!$temAprovacao`:

Trocar:
```php
                        <button class="btn-admin btn-admin-danger" onclick="acaoRejeitarItens()">
                            <i class="bi bi-x-circle"></i> Rejeitar
                        </button>
```
por:
```php
                        <?php if (!$temAprovacao): ?>
                        <button class="btn-admin btn-admin-danger" onclick="acaoRejeitarItens()">
                            <i class="bi bi-x-circle"></i> Rejeitar
                        </button>
                        <?php endif; ?>
```

- [ ] **Step 3: Injetar o switch na célula `.item-actions` (linha 1228)**

Em `designacao_painel.php`, logo após a abertura `<div class="item-actions">` (linha 1228) e ANTES do bloco PHP `<?php $missaoNaoTerminal = ...`, inserir:
```php
                                <?php if ($temAprovacaoColuna && $itemStatus === 'concluido'): ?>
                                    <div class="apr-switch-wrap form-check form-switch" title="<?= show('Ligado: aprovado (enviar) · Desligado: devolver para refazer') ?>" style="display:inline-flex; align-items:center; gap:4px; margin-right:8px;">
                                        <input class="form-check-input apr-switch" type="checkbox" data-id="<?= (int)$item->id ?>" <?= (($item->aprovacao_gestor ?? 'aprovado') !== 'rejeitado') ? 'checked' : '' ?>>
                                        <span class="apr-switch-label" style="font-size:0.7rem; color:var(--admin-muted);"><?= show('Aprovado') ?></span>
                                    </div>
                                <?php endif; ?>
```

- [ ] **Step 4: Adicionar o rodapé + flag global + init no painel (separação)**

Em `designacao_painel.php`, logo após o fechamento do `.items-list` na linha 1309 (`</div>`) e ANTES do fechamento de `.missions-table` na linha 1310 (`</div>`), inserir:
```php
        <?php if ($temAprovacaoColuna): ?>
        <div class="apr-footer" style="display:flex; align-items:center; gap:.5rem; padding:1rem 1.5rem; border-top:1px solid var(--admin-border);">
            <button type="button" class="btn-admin btn-admin-secondary" id="apr-all-on"><?= show('Confirmar todos') ?></button>
            <button type="button" class="btn-admin btn-admin-secondary" id="apr-all-off"><?= show('Descartar todos') ?></button>
            <span id="apr-count" style="color:var(--admin-muted); margin-left:.5rem;"></span>
            <button type="button" class="btn-admin btn-admin-danger" style="margin-left:auto;" id="apr-enviar"
                <?= $galaxia->setConstelacao('Logistica')->setEstrela('MissaoLogistica')->setSatelite('MissoesAprovacao')->setOpcao('aprovarRevisao')->acaoElemento('missao_id=' . (int)$missao->id); ?>>
                <i class="bi bi-arrow-counterclockwise"></i> <?= show('Devolver reprovados') ?> <span id="apr-enviar-n"></span>
            </button>
        </div>
        <script>
            window.__aprColuna = { ativo: true };
            document.addEventListener('DOMContentLoaded', function () {
                if (!window.AprovacaoSwitches) { return; }
                AprovacaoSwitches.init({
                    mode: 'coluna',
                    missaoId: <?= (int)$missao->id ?>,
                    editavel: true,
                    base: '/labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/',
                    rootId: 'items-container',
                    btnEnviarId: 'apr-enviar',
                    countId: 'apr-count',
                    enviarNId: 'apr-enviar-n',
                    allOnId: 'apr-all-on',
                    allOffId: 'apr-all-off',
                    i18n: { salvando: '<?= show('Salvando…') ?>' }
                });
            });
        </script>
        <?php endif; ?>
```
> O contador "Devolver reprovados (N)" mostra o nº de itens DESLIGADOS (reprovados), conforme o módulo no modo 'coluna'.

- [ ] **Step 5: Espelhar o switch no renderer JS do load-more (`carregarMaisItens`)**

Em `designacao_painel.js`, no `carregarMaisItens`, na construção da célula `.item-actions` (linha ~1650), trocar:
```javascript
                '<div class="item-actions" style="display: flex; gap: 6px; justify-content: flex-end;">' +
                (DesignacaoPainel.getAuditoria() && DesignacaoPainel.getAuditoria()[item.id] ?
```
por:
```javascript
                '<div class="item-actions" style="display: flex; gap: 6px; justify-content: flex-end;">' +
                ((window.__aprColuna && window.__aprColuna.ativo && item.status === 'concluido') ?
                    '<div class="apr-switch-wrap form-check form-switch" style="display:inline-flex;align-items:center;gap:4px;margin-right:8px;"><input class="form-check-input apr-switch" type="checkbox" data-id="' + item.id + '"' + ((item.aprovacao_gestor === 'rejeitado') ? '' : ' checked') + '><span style="font-size:0.7rem;color:var(--admin-muted);">Aprovado</span></div>'
                    : '') +
                (DesignacaoPainel.getAuditoria() && DesignacaoPainel.getAuditoria()[item.id] ?
```
(A delegação de eventos no `#items-container` cobre esses switches automaticamente.)

- [ ] **Step 6: Lint**

Run:
```bash
php -l componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php
node --check componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.js
```
Expected: `No syntax errors detected` (PHP) e sem erro de JS.

- [ ] **Step 7: Smoke manual (anotar p/ o usuário)**

Missão de SEPARAÇÃO em `aguardando_aprovacao`: cada item concluído mostra o switch "Aprovado" nas Ações; o header NÃO mostra "Rejeitar"; o rodapé mostra Confirmar/Descartar todos + "Devolver reprovados (N)"; desligar itens e clicar "Devolver reprovados" reabre a missão só com aqueles itens em pendente. Missão de tipo SEM strategy (ex.: conferência) fica inalterada.

---

### Task 9: i18n, limpeza e regressão final

**Files:**
- Modify: `linguagens/pt-br.json`, `linguagens/en.json`, `linguagens/es.json` (via harvest)
- Verify: todos os arquivos tocados

**Interfaces:**
- Consumes: tudo das tasks anteriores.
- Produces: strings i18n nas 3 línguas; suíte verde; zero refs órfãs.

- [ ] **Step 1: Harvest de i18n**

Abrir no browser (dev) o `designacao_painel/{ID}` de uma missão WMS e de uma de separação em `aguardando_aprovacao` (o auto-harvest do Galaxia coleta as `show()` novas). Em seguida conferir/instalar as traduções EN/ES das chaves novas: `Aprovado`, `Devolver reprovados`, `Ligado: aprovado (enviar) · Desligado: devolver para refazer`, `Salvando…`, `Carregando…`, `Nenhum movimento.`, `Erro ao carregar.`, `Confirmar todos`, `Descartar todos`, `Aprovar missão`, `Movimentos`, `Produto`, `Endereço`, `Qtd`, `Quando`, `Aprovar`.

- [ ] **Step 2: Verificar consistência de chaves duplicadas**

Run:
```bash
grep -rn "Carregando\.\.\.\|Carregando…" componentes/Missoes/Logistica/sinais/Designacoes/ | grep -v node_modules
```
Expected: só `Carregando…` (com reticências unicode) — sem `Carregando...` ASCII (a seção WMS foi reescrita usando `…`).

- [ ] **Step 3: Confirmar zero refs órfãs do lab antigo e da ação antiga**

Run:
```bash
grep -rn "RevisarMovimentosLaboratorio\|rmwRecarregar\|rmw-switch\|aprovarOperacaoWms" componentes/ tests/ 2>/dev/null
```
Expected: `aprovarOperacaoWms` aparece SÓ no dispatcher/alias de `MissoesAprovacao.php` (retrocompat). Nada de `RevisarMovimentosLaboratorio`/`rmwRecarregar`/`rmw-switch` (tudo migrado para o genérico/`apr-*`).

- [ ] **Step 4: Regressão completa (todos os TddTest WMS + novos)**

Run:
```bash
find componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes -name '*TddTest.php' -print0 | while IFS= read -r -d '' t; do
  echo "== $t =="; php "$t" >/tmp/aprtest.out 2>&1; tail -n 2 /tmp/aprtest.out; grep -q "FAIL=0" /tmp/aprtest.out && grep -q "PASSARAM" /tmp/aprtest.out || echo "!!! VERIFICAR: $t";
done
```
Expected: cada suíte termina em `TODOS OS TESTES PASSARAM` / `FAIL=0`. Nenhuma linha `!!! VERIFICAR`.

- [ ] **Step 5: Lint final de todos os arquivos tocados**

Run:
```bash
for f in \
 componentes/Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoResultado.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategy.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/WmsAprovacaoStrategy.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/SeparacaoAprovacaoStrategy.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/AprovacaoStrategyFactory.php \
 componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php \
 componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/RevisarAprovacaoLaboratorio.php \
 componentes/Missoes/Logistica/sinais/Designacoes/forms/wms_movimentos_secao.php \
 componentes/Missoes/Logistica/sinais/Designacoes/html/designacao_painel.php; do
  php -l "$f"; done
```
Expected: `No syntax errors detected` em todos.

- [ ] **Step 6: NÃO commitar**

Confirmar que nada foi commitado (`git status` mostra as mudanças no working tree). Reportar ao usuário; ele decide quando commitar.

---

## Self-Review (writing-plans)

**1. Cobertura da spec:**
- D1 (switch genérico, efeito strategiado) → Tasks 1-4 (interface/factory/strategies). ✓
- D2 (v1 = WMS refator + Separação) → Tasks 2, 3. ✓
- D3 (separação = rework, sem envio parcial) → Task 3 commit + Task 6 reabrir. ✓
- D4 (switch na célula de Ações; WMS mantém seção) → Tasks 7, 8. ✓
- D5 (Interface + Factory; tipo sem strategy = intacto) → Task 4 + guards `$temAprovacao`. ✓
- Migration/enum → Task 1. Lab genérico → Task 5. Orquestrador `aprovarRevisao`+alias → Task 6. JS único → Task 7. Não-regressão WMS → Tasks 6/9. i18n → Task 9. ✓

**2. Placeholders:** sem TBD/"implementar depois"; todo step com código/coando real. Duas notas TDD explícitas (colunas NOT NULL do INSERT de itens; literal do enum de extrato) — são checagens de execução, não placeholders de design.

**3. Consistência de tipos/nomes:** `AprovacaoStrategy`/`AprovacaoResultado`/`AprovacaoStrategyFactory`; método genérico `aprovarRevisao` (≠ `aprovarMissao` existente); switch class `apr-switch`; ids `apr-*`; resposta do lab `unidades`; hooks `aprRecarregar`/`aprPosAprovar`; coluna `aprovacao_gestor`. Conferidos entre tasks.

**4. Riscos sinalizados:** grid legado evitado (switch nas Ações, não coluna de grid); `update()` confirma NULL; faltas da separação preservadas (rework-only, conclusão fora). 
