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

> Data: 2026-06-28 · Feature: operacao-wms (sub-feature / generalização) · Tipo: spec de design (brainstorming)
> Pré-requisitos: a aprovação por movimento WMS (`aprovacao_gestor` em `wms_movimentos`, seção inline
> `wms_movimentos_secao.php`, `RevisarMovimentosLaboratorio`, `MissoesAprovacao::aprovarOperacaoWms` com
> snapshot scoping) já está implementada — ver `14-aprovacao-movimento-wms-spec.md` e
> `15-aprovacao-painel-inline-spec.md`. **Esta spec generaliza** essa UX para qualquer missão logística e
> realoca o caso WMS para dentro do novo frame como a primeira strategy concreta.

## 1. Problema / intenção

Hoje "aprovar/reprovar" significa coisas diferentes em cada mundo:

- **Missão genérica** (separação, conferência, etc.): itens em `missao_logistica_itens` com `status`
  (pendente/concluído/…). A aprovação é **binária no nível da missão** — o gestor ou **aprova tudo**
  (`Aprovar` → missão 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** (default ligado); ao "Aprovar missão" só os ligados vão à integração; os desligados
  viram `cancelada` (descartados).

O usuário quer **um switch único de aprovado/reprovado por-item no layout de qualquer missão logística**,
**adaptado/strategiado** para que o caso WMS seja só mais uma implementação do mesmo mecanismo. O switch é
sempre o mesmo idioma (um por unidade, default ligado = vai pro destino final); **o que "desligado" faz é
decidido por uma Strategy por tipo de missão**.

## 2. Decisões tomadas (brainstorming)

| # | Decisão | Escolha |
|---|---------|---------|
| D1 | Semântica do "reprovado" | **A strategy decide por tipo.** Switch genérico (ligado = vai pro destino; desligado = não vai); o efeito do desligado é strategiado. |
| D2 | Arquitetura | **Frame genérico + strategies por tipo** (Interface + Factory/Registry, padrão V2). Painel genérico pergunta à strategy o que renderizar/commitar. |
| D3 | Escopo v1 | **WMS + Separação.** Frame genérico em todas; duas strategies concretas funcionando. Demais tipos seguem o comportamento atual até ganharem strategy. |
| D4 | Commit da Separação | **Reabre só os desligados (rework, sem envio parcial).** Itens desligados → `pendente`, missão reabre (`em_andamento`) só pra refazer esses; ligados ficam `concluido`; nada é enviado até 100% aprovado. |
| D5 | Commit do WMS | Inalterado: pendente→aprovado (integra) / rejeitado→cancelada (descarta); **sempre conclui** a missão. |
| D6 | Persistência do toggle | Coluna **`aprovacao_gestor`** em `missao_logistica_itens` (espelha o WMS), **ortogonal** ao `status`. Toggle persiste ao vivo (igual WMS). |
| D7 | Sites de render | **2 sites, 1 backend.** WMS = seção dedicada de movimentos (já existe). Separação = **coluna de switch na tabela de itens existente** (preserva filtros/badges/ocorrências). |
| D8 | Binários antigos | Os botões **"Aprovar"/"Rejeitar"** do header da separação **saem** — o switch + "Aprovar missão" os substituem. "Ver Itens"/LIFO ficam só onde fazem sentido. |
| D9 | Endpoints | `listar \| alternar \| alternarTodos` num **Laboratório genérico** strategy-aware (`AprovacaoMissaoLaboratorio`, substitui `RevisarMovimentosLaboratorio`); `aprovar` generalizado no satélite **`MissoesAprovacao`**. |
| D10 | Reset imediato por-item | **JÁ EXISTE — reusar.** O botão "Reiniciar" do painel já chama `MissoesItensEdicao::voltarItemPendente`, que faz exatamente o reset imediato (item→`pendente`, limpa dados de execução, reabre a missão p/ `em_andamento`, extratos `ITEM_REINICIADO`/`MISSAO_REABERTA`). **Sem `resetar` novo nem interface segregada**; só garantir que o botão apareça no contexto de aprovação da separação (relabel opcional "Refazer este item"). WMS não tem (o switch já descarta). |

## 3. Arquitetura

### 3.1 Contrato (backend) — `Strategies/Aprovacao/`

Em `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/Aprovacao/` (ao lado das `*HookStrategy`
que já moram lá):

**`AprovacaoStrategyInterface`**
- `colunas(): array` — cabeçalhos da tabela (cada tipo define os seus; ex.: WMS = produto/endereço/qtd/quando;
  separação = produto/conf-total/quando).
- `listarUnidades(object $missao): array` — linhas no shape genérico
  `[ { id:int, ligado:bool, celulas: { <coluna>: string } }, … ]`. A view é burra; a strategy formata.
- `alternar(int $unidadeId, bool $ligado, object $missao): array` — `{ ok:bool, id:int, ligado:bool }`.
- `alternarTodos(int $missaoId, bool $ligado): array` — `{ ok:bool, afetados:int }`.
- `contarPendentesNovos(int $missaoId, int $snapshotMaxId): int` — snapshot scoping (ids sequenciais).
- `commit(object $missao): AprovacaoResultado` — aplica a mudança **por-unidade** e devolve o desfecho.

**`AprovacaoStrategyFactory`**
- `criar(object $missao): ?AprovacaoStrategyInterface` — resolve por `tipo_missao_id` via registry estática
  (mesmo padrão de `OperacaoAdapterFactory`). **Retorna `null`** se o tipo não tem strategy → o painel cai no
  comportamento atual (sem switch, tabela de itens normal, binário antigo).
- Registry inicial: `[ TipoMissao::ALOCACAO_WMS->id() => WmsAprovacaoStrategy::class,
  TipoMissao::SEPARACAO->id() => SeparacaoAprovacaoStrategy::class ]` (ids reais confirmados na implementação).

**`AprovacaoResultado`** — value object readonly:
- `desfecho: 'concluir' | 'reabrir'`
- `aprovados: int`, `reprovados: int`
- `mensagem: string`

**Reset imediato por-item (D10): reusa o existente.** Não há método/interface novos. O botão "Reiniciar"
do painel (`MissoesItensEdicao::voltarItemPendente`) já faz o reset imediato e reabre a missão. A strategy
de separação não precisa expor reset; só o painel garante que o botão apareça no contexto de aprovação.

### 3.2 As duas strategies

**`WmsAprovacaoStrategy`** (refatora a feature 15, comportamento idêntico):
- unidade = `wms_movimentos`; envolve o `WmsMovimentoSonda` existente
  (`listarParaRevisao`, `alternarAprovacao`, `alternarAprovacaoTodos`, `commitAprovacao`,
  `contarPendentesNovos`).
- `commit` → `commitAprovacao` (pendente→aprovado / rejeitado→cancelada); **sempre** `desfecho='concluir'`.

**`SeparacaoAprovacaoStrategy`** (nova):
- unidade = `missao_logistica_itens` com `status='concluido'`. Estado lido de `aprovacao_gestor`
  (NULL/`pendente` = ligado; `rejeitado` = desligado).
- `alternar`/`alternarTodos` → UPDATE de `aprovacao_gestor` nos itens (guard: só itens `concluido`).
- `contarPendentesNovos` → itens `concluido` com `id > snapshot` e ainda não decididos.
- `commit`:
  - **todos ligados** → `desfecho='concluir'` (segue o `finalizarMissao`/hooks/geração de carregamento atuais).
  - **algum desligado** → desligados voltam pra `pendente` (reusa `voltarItensParaPendente`), ligados ficam
    `concluido`, **nada enviado**, `desfecho='reabrir'`.
- **Reset imediato (D10): nada novo na strategy** — o botão "Reiniciar" do painel já cobre (`voltarItemPendente`).

### 3.3 Fluxo do "Aprovar missão" (genérico, no satélite `MissoesAprovacao`)

1. Valida `missao_id`; carrega missão; checa `status = aguardando_aprovacao` e permissão de gestor logístico.
2. Resolve `strategy = AprovacaoStrategyFactory::criar($missao)`; se `null`, erro defensivo (tipo sem strategy
   não deveria chegar aqui).
3. **Snapshot guard**: se `strategy->contarPendentesNovos(missaoId, snapshot_max_id) > 0` → BLOQUEIA e recarrega
   a lista no front via `chamarFuncao('rmwRecarregar')` (mecanismo atual do WMS, generalizado).
4. `resultado = strategy->commit($missao)`.
5. Aplica o `desfecho` (parte transversal, única):
   - `'concluir'` → **CAS atômico** `UPDATE … SET status=CONCLUIDA WHERE id=? AND status=AGUARDANDO_APROVACAO`
     (anti double-submit; rowCount==0 ⇒ "já foi concluída"); depois `finalizarMissao(…, jaTransicionado=true)`;
     `registrarExtrato(MISSAO_CONCLUIDA, {aprovados, reprovados})`.
   - `'reabrir'` → `UPDATE … SET status=EM_ANDAMENTO WHERE id=? AND status=AGUARDANDO_APROVACAO`;
     `registrarExtrato(MISSAO_RETOMADA, {reprovados})`.
6. Resposta Galaxia (remove a linha da lista / atualiza painel).

## 4. Banco

Migration versionada adicionando à `missao_logistica_itens`:

```sql
ALTER TABLE missao_logistica_itens
  ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NULL;
```

- **Ortogonal ao `status`**: o switch mexe só em `aprovacao_gestor`; o `status` operacional só muda no `commit`.
- **Sem `DEFAULT`** fixo; `NULL` = ainda não decidido = ligado (default ON). Valores: `pendente` (ligado),
  `rejeitado` (desligado). (Não há `aprovado` persistido em item — a conclusão é representada pelo `status`.)
- Colunas `block_*` não se aplicam (a tabela já existe e é multi-tenant pelos seus próprios campos).
- Cron/gate de integração WMS **não muda** (continua lendo `wms_movimentos.aprovacao_gestor='aprovado'`).

## 5. Frontend

A diferença entre os tipos é só **onde** o switch aparece; o backend e o fluxo de aprovar são únicos.

- **WMS**: mantém a seção dedicada `forms/wms_movimentos_secao.php`, repontada pros endpoints genéricos do
  `MissoesAprovacao` (em vez do `RevisarMovimentosLaboratorio`).
- **Separação**: ganha uma **coluna de switch na tabela de itens que já existe** no `designacao_painel.php`
  (preserva filtros, busca, badges de status, ocorrências — o gestor precisa disso pra decidir), + o **rodapé
  compartilhado "Aprovar missão"**. O **reset imediato (D10)** é o **botão "Reiniciar" que já existe** na
  coluna de Ações de cada item (relabel opcional "Refazer este item"); após o reset a missão vai pra
  `em_andamento` (switches voltam a read-only/ocultos) e as decisões de switch dos demais itens persistem
  em `aprovacao_gestor`.
- **Módulo JS compartilhado** (`forms/aprovacao.js` ou helper no JS do painel) com toggle/batch/snapshot/resync/
  `rmwRecarregar`/aprovar, parametrizado por seletores + endpoint. Os dois sites consomem o mesmo módulo.
- **Binários antigos** (`Aprovar`/`Rejeitar` no header da separação) **saem**; o switch + "Aprovar missão" os
  substituem. "Ver Itens"/LIFO/seletor de ordenação ficam só onde fazem sentido (resolve de quebra o minor #1
  da feature 15: controles de item não vazam pro WMS).
- **Visibilidade por estado** (regra D2 do feature 15, uniforme): switch **editável só em
  `aguardando_aprovacao`**; **read-only** em `concluido/cancelado` (histórico). Na separação, durante
  `em_andamento` a coluna de switch fica **oculta** pra não poluir a tabela ao vivo (no WMS a seção dedicada
  já segue a regra read-only do feature 15).
- **XSS / robustez**: o módulo JS preserva os fixes do code-review do feature 14/15 (escape de texto, fail-open
  desabilitando "Aprovar" até carregar, resync na falha, batch).

## 6. Endpoints

Dois mecanismos (espelham o que já existe no WMS): JSON leve via **Laboratório** pra listar/alternar; ação
Galaxia via **satélite** pra aprovar. Ambos strategy-aware (resolvem a strategy pela missão).

**Laboratório genérico `AprovacaoMissaoLaboratorio`** (em `Designacoes/Laboratorios/`, substitui
`RevisarMovimentosLaboratorio`) — `/labs/apis/Logistica/Designacoes/AprovacaoMissaoLaboratorio/<metodo>`:

| Método | Entrada | Saída | Delegação |
|--------|---------|-------|-----------|
| `listar` | `missao_id` | `{ ok, colunas, movimentos:[{id,ligado,celulas}] }` | `strategy->colunas()` + `strategy->listarUnidades()` (usado pelo WMS; separação renderiza inline) |
| `alternar` | `missao_id, unidade_id, ligado` | `{ ok, id, ligado }` | `strategy->alternar()` |
| `alternarTodos` | `missao_id, ligado` | `{ ok, afetados }` | `strategy->alternarTodos()` |

**Satélite `MissoesAprovacao`** — ação Galaxia `acaoElemento`:

| Opção | Entrada | Saída | Delegação |
|-------|---------|-------|-----------|
| `aprovar` | `missao_id, snapshot_max_id` | resposta Galaxia | resolve strategy; snapshot guard + `strategy->commit()` + aplica desfecho (concluir/reabrir) |

- `aprovar` é generalizado: se a missão tem strategy → fluxo novo; senão → comportamento atual (aprovação
  binária existente). `aprovarOperacaoWms` é mantido como **alias** de `aprovar` (retrocompat dos testes).
- `RevisarMovimentosLaboratorio` é **aposentado** (lógica vai pro Lab genérico + `WmsAprovacaoStrategy`).
  Tudo uncommitted ⇒ sem migração de rota externa.
- Reset imediato (D10): **sem endpoint novo** — usa o `MissoesItensEdicao::voltarItemPendente` já existente.

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

- A suíte WMS existente (13 testes / 159 asserts) deve seguir **verde** após o refactor — a `WmsAprovacaoStrategy`
  só embrulha as mesmas chamadas do `WmsMovimentoSonda`; gates de integração, snapshot scoping, CAS e conclusão
  permanecem idênticos.
- Missões **sem strategy** (tipo não wired): painel e fluxo de aprovação **inalterados** (tabela de itens +
  binário antigo). A Factory retornando `null` é o gate.
- Cron de integração WMS inalterado.
- Aprovação continua **só para gestor logístico**, só em `aguardando_aprovacao`.

## 8. Plano de testes

- **Reúso**: suíte WMS (13 testes) verde após refactor.
- **Factory**: `AprovacaoStrategyFactory::criar()` resolve `WmsAprovacaoStrategy` para WMS,
  `SeparacaoAprovacaoStrategy` para separação, `null` para tipo sem strategy.
- **`SeparacaoAprovacaoStrategy::commit`**:
  - todos ligados → `desfecho='concluir'`, missão CONCLUÍDA, itens seguem `concluido`.
  - algum desligado → `desfecho='reabrir'`, itens off → `pendente`, itens on ficam `concluido`,
    missão → `em_andamento`.
- **Snapshot guard genérico**: chega item `concluido` novo (id > snapshot) ⇒ `aprovar` bloqueia e recarrega.
- **Reset imediato (D10)**: coberto pelo comportamento existente de `voltarItemPendente` (já testado/validado);
  o painel só passa a expor o botão no contexto de aprovação — validado por smoke no browser.
- **View/controller** (validados por `php -l` + smoke no browser): coluna de switch na tabela de itens da
  separação em `aguardando_aprovacao`; seção WMS repontada; binários antigos removidos; tipo sem strategy
  intacto.
- **i18n**: strings novas via `show('texto PT')` nas 3 línguas (auto-harvest + conferir no browser).

## 9. Fora de escopo

- Strategies dos demais tipos logísticos (conferência, pesagem, carregamento, retorno) — entram depois, uma a
  uma, sem tocar no frame.
- Live-update por socket/polling dos itens/movimentos durante `em_andamento`.
- Envio parcial / partial-ship na separação (decidido: rework, sem envio parcial — D4).
- Mudanças no fluxo de integração backend do WMS (cron/hooks/gates) — inalterado.
