---
name: antigravity-pedido
description: "Use esta skill em qualquer tarefa do produto Pedido (COMEX) — lista, formulário, edição em massa, consolidação, transferência, banco do Pedido. Define regras de negócio do produto, convenções específicas (campos @@unique, cascade Pedido→Item) e anti-padrões já erradicados."
---

# Gravity — Pedido (COMEX)

## O Que é o Pedido

Produto que gerencia ordens de compra/venda internacional (COMEX) com hierarquia
**Pedido → Itens**, suporte a importação e exportação, e ciclo de vida completo:
rascunho → aberto → consolidado/transferido → cancelado.

**Características-chave:**
- Hierarquia 1:N (Pedido tem N PedidoItem)
- Multi-tenant por `id_organizacao` (Mand. 04)
- DDD-puro em todas as camadas (banco/back/front) — sem ACL legado
- Cascade automático Pedido→Item em campos específicos (aba Combinado)
- 57 pares diretos Pedido→Item (SSOT em `shared/mapaPropagacaoPedidoItem.ts`) + 4 exclusivos da edição em massa

---

## Localização na Arquitetura

```text
servicos-global/produto/pedido/
├── shared/
│   ├── campos-pedido-ddd.ts           ← dicionário DDD
│   ├── camposEdicaoMassa.ts           ← SSOT edição em massa (blocklist + derivação)
│   ├── mapaPropagacaoPedidoItem.ts    ← cascade Pedido→Item
│   └── visaoGeralResumoAggregate.ts   ← agregação Insights (server + unit)
├── client/src/
│   ├── App.tsx                        ← rotas irmãs → <PedidosMultiView />
│   ├── components/
│   │   ├── PedidosVisualizacaoLayout.tsx
│   │   ├── PedidosVisualizacaoTabs.tsx
│   │   ├── PedidosMultiView.tsx       ← keep-alive 4 visualizações
│   │   ├── pedidos-visualizacao-context.tsx
│   │   └── lista/ … modais …
│   ├── pages/
│   │   ├── PedidosVisaoGeral.tsx      ← Insights (lazy no MultiView)
│   │   ├── Pedidos.tsx, PedidosDashboard.tsx, PedidosKanban.tsx
│   │   └── Configuracoes.tsx
│   └── shared/
│       ├── pedidos-prefetch.ts
│       ├── visao-geral-schemas.ts
│       └── api.ts                     ← pedidoVisaoGeralApi.agregado
└── server/src/routes/
    └── visao-geral-agregado.ts        ← GET …/visao-geral/agregado
```

**Seletor universal (pills):** [seletor-universal-visualizacoes.md](../../../documentos-tecnicos/arquitetura/seletor-universal-visualizacoes.md) · API agregada: [VISAO-GERAL-AGREGADO.md](../../../documentos-tecnicos/produtos-gravity/pedido/VISAO-GERAL-AGREGADO.md) (incl. § KPIs fixos do topo) · Testes: `TST-*-MBOTO-*`

> **Config KPIs topo:** aba `dashboard-kpi` removida (TASK-000325). Mapeamento fixo em `useDashboardTopKpiStatus.ts` — ver doc acima.

> **GABI Insights (Fases 1–3):** SSOT [GABI-INSIGHTS-PERSONALIZADOS.md](../../../documentos-tecnicos/produtos-gravity/gabi/GABI-INSIGHTS-PERSONALIZADOS.md). Fase 2: `useTrackBehavior` → `POST /api/v1/pedidos/eventos-comportamento`; migration `user_behavior_events` (`20260628130000`). Fase 3: `GABI_INSIGHTS_LLM=true` opt-in; logs com `tenantId`/`motivo`. Runbook: [GABI-RUNBOOK-OPS.md](../../../documentos-tecnicos/produtos-gravity/gabi/GABI-RUNBOOK-OPS.md).

---

## Regras Absolutas (Referências SSOT)

> ⚠️ **Esta skill NÃO redefine regras absolutas. Apenas referencia.**

| Regra | Onde mora |
|-------|-----------|
| Schema intocável (`fragment.prisma` → script `compose-pedido-schema.ts`) | [Mand. 02](../../governanca/lei/9-mandamentos/SKILL.md) |
| Nomenclatura DDD (`id_pedido`, `tipo_operacao_pedido`, `id_organizacao`) | [ddd-nomenclatura](../../governanca/lei/ddd-nomenclatura/SKILL.md) |
| Frontend label canonical PT-BR via `rotulo`, não `t('key')` | [ddd-nomenclatura REGRA 9](../../governanca/lei/ddd-nomenclatura/SKILL.md) |
| Isolamento de organização via `withOrganizacao` | [isolamento-organizacao](../../governanca/lei/isolamento-organizacao/SKILL.md) |
| Sem fallback silencioso em DEV (mock que mascara API real) | [Mand. 08](../../governanca/lei/9-mandamentos/SKILL.md) |
| Zod = contrato bilateral (back valida = front parseia) | [Mand. 06 + 09](../../governanca/lei/9-mandamentos/SKILL.md) |

