mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
Filter Praças map by the same URL filters as the list (B7).
Wire buildPlazaListWhere into loadPlazaMapBundle, export PlazaListSearchParams from plazaUi, add integration tests, and mark B7 delivered in roadmap/docs.
This commit is contained in:
parent
b333609726
commit
936323c7ee
9 changed files with 136 additions and 49 deletions
|
|
@ -120,7 +120,7 @@ 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.
|
||||
- **B7 entregue (2026-07-21):** mapa em `/campanha/pracas` usa `buildPlazaListWhere` (lista/overview/mapa alinhados). Plano: [`docs/plans/mapa-pracas-filtrado.md`](../../../docs/plans/mapa-pracas-filtrado.md). Débito perf triplicado → **A9+**.
|
||||
- **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).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Escala e DRY pós-A9 (loader da lista de Praças)
|
||||
|
||||
Status: registrado no roadmap (Fase 1 pendente)
|
||||
Atualizado em: 2026-07-21 (`capture-review-debts` pós-A9 + `/simplify`)
|
||||
Status: registrado no roadmap (Fase 1 pendente; **B7 entregue 2026-07-21** — prioridade sobe)
|
||||
Atualizado em: 2026-07-21 (`capture-review-debts` pós-B7 + `/simplify`)
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, fill-in **A9+** pós-A9)
|
||||
Impeccable: A — N/A (sem superfície UI; otimização de loader)
|
||||
Appetite: ~0,5–1 dia eng (1 fase; PR único)
|
||||
|
|
@ -15,11 +15,15 @@ Uma passagem `/simplify` pós-entrega já limpou o que cabia em cleanup: form `o
|
|||
|
||||
O revisor de **performance** marcou como **maior que simplify** o follow-up abaixo. Sem registro, cada abertura de `/campanha/pracas` (staff) dispara **três** agregações de pledges sobre conjuntos sobrepostos.
|
||||
|
||||
1. **Query triplicada de pledges na página `/campanha/pracas`.** `page.tsx` chama em paralelo `loadPlazaListPageData` (pledges da página atual), `loadPlazaListOverviewData` (pledges de **todo** o conjunto filtrado) e `loadPlazaMapBundle` (pledges de **todas** as Praças do escopo do mapa). Cada loader invoca `aggregatePledgesByPlaza` de forma independente — padrão análogo ao E6 F1 em núcleos ([escala-dry-pos-e1.md](escala-dry-pos-e1.md)), mas na vertical Praças pós-remodelagem.
|
||||
1. **Query triplicada de pledges na página `/campanha/pracas`.** `page.tsx` chama em paralelo `loadPlazaListPageData` (pledges da página atual), `loadPlazaListOverviewData` (pledges de **todo** o conjunto filtrado) e `loadPlazaMapBundle` (pledges do conjunto filtrado do mapa — desde **B7** o escopo segue `buildPlazaListWhere`, não mais todas as Praças acessíveis). Cada loader ainda invoca `aggregatePledgesByPlaza` de forma independente.
|
||||
|
||||
**Já resolvido no simplify/critique (não reabrir):** parsing duplo do form `expectedVotes`; `hasStaffVoteData` derivado de `staffVoteTotal > 0`; `rollupPlazaStaffVotes` + `sumStaffPledgeEffectiveTotal`; `StaffPlazaVotesDisplay`; assinatura estreita de `resolvePlazaStaffVoteTotal`; testes unit do rollup.
|
||||
2. **Dois `find` de Praças no mesmo filtro (overview + mapa).** Ambos usam `buildPlazaListWhere` com `pagination: false` e `select` distinto — duplicação de round-trip por request quando o mapa está visível (staff, conjunto não vazio).
|
||||
|
||||
**Explicitamente fora (triage `capture-review-debts` 2026-07-21):** semântica lista (só `expectedVotes` manual) vs mapa/overview (fallback) — decisão de produto A9; merge de forms Estratégia + Votos no `/editar`; grid cosmético do `PlazaStrategyCard`; `loadPlazaPledges` em abas distintas do detalhe (uma aba por request); débitos adiados/não escopo do plano A9 (nota/autor, chip auto-sugerir, histórico, filtro `?expectedVotes=`); critique Impeccable formal por superfície → **R6**.
|
||||
**Já resolvido no simplify/critique (não reabrir):** parsing duplo do form `expectedVotes`; `hasStaffVoteData` derivado de `staffVoteTotal > 0`; `rollupPlazaStaffVotes` + `sumStaffPledgeEffectiveTotal`; `StaffPlazaVotesDisplay`; assinatura estreita de `resolvePlazaStaffVoteTotal`; testes unit do rollup. **Pós-B7 simplify:** `PlazaListSearchParams` exportado de `plazaUi.ts`; parse único no map loader (`loadScopedPlazas` recebe `PlazaListState`).
|
||||
|
||||
**Explicitamente fora (triage `capture-review-debts` 2026-07-21 pós-A9):** semântica lista (só `expectedVotes` manual) vs mapa/overview (fallback) — decisão de produto A9; merge de forms Estratégia + Votos no `/editar`; grid cosmético do `PlazaStrategyCard`; `loadPlazaPledges` em abas distintas do detalhe (uma aba por request); débitos adiados/não escopo do plano A9 (nota/autor, chip auto-sugerir, histórico, filtro `?expectedVotes=`); critique Impeccable formal por superfície → **R6**.
|
||||
|
||||
**Explicitamente fora (triage `capture-review-debts` 2026-07-21 pós-B7):** `Promise.all` nos loads TSE por ano em `plazaMapData` (micro-opt pré-existente; gatilho abaixo); testes int extras em `plazaMapData` (`region`/`leader`/`compare`) — cobertura opcional no próximo toque no loader; split do teste advisor por `kind` (legibilidade).
|
||||
|
||||
## Objetivos
|
||||
|
||||
|
|
@ -33,7 +37,7 @@ O revisor de **performance** marcou como **maior que simplify** o follow-up abai
|
|||
- **Um fill-in A9+, uma fase (F1).** Mesmo racional de A7/C6/A8+: um slug de plano, PR único. **Rejeitado:** item de roadmap separado `A10` — follow-up pós-entrega, não feature nova.
|
||||
- **Dependência dura de A9.** Só faz sentido com `expectedVotes`, `rollupPlazaStaffVotes` e os três loaders já no produto; não reabre escopo de schema ou UI de edição.
|
||||
- **F1 não remove overview nem mapa.** Otimiza como os dados chegam; paridade com a plano A9 permanece. **Rejeitado:** unificar lista+overview num único `find` unpaginado sem validar regressão de paginação/`depth`.
|
||||
- **Cortável** se a lista com mapa permanecer aceitável em campo com dezenas de assessores; vira não-cortável quando B7 (filtro) + mapa sempre visível multiplicarem o conjunto filtrado (436 Praças para coordenador).
|
||||
- **Cortável** se a lista com mapa permanecer aceitável em campo com dezenas de assessores; **menos cortável desde B7 entregue** (mapa filtrado + sempre visível para staff amplifica o custo do triplo agregado quando o filtro é amplo).
|
||||
- **i18n e naming** (AGENTS.md): identificadores em inglês (`loadPlazaListPageBundle`, `PlazaListPledgeAggregates`); strings visíveis inalteradas em pt-BR.
|
||||
|
||||
## Questões em aberto
|
||||
|
|
@ -50,7 +54,7 @@ flowchart TD
|
|||
F1 --> List["PlazaList página"]
|
||||
F1 --> Overview["PlazaListOverview KPIs"]
|
||||
F1 --> Map["PlazaMapBundle 2026"]
|
||||
B7["B7 mapa filtrado"] -.mesmo bundle.-> F1
|
||||
B7["B7 mapa filtrado ✓"] -.mesmo bundle.-> F1
|
||||
```
|
||||
|
||||
### Fase 1 — Loader compartilhado da lista de Praças
|
||||
|
|
@ -68,7 +72,7 @@ flowchart TD
|
|||
## Dependências
|
||||
|
||||
- **Dura:** A9 [estimativa-votos-praca.md](estimativa-votos-praca.md) — campo, agregadores e superfícies (merge em `main`).
|
||||
- **Suave:** B7 [mapa-pracas-filtrado.md](mapa-pracas-filtrado.md) — mesmo URL de filtro; F1 facilita B7 ao centralizar o where.
|
||||
- **Suave:** B7 [mapa-pracas-filtrado.md](mapa-pracas-filtrado.md) — entregue 2026-07-21; mesmo URL de filtro; F1 deduplica pledges e pode compartilhar parse/`find` onde os escopos coincidem.
|
||||
- Reusa: `aggregatePledgesByPlaza`, `rollupPlazaStaffVotes`, `resolvePlazaStaffVoteTotal`, precedente E6 F1 em [escala-dry-pos-e1.md](escala-dry-pos-e1.md).
|
||||
|
||||
## Não escopo
|
||||
|
|
@ -88,6 +92,7 @@ flowchart TD
|
|||
|
||||
- **SQL aggregate espelhando `buildPlazaListWhere`.** Revisitar quando profiling mostrar `aggregatePledgesByPlaza` dominando TTFB com filtros amplos + mapa sempre montado.
|
||||
- **Compartilhar bundle com dashboard geral.** Revisitar se o dashboard passar a carregar mapa+lista na mesma rota.
|
||||
- **Loads TSE sequenciais por ano em `loadPlazaMapBundle`.** Revisitar com `Promise.all` só se profiling com `?compare=` mostrar latência dominante (pré-existente; fora do escopo A9+).
|
||||
|
||||
## Referências
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# Mapa das Praças filtrado pela lista
|
||||
|
||||
Status: rascunho
|
||||
Status: entregue
|
||||
Atualizado em: 2026-07-21
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B7)
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B7 — entregue 2026-07-21)
|
||||
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: —
|
||||
|
||||
_Revisão 2026-07-21 (pós-implementação + `/simplify`): `loadPlazaMapBundle` aceita `rawSearchParams`; `loadScopedPlazas` usa `buildPlazaListWhere(state)`; tipo `PlazaListSearchParams` exportado de `plazaUi.ts`; testes em `tests/int/plazaMapData.int.spec.ts`. Cleanup simplify: parse único no map loader (`PlazaListState` passado a `loadScopedPlazas`). Débito de perf triplicado → **A9+**._
|
||||
|
||||
## Design (Impeccable)
|
||||
|
||||
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `PlazaFilters`, `PlazaMapPanel`).
|
||||
|
|
@ -22,12 +24,9 @@ Brief compacto:
|
|||
|
||||
## 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:
|
||||
Em `/campanha/pracas` a lista e o overview já aplicavam `buildPlazaListWhere(state)` a partir da URL (`q`, `region`, `kind`, `coverage`, `priority`, `trend`). O mapa carregava **todas** as Praças acessíveis ao papel (`where: {}` em `loadScopedPlazas`). Pedido de produto (2026-07-21): o Mapa das Praças deve usar o mesmo filtro da lista abaixo.
|
||||
|
||||
- `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.
|
||||
**Entregue 2026-07-21:** `loadPlazaMapBundle(payload, user, rawSearchParams)` em `page.tsx`; `loadScopedPlazas` com `buildPlazaListWhere`; conjunto vazio → `null` (painel omitido); `compare` só modo comparativo.
|
||||
|
||||
## Objetivos
|
||||
|
||||
|
|
@ -63,8 +62,8 @@ flowchart LR
|
|||
|
||||
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`.
|
||||
- **`loadScopedPlazas` / `loadPlazaMapBundle`** (`src/utilities/plazaMapData.ts`): `searchParams` → `parsePlazaListParams` → `buildPlazaListWhere(state)` no `payload.find` (`user` + `overrideAccess: false`). `loadScopedPlazas` recebe `PlazaListState` (parse único no bundle). Zero docs → `null`. `compare` de `state.compare`.
|
||||
- **`pracas/page.tsx`**: `loadPlazaMapBundle(payload, user, rawSearchParams)` — paridade com lista/overview.
|
||||
- **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.
|
||||
|
|
@ -98,8 +97,9 @@ Componentes:
|
|||
|
||||
- `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/plazaMapData.ts` — `loadScopedPlazas` / `loadPlazaMapBundle`
|
||||
- `src/utilities/plazaUi.ts` — `buildPlazaListWhere`, `PlazaListState`, `PlazaListSearchParams`
|
||||
- `tests/int/plazaMapData.int.spec.ts` — contrato filtro + access
|
||||
- `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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap — Teqo
|
||||
|
||||
Atualizado em: 2026-07-21 (A9 entregue; A9+ loader lista; B9 desbloqueado; Pixel Meta)
|
||||
Atualizado em: 2026-07-21 (B7 entregue; A9+ loader lista; B9 desbloqueado; 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).
|
||||
|
||||
|
|
@ -70,22 +70,21 @@ flowchart TD
|
|||
R2 --> D2["D2 Push + sino"]
|
||||
R2 --> A9["A9 Estimativa votos ✓"]
|
||||
A9 --> B9["B9 Edição rápida lista Praças"]
|
||||
R2 -.-> B7["B7 Mapa filtrado pela lista"]
|
||||
R2 -.-> B7["B7 Mapa filtrado ✓"]
|
||||
R2 -.-> B8["B8 Polígonos Praças-zona<br/>(SSA/CMS)"]
|
||||
R2 -.-> B10["B10 Hover/tap mapa<br/>(destaque + nav)"]
|
||||
R2 -.-> B11["B11 Escala % válidos<br/>no mapa"]
|
||||
A9 -.métrica 2026.-> B7
|
||||
A9 -.métrica 2026.-> B10
|
||||
A9 -.numerador 2026.-> B11
|
||||
JUR -.chave push.-> D2
|
||||
C2prod --> C5["C5 GOTV (validar)"]
|
||||
```
|
||||
|
||||
Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2): **B7**, **B10**, **B11**, fill-ins da lista. **A9** entregue — **B9** desbloqueado (seta dura A9 → B9).
|
||||
Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2): **B10**, **B11**, fill-ins da lista. **A9** entregue — **B9** desbloqueado (seta dura A9 → B9).
|
||||
|
||||
### Sequência por janela
|
||||
|
||||
**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos da Praça~~ entregue; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **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 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos da Praça~~ entregue; ~~**B7** mapa filtrado pela lista~~ entregue; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **B9** edição rápida na lista (Assessores / Tendência / total esperado); **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.
|
||||
|
||||
|
|
@ -97,7 +96,7 @@ Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2):
|
|||
|
||||
**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**~~ (total esperado da Praça — entregue 2026-07-21).
|
||||
|
||||
**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** (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); 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; **B11** (mapa continua em escala absoluta); **B10** (mapa legível sem hover); **B9** (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); débitos/fill-ins.
|
||||
|
||||
## Já entregue (resumo)
|
||||
|
||||
|
|
@ -105,6 +104,7 @@ Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2):
|
|||
- **Plataforma** — local Postgres + guards, migrations baselined, posts/tags do site público com cache `posts`, Onda 0 textos provisórios + `/privacidade`.
|
||||
- **Site público (2026-07-21)** — **Pixel do Meta nos abaixo-assinados** (`tracking.facebookPixelId` no admin `petition`, `PageView`/`Lead` na página pública via `MetaPixel` + `trackMetaLead`; migration `20260721_133531_add_petition_facebook_pixel_id`) — [plano](plans/pixel-meta-abaixo-assinado.md).
|
||||
- **A9 (2026-07-21)** — **Estimativa de votos da Praça** (`plaza.expectedVotes` staff-only; fallback `expectedVotes ?? effectiveTotal` em mapa 2026/overview/dashboard; UI `/editar` + leitura lista/detalhe; migration `20260721_133444_add_plaza_expected_votes`) — [plano](plans/estimativa-votos-praca.md). Fill-in pós-`/simplify`: **A9+** [escala-dry-pos-a9.md](plans/escala-dry-pos-a9.md).
|
||||
- **B7 (2026-07-21)** — **Mapa das Praças filtrado pela lista** (`buildPlazaListWhere` em `loadPlazaMapBundle`; `rawSearchParams` na página; empty → omitir painel; int `plazaMapData.int.spec.ts`) — [plano](plans/mapa-pracas-filtrado.md).
|
||||
|
||||
## Supersedidos pela remodelagem (2026-07-20)
|
||||
|
||||
|
|
@ -127,14 +127,13 @@ Paralelizáveis sem seta dura de entrada (além das tracejadas a partir de R2):
|
|||
- **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)
|
||||
- **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) · 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)
|
||||
- **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/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 · (opcional: mover filtros acima do mapa — ver B7)
|
||||
- **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)
|
||||
- **Fill-ins:** filtros auto na lista de Praças (debounce 1s no `q`, selects já imediatos; remove Buscar) · [plano](plans/filtros-auto-pracas.md) · **A9+** loader compartilhado lista/mapa ([plano](plans/escala-dry-pos-a9.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
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default async function PlazasPage({ searchParams }: PlazasPageProps) {
|
|||
const [listData, overview, mapBundle] = await Promise.all([
|
||||
loadPlazaListPageData(payload, user, rawSearchParams),
|
||||
loadPlazaListOverviewData(payload, user, rawSearchParams),
|
||||
loadPlazaMapBundle(payload, user, canonicalUrl.state.compare),
|
||||
loadPlazaMapBundle(payload, user, rawSearchParams),
|
||||
])
|
||||
const candidateOptions = mapBundle ? await loadFederalCandidateOptions(payload, user) : []
|
||||
const resolvedUrl = resolvePlazaListUrl(rawSearchParams, listData.totalPages)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ import {
|
|||
loadCandidateVotesByCityZone,
|
||||
sumVotesForGeography,
|
||||
} from '@/utilities/plazaElectoralBaseline'
|
||||
import {
|
||||
buildPlazaListWhere,
|
||||
parsePlazaListParams,
|
||||
type PlazaListSearchParams,
|
||||
type PlazaListState,
|
||||
} from '@/utilities/plazaUi'
|
||||
import {
|
||||
aggregatePledgesByPlaza,
|
||||
emptyPlazaPledgeAggregate,
|
||||
|
|
@ -59,14 +65,18 @@ type ScopedPlaza = {
|
|||
geography: PlazaElectionGeography
|
||||
}
|
||||
|
||||
const loadScopedPlazas = async (payload: Payload, user: CampaignUser): Promise<ScopedPlaza[]> => {
|
||||
const loadScopedPlazas = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: PlazaListState,
|
||||
): Promise<ScopedPlaza[]> => {
|
||||
const result = await payload.find({
|
||||
collection: 'plaza',
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
select: { slug: true, name: true, kind: true, ibgeCode: true, expectedVotes: true },
|
||||
where: {},
|
||||
where: buildPlazaListWhere(state),
|
||||
user,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
|
@ -89,20 +99,20 @@ const loadScopedPlazas = async (payload: Payload, user: CampaignUser): Promise<S
|
|||
}
|
||||
|
||||
/**
|
||||
* Map data for the Praças overview: every accessible plaza contributes its
|
||||
* geography's votes (or 2026 pledge estimates) to its municipality polygon.
|
||||
* An advisor therefore sees only their plazas' share shaded on the map. When
|
||||
* `compareCandidateNumber` is set, TSE years also carry a red↔white↔blue
|
||||
* diff (Solla − other candidate).
|
||||
* Map data for the Praças overview: plazas matching the list URL filters (and
|
||||
* role access) contribute their geography's votes (or 2026 pledge estimates)
|
||||
* to municipality polygons. When `state.compare` is set, TSE years also carry
|
||||
* a red↔white↔blue diff (Solla − other candidate).
|
||||
*/
|
||||
export const loadPlazaMapBundle = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
compareCandidateNumber?: number,
|
||||
searchParams: PlazaListSearchParams,
|
||||
): Promise<PlazaMapBundle | null> => {
|
||||
if (user.role === 'leader') return null
|
||||
|
||||
const plazas = await loadScopedPlazas(payload, user)
|
||||
const state = parsePlazaListParams(searchParams)
|
||||
const plazas = await loadScopedPlazas(payload, user, state)
|
||||
if (plazas.length === 0) return null
|
||||
|
||||
const valuesByYear: Record<string, Record<string, number>> = {}
|
||||
|
|
@ -155,12 +165,12 @@ export const loadPlazaMapBundle = async (
|
|||
.sort((left, right) => left.name.localeCompare(right.name, 'pt-BR'))
|
||||
|
||||
let comparison: PlazaMapComparison | null = null
|
||||
if (compareCandidateNumber) {
|
||||
if (state.compare) {
|
||||
const candidates = await payload.find({
|
||||
collection: 'electionCandidate',
|
||||
where: {
|
||||
and: [
|
||||
{ candidateNumber: { equals: compareCandidateNumber } },
|
||||
{ candidateNumber: { equals: state.compare } },
|
||||
{ office: { equals: 'deputado_federal' } },
|
||||
{ turn: { equals: '1' } },
|
||||
],
|
||||
|
|
@ -179,7 +189,7 @@ export const loadPlazaMapBundle = async (
|
|||
for (const year of HISTORICAL_SERIES_YEARS) {
|
||||
const otherVotes = await loadCandidateVotesByCityZone(payload, user, {
|
||||
year,
|
||||
candidateNumber: compareCandidateNumber,
|
||||
candidateNumber: state.compare,
|
||||
})
|
||||
const sollaVotes = sollaVotesByYear.get(year) ?? new Map<string, number>()
|
||||
const values: Record<string, number> = {}
|
||||
|
|
@ -192,7 +202,7 @@ export const loadPlazaMapBundle = async (
|
|||
}
|
||||
|
||||
comparison = {
|
||||
candidateNumber: compareCandidateNumber,
|
||||
candidateNumber: state.compare,
|
||||
candidateName: `${candidate.urnaName}${candidate.party ? ` (${candidate.party})` : ''}`,
|
||||
diffByYear,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Payload } from 'payload'
|
|||
import { getPlazaCatalogEntry } from '@/lib/plazaCatalog'
|
||||
import type { CampaignUser, Plaza } from '@/payload-types'
|
||||
import { isCampaignStaff } from '@/utilities/campaignAccess'
|
||||
import { buildPlazaListWhere, parsePlazaListParams, plazaPageSize } from '@/utilities/plazaUi'
|
||||
import { buildPlazaListWhere, parsePlazaListParams, plazaPageSize, type PlazaListSearchParams } from '@/utilities/plazaUi'
|
||||
import {
|
||||
plazaListSelect,
|
||||
toPlazaDetailViewModel,
|
||||
|
|
@ -17,8 +17,6 @@ import {
|
|||
rollupPlazaStaffVotes,
|
||||
} from '@/utilities/votePledgeData'
|
||||
|
||||
type PlazaListSearchParams = Record<string, string | string[] | undefined>
|
||||
|
||||
export class PlazaNotFoundError extends Error {
|
||||
override name = 'PlazaNotFoundError'
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export type PlazaListState = {
|
|||
compare?: number
|
||||
}
|
||||
|
||||
type RawSearchParams = CampaignListRawSearchParams
|
||||
export type PlazaListSearchParams = CampaignListRawSearchParams
|
||||
|
||||
export const plazaListParamNames = [
|
||||
'q',
|
||||
|
|
@ -63,7 +63,7 @@ const canonicalTerritoryBySearchValue = new Map(
|
|||
bahiaIdentityTerritories.map((territory) => [normalizeSearchPhrase(territory), territory]),
|
||||
)
|
||||
|
||||
export const parsePlazaListParams = (params: RawSearchParams): PlazaListState => {
|
||||
export const parsePlazaListParams = (params: PlazaListSearchParams): PlazaListState => {
|
||||
const rawPage = strictDecimalInteger(firstValue(params.page))
|
||||
const q = normalizedText(firstValue(params.q))
|
||||
const rawRegion = normalizedText(firstValue(params.region))
|
||||
|
|
@ -153,7 +153,7 @@ export const buildPlazaListHref = (state: PlazaListState, page: number): string
|
|||
buildListHref(state, buildPlazaListSearchParams, '/campanha/pracas', page)
|
||||
|
||||
export const resolvePlazaListUrl = (
|
||||
params: RawSearchParams,
|
||||
params: PlazaListSearchParams,
|
||||
totalPages?: number,
|
||||
): {
|
||||
state: PlazaListState
|
||||
|
|
|
|||
75
tests/int/plazaMapData.int.spec.ts
Normal file
75
tests/int/plazaMapData.int.spec.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import type { Payload } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import config from '@/payload.config'
|
||||
import { plazaCatalog } from '@/lib/plazaCatalog'
|
||||
import { loadPlazaMapBundle } from '@/utilities/plazaMapData'
|
||||
|
||||
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
||||
|
||||
const ZONE_PLAZA_COUNT = plazaCatalog.filter((entry) => entry.kind === 'zona').length
|
||||
const ZONE_MUNICIPALITY_CODES = new Set(
|
||||
plazaCatalog.filter((entry) => entry.kind === 'zona').map((entry) => entry.ibgeCode),
|
||||
)
|
||||
|
||||
let payload: Payload
|
||||
const campaignFixtures = installCampaignFixtures({
|
||||
getPayload: () => payload,
|
||||
setPayload: (nextPayload) => {
|
||||
payload = nextPayload
|
||||
},
|
||||
})
|
||||
|
||||
describe('loadPlazaMapBundle — list URL filters', () => {
|
||||
beforeAll(async () => {
|
||||
payload = await getPayload({ config: await config })
|
||||
})
|
||||
|
||||
it('scopes the map to zone plazas when kind=zona is in the URL', async () => {
|
||||
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
|
||||
|
||||
const bundle = await loadPlazaMapBundle(payload, coordinator, { kind: 'zona' })
|
||||
|
||||
expect(bundle).not.toBeNull()
|
||||
expect(bundle!.zoneBreakdown).toHaveLength(ZONE_PLAZA_COUNT)
|
||||
|
||||
const ibgeCodes = Object.keys(bundle!.valuesByYear['2022'] ?? {})
|
||||
expect(ibgeCodes.length).toBeGreaterThan(0)
|
||||
for (const code of ibgeCodes) {
|
||||
expect(ZONE_MUNICIPALITY_CODES.has(code)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('returns null when the filtered plaza set is empty', async () => {
|
||||
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
|
||||
|
||||
const bundle = await loadPlazaMapBundle(payload, coordinator, { q: 'zzznomatch' })
|
||||
|
||||
expect(bundle).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps advisor access and applies URL filters on top', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const advisor = await fixtures.createCampaignUser('advisor')
|
||||
const administered = await fixtures.getPlaza()
|
||||
await fixtures.assignPlazaAdvisors(administered.id, [advisor.id])
|
||||
|
||||
const included = await loadPlazaMapBundle(payload, advisor, { q: administered.name })
|
||||
expect(included).not.toBeNull()
|
||||
expect(included!.zoneBreakdown.some((row) => row.slug === administered.slug)).toBe(
|
||||
administered.kind === 'zona',
|
||||
)
|
||||
if (administered.kind !== 'zona') {
|
||||
expect(Object.keys(included!.valuesByYear['2022'] ?? {})).toEqual([administered.ibgeCode])
|
||||
}
|
||||
|
||||
const excluded =
|
||||
administered.kind === 'zona'
|
||||
? await loadPlazaMapBundle(payload, advisor, { kind: 'municipio' })
|
||||
: await loadPlazaMapBundle(payload, advisor, { kind: 'zona' })
|
||||
expect(excluded).toBeNull()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue