# Permissão de desbloqueio da Alocação WMS — Implementation Plan

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

**Goal:** Gatear o acesso do operador à Alocação WMS no padrão Pesagem: card bloqueado por padrão → operador solicita → gestor aprova → liberado pelo dia.

**Architecture:** Espelha o fluxo `ACESSO_PESAGEM` (sinal Solicitacoes + App). Reusa tabela `solicitacoes_globais`, tela de gestão genérica e o campo JSON `usuario.dia_efetivo_liberacoes`. Pontos hardcoded por tipo recebem o novo `ACESSO_ALOCACAO_WMS`.

**Tech Stack:** PHP 8, PostgreSQL (`dev_gx_goldie`), JS/Bootstrap no App.

## Global Constraints

- Persistência da liberação: reusar `usuario.dia_efetivo_liberacoes` (JSON), ação `ACESSO_ALOCACAO_WMS`, `data_limite_liberacao = hoje`. Sem coluna nova.
- Rejeição: sem efeito colateral (igual Pesagem).
- Satélites do App resolvem por convenção em `App/Laboratorios/<Nome>.php`; o `acaoElemento` de solicitação usa `setVersao(2)`.
- TDD: teste falhando primeiro, ver falhar, implementação mínima, ver passar, commit.
- Integração roda só em `dev_gx_goldie`; bootstrap seta `$_SESSION['galaxiaUserInfo']` na raiz E em `$_SESSION['dashboard']`.
- Commits terminam com: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`

---

## File Structure

- **Modify:** `Solicitacoes/Enums/SolicitacaoTipo.php` — nova const.
- **Modify:** `Solicitacoes/Validators/CriarSolicitacaoValidator.php` — aceitar o tipo.
- **Create:** `Solicitacoes/Strategies/AcessoAlocacaoWmsAprovadoStrategy.php` — side-effect da aprovação.
- **Modify:** `Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php` — registrar a strategy.
- **Create:** `App/Laboratorios/SolicitarAcessoAlocacaoWms.php` — satélite de solicitação.
- **Modify:** `App/Handlers/AlocacaoWmsHandler.php` — guard de acesso.
- **Modify:** `App/html/estrutura.php` — card de 3 estados + wiring do modal.
- **Create (testes):** `Solicitacoes/Testes/Validators/CriarSolicitacaoValidatorAlocacaoWmsTddTest.php`, `Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php`.

(Base dos caminhos: `componentes/Missoes/Logistica/sinais/`.)

---

## Task 1: Tipo ACESSO_ALOCACAO_WMS aceito (enum + validator)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Enums/SolicitacaoTipo.php`
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Validators/CriarSolicitacaoValidator.php:37-44`
- Test: `componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Validators/CriarSolicitacaoValidatorAlocacaoWmsTddTest.php`

**Interfaces:**
- Produces: `SolicitacaoTipo::ACESSO_ALOCACAO_WMS = 'ACESSO_ALOCACAO_WMS'`; `CriarSolicitacaoValidator::validar()` retorna `[]` para esse tipo com referência válida.

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

Create `componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Validators/CriarSolicitacaoValidatorAlocacaoWmsTddTest.php`:

```php
<?php

/**
 * TddTest — CriarSolicitacaoValidator aceita ACESSO_ALOCACAO_WMS (CLI standalone).
 */

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';

use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Validators\CriarSolicitacaoValidator;
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Enums\SolicitacaoTipo;

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

echo "== CriarSolicitacaoValidator: ACESSO_ALOCACAO_WMS ==\n";

// const existe
check(defined(SolicitacaoTipo::class . '::ACESSO_ALOCACAO_WMS')
      || SolicitacaoTipo::ACESSO_ALOCACAO_WMS === 'ACESSO_ALOCACAO_WMS',
      'const ACESSO_ALOCACAO_WMS === string esperada');

$validator = new CriarSolicitacaoValidator();

$erros = $validator->validar([
    'tipo'            => SolicitacaoTipo::ACESSO_ALOCACAO_WMS,
    'referencia_tipo' => 'USUARIOS',
    'referencia_id'   => 1,
    'solicitante_id'  => 1,
]);
check($erros === [], 'payload válido com ACESSO_ALOCACAO_WMS → sem erros');

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/Solicitacoes/Testes/Validators/CriarSolicitacaoValidatorAlocacaoWmsTddTest.php 2>/dev/null`
Expected: erro fatal (const indefinida) OU `[FAIL] payload válido ... → sem erros` (tipo inválido). FALHA.

- [ ] **Step 3: Adicionar a const no enum**

Em `Solicitacoes/Enums/SolicitacaoTipo.php`, após a linha `public const EDICAO_ROTA = 'EDICAO_ROTA';`:

```php
    public const ACESSO_ALOCACAO_WMS = 'ACESSO_ALOCACAO_WMS';