---

## Parte 1 — Edição em Massa

> Doc completo: [`documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-TECNICO.md)
> Regras de negócio: [`EDICAO-EM-MASSA-REGRAS-NEGOCIO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-REGRAS-NEGOCIO.md)

### Níveis de edição (3 abas)

| Aba | O que faz | Cascade automático? |
|-----|-----------|---------------------|
| **Combinado** (default) | Edita Pedido + Item com cascade automático em pares mapeados | ✅ 61 pares |
| **Pedido** | Edita só Pedido — itens permanecem | ❌ |
| **Item** | Edita só PedidoItem dos pedidos selecionados | ❌ |

### Cascade Pedido → Item — composição SSOT (~61 pares)

**SSOT:** `shared/mapaPropagacaoPedidoItem.ts` → `MAPA_PROPAGACAO_PEDIDO_ITEM` (57 pares diretos).
**Composição:** `edicaoEmMassaService.ts` importa o SSOT + 4 pares exclusivos da edição em massa:

- **SSOT (57):** identidade comercial, casas decimais, câmbio, referências, 35 datas (rascunho/proforma/invoice/consolidação/transferência), pronto/inspeção/coleta
- **Exclusivos massa (4):** `tipo_operacao_pedido→tipo_operacao_item` + 3× JSON `nome_*→nome_*_item`

**Regra:** campo item explícito vence sobre cascade do mesmo destino.

### SSOT campos editáveis — paridade lista ↔ massa

**SSOT:** `shared/camposEdicaoMassa.ts` — deriva `CAMPOS_EDICAO_MASSA_PEDIDO` e `CAMPOS_EDICAO_MASSA_ITEM` de `campos-pedido-ddd.ts` com blocklist única. Inclui colunas do usuário (EAV) via prefixo `coluna_usuario:<id_coluna_usuario_pedido>`.

**Regra:** editável na lista ⇒ editável em massa. Não manter listas paralelas no modal ou no service.

**Teste EMT:** `TST-EMT-EDICAO-EM-MASSA-LISTA-PEDIDO-000112` · pacote 5 tipos `000108`–`000112` · drift: `drift-lista-massa.test.ts` · Local `:8000`

**Datas no modal:** usar `CampoCalendarioGlobal` + `dateToIso()` — ver [`EDICAO-EM-MASSA-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-TECNICO.md) §Frontend.

### Campos `@@unique` — convenção crítica

Campos com `@@unique` no schema **não podem** ser editados em massa via `substituir` com >1 pedido (geraria P2002).

**Hoje exposto:** `numero_pedido` (em `Pedido.@@unique([id_organizacao, numero_pedido])`).

**Defesa em 3 camadas:**

1. **Frontend** — Set `CAMPOS_UNIQUE` em `ModalPedidosEdicaoMassa.tsx`:
   - Input `disabled` + tooltip + badge quando multi-seleção
   - Botão "Revisar alterações" desabilitado
2. **Backend Zod** — Set espelhado `CAMPOS_UNIQUE_PEDIDO` em `edicoes-em-massa-pedido.ts` + `superRefine`
3. **Backend try/catch P2002** — fast path `updateMany` envolvido, converte em `AppError 422 UNIQUE_VIOLATION`

**Convenção ao expor novo campo `@@unique` no SSOT (`camposEdicaoMassa.ts`):**
- Adicionar a `CAMPOS_UNIQUE` no frontend (modal)
- Adicionar a `CAMPOS_UNIQUE_PEDIDO` no backend Zod
- Sem isso, retorna 500 e ponto cego para o usuário

### Tipos mistos (importação + exportação)

**Padrão Pedido:** AVISAR e permitir (não bloquear). Coerente com Transferir.
- Banner azul no topo do Passo 1: "Pedidos de tipos diferentes selecionados"
- Banner laranja reforçado no Passo 2: "Atenção — tipos de operação diferentes"

**Não confundir** com Consolidar — esse BLOQUEIA (banner vermelho + botão disabled). Operações diferentes têm padrões diferentes.

### Render por tipo de campo

Tabela detalhada (texto/numero/data/select/ncm/usuario) vive em [`EDICAO-EM-MASSA-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-TECNICO.md) → seção "Frontend — Modal". Esta skill referencia para evitar dívida de manutenção quando a Fase 2 (moeda/unidade/decimal) for implementada.

