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

> Data: 2026-06-26 · Feature: aprovacao-item-missao · Tipo: spec de design (brainstorming)
> Pré-requisito: a aprovação por movimento WMS (`wms_movimentos.aprovacao_gestor`,
> `RevisarMovimentosLaboratorio`, `MissoesAprovacao::aprovarOperacaoWms` com snapshot scoping,
> seção inline `forms/wms_movimentos_secao.php`) já existe — ver `PRD/operacao-wms/14-*` e `15-*`.
> Esta spec **generaliza** essa UX para qualquer missão logística e converte o WMS na primeira strategy.
> Estado atual: tudo da operacao-wms está no working tree, **não commitado**.

## 1. Problema / intenção

Hoje "aprovar/reprovar" significa coisas diferentes em dois mundos:

- **Missão genérica** (separação, conferência, etc.): itens em `missao_logistica_itens` com `status`.
  Aprovação é **binária no nível da missão** — o gestor *aprova tudo* (`Aprovar` → CONCLUÍDA → hooks
  mandam tudo pro ERP) ou *rejeita em bloco* (`Rejeitar`/`acaoRejeitarItens` → todos os itens
  concluídos voltam pra `pendente` e a missão **reabre**). Não há switch por-item.
- **WMS**: movimentos em `wms_movimentos` com `aprovacao_gestor` (aprovado/pendente/rejeitado).
  Tem switch **por-movimento**; ao "Aprovar missão" só os ligados integram, os desligados viram
  `cancelada` (descartados).

O objetivo: **qualquer missão logística** passa a ter, no seu layout, o switch aprovado/reprovado
por unidade; ao "Aprovar missão" só o aprovado vai ao destino final. O **efeito** do desligado é
decidido por uma **strategy por tipo de missão** (o WMS é uma strategy; a separação é outra).

## 2. Decisões tomadas (brainstorming)

| # | Decisão | Escolha |
|---|---------|---------|
| D1 | Semântica do "desligado" | **A strategy decide por tipo**. Switch é genérico (ligado=vai pro destino / desligado=não vai); o efeito do desligado é strategiado. |
| D2 | Escopo v1 | Frame genérico + painel em todas + **2 strategies concretas funcionando: WMS (refator) + Separação**. |
| D3 | Commit da Separação | **Reabre só os desligados (rework, sem envio parcial)**: desligados voltam pra `pendente` e a missão reabre (`em_andamento`); os ligados ficam concluídos; nada é enviado até 100% aprovado. |
| D4 | Superfície (missão com tabela de itens) | **Switch "Aprovado" na célula de Ações de cada item concluído** + rodapé "Confirmar/Descartar todos" + "Aprovar missão". (O grid da tabela é legado com 2 vias de render que já divergem — coluna dedicada seria frágil; o switch entra nas Ações, presentes nas 2 vias.) WMS mantém a seção dedicada (não tem tabela de itens). |
| D5 | Arquitetura | **Interface + Factory/Registry** (padrão V2). Tipo sem strategy → painel inalterado (back-compat). |

## 3. Comportamento por tipo (commit ao "Aprovar missão")

A `commit(missaoId, gestorId)` da strategy executa os efeitos e devolve `AprovacaoResultado`
(`concluir: bool`, contadores, mensagem). O orquestrador (`MissoesAprovacao`) age conforme:

- **WMS** (`WmsAprovacaoStrategy`, tipo `ALOCACAO_WMS`=14): `commit` = `commitAprovacao()` da Sonda
  (pendente→aprovado libera integração; rejeitado→`status=cancelada` descarta) e **sempre conclui**.
  → `concluir=true`. Comportamento **idêntico** ao de hoje (não-regressão; 13 testes WMS verdes).
- **Separação** (`SeparacaoAprovacaoStrategy`, tipo `SEPARACAO`=2): unidade = item concluído.
  - **todos ligados** → `concluir=true`: CONCLUÍDA + envio normal (`finalizarMissao` → hooks /
    geração de CARREGAMENTO conforme o fluxo atual).
  - **algum desligado** → `concluir=false`: os desligados voltam pra `pendente`
    (`voltarItensParaPendente`, registra motivo), missão **reabre** (`em_andamento`), a intenção é
    resetada (`aprovacao_gestor` dos reabertos volta a `aprovado`), **nada é enviado**. Extrato
    `MISSAO_RETOMADA`. = o "Rejeitar" atual, porém **cirúrgico por-item**.