```

- [ ] **Step 4: Aceitar no validator**

Em `Solicitacoes/Validators/CriarSolicitacaoValidator.php`, o array `$tiposValidos` (linhas 37-44) passa a incluir o novo tipo:

```php
            $tiposValidos = [
                SolicitacaoTipo::DESVINCULO,
                SolicitacaoTipo::RECOMECO,
                SolicitacaoTipo::ACESSO_MISSAO,
                SolicitacaoTipo::CANCELAMENTO,
                SolicitacaoTipo::ACESSO_RETIRA,
                SolicitacaoTipo::ACESSO_PESAGEM,
                SolicitacaoTipo::ACESSO_ALOCACAO_WMS,
            ];
```

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

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

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Enums/SolicitacaoTipo.php \
        componentes/Missoes/Logistica/sinais/Solicitacoes/Validators/CriarSolicitacaoValidator.php \
        componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Validators/CriarSolicitacaoValidatorAlocacaoWmsTddTest.php
git commit -m "$(cat <<'EOF'
feat(solicitacoes): aceitar tipo ACESSO_ALOCACAO_WMS

TDD: CriarSolicitacaoValidatorAlocacaoWmsTddTest (2/2 pass)

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

---

## Task 2: Strategy de aprovação + registro

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/AcessoAlocacaoWmsAprovadoStrategy.php`
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php:120-123`
- Test: `componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php`

**Interfaces:**
- Consumes: `SolicitacaoTipo::ACESSO_ALOCACAO_WMS` (Task 1).
- Produces: `AcessoAlocacaoWmsAprovadoStrategy::executar(object $solicitacao): bool` — adiciona/atualiza entrada `{acao_liberada:'ACESSO_ALOCACAO_WMS', data_limite_liberacao: hoje}` em `usuario.dia_efetivo_liberacoes`. Registrada em `AprovarSolicitacaoLaboratorio::$mapaStrategies`.

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

Create `componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php`:

```php
<?php

/**
 * TddTest — AcessoAlocacaoWmsAprovadoStrategy (integration, dev_gx_goldie).
 *
 * Grava liberação ACESSO_ALOCACAO_WMS no dia_efetivo_liberacoes de um usuário
 * real (block 22). Snapshot + restore no finally.
 */

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

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

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

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

use Galaxia\_Controladores\Connect;
use Base_config\Database\User as UserDb;
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoAlocacaoWmsAprovadoStrategy;

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

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

// Usuário real do block 22 (a strategy escreve no dia_efetivo_liberacoes dele)
$UID = (int)$pdo->query("SELECT id FROM usuario WHERE block = 22 ORDER BY id LIMIT 1")->fetchColumn();
if (!$UID) { fwrite(STDERR, "ABORT: sem usuário no block 22\n"); exit(1); }

$snapshot = $pdo->query("SELECT dia_efetivo_liberacoes FROM usuario WHERE id = {$UID}")->fetchColumn();
$hoje = date('Y-m-d');

echo "== AcessoAlocacaoWmsAprovadoStrategy (user {$UID}) ==\n";

try {
    // Zerar liberações para começar limpo
    $pdo->exec("UPDATE usuario SET dia_efetivo_liberacoes = '[]' WHERE id = {$UID}");

    $strategy = new AcessoAlocacaoWmsAprovadoStrategy();

    $ok = $strategy->executar((object)['solicitante_id' => $UID, 'id' => 0]);
    check($ok === true, 'executar() retorna true');

    $libs = json_decode((new UserDb())->findById($UID)->dia_efetivo_liberacoes ?? '[]', true) ?? [];
    $temWms = false;
    foreach ($libs as $l) {
        if (($l['acao_liberada'] ?? '') === 'ACESSO_ALOCACAO_WMS' && ($l['data_limite_liberacao'] ?? '') === $hoje) {
            $temWms = true;
            break;
        }
    }
    check($temWms, 'dia_efetivo_liberacoes ganhou ACESSO_ALOCACAO_WMS com data de hoje');

    // Idempotência: rodar de novo não duplica
    $strategy->executar((object)['solicitante_id' => $UID, 'id' => 0]);
    $libs2 = json_decode((new UserDb())->findById($UID)->dia_efetivo_liberacoes ?? '[]', true) ?? [];
    $qtdWms = count(array_filter($libs2, fn($l) => ($l['acao_liberada'] ?? '') === 'ACESSO_ALOCACAO_WMS'));
    check($qtdWms === 1, 'idempotente: apenas 1 entrada ACESSO_ALOCACAO_WMS após 2 execuções');

} finally {
    $valor = ($snapshot === false || $snapshot === null) ? 'NULL' : $pdo->quote($snapshot);
    $pdo->exec("UPDATE usuario SET dia_efetivo_liberacoes = {$valor} WHERE id = {$UID}");
}

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/Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php 2>/dev/null`
Expected: erro fatal "Class AcessoAlocacaoWmsAprovadoStrategy not found".

- [ ] **Step 3: Criar a strategy**

Create `componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/AcessoAlocacaoWmsAprovadoStrategy.php` (cópia fiel de `AcessoPesagemAprovadoStrategy`, ação trocada):

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies;

use Base_config\Api\GoldieUsers;
use Base_config\Database\User as UserDb;

/**
 * Strategy de side-effect para aprovação de ACESSO_ALOCACAO_WMS.
 *
 * Ao aprovar, adiciona/atualiza uma entrada no campo JSON `dia_efetivo_liberacoes`
 * do usuário solicitante, liberando acesso à tela de Alocação WMS para hoje.
 * Espelha AcessoPesagemAprovadoStrategy.
 */
class AcessoAlocacaoWmsAprovadoStrategy
{
    public function executar(object $solicitacao): bool
    {
        $solicitanteId = $solicitacao->solicitante_id ?? null;

        if (empty($solicitanteId)) {
            error_log("[AcessoAlocacaoWmsAprovadoStrategy] solicitante_id vazio na solicitação #{$solicitacao->id}");
            return false;
        }

        try {
            $usuarioDb = (new UserDb())->findById($solicitanteId);
            $liberacoes = [];

            if (!empty($usuarioDb->dia_efetivo_liberacoes)) {
                $liberacoes = json_decode($usuarioDb->dia_efetivo_liberacoes, true) ?? [];
            }

            // Remove entrada anterior para ACESSO_ALOCACAO_WMS e reinsere atualizada
            $liberacoes = array_values(array_filter(
                $liberacoes,
                fn($entry) => ($entry['acao_liberada'] ?? '') !== 'ACESSO_ALOCACAO_WMS'
            ));

            $liberacoes[] = [
                'acao_liberada' => 'ACESSO_ALOCACAO_WMS',
                'data_limite_liberacao' => date('Y-m-d'),
            ];

            $atualizacao = new GoldieUsers([
                'opt' => 'update',
                'id' => $solicitanteId,
                'dia_efetivo_liberacoes' => json_encode($liberacoes),
            ]);

            $resposta = $atualizacao->response ?? [];

            if (is_error($resposta)) {
                error_log("[AcessoAlocacaoWmsAprovadoStrategy] Falha ao atualizar dia_efetivo_liberacoes para user #{$solicitanteId}");
                return false;
            }

            return true;
        } catch (\Exception $excecao) {
            error_log("[AcessoAlocacaoWmsAprovadoStrategy] Erro: " . $excecao->getMessage());
            return false;
        }
    }
}
```

- [ ] **Step 4: Registrar no mapaStrategies**

Em `Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php`, o array `$mapaStrategies` (linhas 120-123) ganha a nova linha:

```php
        $mapaStrategies = [
            SolicitacaoTipo::ACESSO_RETIRA  => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoRetiraAprovadoStrategy::class,
            SolicitacaoTipo::ACESSO_PESAGEM => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoPesagemAprovadoStrategy::class,
            SolicitacaoTipo::ACESSO_ALOCACAO_WMS => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoAlocacaoWmsAprovadoStrategy::class,
            SolicitacaoTipo::EDICAO_ROTA    => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\EdicaoRotaAprovadaStrategy::class,
        ];
```

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

Run: `php componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php 2>/dev/null`
Expected: `RESULTADO: PASS=3 FAIL=0` / `TODOS OS TESTES PASSARAM`

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/AcessoAlocacaoWmsAprovadoStrategy.php \
        componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/AcessoAlocacaoWmsAprovadoStrategyTddTest.php
