Register Praças map and vote-estimate roadmap items with implementation plans.

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-21 10:26:33 -03:00
parent cf13c441aa
commit b7cfc609e1
10 changed files with 989 additions and 22 deletions

View file

@ -119,3 +119,9 @@ alwaysApply: false
- **Bloqueador obrigatório de produção (PII real):** antes de lideranças/apoiadores reais ou import em massa, a assessoria jurídica eleitoral deve aprovar os textos definitivos (substituindo os provisórios do [Onda 0 MVP](../../../docs/plans/onda-0.md)). Os textos provisórios já sobem automaticamente via migration; o hold é de **titulares reais**, não de engenharia.
- Em 2026-07-17 o roadmap foi repriorizado orientado ao calendário eleitoral (Res. TSE 23.760/2026: propaganda a partir de 16/08, 1º turno 04/10), com ordem de execução, trilhas paralelas (A dados eleitorais/território, B superfícies de coordenação, C operação de campo, D plataforma) e grafo de dependências. Decisões de sequenciamento: lançar o MVP antes da migração multi-município; multi-município antes de zonas-por-municipio/eventos/demandas; baseline TSE independente de zonas-por-municipio (dependência rebaixada a suave); WhatsApp Business API movido a fora de escopo (vedação Meta + Res. 23.610 art. 33).
- Backlog consolidado do produto (admin RBAC, mapa, eventos, demandas, apoiadores, listas globais, site, white-label): [`docs/roadmap.md`](../../../docs/roadmap.md).
- **B7 registrado (2026-07-21):** mapa em `/campanha/pracas` deve usar o mesmo `buildPlazaListWhere` da lista/overview (hoje só escopo de access). Plano: [`docs/plans/mapa-pracas-filtrado.md`](../../../docs/plans/mapa-pracas-filtrado.md). Quick win Janela 1; cortável se o prazo apertar.
- **A9 registrado (2026-07-21):** total esperado da Praça (`expectedVotes` staff-only), distinto da soma de `votePledge` e de `voteGoals`; mapa/lista usam `expectedVotes ?? effectiveTotal`. Plano: [`docs/plans/estimativa-votos-praca.md`](../../../docs/plans/estimativa-votos-praca.md). Bloqueia B9; não cortável.
- **B9 atualizado (2026-07-21):** edição rápida na lista (Assessores / Tendência / **Votos estimados = `expectedVotes`** direto). Depende de A9. Plano: [`docs/plans/edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md).
- **B8 registrado (2026-07-21):** polígonos das Praças-zona Salvador/Camaçari. Pesquisa: SSA tem lista oficial TRE-BA zona↔bairro (RA 02/2017); CMS **não** — só locais de votação por ZE 170/171 + curadoria; TSE/TRE não publicam polígonos de ZE. Plano: F1 catálogo+UI na Praça; F2 dissolve malha de bairros (IBGE/GeoSalvador) → TopoJSON. Janela 3; F2 cortável. Plano: [`docs/plans/poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md).
- **B10 registrado (2026-07-21):** hover/tap no Mapa das Praças — destaque + faixa com votos; **desktop:** hover = detalhe, click = navega pra Praça; **mobile:** 1º tap seleciona, 2º tap na mesma navega; SSA/CMS com N zonas → 2º gesto foca `zoneBreakdown` (não escolhe ZE às cegas) até B8. Plano: [`docs/plans/hover-mapa-pracas.md`](../../../docs/plans/hover-mapa-pracas.md).
- **B11 registrado (2026-07-21):** opção de escala no Mapa das Praças — cor por % dos votos válidos da Praça (0–100% fixos; denominador `electionTally.votosValidos`; 2026 usa válidos 2022); absoluto permanece. Plano: [`docs/plans/escala-percentual-mapa-pracas.md`](../../../docs/plans/escala-percentual-mapa-pracas.md).

View file

@ -0,0 +1,115 @@
# Edição rápida na lista de Praças
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B9)
Impeccable: B — encaixe em `PlazaList` / cards mobile em `/campanha/pracas` (sem rota nova)
Appetite: ~1–1,5 dia eng; 3 affordances na linha + reuso de actions (A9 + plaza); sem migration própria
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells `CampaignPageShell`, `PlazaList`, shadcn `Table`/`Popover`/`NativeSelect`/`Input`.
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape longo — interação em tabela existente).
Brief compacto:
- **Persona / contexto:** Alex (Coordenador Geral / Assessor) na lista de 436 Praças sob pressão de convenções — precisa designar assessoria, marcar conjuntura e lançar o **total esperado** da Praça sem abrir N páginas `/editar`.
- **Job principal:** editar Assessores, Tendência e **Votos estimados (total da Praça)** sem sair da lista, com refresh RSC após save.
- **Estratégia de cor:** Restrained (inalterada).
- **Anti-goals:** planilha editável full-row; data-grid genérico; Sheet de pledges como substituto do total da Praça; bulk multi-select neste item.
## Contexto
Em `/campanha/pracas`, `PlazaList` é **somente leitura**. Com **A9** entregue, a coluna “Votos estimados” mostra `plaza.expectedVotes` (total esperado staff-only), com a soma de `votePledge` como cobertura secundária (“Nas lideranças”). Tendência já filtra (`?trend=`) mas ainda não aparece na tabela.
Edição hoje exige `/campanha/pracas/[slug]/editar` (assessores, tendência, `expectedVotes` via A9) ou o detalhe para pledges por liderança.
Pedido de produto (2026-07-21): edição rápida na tabela de Assessores, Votos estimados e Tendência. Correção de modelo (2026-07-21): Votos estimados = total da Praça (A9), **não** o agregado de lideranças — por isso B9 **depende de A9**.
## Objetivos
- Staff edita na linha: **Assessores** (só Coordenador Geral), **Tendência** (`favoravel|neutra|desfavoravel` + limpar) e **Votos estimados** (`expectedVotes` — input numérico direto na coluna / popover curto).
- Coluna **Tendência** visível na tabela/cards staff.
- Coluna **Votos estimados** edita `expectedVotes` (action A9); a cobertura “Nas lideranças” permanece **read-only** na lista (edição de pledges continua no detalhe da Praça).
- Access idêntico às actions existentes.
- Guardrails: **sem migration neste item** (A9); sem Consent; leader sem colunas staff.
## Decisões travadas
- **Item B9 na trilha B; dependência dura de A9.** Sem `expectedVotes` a coluna não tem o que editar. (Usuário, 2026-07-21.) **Rejeitado:** B9 só com Sheet de pledges (plano anterior); fundir A9+B9 num único ID (mistura schema caro com UI).
- **Coluna “Votos estimados” = edição direta de `expectedVotes`.** Popover/campo numérico + salvar (ou blur+Enter); reusa `setPlazaExpectedVotes`. **Rejeitado:** Sheet de pledges como affordance principal da coluna; editar `voteGoals` no lugar.
- **Pledges fora do affordance da coluna.** Cobertura “Nas lideranças” é informativa; deep-link “Abrir Praça” para CRM pessoa a pessoa. **Rejeitado:** dois editores na mesma célula (confunde total vs cobertura).
- **Padrão UX: Popover por célula (não contentEditable em massa).** Tendência = select; Assessores = checkboxes; Votos = number input. Touch `min-h-11`. **Rejeitado:** inputs sempre montados em todas as linhas.
- **Tendência v1 = status only; nota no `/editar`.** **Rejeitado:** nota obrigatória na lista.
- **Assessores: reusar `assignPlazaAdvisors`.** Coordenador-only. **Rejeitado:** bulk multi-Praça neste item.
- **i18n e naming** (AGENTS.md): `PlazaListTrendControl`, `PlazaListAdvisorsControl`, `PlazaListExpectedVotesControl`; strings pt-BR.
## Questões em aberto
- **Salvar votos no blur, Enter ou botão explícito?** **Opções:** A) Enter + botão | B) debounce blur | C) só botão. **Recomendação:** A — evita gravação acidental em scroll mobile; alinhado a forms Field Desk.
- **Carregar opções de assessor na página ou sob demanda?** **Opções:** A) SSR staff | B) action ao abrir. **Recomendação:** A se o conjunto for pequeno; default A _(assumido — validar no craft)_.
- **Refresh:** padrão `revalidatePath` / RSC refresh dos formActions da Praça.
## Abordagem proposta
```mermaid
flowchart LR
list["PlazaList row"] --> trend["Popover/Select tendência"]
list --> advisors["Popover assessores"]
list --> votes["Input expectedVotes"]
trend --> a1["setPlazaPoliticalTrend"]
advisors --> a2["assignPlazaAdvisors"]
votes --> a3["setPlazaExpectedVotes"]
a1 --> refresh["RSC refresh lista"]
a2 --> refresh
a3 --> refresh
```
Componentes:
- **`PlazaList`**: coluna Tendência; células Assessores / Votos estimados / Tendência = gatilhos staff; cobertura de pledges read-only; cards mobile espelham. Leader inalterado.
- **`PlazaListTrendControl`**: select status → `setPlazaPoliticalTrend` (A9 já deve ter `politicalTrend.status` no list select se B9 precisar — senão incluir aqui).
- **`PlazaListAdvisorsControl`**: Popover checkboxes como `PlazaAdvisorsForm`; só `coordinator`.
- **`PlazaListExpectedVotesControl`**: input do total → `setPlazaExpectedVotes` (A9). Mostra “—” quando null; após save, refresh.
- **Form actions** thin em `pracas/`: wrappers das actions em `actions/plaza.ts`.
- **Depth check:** sem `plazaQuickEditService`; sem editable-cell genérico (<3 call sites).
- **Migration:** nenhuma (A9).
- **Testes:** access (advisor não assign; leader sem controles); int/e2e leve de update `expectedVotes` pela lista.
## Dependências
- **Dura:** **A9** ([estimativa-votos-praca.md](estimativa-votos-praca.md)) — campo + action `setPlazaExpectedVotes`.
- **Dura:** R2 — entregue (transitiva via A9).
- **Suave:** R6; fill-in [filtros-auto-pracas.md](filtros-auto-pracas.md).
- Reusa: `assignPlazaAdvisors` / `setPlazaPoliticalTrend` / `setPlazaExpectedVotes`, `PlazaAdvisorsForm`, `plazaViewModels`, `campaignAccess`.
## Não escopo
- Schema `expectedVotes` / mapa fallback → A9.
- Edição de pledges por liderança na lista; metas (`voteGoals`); nota de tendência; bulk assign.
- B7 mapa filtrado; B8 polígonos; Pixel Meta.
## Rabbit holes
- **Data-grid / spreadsheet mode.** **Mitigação:** três affordances nomeadas.
- **Optimistic UI do total.** **Mitigação:** save → refresh servidor.
- **Reabrir Sheet de pledges “só para completar”.** **Mitigação:** fora de escopo explícito; detalhe da Praça.
## Adiado com gatilho
- **Nota de tendência no popover.** Feedback de uso.
- **Bulk assign de assessor a N Praças.** Volume real na fila “Sem assessor”.
- **Atalho “ajustar lideranças” a partir da cobertura.** Se assessores pedirem sem abrir a Praça.
## Referências
- `docs/roadmap.md` (Trilha B / B9; depende de A9)
- `docs/plans/estimativa-votos-praca.md` — modelo + action
- `src/components/campaign/PlazaList.tsx` — superfície alvo
- `src/app/(campaign)/campanha/(app)/pracas/page.tsx`
- `src/app/(campaign)/campanha/actions/plaza.ts`
- `src/utilities/plazaViewModels.ts`
- AGENTS.md — Campaign auth, naming, overrideAccess
- `PRODUCT.md` / `DESIGN.md` — Field Desk

View file

@ -0,0 +1,114 @@
# Escala percentual no Mapa das Praças (0–100% dos válidos)
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B11)
Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` / `ChoroplethLegend` (sem rota nova)
Appetite: ~0,5–1 dia eng; tally por ano no bundle + seletor de escala + legenda 0–100%
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`PlazaMapPanel`, `BahiaMap`, `ChoroplethLegend`, `NativeSelect`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (controle + copy da legenda; sem shape longo).
Brief compacto:
- **Persona / contexto:** Assessor/Coordenador Geral (Alex) compara municípios de portes muito diferentes e hoje o coroplético é dominado pelo total absoluto (Salvador “pinta” a escala).
- **Job principal:** escolher ver a cor como **participação nos votos válidos da Praça** (0–100%), não como total relativo à maior Praça.
- **Estratégia de cor:** Restrained — mesma rampa vermelha sequencial; só muda o domínio da métrica. Modo comparar (divergente) inalterado neste item.
- **Anti-goals:** não segunda paleta; não hero-metric SaaS; não redesenhar o mapa; não inventar denominador fora de `votosValidos` do TSE.
## Contexto
O **Mapa das Praças** (`PlazaMapPanel` em `/campanha/pracas`) pinta municípios com `choroplethFillColor(value, max)` onde `max = choroplethMaxValue(values)` — o **maior total absoluto** no escopo (`src/lib/choroplethColorScale.ts`). Municípios pequenos com boa penetração ficam pálidos; Salvador/grandes centros saturam a escala.
Os votos nominais já vêm de `loadCandidateVotesByCityZone` + `sumVotesForGeography` em `plazaMapData.ts`. O denominador pedido — **votos válidos** (sem nulos, brancos nem abstenções) — já existe em `electionTally.votosValidos` (seed TSE; usado no detalhe via `plazaElectoralBaseline` / `PlazaBaselineCard`). Pedido de produto (2026-07-21): opção de escala **percentual na Praça**, domínio fixo **0% → 100%**.
Não cobre: filtro URL (**B7**), hover (**B10**), `setStyle` incremental (**B6**), polígonos-zona (**B8**).
## Objetivos
- Controle no painel do mapa: escala **Total (votos)** | **% dos válidos** (rótulos pt-BR).
- Em `% dos válidos`: cor = `votos_candidato / votosValidos` da geografia da Praça (células `cityCode × zone` no escopo), com `max` fixo = **1** (legenda 0–100%), não o máximo observado entre Praças.
- Anos TSE (2014/2018/2022): denominador = `electionTally.votosValidos` do **mesmo ano** + mesmo cargo/turno federal T1 já usados no mapa.
- Guardrails: sem migration, sem collection, sem Consent, sem server action; leituras de tally com o mesmo padrão de access do baseline (`assertCanReadElectionData` / `overrideAccess: true` após assert, como `loadCandidateVotesByCityZone`).
- Modo **Comparar** (divergente): permanece em diferença absoluta; o seletor de escala percentual some ou fica desabilitado com copy curta (não misturar % e diff no v1).
## Decisões travadas
- **Item próprio B11 (não absorver em B7/B10/B6 nem R6).** É semântica da métrica do coroplético, não filtro, hover, perf nem polish genérico. (2026-07-21, roadmap-item.) **Rejeitado:** fill-in sem plano (denominador + 2026 + compare precisam de decisão); fase de classificação territorial A5 (já usa % no detalhe, não no mapa).
- **Denominador = `electionTally.votosValidos` (TSE).** Exclui nulos, brancos e quem não compareceu (abstenção). Alinha ao card de baseline (“% dos válidos”). **Rejeitado:** `aptos` (inclui abstenção); `comparecimento` (inclui nulos/brancos); denominador “votos nominais totais do cargo” sem tally (mais query, mesma intenção do campo oficial).
- **Escala fixa 0–100%, não max-of-shares.** `choroplethFillColor(share, 1)` (ou equivalente). Um município com 8% dos válidos fica igualmente claro em qualquer escopo. **Rejeitado:** normalizar pelo maior % do mapa (ainda distorce leitura entre filtros).
- **2026 (estimativas): denominador = válidos 2022 da mesma geografia.** Não há tally 2026. Copy da legenda: “% dos válidos 2022”. **Rejeitado:** desabilitar % em 2026 (perde o ano operacional); usar `voteGoals`/metas (outra semântica); inventar eleitorado IBGE.
- **Comparar (divergente) fora do modo % neste item.** Diff absoluto Solla − outro permanece; percentual de shares (ou diff de %) é rabbit hole. **Rejeitado:** forçar % no compare no v1.
- **Default da UI = `% dos válidos`.** Absoluto continua disponível. _(assumido — validar com produto; se preferirem preservar o comportamento atual, inverter o default sem mudar o resto.)_ **Rejeitado:** só absoluto com “avançado” escondido (o pedido é a leitura útil).
- **i18n e naming** (AGENTS.md): identificadores em inglês (`scaleMode: 'absolute' | 'percentValid'`, `validVotesByYear`, `percentValues`); strings visíveis em pt-BR (`Total (votos)`, `% dos válidos`, legenda `0` … `100%`).
## Questões em aberto
- **Persistir escala na URL (`?scale=percent|absolute`)?** **Opções:** A) só estado local (como o seletor Ano) | B) query string. **Recomendação:** A no v1 — barato; B se a coordenação começar a compartilhar links do mapa com a escala (gatilho abaixo).
- **Praça com `votosValidos === 0` ou tally ausente?** **Opções:** A) tratar como sem dado (cinza, como valor ausente) | B) 0%. **Recomendação:** A — evita divisão por zero e “0%” falso quando falta seed.
- **Salvador/Camaçari agregados no polígono municipal:** % = soma dos votos das Praças-zona **no escopo** / soma dos válidos das mesmas células (já é o padrão do absoluto). Breakdown textual continua em votos (ou mostra % se barato). **Recomendação:** breakdown em votos no v1; % só no coroplético + legenda.
## Abordagem proposta
```mermaid
flowchart LR
tally["electionTally.votosValidos<br/>por year × cityZone"] --> bundle["PlazaMapBundle.validVotesByYear"]
votes["valuesByYear<br/>(já existe)"] --> panel["PlazaMapPanel"]
bundle --> panel
panel -->|"scaleMode absolute"| abs["values + max=max(values)"]
panel -->|"scaleMode percentValid"| pct["share = votes/valid · max=1"]
abs --> map["BahiaMap + ChoroplethLegend"]
pct --> map
```
Componentes:
- **`loadValidVotesByCityZone`** (em `src/utilities/plazaElectoralBaseline.ts`, ao lado de `loadCandidateVotesByCityZone`): para um `year`, retorna `Map<cityCode:zone, votosValidos>` a partir de `electionTally` (federal T1). Reusar `sumVotesForGeography` (mesmo shape de chave) — depth check: não criar `plazaMapPercent.ts` pass-through.
- **`loadPlazaMapBundle`** (`src/utilities/plazaMapData.ts`): preencher `validVotesByYear: Record<string, Record<string, number>>` (codarea → válidos agregados por geografia no escopo), espelhando o loop de `valuesByYear` para 2014/2018/2022; para a chave `'2022'` reutilizar no modo 2026. Estender o tipo `PlazaMapBundle`.
- **`PlazaMapPanel`**: `NativeSelect` (ou segmented leve já usado no tema) `scaleMode`; quando `percentValid` e não-compare, derivar `values` como shares 0–1 e passar `max={1}` à legenda; `metricLabel` → `participação nos válidos (…)` / copy 2026 com “válidos 2022”. Em compare: forçar absoluto + ocultar/desabilitar o seletor.
- **`ChoroplethLegend`**: aceitar formatação de extremo direito (`formatElectionNumber` vs `100%` / `percentFormatter`) — prop opcional `maxLabel` ou `formatMax`, sem segundo componente de legenda.
- **Testes:** unit de `share = votes/valid` (0, 1, divisão por zero → omitir); smoke mental/checklist: ano TSE, 2026, compare desliga %, Salvador agregado.
- **Migration:** Sem migration, sem collection, sem server action.
## Dependências
- **Dura:** R2 (mapa Praças) — entregue.
- **Suave:** seed TSE com `electionTally` nos anos do mapa (já no fluxo `pnpm db:seed:tse`); A9 só se a métrica 2026 do numerador mudar (`expectedVotes`) — o denominador % continua 2022.
- Reusa: `plazaMapData.ts`, `plazaElectoralBaseline.ts`, `choroplethFillColor`, `PlazaMapPanel`, `ChoroplethLegend`, `electionTally.votosValidos`.
## Não escopo
- Filtrar mapa pela URL → **B7** ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)).
- Hover com readout → **B10** ([hover-mapa-pracas.md](hover-mapa-pracas.md)); quando ambos existirem, o readout em modo % deve mostrar % (não só votos) — coordenar na implementação do que estiver segundo.
- Diff percentual no modo Comparar → adiado (rabbit hole abaixo).
- Polígonos Praças-zona → **B8**.
- `setStyle` incremental → **B6**.
- Alterar classificação territorial A5 / limiares — já entregue; só compartilha a noção de `% dos válidos`.
## Rabbit holes
- **Diff de shares no modo Comparar.** Se alguém “só completar”: segunda escala divergente em pp, legenda bipolar em %, e copy confusa com o vermelho/azul atual. **Mitigação:** compare = absoluto apenas neste item.
- **Denominador por cargo majoritário ou aptos.** Muda a leitura política e quebra paridade com o baseline. **Mitigação:** só `votosValidos` federal T1.
- **Persistir preferência por usuário / cookie.** Fora do appetite. **Mitigação:** estado local; URL só com gatilho.
## Adiado com gatilho
- **`?scale=` na URL.** Revisitar quando: alguém pedir link compartilhável do mapa já em % (ou o mesmo padrão for adotado para `year`).
- **Modo Comparar em pontos percentuais (Solla% − outro%).** Revisitar quando: a coordenação usar o compare e reclamar que o diff absoluto favorece municípios grandes.
- **Breakdown por zona em %.** Revisitar quando: B8 pintar Praças-zona ou B10 mostrar readout em % e a lista de zonas ficar inconsistente.
## Referências
- `docs/roadmap.md` (Trilha B / Janela 1 — B11)
- `src/utilities/plazaMapData.ts` — bundle e agregação por `ibgeCode`
- `src/utilities/plazaElectoralBaseline.ts` — `loadCandidateVotesByCityZone` / `sumVotesForGeography` / tally no detalhe
- `src/lib/choroplethColorScale.ts` — `choroplethFillColor(value, max)`
- `src/components/campaign/PlazaMapPanel.tsx` / `ChoroplethLegend.tsx` / `BahiaMap.tsx`
- `src/components/campaign/PlazaBaselineCard.tsx` — precedente de “% dos válidos” na UI
- `src/collections/ElectionTally.ts` — campo `votosValidos`
- AGENTS.md — Campaign Praças model (mapa / `plazaMapData`); naming EN/pt-BR
- `PRODUCT.md` / `DESIGN.md` — Field Desk; inteligência a serviço da organização

View file

@ -0,0 +1,123 @@
# Estimativa de votos da Praça (total esperado)
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A9)
Impeccable: B — campo + migration + action + encaixe em `/editar` / detalhe / lista (leitura); edição inline na lista = B9
Appetite: ~1–1,5 dia eng; migration + 1 action + wire view models/mapa/overview + form no `/editar`
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · `PlazaStrategyForm` / `PlazaStrategyCard` / `PlazaList`.
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (campo + rótulos; sem redesign de lista — B9).
Brief compacto:
- **Persona / contexto:** Assessor/Coordenador Geral cobre Praças **sem liderança** (ou com lideranças que não mapeiam o total esperado) e precisa lançar o total da Praça sem inventar `Contact`/`votePledge` fantasma.
- **Job principal:** gravar e ler o **total esperado da Praça** como número staff-only, distinto da soma das declarações/estimativas por liderança e das metas Bom/Regular/Mínimo.
- **Estratégia de cor:** Restrained.
- **Anti-goals:** fundir com `voteGoals`; substituir `votePledge`; expor o número a `leader`; auto-preencher a partir dos pledges (opt-in depois, se pedido).
## Contexto
Hoje existem **dois** conceitos de votos na vertical, e a UI da lista usa o nome errado para o segundo:
| Conceito | Onde vive | Quem vê | Papel |
| ------------------------------------- | --------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
| Declaração × estimativa por liderança | `votePledge.declaredVotes` / `estimatedVotes` | líder só o declarado; staff os dois | CRM de campo pessoa a pessoa |
| Metas de cenário 2026 | `plaza.voteGoals` (Bom / Regular / Mínimo) | staff | Planejamento / projeção |
| **Total esperado da Praça** | **ausente** | — | O que a coluna “Votos estimados” da lista e o mapa 2026 deveriam representar |
A lista e o mapa 2026 usam `aggregatePledgesByPlaza` → `effectiveTotal` (`estimatedVotes ?? declaredVotes`). Isso **zera** Praças sem liderança e **subestima** Praças em que a estrutura nominal não cobre o território. Pedido de produto (2026-07-21): o total esperado deve ser lançável manualmente por assessor/coordenador; a soma das lideranças permanece como cobertura distinta. B9 (edição rápida na lista) depende deste campo para a coluna ser editável de verdade.
A remodelagem rejeitou o fluxo antigo sugerir→confirmar **por Praça** como substituto do pledge ([remodelagem-pracas.md](remodelagem-pracas.md)) — isso **não** invalida um total staff-only **coexistindo** com pledges. São jobs diferentes.
## Objetivos
- Campo staff-only na `plaza` para o total esperado (nullable, inteiro ≥ 0), editável por `coordinator` e `advisor` no escopo de access.
- Superfície mínima de escrita no `/campanha/pracas/[slug]/editar` (e leitura no cartão de estratégia do detalhe) **antes** de B9.
- Lista staff: coluna “Votos estimados” passa a mostrar o total da Praça; a soma de pledges aparece como cobertura secundária (rótulo distinto, ex. “Nas lideranças”).
- Mapa ano 2026 e overview/dashboard que hoje usam só pledges como “estimativas” passam a usar o total da Praça quando preenchido (ver decisão abaixo).
- `leader` nunca lê o campo (field access + view models).
- Guardrails: **uma migration**; sem Consent; sem collection nova; sem apagar/alterar semântica de `votePledge` nem de `voteGoals`.
## Decisões travadas
- **Item A9 próprio (trilha A), dependência dura de B9 — não absorver em B9 nem em `voteGoals`.** Schema + semântica de agregação são caros de reverter e merecem ID/plano; B9 fica só UI rápida. (Usuário + classificação roadmap-item, 2026-07-21.) **Rejeitado:** só B9 com Sheet de pledges (estado anterior do plano B9); reusar `voteGoals.good` como “o” estimado (metas ≠ total operacional); volta do fluxo sugerir→confirmar de núcleo.
- **Campo `expectedVotes` (number, nullable) em `plaza`, label admin/UI “Votos estimados”.** Identificador inglês distinto de `votePledge.estimatedVotes` evita colisão mental/código. **Rejeitado:** `estimatedVotes` na Praça (homônimo perigoso); grupo com nota/autor na v1 (appetite — ver Adiado).
- **Dois números convivem na UI staff.** Total da Praça = `expectedVotes`; cobertura = agregado de pledges (`effectiveTotal`). Nunca sobrescrever um com o outro automaticamente na v1. **Rejeitado:** `expectedVotes` derivado = max(pledges, manual); esconder pledges da lista.
- **Mapa 2026 / overview KPI: `expectedVotes ?? effectiveTotal` (por Praça, depois agrega município).** Praças com total lançado pintam certo; sem total, fallback nos pledges (não apaga o que já existe). **Rejeitado:** mapa só `expectedVotes` (buracos em massa no dia 1); mapa só pledges (ignora o pedido).
- **Access:** leitura/escrita staff (`canReadCampaignStaffField` / `canManageCampaignStaffField`); mesma regra de escopo de Praça do assessor. **Rejeitado:** só `coordinator`; exposição a `leader`.
- **i18n e naming** (AGENTS.md): `expectedVotes`, `setPlazaExpectedVotes`, `PlazaExpectedVotesField`; strings “Votos estimados” / “Nas lideranças” em pt-BR.
## Questões em aberto
- **Validar `expectedVotes` vs metas (`voteGoals`)?** **Opções:** A) sem relação | B) aviso se estimado &lt; mínimo | C) hard reject. **Recomendação:** A na v1; aviso soft só se feedback de campo pedir.
- **Nota/autor no total?** **Opções:** A) só número | B) group com note + recordedBy. **Recomendação:** A; B no Adiado.
- **Backfill na migration?** **Opções:** A) null | B) copiar `effectiveTotal` atual. **Recomendação:** A — pledges mudam; copiar congela um snapshot sem dono. Fallback de leitura cobre o buraco.
## Abordagem proposta
```mermaid
flowchart LR
mig["migration expected_votes"] --> plaza["plaza.expectedVotes"]
plaza --> action["setPlazaExpectedVotes"]
action --> edit["PlazaStrategyForm /editar"]
plaza --> list["PlazaList coluna"]
plaza --> map["plazaMapData 2026"]
pledges["votePledge aggregate"] -.cobertura.-> list
pledges -.fallback.-> map
```
Componentes:
- **`Plaza` collection** (`src/collections/Plaza.ts`): campo `expectedVotes` (number, min 0, index opcional se filtrarem depois, staff access). Admin description: total esperado da Praça; distinto das metas e das lideranças.
- **Migration** `pnpm migrate:create add_plaza_expected_votes`: `ALTER TABLE` nullable; sem backfill.
- **Schema Zod** (`src/lib/schemas/plaza.ts`): `plazaExpectedVotesSchema` `{ plaza, expectedVotes: z.number().int().min(0).max(1_000_000).nullable() }`.
- **`setPlazaExpectedVotesRecord`** em `src/app/(campaign)/campanha/actions/plaza.ts`: parse + `payload.update` com `user` + `overrideAccess: false` (padrão `updatePlazaStrategyRecord`).
- **`plazaViewModels` / `plazaListSelect`**: incluir `expectedVotes`; list VM expõe `expectedVotes` + mantém `pledges` para cobertura.
- **`PlazaStrategyForm` + formAction `/editar`**: input numérico “Votos estimados”; `PlazaStrategyCard` no detalhe mostra o valor.
- **`PlazaList`**: coluna principal = `expectedVotes` (ou “—”); linha/badge secundária com soma das lideranças quando `pledgeCount > 0`.
- **`plazaMapData` / `loadPlazaListOverviewData` / dashboard**: métrica 2026 / totais usam `expectedVotes ?? effectiveTotal` por Praça.
- **Depth check:** não criar collection `plazaVoteEstimate`; não reintroduzir versionamento UUID do núcleo; helper fino em `votePledgeData` ou `plazaVotes.ts` se o fallback se repetir ≥2 call sites.
- **Testes:** int access (leader redacted; advisor scoped); unit do fallback mapa/lista; int create/update `expectedVotes`.
## Dependências
- **Dura:** R2 (collection `plaza` + superfícies) — entregue.
- **Dependentes:** **B9** (edição rápida da coluna) — seta dura A9 → B9.
- **Suave:** B7 (mapa filtrado) — A9 muda a métrica 2026; B7 só o where.
## Não escopo
- Edição inline na lista → [B9](edicao-rapida-lista-pracas.md).
- Alterar assimetria `declaredVotes`/`estimatedVotes` do pledge; import CSV de totais.
- Histórico auditável do total; alertas vs `voteGoals`; filtro `?expectedVotes=` na lista.
- B8 polígonos; Pixel Meta.
## Rabbit holes
- **Reintroduzir estimativa versionada de núcleo (sugerir→confirmar + UUID).** Explode escopo e conflita com a remodelagem. **Mitigação:** um number nullable + action simples.
- **“Fonte da verdade única” fundindo pledges e total.** Apaga o pedido de dois conceitos. **Mitigação:** labels distintos; fallback só na leitura agregada.
- **Layers/DDD cerimônia** (`PlazaVoteEstimateService`). **Mitigação:** campo + action no módulo `plaza` existente.
## Adiado com gatilho
- **Nota + recordedBy no total.** Revisitar quando assessores pedirem justificar mudança sem abrir intel longa.
- **Aviso vs `voteGoals.minimum`.** Revisitar após primeiras semanas com totais preenchidos.
- **Auto-sugerir `expectedVotes` a partir dos pledges (opt-in chip).** Revisitar se muitos totais ≈ soma das lideranças.
## Referências
- `docs/roadmap.md` (Trilha A / A9 → B9)
- `src/collections/Plaza.ts` — `voteGoals`, access staff
- `src/utilities/votePledgeData.ts` — `effectiveTotal`
- `src/utilities/plazaMapData.ts` — métrica 2026
- `src/components/campaign/PlazaList.tsx` — coluna atual
- `src/app/(campaign)/campanha/actions/plaza.ts` — padrão de update staff
- `docs/plans/edicao-rapida-lista-pracas.md` — consumidor B9
- `docs/plans/remodelagem-pracas.md` — assimetria pledge; rejeição do fluxo antigo por Praça
- AGENTS.md — staff-only estimates, naming, migrations
- `PRODUCT.md` / `DESIGN.md` — Field Desk

View file

@ -0,0 +1,112 @@
# Filtros auto-aplicados na lista de Praças
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins)
Impeccable: B — encaixe em `PlazaFilters` (`/campanha/pracas`); sem rota nova
Appetite: ~0,5 dia eng; só client em `PlazaFilters` (+ pending a11y); sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `CampaignSearchInput`, `PlazaFilters`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape longo — interação, não redesign).
Brief compacto:
- **Persona / contexto:** Alex / Assessor monta um recorte de Praças sob pressão (TI + tipo + busca por município); espera que a lista reaja sem um passo extra de confirmação.
- **Job principal:** filtrar a lista (e, com B7, o mapa) só mudando controles — busca com atraso curto, selects imediatos.
- **Estratégia de cor:** Restrained (inalterada).
- **Anti-goals:** segundo botão de confirmação; redesign do disclosure de filtros; unificar todas as listas da vertical neste item.
## Contexto
Em `/campanha/pracas`, `PlazaFilters` (`src/components/campaign/PlazaFilters.tsx`) já aplica **selects** na URL via `router.replace` + `buildPlazaListHref` no `onChange` (região, tipo, assessoria, tendência, prioridade). Só o campo de texto `q` fica em estado local e só sobe para a URL no `onSubmit` do form — daí o botão **"Buscar"**.
Na era Núcleos (2026-07-17) a listagem já aplicava filtros pelo estado da URL sem botão de confirmação (notebook). A remodelagem R2 reintroduziu o padrão "digitar + Buscar" só na busca textual. Pedido de produto (2026-07-21): aplicar automaticamente ao selecionar valores; na busca, disparar a atualização após **1 s** sem digitar, para não navegar a cada caractere.
Precedente de pending UI: `ActionPlanFilters` (`useTransition` + `data-pending` + `aria-live` "Atualizando resultados…"). `SupporterFilters` ainda usa Buscar no `q` — fora deste item.
## Objetivos
- Remover a necessidade do botão **Buscar** em `/campanha/pracas`: selects continuam imediatos; `q` atualiza a URL após **1000 ms** de quietude (debounce).
- Enter no campo de busca aplica `q` **imediatamente** (sem esperar o timer) — acessibilidade e atalho de poder.
- Manter **Limpar** (zera local + `router.replace('/campanha/pracas')`).
- Preservar remontagem canônica por `key={buildPlazaFiltersKey(state)}` na página (servidor = fonte da verdade; voltar/avançar sincroniza).
- Feedback de atualização (`isPending` / `aria-busy` / live region) alinhado a `ActionPlanFilters`.
- Guardrails: sem migration, sem collection, sem Consent, sem server action; URL e `parsePlazaListParams` inalterados.
## Decisões travadas
- **Fill-in com plano (não B8 nem fase de R6/B7).** É restauração de interação barata, paralelizável, cortável; não muda contrato lista↔mapa (B7) nem ciclo visual R6. (2026-07-21, classificação roadmap-item.) **Rejeitado:** item de trilha B (infla grafo por polish de ½ dia); absorver só em R6 (atrasa quick win e some no critique largo); fase de [mapa-pracas-filtrado.md](mapa-pracas-filtrado.md) (preocupação distinta: where do mapa).
- **Debounce só em `q` = 1000 ms; selects sem debounce.** Pedido explícito de produto; selects já são baratos (um replace por mudança). **Rejeitado:** debounce em todos os controles (atrasa feedback de select); Buscar só no texto e selects auto (estado atual — deixa o botão como falso afunilamento mental).
- **Remover o botão Buscar; Enter aplica na hora.** Com debounce + Enter, o botão é redundante. **Rejeitado:** manter Buscar como atalho (ruído visual Field Desk; era Núcleos já operava sem).
- **Não extrair hook/shared filter shell neste item.** Um call site de debounce; `ActionPlanFilters` / `SupporterFilters` não unificam ainda. **Rejeitado:** `useDebouncedPlazaFilters` / factory de listas (&lt;3 call sites — classitis).
- **i18n e naming** (AGENTS.md): identificadores em inglês (`SEARCH_DEBOUNCE_MS`, `navigate`, `isPending`); strings visíveis em pt-BR (live region, labels existentes).
## Questões em aberto
- **Espelhar em apoiadores / lideranças / organizações?** **Opções:** A) neste item | B) fill-ins separados sob demanda | C) lote em R6. **Recomendação:** B — escopo = Praças (pedido); cada lista tem forma diferente (disclosure, tabs). Gatilho abaixo.
- **Constante 1000 ms vs prop?** **Opções:** A) `const SEARCH_DEBOUNCE_MS = 1000` no arquivo | B) prop no componente. **Recomendação:** A — um call site; prop só se o 2º consumidor divergir.
## Abordagem proposta
```mermaid
flowchart LR
select["NativeSelect onChange"] --> nav["navigate → router.replace"]
type["CampaignSearchInput onChange"] --> local["setSearch + timer 1s"]
local -->|"quietude"| nav
enter["Enter / form submit"] --> nav
nav --> url["URL ?q=&region=…"]
url --> rsc["RSC refresh lista/overview/(mapa B7)"]
```
Componentes:
- **`PlazaFilters`** (`src/components/campaign/PlazaFilters.tsx`):
- Manter `navigate(next)` com `page: 1` e `scroll: false`.
- Em `onChange` do search: atualizar `search` local; `clearTimeout` + `setTimeout(1000)` que chama `navigate({ ...state, q: trimmed || undefined })` só se o valor canônico diferir de `state.q` (evitar replace no-op).
- Cleanup do timer no unmount; cancelar timer ao Limpar / ao Enter (flush imediato).
- Envolver replaces em `startTransition`; espelhar `data-pending` / `aria-busy` / live region de `ActionPlanFilters`.
- Remover o `Button type="submit"` "Buscar"; form pode permanecer com `onSubmit` = flush imediato do `q` (Enter).
- Depth check: reusar `buildPlazaListHref` / `PlazaListState` / `CampaignSearchInput` — sem utility nova de debounce até o 3º call site.
- **`pracas/page.tsx`**: sem mudança obrigatória (já passa `key={buildPlazaFiltersKey(state)}`). Soft: com B7 entregue, cada replace de filtro também refresca o mapa — aceitável no appetite.
- **Teste:** unit leve do helper de “próximo `q` / skip no-op” se extrair uma função pura de 5 linhas; senão E2E/manual: digitar “salvador”, esperar ≥1s, assert URL `q=`; mudar TI, assert URL imediata sem clique. Precedente de filtros URL: era Núcleos / notebook.
- **Migration:** Sem migration, sem collection, sem server action.
## Dependências
- **Dura:** nenhuma de outro plano (R2 já entregue).
- **Suave:** B7 ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)) — debounce aumenta frequência de troca de filtro; mapa filtrado fica mais útil, mas este item funciona com o mapa ainda “all accessible”.
- Reusa: `plazaUi.ts` (`buildPlazaListHref`, `PlazaListState`), padrão pending de `ActionPlanFilters.tsx`, dwell do `RecentVisitTracker` (já re-arma o timer a cada `href` — debounce 1s + dwell ~2s evita visitar intermediários).
## Não escopo
- Filtro do mapa pela URL → **B7**.
- Critique/polish visual / mover filtros acima do mapa → **R6**.
- Auto-busca em `/campanha/apoiadores`, lideranças, organizações → fill-ins futuros (gatilho abaixo).
- `setStyle` incremental no mapa → **B6**.
## Rabbit holes
- **Hook compartilhado `useCampaignListFilters`.** Se alguém “só generalizar”: redesenha três listas. **Mitigação:** inline neste arquivo; extrair no 3º call site.
- **Debounce via lib (`use-debounce` etc.).** Dependência nova por 10 linhas. **Mitigação:** `setTimeout` + cleanup.
- **Otimistic list client-side.** Duplicaria `buildPlazaListWhere` no browser. **Mitigação:** URL + RSC como hoje.
## Adiado com gatilho
- **Mesmo padrão em `SupporterFilters` (e outras listas com Buscar).** Revisitar quando produto pedir ou quando R6/critique citar “Buscar” como atrito repetido em ≥2 listas.
- **Extrair `useDebouncedSearchParam`.** Revisitar no 3º call site idêntico (Praças + apoiadores + 1).
## Referências
- `docs/roadmap.md` (Fill-ins; B7 suave; R6)
- `src/components/campaign/PlazaFilters.tsx` — alvo da mudança
- `src/components/campaign/ActionPlanFilters.tsx` — precedente auto-apply + pending
- `src/components/campaign/SupporterFilters.tsx` — contraste (ainda tem Buscar no `q`)
- `src/app/(campaign)/campanha/(app)/pracas/page.tsx` — `buildPlazaFiltersKey`
- `src/utilities/plazaUi.ts` — URL canônica
- AGENTS.md — naming; superfície `(campaign)`
- `PRODUCT.md` / `DESIGN.md` — Field Desk / clarity under pressure
- Notebook — precedente 2026-07-17 filtros auto na lista de Núcleos

View file

@ -0,0 +1,111 @@
# Hover no Mapa das Praças (destaque + total de votos + navegação)
Status: rascunho
Atualizado em: 2026-07-21 (navegação: desktop click / mobile 2º tap)
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B10)
Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` (sem rota nova)
Appetite: ~0,5–1 dia eng; handlers Leaflet + faixa + mapa ibge→slug + teste/checklist
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`PlazaMapPanel`, `BahiaMap`, `ChoroplethLegend`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (superfície já existe; interação nova, sem shape longo).
Brief compacto:
- **Persona / contexto:** Assessor/Coordenador Geral (Alex) inspeciona o coroplético, lê o número sob o cursor e, quando quiser, **abre a Praça** sem voltar à lista.
- **Job principal:** destacar o polígono ativo, mostrar nome + total da métrica corrente, e navegar para `/campanha/pracas/[slug]` com o gesto certo (click desktop / 2º tap mobile).
- **Estratégia de cor:** Restrained — borda/weight de destaque com primário `#C51414`; fill do coroplético inalterado.
- **Anti-goals:** não navegar no hover; não tooltip SaaS ornamentado; não segunda legenda; não redesenhar o painel; não exigir `matchMedia` separado se o modelo selected+click unificar desktop/mobile.
## Contexto
O **Mapa das Praças** (`PlazaMapPanel` em `/campanha/pracas`) já pinta municípios pelo valor do ano selecionado (TSE 2014/2018/2022 ou estimativas 2026) e no modo comparar pelo diff Solla − outro. O renderer `BahiaMap` monta `L.geoJSON` com estilo estático e `highlightKeys` **só** para zoom/destaque programático — **não** há `mouseover` / readout / click→Praça.
Pedido de produto (2026-07-21): hover destaca + mostra votos; **click navega**. Em mobile (sem hover): **primeiro toque seleciona** (mesmo readout); **segundo toque na já selecionada** navega.
Não cobre: filtro URL (**B7**), `setStyle` incremental ao trocar ano (**B6**), polígonos por zona (**B8**).
## Objetivos
- Hover (desktop) sobre um município: borda/weight mais forte + readout com **nome** + **valor formatado** da métrica corrente; **não** navega.
- Click (desktop) no polígono sob o hover: `router.push` / `Link` para a Praça correspondente.
- Touch: 1º tap = seleciona (destaque + readout); 2º tap na **mesma** seleção = navega; tap noutro polígono = troca a seleção.
- O valor exibido é o mesmo de `values` já passado ao mapa — sem fetch por hover/click.
- Guardrails: sem migration, sem collection, sem Consent, sem server action; `leader` continua sem mapa; pan/zoom do Leaflet não devem competir com o 2º tap (ver abordagem).
## Decisões travadas
- **Item próprio B10 (não absorver em B6, B7 nem R6).** Interação de inspeção+atalho ≠ filtro URL, ≠ perf `setStyle`, ≠ polish genérico. (2026-07-21.) **Rejeitado:** fill-in sem plano; fase de B7.
- **Modelo unificado selected + click (não forks `pointer: coarse` obrigatórios).** Estado `selectedKey`: hover (desktop) e 1º tap (mobile) **selecionam**; click/tap com `key === selectedKey` **navega**; hover não navega. Em desktop o hover já deixa `selectedKey` igual ao polígono, então o 1º click navega. Em mobile o 1º tap só seleciona. (Produto, 2026-07-21.) **Rejeitado:** só highlight sem navegação; navegação no hover; `matchMedia` mobile/desktop com dois comportamentos divergentes sem o mesmo selected.
- **Cliente-only; bundle traz `plazasByIbgeCode`.** Nome no readout pode vir do GeoJSON; navegação precisa de `slug`(s) por `codarea` — incluir no `PlazaMapBundle` um mapa `ibgeCode → { slug, name }[]` (já filtrado pelo escopo de access / futuro B7). **Rejeitado:** Server Action por click; adivinhar slug a partir do nome do município no client.
- **Faixa de leitura sob o mapa + destaque Leaflet.** Touch-friendly e `aria-live="polite"`. Tooltip Leaflet opcional só no polish. **Rejeitado:** só tooltip hover-only; painel inspect lateral.
- **Mesma métrica do seletor Ano / Comparar.** **Rejeitado:** segundo número no hover v1.
- **Salvador / Camaçari (N>1 Praças no mesmo `ibgeCode`):** seleção mostra agregado municipal + readout; **navegação no 2º gesto não escolhe zona às cegas** — faz scroll/focus na lista `zoneBreakdown` (ou na faixa, com hint “ver Praças por zona abaixo”). Se o escopo do usuário tiver **exatamente 1** Praça-zona naquele município, aí sim navega para o único slug. (2026-07-21.) **Rejeitado:** sempre ir à ZE 1 / primeira do catálogo; inventar hit-test por zona (= B8).
- **i18n e naming** (AGENTS.md): `onFeatureSelect`, `selectedFeature`, `plazasByIbgeCode`, `MapFeatureReadout`; strings em pt-BR.
## Questões em aberto
- **Polígono sem valor (0 / ausente no `values`)?** **Opções:** A) ainda seleciona e mostra “0” / “Sem dados”; navegação só se houver slug(s) no mapa | B) ignora interação. **Recomendação:** A — “Sem dados” se chave ausente; “0” se presente e zero; click-nav segue a regra de slugs (município sem Praça no escopo → sem navegação, só readout).
- **Faixa clicável como atalho de navegação?** **Opções:** A) só o polígono | B) faixa também é link/`button` para o mesmo destino. **Recomendação:** B quando há exatamente 1 slug (a11y / alvo 44px); com N>1 a faixa não navega (só o scroll hint). _(assumido — validar no craft)_
## Abordagem proposta
```mermaid
flowchart LR
bundle["PlazaMapBundle values + plazasByIbgeCode"] --> panel["PlazaMapPanel"]
panel --> map["BahiaMap onFeaturePointer"]
map -->|"hover / 1º tap"| select["selectedKey + destaque + readout"]
map -->|"click / 2º tap same key"| nav{"slugs.length?"}
nav -->|"1"| push["router.push /pracas/slug"]
nav -->|"N>1"| zones["scroll zoneBreakdown"]
map -->|"mouseout desktop"| clear["limpa selected se não pinned touch"]
```
Componentes:
- **`loadPlazaMapBundle` / `PlazaMapBundle`** (`src/utilities/plazaMapData.ts`): acrescentar `plazasByIbgeCode: Record<string, { slug: string; name: string }[]>` a partir das Praças já carregadas (município = 1 entrada; SSA/CMS = N zonas no escopo). Sem query extra.
- **`BahiaMap`** (`src/components/campaign/BahiaMap.tsx`): `onEachFeature` com `mouseover` / `mouseout` / `click`. Hover → callback `onFeatureSelect(info)`; mouseout → `onFeatureSelect(null)` (só se o ponteiro for fino / não após tap recente — ref `pointerType` ou “touch pin” até tap fora). Click → callback `onFeatureActivate(key)` (o painel decide select vs navigate). Destaque via `setStyle` + `bringToFront`; mouseout restaura estilo do coroplético. Depth check: sem wrapper novo.
- **`PlazaMapPanel`**: estado `selected`; readout; em `onFeatureActivate`: se `key !== selected` → select; se igual → `navigateForIbge(key)` (1 slug → `router.push`; N>1 → `scrollIntoView` na seção zonas). Opcional: link na faixa quando `slugs.length === 1`.
- **Alinhamento com B6:** mesma função de estilo no reset pós-hover. Não bloqueia B10.
- **Teste:** unit do resolver `navigateForIbge` (0 / 1 / N slugs); checklist manual desktop (hover→readout, click→ficha) e mobile (tap1 select, tap2 navigate, tap outro troca).
- **Migration:** Sem migration, sem collection, sem server action.
## Dependências
- **Dura:** R2 (mapa Praças) — entregue.
- **Suave:** A9 (métrica 2026); B6 (`setStyle`); B7 (filtro → `plazasByIbgeCode` só do conjunto filtrado); B8 (aí o 2º tap em SSA/CMS pode passar a ir à Praça-zona).
- Reusa: `PlazaMapPanel`, `BahiaMap`, `plazaMapData`, formatters existentes.
## Não escopo
- Filtrar mapa pela URL da lista → **B7** ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)).
- Rebuild/perf do layer ao trocar ano → **B6** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md)).
- Polígonos / hover por Praça-zona SSA/CMS → **B8** ([poligonos-pracas-zona.md](poligonos-pracas-zona.md)).
- Redesenho do painel / mover filtros → **R6**.
## Rabbit holes
- **`react-leaflet` / Popup Radix.** **Mitigação:** Leaflet nativo + faixa HTML.
- **Hit-test de zonas sem polígono.** **Mitigação:** agregado + scroll para `zoneBreakdown` até B8.
- **Detectores `isMobile` frágeis.** **Mitigação:** modelo selected+activate; no máximo `pointerType` no evento para não limpar seleção no “mouseout” sintético pós-tap.
- **Conflito pan vs 2º tap.** **Mitigação:** só navegar em `click` Leaflet do path (não em `moveend`); se pan arrastar, Leaflet não dispara click no feature.
## Adiado com gatilho
- **2º tap em SSA/CMS → Praça-zona específica.** Revisitar quando **B8** entregar polígonos (hit-test real) ou produto pedir picker de zona no readout.
- **Tooltip Leaflet além da faixa.** Revisitar no polish se critique pedir reforço desktop.
## Referências
- `docs/roadmap.md` (Trilha B, item B10)
- `src/components/campaign/BahiaMap.tsx` — GeoJSON sem `onEachFeature` hoje
- `src/components/campaign/PlazaMapPanel.tsx` — ano / compare / `values` / `zoneBreakdown`
- `src/utilities/plazaMapData.ts` — bundle (`valuesByYear` por `ibgeCode`)
- `docs/plans/mapa-pracas-filtrado.md` — B7
- `docs/plans/escala-dry-pos-b3.md` — B6
- `docs/plans/poligonos-pracas-zona.md` — B8
- AGENTS.md — naming, mapa dinâmico `/campanha`, access assessor
- `PRODUCT.md` / `DESIGN.md` — Field Desk

View file

@ -0,0 +1,107 @@
# Mapa das Praças filtrado pela lista
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B7)
Impeccable: B — encaixe em `/campanha/pracas` (`PlazaMapPanel` + loader; sem rota nova)
Appetite: ~0,5 dia eng; wire `buildPlazaListWhere` no loader do mapa + teste + empty state
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `PlazaFilters`, `PlazaMapPanel`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape longo — superfície já existe).
Brief compacto:
- **Persona / contexto:** Alex (coordenador geral) ou Assessor filtra Praças por TI / tipo / cobertura / tendência e espera que o mapa acima diga a mesma coisa que a lista.
- **Job principal:** o coroplético reflete **exatamente** o conjunto filtrado da URL (não só o escopo de access).
- **Estratégia de cor:** Restrained (inalterada); modo comparativo divergente permanece.
- **Anti-goals:** não mover filtros neste item; não redesenhar o mapa; não filtrar por `page` (paginação da lista não reduz o mapa).
## Contexto
Em `/campanha/pracas` a lista e o overview já aplicam `buildPlazaListWhere(state)` a partir da URL (`q`, `region`, `kind`, `coverage`, `priority`, `trend`). O mapa, porém, carrega **todas** as Praças acessíveis ao papel:
- `src/app/(campaign)/campanha/(app)/pracas/page.tsx` chama `loadPlazaMapBundle(payload, user, canonicalUrl.state.compare)` **sem** o estado de filtro.
- `loadScopedPlazas` em `src/utilities/plazaMapData.ts` usa `where: {}` (só access via `overrideAccess: false`).
Isso quebra o contrato mental que a era Núcleos já tinha documentado: o mapa do overview era “escopado pelos mesmos filtros da lista” ([mapa-bahia-geometrias.md](mapa-bahia-geometrias.md)). Na remodelagem Praças (R2) o mapa nasceu com escopo de **access** (assessor vê só as suas), mas não com escopo de **filtro de UI**. Pedido de produto (2026-07-21): o Mapa das Praças deve usar o mesmo filtro da lista abaixo.
## Objetivos
- Com qualquer combinação de filtros da URL (exceto `compare` e `page`), o bundle do mapa agrega votos **somente** das Praças que passam em `buildPlazaListWhere`.
- Conjunto filtrado vazio → mapa omitido (`null`), alinhado ao overview que já retorna `null` sem docs.
- `compare` continua só modo do mapa (não filtra lista nem mapa); `page` continua só paginação da lista.
- Guardrails: sem migration, sem collection, sem Consent, sem server action; leituras com `user` + `overrideAccess: false`.
## Decisões travadas
- **Item próprio B7 (não absorver em R6 nem em B6).** É correção de produto/contrato lista↔mapa, não polish visual (R6) nem `setStyle` incremental (B6). (2026-07-21, classificação roadmap-item.) **Rejeitado:** fill-in sem plano (perde o contrato e o teste); fase de [remodelagem-pracas.md](remodelagem-pracas.md) (R0–R5 já entregues).
- **Mesmo `Where` da lista/overview — reusar `buildPlazaListWhere`.** Uma fonte de verdade; overview já é o precedente (conjunto filtrado inteiro, `pagination: false`). **Rejeitado:** filtro client-side no `PlazaMapPanel` (vazaria dados não filtrados no HTML/Flight); segundo builder de where só para o mapa.
- **Mapa sobre o conjunto filtrado inteiro, não a página corrente.** Paridade com `loadPlazaListOverviewData`. **Rejeitado:** respeitar `page`/`plazaPageSize` no mapa (mapa paginado não faz sentido geográfico).
- **Empty → omitir o painel do mapa.** Mesma regra do overview. **Rejeitado:** Bahia cinza vazia com lista empty (dois empty states competindo).
- **i18n e naming** (AGENTS.md): identificadores em inglês (`loadPlazaMapBundle`, `PlazaListState`, `buildPlazaListWhere`); strings visíveis inalteradas salvo copy mínima se o empty do mapa precisar (preferir só omitir o painel).
## Questões em aberto
- **Mover `PlazaFilters` para cima do mapa?** **Opções:** A) neste item | B) R6 | C) deixar. **Recomendação:** B/C — fora do appetite; o filtro já é URL-driven e o mapa reage no refresh RSC. Marcar como polish visual em R6 se a coordenação reclamar da ordem.
- **Highlight / fitBounds só nos municípios do filtro?** **Opções:** A) só coroplético nos valores filtrados (polígonos restantes cinza) | B) além disso `fitBounds` no footprint filtrado. **Recomendação:** A neste item (já é o comportamento natural quando `values` só tem as chaves filtradas); B como polish se o Assessor filtrar uma TI e a Bahia inteira continuar no viewport — gatilho abaixo.
## Abordagem proposta
```mermaid
flowchart LR
url["URL searchParams"] --> parse["parsePlazaListParams"]
parse --> where["buildPlazaListWhere"]
where --> list["loadPlazaListPageData"]
where --> overview["loadPlazaListOverviewData"]
where --> map["loadPlazaMapBundle"]
map --> panel["PlazaMapPanel"]
```
Componentes:
- **`loadScopedPlazas` / `loadPlazaMapBundle`** (`src/utilities/plazaMapData.ts`): aceitar `PlazaListState` (ou `Where` já montado). Passar `where: buildPlazaListWhere(state)` no `payload.find` (manter `user` + `overrideAccess: false`). Se zero docs → `null`. `compareCandidateNumber` permanece argumento separado (ou lido de `state.compare`).
- **`pracas/page.tsx`**: passar o mesmo `state`/`rawSearchParams` usado pela lista ao loader do mapa (ex.: `loadPlazaMapBundle(payload, user, canonicalUrl.state)`), não só `compare`.
- **Depth check:** não criar `plazaMapFilters.ts` pass-through; não duplicar clauses de `q`/`region`/… — só reusar `buildPlazaListWhere` de `plazaUi.ts`.
- **Teste:** int (ou unit com Payload mock) garantindo que com `region` (ou `kind`) o bundle só contém `ibgeCode`s das Praças filtradas; e que conjunto vazio → `null`. Precedente de access já comentado no loader (assessor).
- **Migration:** Sem migration, sem collection, sem server action.
## Dependências
- **Dura:** R2 superfícies core (mapa + filtros) — já entregue.
- **Suave:** A9 ([estimativa-votos-praca.md](estimativa-votos-praca.md)) — métrica 2026 passa a `expectedVotes ?? effectiveTotal`; B7 continua válido só com pledges até A9. B6 (`setStyle`) independente.
- Reusa: `buildPlazaListWhere` / `parsePlazaListParams` (`plazaUi.ts`), padrão overview em `plazaPageData.ts`, `PlazaMapPanel` / `BahiaMap`.
## Não escopo
- Reordenar mapa vs filtros / redesign do painel → **R6**.
- `BahiaMap` setStyle incremental → **B6** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md)).
- Hover / destaque + total de votos no polígono → **B10** ([hover-mapa-pracas.md](hover-mapa-pracas.md)).
- Polígonos por zona / painel de zona → **B8** ([poligonos-pracas-zona.md](poligonos-pracas-zona.md)).
- Novos filtros de lista; alterar semântica de `compare`.
## Rabbit holes
- **Filtro no cliente após carregar Bahia inteira.** Se alguém “só esconder no Leaflet”: vaza números no Flight e o Assessor ainda baixa dados fora do filtro. **Mitigação:** where no servidor.
- **FitBounds / zoom automático por filtro.** Explode em edge cases (TI pequena vs Salvador multi-zona). **Mitigação:** defer com gatilho.
- **Unificar list+overview+map num único fetch.** DRY prematuro (&lt;3 call sites já compartilham o where). **Mitigação:** só compartilhar o `Where`; fetch único só se profiling pós-B7 mostrar triplo load caro.
## Adiado com gatilho
- **`fitBounds` ao footprint filtrado.** Revisitar quando coordenação filtrar TI/região e reportar que a Bahia inteira no viewport atrapalha a leitura.
- **Mover filtros acima do mapa.** Revisitar no ciclo R6 se critique/smoke apontar ordem confusa.
## Referências
- `docs/roadmap.md` (Trilha B, item B7; Remodelagem R2)
- `src/app/(campaign)/campanha/(app)/pracas/page.tsx` — composição mapa + filtros + lista
- `src/utilities/plazaMapData.ts` — `loadScopedPlazas` / `loadPlazaMapBundle` (hoje `where: {}`)
- `src/utilities/plazaUi.ts` — `buildPlazaListWhere`, `PlazaListState`
- `src/utilities/plazaPageData.ts` — precedente overview filtrado (`pagination: false`)
- `docs/plans/mapa-bahia-geometrias.md` — contrato histórico “mapa escopado pelos mesmos filtros”
- `docs/plans/remodelagem-pracas.md` — mapa v1 / access do assessor
- AGENTS.md — Campaign auth, `overrideAccess: false`, naming
- `PRODUCT.md` / `DESIGN.md` — Field Desk; mapa como instrumento de coordenação, não dashboard SaaS