### Auto-fill ao trocar `tipo_operacao_pedido`

Quando o usuário altera `tipo_operacao_pedido` em massa, o sistema preenche automaticamente o **lado nacional** (importador em IMP, exportador em EXP) com **nome + CNPJ do Workspace** de cada pedido. Não usa Empresa-da-Org do Cadastros.

Regras:
- Cada pedido pega o seu próprio workspace (`pedido.id_workspace`)
- Backend faz 1 chamada S2S batch ao Configurador (`GET /api/v1/internal/workspaces?ids=...`)
- Lado oposto é limpo (nome/CNPJ do tipo antigo viram NULL)
- Cascade auto-fill para `nome_*_item` nos itens
- Edição manual de `nome_exportador`/`nome_importador`/`cnpj_*` no mesmo batch vence sobre auto-fill
- Workspace sem CNPJ → avisa+permite (não bloqueia)
- Status crítico (≠ rascunho/aberto) → banner laranja preventivo
- Configurador offline → AppError 503 (Mand. 08)

Doc completo: [`EDICAO-EM-MASSA-TECNICO.md` §Auto-fill](../../../documentos-tecnicos/produtos-gravity/pedido/EDICAO-EM-MASSA-TECNICO.md).

### Dívidas arquiteturais sinalizadas

| Dívida | Razão |
|--------|-------|
| `ModalPedidoNovo` continua usando `obterEmpresaDaOrganizacao` | Refactor da criação de pedido (próxima entrega) |
| `Organizacao.suid_empresa_organizacao` desnecessário | Modelo Workspace=empresa supera esse conceito |
| Snapshots `PedidoSnapshotEmpresa` capturam empresa-da-org | Refactor do snapshot system |
| Visualização cross-workspace na Lista (Master vê todos) | Entrega arquitetural transversal — afeta todos os produtos |

---

## Parte 2 — Lista de Pedidos

> Regras de negócio: [`LISTA-EDITAR-SALVAR-REGRAS-NEGOCIO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/LISTA-EDITAR-SALVAR-REGRAS-NEGOCIO.md) (§0 tooltips + colunas especiais)  
> Técnico: [`LISTA-EDITAR-SALVAR-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/LISTA-EDITAR-SALVAR-TECNICO.md) (§6 tooltips — arquitetura)  
> Checkbox genérico: [`REPLICAR-PAI-EM-ITENS-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/REPLICAR-PAI-EM-ITENS-TECNICO.md)

### Infraestrutura

- `Pedidos.tsx` + `TabelaVirtualGlobal` — 99 colunas pai, 165 colunas filho
- `ColunasPai.tsx` / `ColunasFilho.tsx` — catálogo; `renderAgregado()` = valor + `⚠` divergência
- SSOT comportamento: `columnBehaviorConfig.ts`, `columnAlertConfig.ts`, `pedidoDivergencias.ts`
- **Tooltips de coluna:** `TooltipRegrasColuna.tsx`, `buildTooltipRegraLista.tsx` (`isLinhaItemLista`, `tooltipNivelCelula`), `pillsTooltipColunaLista.ts`, núcleo `tooltipCelulaResolver.ts` — SSOT único pedido/item para título e pills; ver doc §6 e `/tooltip-pedido`

### Colunas com regra especial (editar-salvar inline)

| Coluna | Pedido | Item | Checkbox replicar | Alerta divergência |
|--------|--------|------|-------------------|-------------------|
| **TIPO DE OPERAÇÃO** | Editável | Travado | ❌ (replica sempre) | ❌ |
| **STATUS** | Editável | Editável (UI) | ✅ | ✅ (`status_divergente`) |
| **Nº pedido / Part Number** | `numero_pedido` | `part_number` | N/A | PN duplicado no pedido |

**TIPO DE OPERAÇÃO:** `replicar_em_itens` forçado `true` em `handleEditar`; item sem popover (`ITEM_EDITAVEL_OVERRIDE.tipo_operacao = false`).

**STATUS:** regras 00–04 do dono; ghost `status_itens_snapshot` para alerta sem expandir; edição no item só em `_p.status` (persistência API = dívida P0).

**Hooks:** `statusOpts` e `pedidos` declarados **antes** de `mapaColunasFilho` / `colunasComUsuario` (TDZ).

### Busca da Lista (2026-06-10)

- Fonte única do WHERE: `processos-core/src/services/filtro-busca-pedido.ts` (`montarCondicoesBuscaPedido`) — consumida por `GET /pedidos`, `GET /pedidos/lista/kpis` e `GET /pedidos/inicializacao`. **Nunca** reimplemente a condição de busca em uma rota.
- Cobre campos fixos + `PedidoSnapshotEmpresa.nome_empresa` + **colunas do usuário por nome e conteúdo** (respeitando visibilidade `todos`/`roles`/`privado`).
- O filtro client-side de `Pedidos.tsx` espelha as mesmas condições sobre `_colunas_usuario` (Mand. 07 — alterar um lado exige alterar o outro).
- Detalhes: [`COLUNAS-USUARIO-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/COLUNAS-USUARIO-TECNICO.md) §Busca da Lista.