git commit -m "$(cat <<'EOF'
feat(solicitacoes): strategy de aprovação ACESSO_ALOCACAO_WMS

Grava liberação no dia_efetivo_liberacoes (JSON) do solicitante, igual
Pesagem. Registrada no mapaStrategies do AprovarSolicitacaoLaboratorio.

TDD: AcessoAlocacaoWmsAprovadoStrategyTddTest (3/3 pass, integração)

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

---

## Task 3: Satélite de solicitação (App)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/App/Laboratorios/SolicitarAcessoAlocacaoWms.php`

**Interfaces:**
- Consumes: `CriarSolicitacaoLaboratorio($galaxia, array $params)`, `SolicitacaoTipo::ACESSO_ALOCACAO_WMS` (Task 1).
- Produces: satélite `SolicitarAcessoAlocacaoWms` (resolvível por convenção em App/Laboratorios) que cria a solicitação `tipo=ACESSO_ALOCACAO_WMS`.

Sem teste automatizado: é um delegador fino idêntico ao `SolicitarAcessoPesagem` (que já é coberto pelo fluxo). Verificação por inspeção + smoke.

- [ ] **Step 1: Criar o satélite (espelho de SolicitarAcessoPesagem)**

Create `componentes/Missoes/Logistica/sinais/App/Laboratorios/SolicitarAcessoAlocacaoWms.php`:

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\App\Laboratorios;

use Base_config\Helpers\Message;
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Laboratorios\CriarSolicitacaoLaboratorio;

/**
 * Laboratorio Satélite para solicitar acesso à tela de Alocação WMS.
 *
 * Rota: V2 satellite via App → SolicitarAcessoAlocacaoWms
 * Delega ao CriarSolicitacaoLaboratorio do domínio Solicitacoes.
 * Espelha SolicitarAcessoPesagem.
 */
class SolicitarAcessoAlocacaoWms
{
    public $response;

    public function __construct(object $galaxia, string $opcao, ?array $data = [])
    {
        $this->response = $this->executar($galaxia, $data);
    }

    private function executar(object $galaxia, array $dados): array
    {
        $usuarioLogado = getSession('galaxiaUserInfo')->userData ?? null;

        if (empty($usuarioLogado)) {
            return (new Message())->error('Usuário não autenticado.')->render();
        }

        $parametrosRequisicao = [
            'tipo' => 'ACESSO_ALOCACAO_WMS',
            'solicitante_id' => $usuarioLogado->id,
            'referencia_tipo' => 'USUARIOS',
            'referencia_id' => $usuarioLogado->id,
            'motivo' => $dados['motivo'] ?? 'Solicito acesso à tela de Alocação WMS',
        ];

        $laboratorio = new CriarSolicitacaoLaboratorio($galaxia, $parametrosRequisicao);

        return $laboratorio->response;
    }
}
```

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

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

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/Laboratorios/SolicitarAcessoAlocacaoWms.php
git commit -m "$(cat <<'EOF'
feat(app): satélite SolicitarAcessoAlocacaoWms

Cria solicitação ACESSO_ALOCACAO_WMS (delega a CriarSolicitacaoLaboratorio).
Espelha SolicitarAcessoPesagem; resolvido por convenção em App/Laboratorios.

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

---

## Task 4: Guard de acesso no AlocacaoWmsHandler

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/Handlers/AlocacaoWmsHandler.php` (método `alocacaowms`)

**Interfaces:**
- Consumes: liberação `ACESSO_ALOCACAO_WMS` em `usuario.dia_efetivo_liberacoes` (Task 2).
- Produces: `alocacaowms()` bloqueia o acesso (lista e item) quando não há liberação do dia, renderizando a tela de bloqueio.

Sem teste automatizado (handler depende de sessão/route). Espelha o guard do `PesagemHandler::pesagem()`. Verificação por inspeção + smoke.

- [ ] **Step 1: Adicionar o import UserDb**

Em `AlocacaoWmsHandler.php`, após a linha `use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\ListarAlocacoesLaboratorio;`:

```php
use Base_config\Database\User as UserDb;
```

- [ ] **Step 2: Inserir o guard no início de `alocacaowms()`**

Substituir o corpo inicial do método `alocacaowms()`:

```php
    public function alocacaowms(array $dados): mixed
    {
        $missaoId = $this->extractMissaoIdFromRoute($dados['sinalGxData'] ?? '');
```

por (guard ANTES do extractMissaoId, espelhando PesagemHandler):

```php
    public function alocacaowms(array $dados): mixed
    {
        // [GUARD] Acesso diário à Alocação WMS (busca FRESCA do banco)
        $usuarioLogado = getSession('galaxiaUserInfo')->userData ?? null;
        $usuarioDb = (new UserDb())->findById($usuarioLogado->id ?? 0);
        $liberacoes = json_decode($usuarioDb->dia_efetivo_liberacoes ?? '[]', true) ?? [];
        $acessoLiberado = false;
        foreach ($liberacoes as $lib) {
            if (($lib['acao_liberada'] ?? '') === 'ACESSO_ALOCACAO_WMS' && ($lib['data_limite_liberacao'] ?? '') >= date('Y-m-d')) {
                $acessoLiberado = true;
                break;
            }
        }

        if (!$acessoLiberado) {
            return $this->renderAppView('html/shared/lista_unificada', [
                'missoes' => [],
                'configuracaoLista' => [
                    'tituloLista' => 'Acesso Bloqueado',
                    'subtituloLista' => 'Solicite acesso na tela inicial',
                    'iconeHeader' => 'bi bi-lock-fill',
                    'corTema' => '#ef4444',
                    'mensagemVazio' => 'Você não possui acesso à tela de Alocação WMS para hoje. Volte à tela inicial e solicite acesso.',
                    'iconeVazio' => 'bi bi-shield-lock-fill',
                    'ocultarFrete' => true,
                ],
                'estatisticas' => [],
                'usuarioId' => $usuarioLogado->id ?? 0,
                'galaxia' => $this->galaxiaRoute
            ]);
        }

        $missaoId = $this->extractMissaoIdFromRoute($dados['sinalGxData'] ?? '');
```

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

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

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/Handlers/AlocacaoWmsHandler.php
git commit -m "$(cat <<'EOF'
feat(app): guard de acesso na Alocação WMS (dia_efetivo_liberacoes)

Sem liberação ACESSO_ALOCACAO_WMS do dia, bloqueia lista e item.
Espelha o guard do PesagemHandler.

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

---

## Task 5: Card de 3 estados na home + wiring do modal

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/html/estrutura.php`

**Interfaces:**
- Consumes: satélite `SolicitarAcessoAlocacaoWms` (Task 3), liberação `ACESSO_ALOCACAO_WMS` (Task 2).
- Produces: card de Alocação WMS com estados liberado / em análise / bloqueado+solicitar.

Sem teste automatizado (view/JS). Espelha o bloco da Pesagem. Verificação por inspeção + smoke manual.

- [ ] **Step 1: Substituir o card simples pelo bloco de 3 estados**

Em `estrutura.php`, localizar o card simples adicionado antes:

```php
                    <div class="col-6"><a <?= $galaxia->proximaRota("logistica/app/alocacaowms"); ?> href="#" id="btn-alocacaowms" class="action-card" style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);">
                            <div class="icon-wrapper"><i class="bi bi-geo-alt-fill"></i></div>
                            <div>
                                <h3 class="text-white">Alocação WMS</h3>
                                <p>Endereçar produtos no armazém.</p>
                            </div>
                        </a></div>