View file

@ -0,0 +1,130 @@
# Pixel do Meta (Facebook) nos abaixo-assinados
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Site público)
Impeccable: B — encaixe no admin `Petition` + script na página `/abaixo-assinado/[id]` e no sucesso do `PetitionForm`
Appetite: ~0,5–1 dia eng; migration + 1 campo admin + componente de script + disparo `Lead` no sucesso
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register `brand` no site público; tema `data-theme='petition'` na página). Admin Payload usa labels pt-BR do grupo `Abaixo-assinados` — sem redesign de paleta.
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (superfície mínima; sem shape longo).
Brief compacto:
- **Persona / contexto:** marketing digital monta campanha de ads no Meta e precisa ver assinaturas como conversões; editor no `/admin` cola o ID do Pixel no abaixo-assinado.
- **Job principal:** atribuir assinaturas reais a um Pixel por petição, sem o editor colar JavaScript arbitrário.
- **Estratégia de cor:** Restrained (tokens `petition` / admin Payload existentes).
- **Anti-goals:** textarea de snippet HTML; GTM/multi-vendor; cookie banner completo; CAPI/Advanced Matching nesta fatia.
## Contexto
A página pública `/abaixo-assinado/[id]` (`src/app/(frontend)/abaixo-assinado/[id]/page.tsx`) renderiza o documento `petition` e o formulário cliente `PetitionForm` (`src/components/PetitionForm.tsx`). Assinatura bem-sucedida chama `submitPetitionSignature` e abre `PetitionSuccessDialog` — hoje **sem** telemetria de ads.
A collection `Petition` (`src/collections/Petition.ts`, grupo admin `Abaixo-assinados`) tem `title`/`subtitle`/`body`/`enabled` e o grupo `form` (título, subtítulo, `consent`). Não há campo de tracking.
O time de marketing digital pediu (2026-07-21) um campo no formulário do abaixo-assinado no Payload para “adicionar o snippet do Pixel do Facebook” e acompanhar assinaturas no Events Manager / otimizar campanhas.
### O que o Meta Pixel exige (pesquisa)
Fontes: [Meta Pixel Get Started](https://developers.facebook.com/docs/meta-pixel/get-started) (atualizado 2026-06-30), [Conversion tracking](https://developers.facebook.com/docs/meta-pixel/implementation/conversion-tracking), [Deduplicate Pixel + CAPI](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/).
1. **Base code** no `<head>` (ou via `next/script`): carrega `fbevents.js`, chama `fbq('init', PIXEL_ID)` e `fbq('track', 'PageView')`. O único dado que o editor precisa fornecer é o **Pixel ID** (número do Dataset no Events Manager) — o restante do “snippet” é boilerplate fixo.
2. **Evento de conversão para lead-gen:** `fbq('track', 'Lead', {…})` **somente após** a assinatura bem-sucedida — nunca no carregamento da página do formulário (erro clássico que infla leads 5–10× e corrompe o otimizador).
3. **Parâmetros úteis no `Lead`:** `content_name` (título/id da petição). `eventID` opcional agora; obrigatório se/quando houver Conversions API (CAPI) para deduplicação.
4. **CAPI + Advanced Matching** (e-mail/telefone hasheados no servidor) melhoram Event Match Quality (~6.0+), mas exigem secret de access token, hashing LGPD-consciente e dedup — fora do appetite desta fatia.
5. **LGPD:** o Pixel define cookies Meta (`_fbp` etc.) e envia sinais de navegação/conversão. Transparência em `/privacidade` (e, se o jurídico pedir, no texto de Consent da petição) é requisito de produto; cookie banner site-wide fica adiado.
## Objetivos
- Campo opcional no admin de `petition` para o marketing configurar o Pixel **por abaixo-assinado**.
- Na página pública: carregar base code + `PageView` quando o ID estiver presente.
- No sucesso da assinatura: disparar exatamente um `Lead` (não no page load).
- Guardrails: **nunca** persistir nem executar HTML/JS colado pelo editor; validar ID numérico; sem env var obrigatória (config por documento); migration com `push: false`; cache já revalidado por `revalidateDocumentById` no `afterChange` existente.
- Sem novo `Consent` key nesta fatia (petição já exige Consent de assinatura); sem CAPI.
## Decisões travadas
- **Persistir só `facebookPixelId` (texto, dígitos), não o snippet HTML.** O “snippet” que o Meta mostra no Events Manager é boilerplate + ID; colar HTML no admin é XSS / injection e quebra CSP. O app injeta o base code controlado em código. **Rejeitado:** `textarea`/`code` com snippet livre; campo `html`/`richText` de tracking; GTM container ID como substituto nesta fatia.
- **Pixel por petição (campo no documento `petition`), não global do site.** Campanhas de ads costumam ser por abaixo-assinado/URL; evita Pixel único vazar PageViews de notícias/home. **Rejeitado:** só env `NEXT_PUBLIC_FACEBOOK_PIXEL_ID` site-wide; Global `SiteSettings` único (pode ser fill-in futuro se marketing unificar Dataset).
- **v1 = browser Pixel only, com os dois eventos:** `PageView` no load da página da petição **e** `Lead` no sucesso do form (confirmado com produto 2026-07-21). Padrão Meta: retargeting de visitantes + conversão de assinatura. Transparência de cookies/Meta fica no polish de `/privacidade` (dependência suave). **Rejeitado nesta fatia:** só `Lead` sem `PageView`; Conversions API; Advanced Matching com e-mail/telefone no browser/servidor; eventos `CompleteRegistration`/`Contact` além de `Lead`; cookie banner / CMP.
- **`Lead` só após `submitPetitionSignature` ok** (mesmo caminho que abre o diálogo de sucesso). **Rejeitado:** `Lead` no `onSubmit` antes da resposta; `Lead` no page load; thank-you page dedicada (não existe hoje — o sucesso é modal same-page).
- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (`facebookPixelId`, `MetaPixel`, `trackMetaLead`); labels admin e copy em pt-BR (“ID do Pixel do Meta (Facebook)”).
## Questões em aberto
- **Um Pixel por petição vs. mesmo Dataset em várias petições?** **Opções:** A) permitir o mesmo ID em N petições (só conteúdo do `Lead` diferencia) | B) forçar uniqueness. **Recomendação:** A — Dataset único da campanha é o caso comum; `content_name` carrega `petition.id` / título.
## Abordagem proposta
```mermaid
flowchart LR
Admin["Payload admin<br/>petition.facebookPixelId"]
Page["abaixo-assinado/[id]<br/>Server Component"]
Pixel["MetaPixel client<br/>next/script + fbq init/PageView"]
Form["PetitionForm"]
Action["submitPetitionSignature"]
Lead["trackMetaLead<br/>fbq track Lead"]
Meta["Meta Events Manager"]
Admin --> Page
Page --> Pixel
Pixel --> Meta
Form --> Action
Action -->|sucesso--> Lead
Lead --> Meta
```
Componentes:
- **Campo `facebookPixelId`** em `src/collections/Petition.ts` (irmão do grupo `form` ou dentro de um group `tracking` com label “Rastreamento / Ads”): `type: 'text'`, opcional, `admin.description` explicando “cole só o ID numérico do Events Manager (ex.: 123456789012345), não o snippet HTML completo”. `validate` rejeita não-dígitos / comprimento fora de ~5–20. Sem `required`.
- **`isValidFacebookPixelId` / `normalizeFacebookPixelId`** (em `src/lib/facebookPixel.ts` ou `src/utilities/facebookPixel.ts`): helper puro testável (unit) — depth check: um módulo pequeno que esconde a regex e a montagem do init; não inventar “AnalyticsService”.
- **`MetaPixel`** (client, `src/components/MetaPixel.tsx`): se `pixelId` válido, renderiza `next/script` (`strategy="afterInteractive"`) que carrega o padrão Meta (`fbevents.js` + `fbq('init', id)` + `fbq('track', 'PageView')`) e noscript img opcional. Tipar `window.fbq` localmente (sem `@types` de terceiros se não existir no repo).
- **`trackMetaLead`** (mesmo módulo lib/utilities ou export do componente): `fbq('track', 'Lead', { content_name }, { eventID? })` se `window.fbq` existir; no-op seguro se Pixel não carregou / adblock.
- **`PetitionForm`**: após sucesso de `submitPetitionSignature`, chamar `trackMetaLead` com `content_name` = título ou id da petição (e `eventID` = UUID gerado no cliente **só se** quisermos preparar CAPI depois — nesta fatia opcional; preferir gerar UUID e passar já, custo zero, facilita fatia futura).
- **Página** `abaixo-assinado/[id]/page.tsx`: passar `petition.facebookPixelId` para `<MetaPixel />` quando presente; incluir o campo no `select`/cache se a leitura de documento filtrar campos (hoje `getCachedDocumentById` carrega o doc — confirmar após `generate:types`).
- **Migration**: `pnpm migrate:create add_petition_facebook_pixel_id` — coluna nullable em `petition` (+ drafts/versions se Payload espelhar). Sem backfill.
- **Testes:** unit no validador de ID; int opcional na collection validate se o repo já testa Petition; E2E não obrigatório (terceiro externo) — smoke manual com [Meta Pixel Helper](https://developers.facebook.com/docs/meta-pixel/support/pixel-helper).
## Dependências
- Nenhuma de outro plano de trilha A/B/C/D.
- **Suave:** polish do Aviso de Privacidade (Site público / Onda 0 textos finais) deve mencionar cookies/ferramentas de ads (Meta) quando Pixel estiver em uso — não bloqueia a engenharia do campo.
- Reusa: `revalidateDocumentById` já no `afterChange` de `Petition`; `PetitionForm` success path; `next/script`.
## Não escopo
- Conversions API (CAPI) / access token / Event Match Quality server-side → fatia futura com gatilho abaixo.
- Google Tag Manager, TikTok Pixel, Google Ads gtag — outro item se marketing pedir.
- Cookie consent banner site-wide / CMP.
- Pixel no layout global do site (home, notícias, `/campanha`).
- Thank-you page dedicada / redirect pós-assinatura.
- Alterar schema de `signature`/`contact` para guardar `event_id` Meta.
## Rabbit holes
- **Colar snippet HTML “como o Meta manda”.** Se alguém “só completar” o pedido literal do marketing: XSS + HTML no CMS. **Mitigação:** campo só ID + description no admin + validação server-side.
- **CAPI “já que estamos no servidor”.** Explode em secrets, hashing PII, dedup, retries. **Mitigação:** defer com gatilho.
- **Cookie banner completo por causa deste campo.** Projeto de CMP. **Mitigação:** transparência em `/privacidade` + decisão jurídica sobre PageView; não construir banner aqui.
- **Advanced Matching no browser com e-mail/telefone em claro.** Vazamento + LGPD. **Mitigação:** fora; CAPI hasheado só em fatia futura.
## Adiado com gatilho
- **Conversions API + dedup com `event_id` compartilhado.** Revisitar quando: marketing reportar perda material de atribuição (iOS/adblock) **ou** Event Match Quality &lt; 6 no Dataset em uso com volume real de ads.
- **Pixel / Dataset global em `SiteSettings`.** Revisitar quando: o mesmo ID for colado em ≥3 petições e o admin reclamar de repetição.
- **Cookie banner / CMP.** Revisitar quando: jurídico exigir bloqueio pré-consent de cookies de ads (não só transparência).
## Referências
- `docs/roadmap.md` (Site público)
- `src/collections/Petition.ts` — schema admin do abaixo-assinado
- `src/app/(frontend)/abaixo-assinado/[id]/page.tsx` — página pública
- `src/components/PetitionForm.tsx` — sucesso da assinatura
- `src/app/(frontend)/actions/submitPetitionSignature.ts` — escrita transacional (inalterada no fluxo de tracking cliente)
- Meta: [Get Started](https://developers.facebook.com/docs/meta-pixel/get-started) · [Conversion tracking](https://developers.facebook.com/docs/meta-pixel/implementation/conversion-tracking) · [Deduplication](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/)
- Next.js: `next/script` + `strategy="afterInteractive"` (App Router)
- AGENTS.md — naming, migrations `push: false`, Consent por chave (não inventar paralelo aqui), revalidate de documentos
- `PRODUCT.md` / `DESIGN.md` — canal público / tema `petition`

View file

@ -0,0 +1,131 @@
# Polígonos das Praças-zona (Salvador / Camaçari)
Status: rascunho
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B8)
Impeccable: B — encaixe em `/campanha/pracas/[slug]` (lista de bairros) + `PlazaMapPanel`/`BahiaMap` (polígonos); sem rota nova
Appetite: ~2,5–3,5 dias eng; F1 catálogo+UI (~1 dia) shipável sozinha; F2 dissolve+TopoJSON+mapa (~1,5–2,5 dias)
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `PlazaMapPanel`, `BahiaMap`, cards do detalhe da Praça).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (superfícies já existem; F1 é bloco de leitura; F2 estende o mapa).
Brief compacto:
- **Persona / contexto:** Assessor (ou Alex) abre uma Praça-zona de Salvador/Camaçari e precisa saber **quais bairros** ela cobre; no mapa, quer ver a zona colorida sozinha — não o município inteiro agregado.
- **Job principal:** geografia operacional da Praça-zona legível (bairros) e desenhável (polígono) sem inventar camada estadual de ZE.
- **Estratégia de cor:** Restrained; coroplético existente (sequencial / divergente) inalterado em tokens.
- **Anti-goals:** não geocodificar seções; não PostGIS; não redesign do mapa; não Praça = seção; não segundo sistema de geometrias paralelo ao padrão B2.
## Contexto
As Praças-zona (19 em Salvador ZE 1–19 + 2 em Camaçari ZE 170/171) são unidades operacionais reais, mas o mapa ainda pinta **só o polígono municipal** e lista as zonas num painel textual:
- `PlazaMapPanel` copy explícita: “Zonas não têm polígono oficial — Salvador e Camaçari aparecem agregadas…”
- `loadPlazaMapBundle` (`plazaMapData.ts`) soma votos da zona no `ibgeCode` do município; `zoneBreakdown` é a única visão por Praça-zona.
- [remodelagem-pracas.md](remodelagem-pracas.md) e o roadmap marcavam polígonos de zona como rabbit hole / fora de escopo (B4 histórico = dissolver municípios membros de ZE multi-município — **problema diferente**).
Pedido de produto (2026-07-21): mapear polígonos das Praças-zona; bairros por zona já são úteis na página da Praça; pesquisar Camaçari; se não houver fonte online de polígonos, planejar construção a partir dos bairros.
### Pesquisa de fontes (2026-07-21)
| Necessidade | Salvador | Camaçari |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Lista zona → bairros/localidades | **Oficial:** TRE-BA — Resolução Administrativa nº 2/2017 (Anexo I, circunscrição por bairro das ZE 1–19) + PDFs de rezoneamento em [tre-ba.jus.br/servicos-eleitorais/rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento). Ainda **não** está no repo. | **Não há** lista oficial zona↔bairro comparável. Aprox.: relação TRE de **locais de votação** por zona (bairro/povoado no endereço) — ZE 171 concentra Abrantes, Arembepe, orla/povoados; ZE 170 a sede urbana — + curadoria manual com a coordenação. |
| Polígonos oficiais de ZE | **Não existem** no TSE/TRE-BA (só tabular). Consenso comunitário ([mapaslivres/zonas-eleitorais](https://github.com/mapaslivres/zonas-eleitorais)): construir por cruzamento. | Idem. |
| Polígonos de bairro | IBGE Censo 2022 `BA_bairros_CD2022` ([geoftp](https://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_de_setores_censitarios__divisoes_intramunicipais/censo_2022/bairros/shp/UF/)); alternativa municipal GeoSalvador. Bahia tem **455** bairros na malha IBGE — cobertura intramunicipal parcial; gaps exigem fallback. | IBGE pode ou não cobrir todos os nomes locais; gaps + orla/povoados → polígonos manuais ou casco aproximado. |
## Objetivos
- **F1:** catálogo estático versionado `zoneNumber × city → neighborhoods[]` (Salvador completo a partir do TRE; Camaçari derivado + curado), com fixture/teste; bloco “Bairros desta Praça” no detalhe `/campanha/pracas/[slug]` quando `kind === 'zona'`.
- **F2:** TopoJSON das 21 Praças-zona (`properties: { plazaSlug, zoneNumber, cityCode }`) por **dissolução** dos polígonos de bairro mapeados; script re-executável no padrão B2; mapa pinta Praça-zona individualmente (não só o município).
- Guardrails: sem migration, sem collection, sem Consent, sem PostGIS; leitura com access existente; geometrias estáticas no repo; disclaimer de aproximação (não é limite oficial TSE).
## Decisões travadas
- **Item novo B8 (não reviver B4 nem absorver em B7).** B4 era “camada ZE estadual por dissolução de municípios membros”; aqui o gap é **intra-municipal** (19+2 Praças dentro de 2 municípios). B7 é filtro URL do mapa. (2026-07-21, roadmap-item.) **Rejeitado:** reabrir B4 com o desenho antigo (não resolve Salvador/Camaçari); fill-in sem plano (escopo explode em geoprocessamento).
- **F1 shipável sem F2.** Catálogo + UI na Praça entregam valor imediato e são pré-requisito do dissolve. **Rejeitado:** só polígonos sem lista (assessor não vê bairros no detalhe); só lista sem plano de polígono (não responde ao pedido do mapa).
- **Fonte Salvador = TRE-BA RA 02/2017 (+ PDFs de rezoneamento se divergirem).** Circunscrição oficial por bairro. **Rejeitado:** scrapers de “Onde Votar” / e-Título (frágil, PII-adjacent); aba SALVADOR da planilha E5 (DE-PARA não-oficial).
- **Fonte Camaçari = locais de votação TRE → localidades + curadoria.** Sem lista oficial zona↔bairro. **Rejeitado:** inventar paridade com Salvador sem evidência; esperar shapefile TRE (não existe).
- **Polígonos = dissolve de bairros (IBGE e/ou malha municipal), não geocodificar seções.** Mesmo padrão de dissolução já usado em TIs (B2). Gaps: polígono manual leve ou omitir pedaço com nota. **Rejeitado:** geocodificar milhares de seções → casco (rabbit hole da remodelagem); pedir shapefile ao TRE neste ciclo; PostGIS.
- **Escopo geográfico = só as 21 Praças-zona.** Demais ZE da BA continuam fora. **Rejeitado:** camada ZE estadual no mapa.
- **i18n e naming** (AGENTS.md): identificadores `plazaZoneNeighborhoods`, `buildPlazaZoneGeometries`, `bahia-plaza-zones.topo.json`, `getPlazaZoneFeature(slug)`; strings UI em pt-BR.
## Questões em aberto
- **Malha de bairros: IBGE 2022 vs GeoSalvador (só capital)?** **Opções:** A) só IBGE BA | B) GeoSalvador + IBGE/manual Camaçari | C) só desenho manual. **Recomendação:** A na F2 com reconciliação de nomes TRE↔`NM_BAIRRO`; se cobertura de Salvador for insuficiente no spike de ½ dia, pivotar B para a capital. Validar no spike antes de lock de artefato.
- **Camaçari F2 se a malha de bairros for pobre?** **Opções:** A) dois polígonos manuais grossos (sede × orla/Abrantes) | B) adiar F2 Camaçari | C) casco por geocoding de locais de votação. **Recomendação:** A se F1 já tiver localidades estáveis; B se a coordenação aceitar mapa só Salvador; C só se A falhar (ainda sem seção-a-seção).
- **Chave do coroplético no mapa: `plazaSlug` ou continuar `ibgeCode` + overlay?** **Opções:** A) mode/`values` por `plazaSlug` para Praças-zona + município para o resto | B) split Salvador/Camaçari em features de zona substituindo o polígono municipal. **Recomendação:** B no footprint SSA/CMS (um polígono por Praça-zona; município some nesses dois) e A/`ibgeCode` no restante da Bahia — evita dupla contagem visual. _(assumido — validar no craft)_
## Abordagem proposta
```mermaid
flowchart TD
treSSA["TRE-BA RA 02/2017<br/>ZE → bairros Salvador"] --> catalog["src/lib/plazaZoneNeighborhoods.ts<br/>+ fixture"]
treCMS["Locais de votação ZE 170/171<br/>+ curadoria"] --> catalog
catalog --> ui["Bloco Bairros<br/>pracas/slug overview"]
catalog --> reconcile["Reconciliação nomes<br/>↔ NM_BAIRRO IBGE/Geo"]
ibge["BA_bairros_CD2022<br/>(ou GeoSalvador)"] --> script["scripts/build-plaza-zone-geometries.mjs"]
reconcile --> script
script --> topo["bahia-plaza-zones.topo.json"]
topo --> map["BahiaMap + plazaMapData<br/>Praça-zona = feature própria"]
```
Componentes:
- **`src/lib/plazaZoneNeighborhoods.ts`** — mapa estático `{ city: 'Salvador'|'Camaçari', zoneNumber, neighborhoods: string[] }` indexado por `plazaSlug` (via `plazaCatalog`); helpers `neighborhoodsForPlazaSlug(slug)`; cabeçalho de proveniência (URL + data da resolução / lista de locais). Fixture `tests/fixtures/plaza-zone-neighborhoods.official.json` + int (19+2 zonas, sem bairro órfão duplicado entre ZE do mesmo município quando a fonte for exclusiva).
- **UI detalhe** (`pracas/[slug]/page.tsx` overview): se `view.kind === 'zona'`, card/lista “Bairros desta Praça” a partir do helper (sem Payload). Copy: fonte TRE / aproximação Camaçari.
- **`scripts/build-plaza-zone-geometries.mjs`** (`pnpm build:plaza-zone-geometries`): baixa/cache malha de bairros (padrão `downloadToBuffer` / cache `data/geometries/`), filtra município, dissolve por zona via catálogo reconciliado (`topojson` merge como TIs), emite `src/lib/geometries/bahia-plaza-zones.topo.json`; reporta bairros sem match.
- **`src/lib/bahiaGeometries.ts`** (ou módulo irmão profundo, sem pass-through): `getPlazaZoneFeature(slug)` + tipo da topologia; lazy com o mapa (alinhar B5 F1 se ainda aberto).
- **`plazaMapData.ts` / `PlazaMapPanel` / `BahiaMap`:** valores por `plazaSlug` (ou chave estável da feature) para Praças-zona; remover/ajustar copy “sem polígono oficial”; manter disclaimer de aproximação. `zoneBreakdown` pode permanecer como lista ou virar highlight no mapa.
- **Depth check:** reusar padrão B2 (`bahiaGeometries`, `build-bahia-geometries`, fixtures); não criar collection `plazaGeometry`; não segundo Leaflet stack.
- **Migration:** Sem migration, sem collection, sem server action.
### Fases
| Fase | Entrega | Appetite |
| ---- | ------------------------------------------------ | ------------- |
| F1 | Catálogo + testes + bloco na Praça | ~1 dia |
| F2 | Spike cobertura malha → script → TopoJSON → mapa | ~1,5–2,5 dias |
## Dependências
- **Dura:** R2 (mapa + detalhe Praça) — entregue.
- **Suave:** padrão B2 geometrias; B7 (filtro do mapa) independente — B8 deve respeitar o mesmo `buildPlazaListWhere` quando B7 existir.
- Reusa: `plazaCatalog`, `PlazaMapPanel`, `BahiaMap`, `plazaMapData`, `bahiaGeometries` / script B2.
## Não escopo
- Geocodificação de seções / Praça = seção → permanece **fora de escopo** no roadmap.
- Camada ZE para toda a Bahia / B4 histórico (dissolve multi-município) — não reabrir.
- E5 Salvador por bairro como unidade operacional (supersedido; aqui bairro é **atributo** da Praça-zona).
- Filtro URL do mapa → **B7**; `setStyle` incremental → **B6**.
- PostGIS; edição de geografia no admin.
## Rabbit holes
- **Geocodificar seções “só para fechar o polígono”.** Semanas + cascos frágeis. **Mitigação:** dissolve por bairro + gaps manuais; seções fora deste item.
- **Reconciliação perfeita de 100% dos nomes TRE↔IBGE.** Explode o appetite. **Mitigação:** cobertura mínima documentada (ex. ≥90% Salvador); lista de unmatched no script; gaps manuais só nos críticos.
- **Desenhar 19 polígonos Salvador à mão no QGIS sem catálogo.** Irrepetível e sem teste. **Mitigação:** F1 obrigatória; manual só para gaps Camaçari/ unmatched.
- **Tratar polígono derivado como limite oficial TSE.** Risco jurídico/comunicação. **Mitigação:** copy “aproximação a partir de bairros / locais de votação; não é limite oficial”.
## Adiado com gatilho
- **Malha GeoSalvador em vez de IBGE.** Revisitar quando: spike F2 mostrar unmatched críticos em Salvador (>10% bairros TRE sem polígono).
- **Refino fino de Camaçari (povoados).** Revisitar quando: coordenação de campo reportar confusão operacional entre ZE 170/171 no mapa.
- **Camada ZE estadual (B4 antigo).** Revisitar quando: produto pedir mapa de ZE fora de SSA/CMS (improvável neste ciclo).
## Referências
- `docs/roadmap.md` (B8; fora de escopo estreitado; supersedido B4)
- `docs/plans/mapa-bahia-geometrias.md` — padrão estático + dissolução; B4 histórico (multi-município)
- `docs/plans/remodelagem-pracas.md` — rabbit hole de polígono de zona; 21 Praças-zona
- `docs/plans/mapa-pracas-filtrado.md` — B7 (filtro; independente)
- `src/utilities/plazaMapData.ts`, `src/components/campaign/PlazaMapPanel.tsx`, `src/lib/plazaCatalog.ts`, `src/lib/bahiaGeometries.ts`, `scripts/build-bahia-geometries.mjs`
- TRE-BA RA 02/2017 Anexo I; [rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento); relação de locais de votação TRE-BA 2024
- IBGE `BA_bairros_CD2022.zip` (geoftp)
- AGENTS.md — geometrias B2, naming, Praças-zona
- `PRODUCT.md` / `DESIGN.md` — Field Desk / Restrained