### + Novo → Smart Docs (TASK-000408)

> SSOT cross-produto: [SMART-READ-CRIAR-PEDIDO-TECNICO.md](../../../documentos-tecnicos/produtos-gravity/smart-read/SMART-READ-CRIAR-PEDIDO-TECNICO.md)

| Aspecto | Regra |
|---------|-------|
| UI | `BarraAcoesPedido.tsx` + menu inline `Pedidos.tsx`; flag `PRODUCT_CONFIG.features.smart_read` |
| Redirect | `/smart-read/lista?origem=pedido&acao=nova-leitura` |
| API Pedido | `POST /api/v1/pedidos/importacoes-smart-read/criar` (S2S; service `smartReadParaPedidoService`) |
| Histórico | `canal_criacao: smart_read` + `id_leitura` em `auditLog` |

---

## Parte 3 — Consolidar / Transferir / Outras Features

> A consolidar.

- **Consolidar:** BLOQUEIA mistura importação+exportação (regra de negócio)
- **Transferir:** AVISA mistura mas permite (cross-tenant possível)
- **Lista — Qtd. Transferida:** coluna somente leitura; `reducao_simples` incrementa `quantidade_cancelada_item` (não transferida) — ver `LISTA-EDITAR-SALVAR-REGRAS-NEGOCIO.md` §8C e `TRANSFERIR-REGRAS-NEGOCIO.md`
- Ambos usam `bulkSchemas.ts` — `detectarTiposMistos()` síncrono e `assertTiposHomogeneos()` (refinement Zod)

---

## Parte 4 — Duplicar Pedido + Item (modal misto)

> Aprovado por Coordenador + Líder Técnico em 2026-05-11. Implementação em `ModalPedidosDuplicar.tsx` + backend `duplicarExcluirService.ts`.

O botão **Duplicar** trata 3 cenários numa interface única:

| Seleção | Backend chama | Resultado |
|---|---|---|
| Só pedidos | `POST /duplicacoes/confirmar` (1×) | N pedidos novos com todos os itens via cascade. Usuário digita o `numero_pedido` de cada cópia |
| Só itens | `POST /duplicacoes/itens` (1× por pedido pai) | M itens duplicados dentro do(s) pedido(s) pai(s) — sequência logo abaixo do original |
| Misto (pedido + item) | Ambos em paralelo (`Promise.all`) | Toast consolidado: "N pedidos e M itens duplicados" |

### Regra de ordenação após duplicação

**Inviolável** — qualquer alteração na ordenação da Lista deve respeitar esta regra:

- **Pedido novo** (criado por duplicação) → **primeira linha da Lista**
  - Garantido pelo `orderBy: data_criacao_pedido DESC` do `GET /pedidos` (offset pagination, `processos-core/routes/pedidos.ts:643-650`)
  - Como `data_criacao_pedido DateTime @default(now())` é `now()` no INSERT, o duplicado nasce no topo

- **Item novo** (duplicado dentro do mesmo pedido) → **linha imediatamente abaixo do original**
  - Implementado via renumeração 1..N em `duplicarExcluirService.ts:duplicarItens`
  - Para cada item duplicado: original na posição X → cópia na X+1, e itens seguintes shiftam +1
  - Lista virtual de ordem é construída antes; depois um único UPDATE por item renumera

### Quantidades de execução SEMPRE zeradas no item duplicado

`quantidade_pronta_item`, `quantidade_transferida_item` e `quantidade_cancelada_item` viram `0` no item novo.

**Motivo:** essas 3 colunas representam **execução real** (transferências para embarque, marcações de pronto, cancelamentos). Copiar literalmente criaria **saldo fantasma** — o item novo apareceria com "50 transferidas" sem nenhum processo de embarque correspondente.

`quantidade_inicial_item` e `quantidade_atual_item` são copiadas (o item nasce íntegro, como se fosse novo do zero).

### Aviso pré-duplicação