```
┌ designacao_painel/{ID} — Separação #1234 (aguardando aprovação) ─────────────┐
│ Filtros: [Status ▾][Operador ▾][Ocorrências ▾]              🔍 buscar         │
├──────────────────────────────────────────────────────────────────────────────┤
│  #  Item            Conf/Total   Status        Ações        Aprovado          │
│  1  Produto X (123)    7/7        ✔ concluído    ⋯           [ ON  ]          │
│  2  Produto Y (456)    3/5        ⚠ ocorrência   ⋯           [ OFF ]          │
│  3  Produto Z (789)    4/4        ✔ concluído    ⋯           [ ON  ]          │
├──────────────────────────────────────────────────────────────────────────────┤
│  [Confirmar todos] [Descartar todos]                  [ Aprovar missão (2) ▸ ]│
└──────────────────────────────────────────────────────────────────────────────┘
   ON em todos → conclui e envia.   Algum OFF → reabre só os OFF (rework).
```

## 4. Arquitetura (componentes e interfaces)

### 4.1 Núcleo strategiado — `Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/`
(junto das `*HookStrategy` que já moram em `Constelacoes/.../Strategies/`)

- **`AprovacaoStrategy`** (interface):
  - `listar(int $missaoId): array` → `[{id, ligado:bool, ...colunas p/ render}]` (unidades aprováveis).
  - `alternar(int $unidadeId, bool $ligado): int` → rowCount afetado (guard: só unidade ainda aberta).
  - `alternarTodos(int $missaoId, bool $ligado): int` → rowCount.
  - `contarPendentesNovos(int $missaoId, int $snapshotMaxId): int` → snapshot scoping; **no-op** (retorna 0)
    onde unidades não chegam durante a aprovação (caso da separação).
  - `commit(int $missaoId, int $gestorId): AprovacaoResultado` → executa efeitos, devolve resultado.
- **`AprovacaoResultado`** (DTO simples): `concluir: bool`, `mensagem: string`, `aprovados: int`,
  `rejeitados: int`.
- **`AprovacaoStrategyFactory`**: registry estática `[tipoMissaoId => Strategy::class]` + `registrar()`
  para extensão sem editar o core. `fromMissao(object|int $missao): ?AprovacaoStrategy` →
  resolve por `tipo_missao_id` (via `TipoMissao::fromId()`); **tipo sem strategy → `null`**.
  v1 registra: `14 (ALOCACAO_WMS) → WmsAprovacaoStrategy`, `2 (SEPARACAO) → SeparacaoAprovacaoStrategy`.

### 4.2 Strategies concretas (v1)
- **`WmsAprovacaoStrategy`** — unidade = movimento (`wms_movimentos`). Absorve a lógica do atual
  `RevisarMovimentosLaboratorio` (`listar`/`alternar`/`alternarTodos`) usando `WmsMovimentoSonda`
  (`listarParaRevisao`, `alternarAprovacao`, `alternarAprovacaoTodos`, `contarPendentesNovos`,
  `commitAprovacao`). `commit` → `commitAprovacao(missaoId)` + `concluir=true`. (Constelacoes→Sonda do
  sinal: mesma direção de dependência já usada por `ErpAlocacaoWmsHookStrategy`.)
- **`SeparacaoAprovacaoStrategy`** — unidade = item (`missao_logistica_itens` em `status='concluido'`).
  `listar` retorna itens concluídos com `ligado = (aprovacao_gestor != 'rejeitado')`. `alternar`/
  `alternarTodos` gravam `aprovacao_gestor` ('aprovado'/'rejeitado') via model (`update()`, **sem raw
  SQL**). `commit` aplica D3.

### 4.3 Dado
- **Migration** `Constelacoes/Logistica/MissaoLogistica/migrations/2026_06_26_add_aprovacao_gestor_itens.sql`:
  `ALTER TABLE missao_logistica_itens ADD COLUMN aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT 'aprovado';`
  (default não quebra missões existentes). Idempotente (`IF NOT EXISTS`) seguindo o padrão das migrations
  do diretório.