```

e substituí-lo por (checagem + 3 estados, espelhando Pesagem):

```php
                    <?php
                    // [ACESSO ALOCACAO WMS] liberação diária (busca FRESCA do banco)
                    $usuarioLogadoWms = getSession('galaxiaUserInfo')->userData ?? null;
                    $usuarioDbWms = (new \Base_config\Database\User())->findById($usuarioLogadoWms->id ?? 0);
                    $liberacoesWms = json_decode($usuarioDbWms->dia_efetivo_liberacoes ?? '[]', true) ?? [];
                    $acessoWmsLiberado = false;
                    foreach ($liberacoesWms as $lib) {
                        if (($lib['acao_liberada'] ?? '') === 'ACESSO_ALOCACAO_WMS' && ($lib['data_limite_liberacao'] ?? '') >= date('Y-m-d')) {
                            $acessoWmsLiberado = true;
                            break;
                        }
                    }
                    $solicitacaoWmsPendente = false;
                    $solicitacaoWmsExistente = null;
                    if (!$acessoWmsLiberado && $usuarioLogadoWms) {
                        try {
                            $sondaSolWms = new \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Sondas\SolicitacaoSonda();
                            $solicitacaoWmsExistente = $sondaSolWms->find(
                                "tipo = :tipo AND solicitante_id = :uid AND status = :st AND DATE(data_cadastro) = :hoje",
                                "tipo=ACESSO_ALOCACAO_WMS&uid={$usuarioLogadoWms->id}&st=pendente&hoje=" . date('Y-m-d')
                            )->fetch();
                            $solicitacaoWmsPendente = !empty($solicitacaoWmsExistente);
                        } catch (\Exception $e) {}
                    }
                    ?>
                    <div class="col-6">
                        <?php if ($acessoWmsLiberado): ?>
                            <a <?= $galaxia->proximaRota("logistica/app/alocacaowms"); ?> href="#" id="btn-alocacaowms" class="action-card" style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);">
                                <div class="icon-wrapper"><i class="bi bi-geo-alt-fill"></i></div>
                                <div>
                                    <h3 class="text-white">Alocação WMS</h3>
                                    <p>Endereçar produtos no armazém.</p>
                                </div>
                            </a>
                        <?php elseif ($solicitacaoWmsPendente): ?>
                            <div id="btn-alocacaowms-bloqueado" class="action-card" style="background: linear-gradient(135deg, #374151 0%, #1f2937 100%); opacity: 0.85; cursor: not-allowed; position: relative;">
                                <span class="badge-em-analise">Em análise</span>
                                <div class="icon-wrapper" style="background-color: rgba(245, 158, 11, 0.2);"><i class="bi bi-hourglass-split icon-pulse" style="color: #fbbf24;"></i></div>
                                <div>
                                    <h3 class="text-white" style="font-size: 1rem;">Alocação WMS</h3>
                                    <p style="color: #fbbf24; margin-bottom: 0.5rem;">Aguardando aprovação...</p>
                                    <div class="p-2 rounded" style="background-color: rgba(0,0,0,0.2); font-size: 0.75rem; color: #d1d5db;">
                                        <strong>Usuário:</strong> <?= htmlspecialchars($usuarioLogadoWms->first_name ?? 'Desconhecido') ?><br>
                                        <strong>Motivo:</strong> <?= htmlspecialchars($solicitacaoWmsExistente->motivo ?? 'Liberação diária') ?>
                                    </div>
                                </div>
                            </div>
                        <?php else: ?>
                            <div id="btn-solicitar-alocacaowms" class="action-card" style="background: linear-gradient(135deg, #374151 0%, #1f2937 100%); position: relative; cursor: pointer;">
                                <button id="btn-solicitar-alocacaowms-real" style="display:none;"
                                    <?= $galaxia->setVersao(2)
                                        ->setConstelacao('Logistica')
                                        ->setEstrela('App')
                                        ->setSatelite('SolicitarAcessoAlocacaoWms')
                                        ->setOpcao('solicitar')
                                        ->acaoElemento(''); ?>></button>
                                <div class="icon-wrapper" style="background-color: rgba(239, 68, 68, 0.2);"><i class="bi bi-geo-alt-fill" style="color: #f87171;"></i></div>
                                <div>
                                    <h3 class="text-white" style="font-size: 1rem;">Alocação WMS</h3>
                                    <p style="color: #f87171;">Toque para solicitar acesso</p>
                                </div>
                            </div>
                            <script>
                                (function() {
                                    var btn = document.getElementById('btn-solicitar-alocacaowms');
                                    if (!btn) return;

                                    btn.addEventListener('click', function(e) {
                                        if (btn.dataset.processando === '1') return;
                                        window.pendenteSolicitacaoAlocacaoWms = true;
                                        $('#confirmationModalLabel').text('Solicitar Liberação?');
                                        $('#modal-body-text').text('Deseja realmente solicitar a liberação de Alocação WMS?');
                                        $('#btn-confirm-action').text('Solicitar').removeClass('btn-danger').addClass('btn-primary');
                                        var modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('confirmationModal'));
                                        modal.show();
                                    });

                                    window.processarUISolicitacaoAlocacaoWms = function() {
                                        if (!btn || btn.dataset.processando === '1') return;
                                        btn.dataset.processando = '1';
                                        btn.style.pointerEvents = 'none';
                                        btn.style.cursor = 'not-allowed';
                                        btn.style.opacity = '0.85';
                                        var iw = btn.querySelector('.icon-wrapper');
                                        if (iw) {
                                            iw.style.backgroundColor = 'rgba(245, 158, 11, 0.2)';
                                            iw.innerHTML = '<i class="bi bi-hourglass-split icon-pulse" style="color: #fbbf24;"></i>';
                                        }
                                        var p = btn.querySelector('p');
                                        if (p) { p.textContent = 'Solicitando acesso...'; p.style.color = '#fbbf24'; }
                                        var badge = document.createElement('span');
                                        badge.className = 'badge-em-analise';
                                        badge.textContent = 'Em análise';
                                        btn.appendChild(badge);
                                        var btnReal = document.getElementById('btn-solicitar-alocacaowms-real');
                                        if (btnReal) btnReal.click();
                                    };
                                }());
                            </script>
                        <?php endif; ?>
                    </div>