Se algum item selecionado tem qualquer das 3 quantidades > 0, o modal mostra um banner amarelo antes do botão Duplicar explicando que esses campos serão zerados (transparência total com o usuário).

### Sincronização pai↔filhos na seleção

Como a `TabelaVirtualGlobal` agora sincroniza pais e filhos (ver skill `arquitetura/nucleo-global`), marcar o checkbox de um pedido marca todos os itens dele. **O modal deve filtrar duplicação dupla:**

```ts
const itensFiltrados = useMemo(
  () => itens.filter(it => !idsPedidosSelecionados.has(it.pedido_id)),
  [itens, idsPedidosSelecionados],
)
```

Itens cujo pai já está em `pedidos` ficam de fora do `duplicarItens` — o cascade do pedido já cria as cópias. Sem o filtro, cada item seria duplicado 2 vezes: uma vez no pedido novo (via cascade) e outra no pedido original (via `/duplicacoes/itens`).

---

## Parte 4.1 — Excluir Pedido + Item

> Regras de negócio: [`DUPLICAR-EXCLUIR-REGRAS-NEGOCIO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/DUPLICAR-EXCLUIR-REGRAS-NEGOCIO.md) · Técnico: [`DUPLICAR-EXCLUIR-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/DUPLICAR-EXCLUIR-TECNICO.md)

### Blacklist opt-out (2026-05-26)

Coluna DB `excluir_status_permitidos` guarda status **bloqueados** (nome legado — semântica invertida):

| Valor config | Comportamento |
|---|---|
| `[]` (default) | **Todos** os status permitidos — inclusive custom (`pagamento_aprovado`, etc.) |
| `['consolidado', ...]` | Apenas os listados são **bloqueados** |

- Backend: `normalizarStatusBloqueadosExclusao()` + `statusBloqueiaExclusao()` em `duplicarExcluirService.ts`
- UI Config: checkbox marcado = status bloqueado; whitelist legada (7 canônicos) migra para `[]`
- Rotas `/exclusoes/confirmar` e `/exclusoes/itens`: timeout 30s; renumeração paralela pós-delete de itens

### Endpoints

| Seleção | Backend |
|---|---|
| Preview pedidos | `POST /exclusoes/preview` |
| Hard delete pedidos | `POST /exclusoes/confirmar` |
| Delete itens avulsos | `POST /exclusoes/itens` |

---

## Parte 4.2 — Transferir — encoding de URL

> Técnico: [`TRANSFERIR-TECNICO.md`](../../../documentos-tecnicos/produtos-gravity/pedido/TRANSFERIR-TECNICO.md)

`pedidoTransferirApi` usa `pid(id) = encodeURIComponent(id)` em **todos** os segmentos `:id_pedido` e `:id_transferencia_pedido`. Obrigatório para IDs legados com `/` — sem encode, modal de revisão retorna **404 Rota não encontrada**.

---

## Anti-padrões proibidos

### A1 — Mock fallback silencioso em DEV

```ts
// ❌ NUNCA
preview: (payload) =>
  request('/api/...').catch(err => {
    if (import.meta.env.DEV) return mockX(payload)
  })

// ✅ Falha ruidoso
preview: (payload) =>
  request('/api/...')
```

Mascarar erro real com mock em DEV viola Mand. 08. Caso real corrigido em 2026-05-12: preview retornava "5 itens afetados" mockado quando a API real respondia 400 — usuário não sabia.

### A2 — ACL legado→DDD no backend

Sistema é DDD-puro end-to-end. Frontend envia nome exato da coluna do Prisma (`incoterm_pedido`, `quantidade_inicial_item`, etc.). Sem `LEGACY_TO_DDD` map. Ver [`ddd-nomenclatura`](../../governanca/lei/ddd-nomenclatura/SKILL.md) — glossário canônico.

### A3 — Editar `schema.prisma` diretamente

Sempre editar `fragment.prisma` + rodar `npx tsx scripts/ativamente/compose-pedido-schema.ts` + `npx prisma db push`.

---

## Filtro Multi-Workspace (entrega 2026-05-13, commit `4bafb1b6`)

### Visão de 1 minuto

A Lista de Pedidos suporta filtro multi-workspace: usuário escolhe N workspaces no popover do header da coluna "Workspace" e vê pedidos+itens de todos juntos. Defesa em 3 camadas: UI (popover só mostra acessíveis), backend (`validarMultiWorkspace` via S2S → 403 com `workspaces_bloqueados[]`), Portão 3 (header `x-id-workspace` inalterado).

### Contratos críticos