- **Enum compartilhado** `Constelacoes/Logistica/MissaoLogistica/Enums/AprovacaoGestor.php`
  (`APROVADO`/`PENDENTE`/`REJEITADO`, valores idênticos ao do WMS). O enum do WMS
  (`AlocacaoWms/Enums/AprovacaoGestor.php`) **permanece** para não mexer nos testes verdes; os valores
  string coincidem. *Nota:* consolidar os dois num só fica como dívida futura (fora de escopo).

### 4.4 Lab genérico (dados/toggles da UI)
- **`Designacoes/Laboratorios/RevisarAprovacaoLaboratorio.php`** (`listar | alternar | alternarTodos`):
  resolve a strategy via `AprovacaoStrategyFactory::fromMissao()` e **delega**. O id da unidade é opaco
  (movimento ou item — a strategy sabe interpretar). Rotas:
  `GET/POST /labs/apis/Logistica/Designacoes/RevisarAprovacaoLaboratorio/{listar|alternar|alternarTodos}`.
  - `listar?missao_id=N` → `{unidades: [...]}` (chave neutra; cada strategy preenche suas colunas).
    O JS compartilhado lê sempre `unidades` (a seção WMS, que hoje lê `movimentos`, passa a ler `unidades`).
  - `alternar` POST `{unidade_id, ligado}`; `alternarTodos` POST `{missao_id, ligado}`.
- **`RevisarMovimentosLaboratorio` (WMS) é aposentado** — sua lógica passa para `WmsAprovacaoStrategy`.

### 4.5 Backend orquestrador — `Constelacoes/.../Satelites/MissoesAprovacao.php`
- Novo método genérico **`aprovarRevisao(array $data)`** (o nome `aprovarMissao` **já existe** no
  dispatcher → `aprovar()`, fluxo AJUSTE_DADOS; por isso o genérico é `aprovarRevisao`).
  `aprovarOperacaoWms` permanece no dispatcher como **alias fino** → `aprovarRevisao` (mantém os 13
  testes WMS verdes; a factory resolve WMS por tipo, comportamento idêntico). Fluxo de `aprovarRevisao`:
  1. valida `missao_id`, carrega missão, exige `status = aguardando_aprovacao`;
  2. `strategy = AprovacaoStrategyFactory::fromMissao($missao)`; se `null` → erro amigável;
  3. **snapshot guard**: `strategy->contarPendentesNovos(missaoId, snapshot_max_id)` (default -1 = off);
     se houver unidade nova → bloqueia e `chamarFuncao('aprRecarregar')` (recarrega a lista);
  4. `res = strategy->commit(missaoId, gestorId)`;
  5. se `res->concluir`: **CAS** `UPDATE ... SET status=CONCLUIDA WHERE id=? AND status=AGUARDANDO`
     (anti duplo-submit, mantém o fix atual) + `finalizarMissao(..., jaTransicionado=true)`;
  6. senão (reabriu p/ rework): grava `status=em_andamento` + extrato `MISSAO_RETOMADA`;
  7. resposta Galaxia: WMS → `removerElemento` da linha + carrega próxima (como hoje); Separação →
     recarrega o painel (reabriu ou concluiu).
- Mantém `rejeitar`/`acaoRejeitarItens` existentes intactos (usados por tipos sem strategy).

### 4.6 Superfície no painel — `Designacoes/html/designacao_painel.php` + `.js`
- Controller calcula: `$temAprovacao = AprovacaoStrategyFactory::fromMissao($missao) !== null` e
  `$superficie` ∈ {`secao` (WMS, sem tabela de itens), `coluna` (tipos com tabela de itens)}.
- **WMS** (`secao`): mantém `forms/wms_movimentos_secao.php`, **refatorada** para apontar para o lab e
  ação genéricos (`RevisarAprovacaoLaboratorio` + `aprovarMissao`). Visual idêntico.