```

- [ ] **Step 2: Resetar a flag no fechar do modal**

Em `estrutura.php`, no handler `$('#confirmationModal').on('hidden.bs.modal', ...)` (linhas 1594-1597), adicionar o reset:

```php
            $('#confirmationModal').on('hidden.bs.modal', function () {
                window.pendenteSolicitacaoRetira = false;
                window.pendenteSolicitacaoPesagem = false;
                window.pendenteSolicitacaoAlocacaoWms = false;
            });
```

- [ ] **Step 3: Adicionar o branch no confirm do modal**

Em `estrutura.php`, no `$('#btn-confirm-action').on('click', ...)`, após o bloco `else if (window.pendenteSolicitacaoPesagem) { ... }` (linhas 1623-1627), adicionar:

```php
                } else if (window.pendenteSolicitacaoAlocacaoWms) {
                    if (typeof window.processarUISolicitacaoAlocacaoWms === 'function') {
                        window.processarUISolicitacaoAlocacaoWms();
                    }
                    window.pendenteSolicitacaoAlocacaoWms = false;
                }
```

(antes do `confirmationModal.hide();`)

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

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

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

Recarregar Apache (`sudo -n systemctl reload apache2`) e, na home do operador em `dev-genesis`:
1. Card "Alocação WMS" aparece **bloqueado** ("Toque para solicitar acesso").
2. Tocar → modal → "Solicitar" → card vira "Em análise".
3. Como gestor, aprovar a solicitação na tela de gestão de solicitações.
4. Operador recarrega → card **liberado** → abre `logistica/app/alocacaowms`.
5. Sem liberação, abrir a URL direto → tela "Acesso Bloqueado".

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/estrutura.php
git commit -m "$(cat <<'EOF'
feat(app): card Alocação WMS com permissão de desbloqueio (3 estados)

Bloqueado por padrão → solicitar (SolicitarAcessoAlocacaoWms) → em análise
→ liberado após aprovação. Espelha o card da Pesagem; wiring do modal de
confirmação.

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

---

## Self-Review

**Spec coverage:**
- Enum + validator → Task 1. ✔
- Strategy de aprovação + registro → Task 2. ✔
- Satélite de solicitação → Task 3. ✔
- Guard no handler → Task 4. ✔
- Card 3 estados + wiring modal → Task 5. ✔
- Reusa `dia_efetivo_liberacoes`, sem migration → Tasks 2/4/5. ✔
- Rejeição sem efeito → não há tarefa (intencional, igual Pesagem). ✔
- Tela de gestão genérica → sem alteração (confirmado na investigação). ✔

**Placeholder scan:** Sem TBD/TODO; todo passo de código mostra o código completo (novos arquivos são cópias fiéis dos de Pesagem, com a ação trocada). ✔

**Type consistency:** `AcessoAlocacaoWmsAprovadoStrategy::executar(object): bool` definido na Task 2 e registrado na mesma; `SolicitacaoTipo::ACESSO_ALOCACAO_WMS` definido na Task 1 e usado nas Tasks 2/3; flags JS `window.pendenteSolicitacaoAlocacaoWms` / `window.processarUISolicitacaoAlocacaoWms` consistentes entre os passos da Task 5. ✔

**Riscos conhecidos:**
- O guard usa `getSession('galaxiaUserInfo')` e `UserDb` — mesmos do PesagemHandler; assume que o handler tem `getSession` no escopo (helper global, já usado em estrutura.php).
- A `SolicitacaoSonda::find(...)->fetch()` no card espelha exatamente a query da Pesagem (mesma assinatura).