- **Endpoint S2S**: `GET /api/v1/internal/usuarios/:id/workspaces-habilitados?id_organizacao=X` no Configurador retorna `{ tipo_usuario, workspaces_habilitados: string[] }`. SSOT da regra de visibilidade replica `/hub/init`.
- **Helper SDK**: `obterWorkspacesHabilitadosDoUsuario` em `@gravity/resolver-organizacao` — consumido pelos produtos para validar listas.
- **Query param**: `GET /api/v1/pedidos?ids_workspaces=cmo1,cmo2` (CSV). **Sobrepõe** o header `x-id-workspace` quando vem com ≥1 valor. Header continua sendo validado pelo Portão 3 separadamente.
- **Mand. 08**: ids fora da lista do usuário → 403 com `workspaces_bloqueados[]` explícito. NÃO há fallback silencioso.

### Regra de visibilidade (idêntica em `/hub/init` e endpoint S2S)

| Tipo | Workspaces visíveis |
|------|---------------------|
| MASTER / SUPER_ADMIN / ADMIN | Todos com `status_workspace='ATIVO'` da org |
| PADRAO / FORNECEDOR | ATIVO **AND** `UsuarioWorkspace.ativo_usuario_workspace=true` |

FORNECEDOR pode ser cross-organização (não exige org match). Mand. 04 NÃO se aplica a PADRAO/FORNECEDOR (sem bypass).

### Comportamento frontend (Lista)

- **Coluna "Workspace"** sempre visível na 3ª posição (após "Tipo de Operação"). Migração automática reposiciona para usuários com prefs antigas.
- **Mount**: filtro inicia com workspace ATIVO pré-marcado (init useEffect, UMA vez via `initializedFilterRef`).
- **Empty selection**: usuário desmarcar tudo = lista vazia (curto-circuito local, **sem fetch**). Coerência popover ↔ dados.
- **Chip híbrido**: 1-2 nomes diretos, 3+ "N selecionados". Clicável (reabre popover ancorado no chip). Tooltip `TooltipGlobal` com lista numerada.

### Cache local do escopo (PR #80)

- **`sessionStorage` por org:** `pedido:workspaces_escopo:{id_organizacao}` — SSOT alinhado com `useEscopoWorkspacesPedido` e `pedido-escopo-hub.ts`.
- **Legado:** chave global `pedido:workspaces_escopo` — só leitura; **nunca gravar** em código novo.
- **Hidratação:** IDs fora de `idsDisponiveis` são descartados antes de usar escopo (Mand. 08 — sem fallback silencioso para outra org).
- **Hub:** modal de divergência resolve nomes via `GET /api/v1/me/workspaces`, não pelo carousel do Hub.

### Quando mexer aqui — checklist

- [ ] Mudou regra de visibilidade? Atualizar **DOIS** lugares: `/hub/init` E `workspaces-habilitados-internal.ts`. Dívida D11 pede extração para serviço comum.
- [ ] Adicionou novo workspace status (além de ATIVO/INATIVO)? Verificar ambos endpoints + Portão 3.
- [ ] Tocou em `parseCsvQueryParam` no `processos-core/pedidos.ts`? Mantém dedup, trim, ignore vazios — invariantes do contrato.
- [ ] Mudou shape do `WorkspaceDisponivel` no `/hub/init`? Atualizar Zod schema `workspacesDisponiveisApi` em api.ts (Mand. 09 — Zod bilateral).

### Anti-padrões específicos (não repetir)

- **AP1**: enviar `?ids_workspaces=` quando `workspacesSelecionados === [workspaceAtivo]` — duplicaria trabalho do header. O `ehSelecaoDefault` checa exatamente isso.
- **AP2**: fazer fetch quando `workspacesSelecionados.length === 0` — backend cairia no header e mostraria pedidos do ativo. Curto-circuito local força lista vazia.
- **AP3**: repopular filtro automaticamente após "× Limpar" — quebra o modelo mental (usuário desmarcou de propósito). Init é UMA vez no mount.
- **AP4**: hardcoded "consolidar quando há N+" no chip — usar `rotulofiltro` único (`<=2 nomes / 3+ contagem`). Vale para todos os filtros enum.
- **AP5**: usar `localStorage` (`gravity:idOrganizacao`) como fonte do `x-id-organizacao` em request. A **fonte única do tenant é o store** (`getDynamicTenantId` / `getApiContext` lendo `currentUser.idOrganizacao`). `lsGet()` só hidrata o `injectTenantGetter` (validado contra o store live), NUNCA alimenta request direto. Motivo: o localStorage sobrevive a logout/troca de org/sessão e, na janela sem-JWT (Clerk hidratando), resolve a **org errada** → 404 "Organização não encontrada" intermitente. O logout limpa `gravity:idOrganizacao` + `gravity_id_organizacao` + `gravity_company_id` em `shell/hooks/useMeSync.ts`. Correção 2026-06-11.