- **Separação** (`coluna`): quando `aguardando_aprovacao` **e** `$temAprovacao`, cada item concluído
  ganha um switch **"Aprovado"** na sua **célula de Ações** (`.item-actions`, presente nas 2 vias de
  render — PHP e JS load-more), e abaixo da tabela aparece o rodapé
  **Confirmar/Descartar todos** + **Aprovar missão**. Os botões antigos **Aprovar/Rejeitar** do header
  (e os controles que não se aplicam — "Ver Itens", select "Normal/Invertido (LIFO)") ficam **ocultos
  para tipos com strategy** (resolve de quebra o Minor da review anterior). Tipos **sem** strategy: tudo
  como hoje.
- **JS compartilhado**: um único controlador (toggle individual, batch "todos", snapshot scoping,
  resync na falha via `carregar()`, `window.aprRecarregar`, "Aprovar missão" com `{snapshot_max_id}`),
  parametrizado pelo `missao_id` e pela base de rota do lab — usado pelas duas superfícies (DRY).
  Reaproveita a lógica de escape XSS, `carregado`-flag (Enviar desabilitado até carregar) e
  `pendingWrites` já presentes em `wms_movimentos_secao.php`.

## 5. Estados / colunas do switch

- `missao_logistica_itens.aprovacao_gestor`: `aprovado` (switch ON, default) / `rejeitado` (OFF).
  `pendente` não é usado para itens (default já é "aprovado"; não há gate prévio como no WMS).
- **Aprováveis** na separação = itens com `status='concluido'`. Itens em outro status (`pendente`,
  `em_andamento`, `pulado`, `removido`, `ocorrencia`) **não** recebem switch (célula vazia/disabled).
- `wms_movimentos.aprovacao_gestor`: inalterado (pendente/aprovado/rejeitado, como hoje).

## 6. Não-regressão (invariantes)

- WMS: fluxo, gates de integração, snapshot scoping, CAS e conclusão **idênticos** → os 13 testes WMS
  existentes seguem verdes (ajuste só de imports/rota se o nome da ação mudar).
- Tipos **sem** strategy (conferência, pesagem, retorno, etc.): painel inalterado — sem coluna, sem
  seção, botões Aprovar/Rejeitar antigos intactos.
- Migration com default `'aprovado'` → missões/itens pré-existentes mantêm o comportamento de envio.
- `acaoRejeitarItens`/`rejeitar` continuam disponíveis para tipos sem strategy.

## 7. Plano de testes

- **WMS** (regressão): suíte existente (13 testes) verde após o refator para strategy.
- **Strategy de Separação** (novos):
  - `commit` com **todos ligados** → missão CONCLUÍDA + envio disparado (`concluir=true`).
  - `commit` com **algum desligado** → desligados voltam a `pendente`, missão `em_andamento`,
    `aprovacao_gestor` dos reabertos resetado, **nada enviado** (`concluir=false`), extrato `MISSAO_RETOMADA`.
  - `alternar`/`alternarTodos` gravam `aprovacao_gestor` corretamente (via model, sem raw SQL).
  - `contarPendentesNovos` retorna 0 (snapshot no-op em separação).
- **Factory**: `fromMissao` resolve WMS→WmsAprovacaoStrategy, SEPARACAO→SeparacaoAprovacaoStrategy,
  tipo sem registro → `null`.
- **Lab genérico**: `listar/alternar/alternarTodos` delegam à strategy correta por `missao_id`.
- **View/controller** (`php -l` + smoke no browser): painel de separação em `aguardando_aprovacao`
  mostra a coluna de switch + rodapé; aprovar com tudo on conclui+envia; com algum off reabre só os off;
  painel de tipo sem strategy inalterado; painel WMS idêntico.
- **i18n**: strings novas via `show('texto PT')` nas 3 línguas (auto-harvest + conferir no browser).
  Unificar `Carregando…` (uma chave md5 só) ao mover o JS para o controlador compartilhado.

## 8. Fora de escopo

- Strategies de aprovação por-item para os demais tipos (conferência, pesagem, carregamento, retorno,
  inventário…) — entram depois, uma a uma, registrando no factory.
- Consolidar os dois enums `AprovacaoGestor` (WMS + compartilhado) num só.
- Atualização ao vivo (socket/polling) das unidades durante a aprovação — carrega no open + refresh
  manual (mantém o comportamento atual do WMS).
- Mudanças no fluxo de integração/hooks de qualquer tipo (só se decide **o que** entra no envio).
- Envio parcial de pedido na separação (D3 escolheu rework, sem envio parcial).
