diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index a4d9aad9..4f6c4fa5 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -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). diff --git a/docs/plans/edicao-rapida-lista-pracas.md b/docs/plans/edicao-rapida-lista-pracas.md new file mode 100644 index 00000000..0f1018d6 --- /dev/null +++ b/docs/plans/edicao-rapida-lista-pracas.md @@ -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 diff --git a/docs/plans/escala-percentual-mapa-pracas.md b/docs/plans/escala-percentual-mapa-pracas.md new file mode 100644 index 00000000..e27285ca --- /dev/null +++ b/docs/plans/escala-percentual-mapa-pracas.md @@ -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
por year × cityZone"] --> bundle["PlazaMapBundle.validVotesByYear"] + votes["valuesByYear
(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` 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>` (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 diff --git a/docs/plans/estimativa-votos-praca.md b/docs/plans/estimativa-votos-praca.md new file mode 100644 index 00000000..da3515b3 --- /dev/null +++ b/docs/plans/estimativa-votos-praca.md @@ -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 < 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 diff --git a/docs/plans/filtros-auto-pracas.md b/docs/plans/filtros-auto-pracas.md new file mode 100644 index 00000000..3cd3db90 --- /dev/null +++ b/docs/plans/filtros-auto-pracas.md @@ -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 (<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=®ion=…"] + 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 diff --git a/docs/plans/hover-mapa-pracas.md b/docs/plans/hover-mapa-pracas.md new file mode 100644 index 00000000..4732e1e5 --- /dev/null +++ b/docs/plans/hover-mapa-pracas.md @@ -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` 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 diff --git a/docs/plans/mapa-pracas-filtrado.md b/docs/plans/mapa-pracas-filtrado.md new file mode 100644 index 00000000..7381d854 --- /dev/null +++ b/docs/plans/mapa-pracas-filtrado.md @@ -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 (<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 diff --git a/docs/plans/pixel-meta-abaixo-assinado.md b/docs/plans/pixel-meta-abaixo-assinado.md new file mode 100644 index 00000000..47c8e887 --- /dev/null +++ b/docs/plans/pixel-meta-abaixo-assinado.md @@ -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 `` (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
petition.facebookPixelId"] + Page["abaixo-assinado/[id]
Server Component"] + Pixel["MetaPixel client
next/script + fbq init/PageView"] + Form["PetitionForm"] + Action["submitPetitionSignature"] + Lead["trackMetaLead
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 `` 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 < 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` diff --git a/docs/plans/poligonos-pracas-zona.md b/docs/plans/poligonos-pracas-zona.md new file mode 100644 index 00000000..d71e70ba --- /dev/null +++ b/docs/plans/poligonos-pracas-zona.md @@ -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
ZE → bairros Salvador"] --> catalog["src/lib/plazaZoneNeighborhoods.ts
+ fixture"] + treCMS["Locais de votação ZE 170/171
+ curadoria"] --> catalog + catalog --> ui["Bloco Bairros
pracas/slug overview"] + catalog --> reconcile["Reconciliação nomes
↔ NM_BAIRRO IBGE/Geo"] + ibge["BA_bairros_CD2022
(ou GeoSalvador)"] --> script["scripts/build-plaza-zone-geometries.mjs"] + reconcile --> script + script --> topo["bahia-plaza-zones.topo.json"] + topo --> map["BahiaMap + plazaMapData
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 diff --git a/docs/roadmap.md b/docs/roadmap.md index e5d056d8..39e7778e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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
(após 15/08)"] --> A6["A6 Dobradinha 2026
(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
(SSA/CMS)"] + R2 -.-> B10["B10 Hover/tap mapa
(destaque + nav)"] + R2 -.-> B11["B11 Escala % válidos
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)