### Documentos relacionados

- **Técnico**: `documentos-tecnicos/produtos-gravity/pedido/FILTRO-MULTI-WORKSPACE-TECNICO.md`
- **Regras**: `documentos-tecnicos/produtos-gravity/pedido/FILTRO-MULTI-WORKSPACE-REGRAS-NEGOCIO.md`
- **Auditoria DB**: `scripts/auditar-workspaces-pedidos.mjs`

---

## Helpers compartilhados (`shared/`)

### `shared/migracaoColunas.ts` (refactor D12 — 2026-05-13)

Helpers puros para migração de preferências de coluna do usuário quando uma entrega adiciona/reposiciona colunas built-in:

| Helper | Caso de uso | Idempotente? |
|--------|-------------|--------------|
| `inserirColunaAposAncora(visiveis, key, ancoras)` | Adicionar coluna NOVA nas prefs do usuário (entrega aumenta o set de colunas built-in) | ✅ Sim — se já existe, retorna no-op |
| `moverColunaParaAposAncora(visiveis, keyMover, keyApos)` | Reposicionar coluna EXISTENTE quando entrega muda posição padrão | ✅ Sim — se já está depois da âncora, retorna no-op |

**Composição padrão** (cobre ambos os casos numa migração):
```ts
const passoInserir = inserirColunaAposAncora(saved, 'nova_coluna', ['ancora1', 'ancora2'])
const passoMover   = moverColunaParaAposAncora(passoInserir.resultado, 'nova_coluna', 'ancora1')
const visiveis     = passoMover.resultado
const persistir    = passoInserir.mudou || passoMover.mudou
```

**Quando usar**: toda vez que uma entrega adicionar/reposicionar coluna built-in no Pedido. Substitui ~40 linhas inline por 2 chamadas testadas.

**Cobertura**: 16 testes unitários em `__tests__/migracaoColunas.test.ts` cobrindo edge cases (lista vazia, âncoras ausentes, idempotência, preservação de customização do usuário).

---

## Parte 5 — Internacionalização (i18n)

> Entrega 2026-05-22 — produto **100% i18n'd** em PT/EN/ES. Veja [arquitetura/traducao](../../arquitetura/traducao/SKILL.md) para o pipeline geral.

### Cobertura atual

- **2743 referências `t('pedido.*')`** no `pedido/client/src` — 0 missing (todas resolvem em pt.json).
- **24 arquivos `.tsx`** convertidos (todos os modais, formulários, lista, dashboard, kanban, configurações, smart import, snapshot, anexos, visão geral).
- Paridade pt/en/es 100% nas keys de `pedido.*`.

### Namespaces de keys do produto