View file

@ -1,6 +1,6 @@
# Roadmap — Teqo
Atualizado em: 2026-07-20 (Remodelagem Praças R0–R5 registrada como caminho crítico; itens núcleo-cêntricos supersedidos/reenquadrados)
Atualizado em: 2026-07-21 (B10 click-nav; B11 escala % mapa; A9→B9; B8; Fill-in filtros; B7; Pixel Meta)
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
@ -68,25 +68,36 @@ flowchart TD
TSE2026["TSE candidaturas 2026<br/>(após 15/08)"] --> A6["A6 Dobradinha 2026<br/>(reenquadrada p/ Praça)"]
R4 -.baseline por Praça.-> A6
R2 --> D2["D2 Push + sino"]
R2 --> A9["A9 Estimativa votos da Praça"]
A9 --> B9["B9 Edição rápida lista Praças"]
R2 -.-> B7["B7 Mapa filtrado pela lista"]
R2 -.-> B8["B8 Polígonos Praças-zona<br/>(SSA/CMS)"]
R2 -.-> B10["B10 Hover/tap mapa<br/>(destaque + nav)"]
R2 -.-> B11["B11 Escala % válidos<br/>no mapa"]
A9 -.métrica 2026.-> B7
A9 -.métrica 2026.-> B10
A9 -.numerador 2026.-> B11
JUR -.chave push.-> D2
C2prod --> C5["C5 GOTV (validar)"]
```
Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2): **B7**, **B10**, **B11**, fill-ins da lista, Pixel Meta (site público). **A9** bloqueia **B9** (seta dura).
### Sequência por janela
**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; R6 critique/polish; Onda 0 jurídica em paralelo.
**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **A9** estimativa de votos da Praça → **B9** edição rápida na lista (Assessores / Tendência / total esperado); **B7** mapa filtrado, **B10** hover+click-nav e **B11** escala % dos válidos em paralelo; R6 critique/polish; Onda 0 jurídica em paralelo.
**Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; D2 se sobrar folga.
**Janela 3 — 16/08 → set:** A6 dobradinha (pós-TSE 15/08), B6 `setStyle` incremental (mapa com troca de ano/métrica frequente), débitos sobreviventes (abaixo).
**Janela 3 — 16/08 → set:** A6 dobradinha (pós-TSE 15/08), B6 `setStyle` incremental (mapa com troca de ano/métrica frequente), **B8** polígonos das Praças-zona Salvador/Camaçari (F1 catálogo de bairros shipável antes; F2 dissolve), débitos sobreviventes (abaixo).
**Janela 4 — set → 04/10:** C5 GOTV _(validar)_, congelamento ~20/09 (só bugfix/dados).
### Cortes seguros / não cortáveis
**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo).
**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo); **A9** (sem total esperado da Praça, lista/mapa mentem onde não há liderança — schema do modelo operacional).
**Cortes seguros** (se o prazo apertar, nesta ordem): R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; B6; débitos/fill-ins.
**Cortes seguros** (se o prazo apertar, nesta ordem): R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; B6; B7 (mapa ainda mostra escopo de access sem filtro de UI); **B11** (mapa continua em escala absoluta); **B10** (mapa legível sem hover); **B9** (com A9, edição do total fica só no `/editar`); **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); Pixel Meta nos abaixo-assinados (ads do site público; vertical `/campanha` independente); débitos/fill-ins.
## Já entregue (resumo)
@ -95,29 +106,35 @@ flowchart TD
## Supersedidos pela remodelagem (2026-07-20)
| Item antigo | Destino |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| A5 competitiva (parte aberta) | Absorvido por **R4** (comparativo multi-candidato por Praça é a versão pedida pela coordenação) |
| A7 F3–F5 (escala pós-A4) | Morre com os loaders de núcleo; fetch único lista+mapa vira requisito nativo do mapa de Praças (R2) |
| A8+ (escala pós-A8) | Reavaliar pós-R2 se o Eleitorado IBGE for portado ao detalhe da Praça |
| B4 zonas TSE no mapa | Rabbit hole documentado no plano-mestre (agregação municipal + painel por zona; polígono de zona só com decisão explícita) |
| C4 demandas | Absorvido por **R3** (agora nascem da liderança, com workflow assessor→coordenador geral e comprovantes staff-only) |
| C10 / C11 (escala apoiadores/planos) | Reavaliar pós-R2/R3 nas superfícies adaptadas; gatilhos originais (base nominal crescendo / volume medido) continuam valendo |
| E5 Salvador por bairro | Praças-zona de Salvador cobrem a granularidade; bairro só com pedido real pós-eleição |
| E6 (escala pós-E1) / E7 (escala pós-E2) | Overview de núcleos e trend VM morrem; débitos int reavaliar pós-R4 |
| FD+ / FD2 (Field Desk pós-critique) | Superfícies redesenhadas em R2; princípios (glossário inline, filas, bulk) entram no critique de R5 |
| VR+ (visitados recentemente) | Painel portado para Praças em R2; débito de refresh mantém gatilho original |
| Listas globais (lideranças/atualizações) | CRM de liderança já nasce global em R2 (liderança é 1 registro por pessoa) |
| Item antigo | Destino |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| A5 competitiva (parte aberta) | Absorvido por **R4** (comparativo multi-candidato por Praça é a versão pedida pela coordenação) |
| A7 F3–F5 (escala pós-A4) | Morre com os loaders de núcleo; fetch único lista+mapa vira requisito nativo do mapa de Praças (R2) |
| A8+ (escala pós-A8) | Reavaliar pós-R2 se o Eleitorado IBGE for portado ao detalhe da Praça |
| B4 zonas TSE no mapa | Rabbit hole documentado no plano-mestre; **B8** reabre só Praças-zona SSA/CMS (dissolve por bairro — não o B4 multi-município) |
| C4 demandas | Absorvido por **R3** (agora nascem da liderança, com workflow assessor→coordenador geral e comprovantes staff-only) |
| C10 / C11 (escala apoiadores/planos) | Reavaliar pós-R2/R3 nas superfícies adaptadas; gatilhos originais (base nominal crescendo / volume medido) continuam valendo |
| E5 Salvador por bairro | Praças-zona de Salvador cobrem a granularidade; bairro só com pedido real pós-eleição |
| E6 (escala pós-E1) / E7 (escala pós-E2) | Overview de núcleos e trend VM morrem; débitos int reavaliar pós-R4 |
| FD+ / FD2 (Field Desk pós-critique) | Superfícies redesenhadas em R2; princípios (glossário inline, filas, bulk) entram no critique de R5 |
| VR+ (visitados recentemente) | Painel portado para Praças em R2; débito de refresh mantém gatilho original |
| Listas globais (lideranças/atualizações) | CRM de liderança já nasce global em R2 (liderança é 1 registro por pessoa) |
## Próximos — sobreviventes e pós-remodelagem
- **A6** dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · reenquadrar para Praça · [plano](plans/insight-dobradinha-2026.md)
- **A9** Estimativa de votos da Praça (`expectedVotes` — total esperado staff-only, distinto da soma de pledges e de `voteGoals`) · depende de R2 · bloqueia B9 · appetite ~1–1,5 dia · [plano](plans/estimativa-votos-praca.md)
- **B5 F2–F3** cache CLI compartilhado + factory mun/TI (scripts continuam) · [plano](plans/escala-dry-pos-b2.md)
- **B6** `BahiaMap` setStyle incremental ao trocar métrica/ano · gatilho: troca frequente no mapa de Praças · [plano](plans/escala-dry-pos-b3.md)
- **B7** Mapa das Praças filtrado pela mesma URL da lista (`buildPlazaListWhere`) · depende de R2 (entregue) · suave: A9 (métrica 2026) · paralelizável agora · [plano](plans/mapa-pracas-filtrado.md)
- **B8** Polígonos das Praças-zona (Salvador ZE 1–19 / Camaçari ZE 170–171): F1 catálogo zona→bairros + UI na Praça; F2 dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável (F2) · [plano](plans/poligonos-pracas-zona.md)
- **B9** Edição rápida na lista de Praças (Assessores, Tendência, **Votos estimados = `expectedVotes`**) · **depende de A9** · appetite ~1–1,5 dia · [plano](plans/edicao-rapida-lista-pracas.md)
- **B10** Hover/tap no Mapa das Praças: destaque + votos; desktop click navega; mobile 2º tap na selecionada navega · depende de R2 · suave: A9/B6/B7/B8 · paralelizável agora · appetite ~0,5–1 dia · [plano](plans/hover-mapa-pracas.md)
- **B11** Escala de cor por **% dos votos válidos** da Praça (0–100%, não pelo total da maior Praça) · depende de R2 (entregue) · suave: A9 (numerador 2026) · paralelizável agora · appetite ~0,5–1 dia · [plano](plans/escala-percentual-mapa-pracas.md)
- **C5** operação dia D / GOTV _(validar com produto)_ · design [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) · depende de C2 dados reais
- **D2** push + sino in-app · soft: chave `campanha-notificacoes-push` (Onda 0) · [plano](plans/notifications.md)
- **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · gatilho: antes de 16/08
- **Fill-ins:** O0+ ([plano](plans/escala-dry-pos-onda0.md)) · RS+ auth read leve + shells de senha ([plano](plans/escala-dry-pos-reset-senha-perfil.md)) · higiene PascalCase
- **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · gatilho: antes de 16/08 · (opcional: mover filtros acima do mapa — ver B7)
- **Fill-ins:** filtros auto na lista de Praças (debounce 1s no `q`, selects já imediatos; remove Buscar) · [plano](plans/filtros-auto-pracas.md) · O0+ ([plano](plans/escala-dry-pos-onda0.md)) · RS+ auth read leve + shells de senha ([plano](plans/escala-dry-pos-reset-senha-perfil.md)) · higiene PascalCase
## Bloqueadores atuais
@ -135,7 +152,8 @@ flowchart TD
**Próximos:**
- Textos finais de privacidade + polish O0+ (revalidate globals, DRY Lexical)
- **Pixel do Meta (Facebook) nos abaixo-assinados** — campo `facebookPixelId` no admin `petition` + `PageView`/`Lead` na página pública (não colar snippet HTML) · appetite ~0,5–1 dia · paralelizável · [plano](plans/pixel-meta-abaixo-assinado.md)
- Textos finais de privacidade + polish O0+ (revalidate globals, DRY Lexical) — mencionar cookies/Meta quando o Pixel estiver em uso _(suave com o item acima)_
- `Pages` institucionais (bio, mandato, propostas) + hero/copy editáveis
- Agenda/multimídia via links oficiais; CTA Doar → QueroApoiar
- Migrar Consent dos fluxos públicos para chave estável
@ -152,7 +170,7 @@ flowchart TD
## Fora de escopo (por enquanto)
- Serviço Rust separado; self-host/Coolify enquanto Vercel atender; doações in-app
- Polígonos por zona eleitoral (geocodificação de seções); Praça = seção eleitoral
- Geocodificação de seções eleitorais / Praça = seção (polígonos **aproximados** das 21 Praças-zona SSA/CMS = **B8**, sem seções)
- PWA do site/`/admin`; PostGIS sem query espacial real
- WhatsApp Business API / disparo em massa; previsão estatística de votos neste ciclo
- Import automático de planilhas de projeção (decisão E4 mantida: dados estratégicos via UI)