| Namespace | Origem |
|-----------|--------|
| `pedido.dashboard.*` | PedidosDashboard.tsx |
| `pedido.kanban.*` / `pedido.kanban_colunas.*` | PedidosKanban + SecaoKanbanColunas |
| `pedido.excluir.*` | ModalPedidosExcluir |
| `pedido.lista.*` / `pedido.barra.*` / `pedido.coluna_pai.*` / `pedido.popover_filtro.*` | Pedidos.tsx + ColunasPai + barra/filtro |
| `pedido.visao_geral.*` | PedidosVisaoGeral.tsx |
| `pedido.config.*` / `pedido.config_colunas.*` | Configuracoes.tsx + ConfiguracaoColunas/* |
| `pedido.modal_novo.*` / `pedido.modal_item.*` | ModalPedidoNovo + ModalItemNovo |
| `pedido.modal_dup.*` / `pedido.modal_transf.*` / `pedido.modal_massa.*` / `pedido.modal_pdf.*` | modais correspondentes |
| `pedido.modal_col.*` / `pedido.card_usuario.*` | ConfiguracaoColunas/ModalNova + ConfiguracaoCards/ModalNovo |
| `pedido.smart_import.*` / `pedido.smart_preview.*` | SmartImport/* |
| `pedido.anexos.*` / `pedido.cel_anexos.*` | AnexosPainel + CelulaAnexosColuna |
| `pedido.drawer.*` / `pedido.formulario.*` | DrawerPedido + PedidoFormulario |
| `pedido.massa_campos.*` | rótulos de campo da edição em massa |

### Proteção contra regressão

Teste unitário `testes/testes-unitarios/produto-gravity/pedido/i18n-paridade.test.ts` cobre:
- Toda key `t('pedido.*')` no client/src tem entry em `pt.json`
- Paridade pt → en e pt → es (sem keys faltantes)
- Sem valores vazios em `pedido.*`
- Variáveis `{{var}}` preservadas entre os 3 idiomas

Roda no CI a cada PR — adicionou key no pt sem rodar `npm run translate`, falha.

### Items adiados (refactor de assinatura de função pendente)

Hardcoded strings que ficaram fora do escopo i18n porque a função/módulo onde vivem **não recebe `t: TFunction`**:

1. `components/lista/ColunasFilho.tsx` — função `mapColunaUsuarioParaGTColuna` (1 string)
2. `pages/Pedidos.tsx` — constantes `COLUNAS_FILHO` (linhas ~657-2483) e `buildMapaColunasFilho` (linhas ~2486-3160), ~50 strings de metadata de coluna
3. `components/lista/ColunasPai.tsx` — helpers `renderQtdPedido`, `renderAgregado` (~3 strings)
4. `pages/PedidosVisaoGeral.tsx` — mocks de demo de alertas (fornecedores fictícios) — esperam dados reais do backend (Mand. 05)

**Quem corrigir**: passar `t: TFunction` como parâmetro nas funções acima e atualizar todos os callers no mesmo commit (Mand. 07 — sincronia de contratos).

### Anti-padrões i18n específicos do Pedido

- **AP4 — Shadowing de `t` em `.map(t => ...)`**: parâmetros de callback em arrays como tabs/colunas/status que usam `t` sobrescrevem o hook silenciosamente. Caso real corrigido em 2026-05-22 em `PedidosVisaoGeral.tsx` L3623. Sempre renomear o param antes de inserir `t()` dentro do bloco.
- **AP5 — Constantes module-level com labels**: declarar `const TIPO_LABELS = { ... }` fora do componente impede tradução. Mover para dentro do componente como `useMemo([t])` OU manter keys-only e resolver via `t()` no render.
- **AP6 — Helpers exportados sem `t`**: `mapColunaUsuario*`, `renderAgregado` etc. recebem dados mas não `t`. Tradução exige refactor de assinatura — Item 1-3 da lista de adiados acima.

---

## Status da skill

| Parte | Status |
|-------|--------|
| 1 — Edição em Massa | ✅ Consolidada |
| 2 — Lista de Pedidos | 🟡 Em evolução — editar-salvar STATUS/TOP documentado 2026-06-03 |
| 2.2 — Painéis da Lista | ✅ `ListaPainelUsuarioGlobal`, `/lista/paineis`, `PedidosListaPainelBar` — menu ⋮ Renomear/Excluir via portal (`createPortal` + `position: fixed`; ver `PAINEL-LISTA-CONTRATO.md` §5.3, PRs #610/#615) |
| 2.1 — Filtro Multi-Workspace | ✅ Consolidada (2026-05-13) |
| 3 — Consolidar / Transferir | 🟡 Placeholder — regras de negócio em docs; transferir implementado |
| 4 — Duplicar | ✅ Consolidada |
| 4.1 — Excluir (blacklist opt-out) | ✅ Consolidada (2026-05-26) |
| 4.2 — Transferir (pid URL) | ✅ Nota técnica (2026-05-26) |
| 5 — Internacionalização (i18n) | ✅ Consolidada (2026-05-22) |

---

## Referências cruzadas

| Para | Consultar |
|------|-----------|
| Lista editar-salvar (STATUS, TOP, Nº) | [LISTA-EDITAR-SALVAR-REGRAS-NEGOCIO.md](../../../documentos-tecnicos/produtos-gravity/pedido/LISTA-EDITAR-SALVAR-REGRAS-NEGOCIO.md) |
| Schema composition | [arquitetura/schema-composition](../../arquitetura/schema-composition/SKILL.md) |
| Isolamento de org | [governanca/lei/isolamento-organizacao](../../governanca/lei/isolamento-organizacao/SKILL.md) |
| DDD nomenclatura | [governanca/lei/ddd-nomenclatura](../../governanca/lei/ddd-nomenclatura/SKILL.md) |
| 9 Mandamentos | [governanca/lei/9-mandamentos](../../governanca/lei/9-mandamentos/SKILL.md) |
| Cadastros snapshot policy (quando consumir Empresa/Moeda/NCM) | [governanca/lei/cadastros-snapshot-policy](../../governanca/lei/cadastros-snapshot-policy/SKILL.md) |
| Segurança 5 camadas | [seguranca/seguranca-5-camadas](../../seguranca/seguranca-5-camadas/SKILL.md) |
| UX criação de telas | [ux/criacao-telas](../../ux/criacao-telas/SKILL.md) |
| Testes | [testes](../../testes/SKILL.md) |
