mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:05:19 +00:00
Deliver B8 F1 plaza zone neighborhoods catalog and absorb simplify debts.
Ship static zone-to-neighborhood mapping for Salvador/Camaçari Praças-zona, overview UI card, tests/fixture, and roadmap/plan updates including F2 prep follow-ups from capture-review-debts.
This commit is contained in:
parent
7f437741ba
commit
e497fc3982
8 changed files with 936 additions and 23 deletions
|
|
@ -8,6 +8,7 @@ alwaysApply: false
|
|||
## Status
|
||||
|
||||
- **B6 entregue em código (2026-07-21; deploy pendente com remodelagem):** `BahiaMap` setStyle incremental — layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers puros em `bahiaMapStyle.ts` + unit `bahiaMapStyle.unit.spec.ts`. Plano: [`escala-dry-pos-b3.md`](../../../docs/plans/escala-dry-pos-b3.md). Fecha débitos de perf de **B10** e **B11**.
|
||||
- **B8 F1 entregue em código (2026-07-21; deploy pendente com remodelagem):** catálogo estático `plazaZoneNeighborhoods` (21 Praças-zona SSA/CMS; Salvador = TRE-BA RA 02/2017; Camaçari = locais de votação + curadoria) + fixture `plaza-zone-neighborhoods.official.json` + int `plazaZoneNeighborhoods.int.spec.ts`; card **Bairros desta Praça** na aba Visão geral de `/campanha/pracas/[slug]` quando `kind === 'zona'` (staff e líder). Plano: [`poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md). **F2 pendente** (dissolve IBGE → TopoJSON + mapa).
|
||||
- **A9+ entregue em código (2026-07-21; deploy pendente com remodelagem):** loader compartilhado da lista — `loadPlazaListPageBundle` (1× `aggregatePledgesByPlaza`), `buildPlazaMapBundleFromPlazas`, `plazaRevalidation.ts` + hidden `plazaSlug`; int `plazaPageData.int.spec.ts`. Plano: [`escala-dry-pos-a9.md`](../../../docs/plans/escala-dry-pos-a9.md). Débito restante: twin `listFormActions` ↔ `/editar` → **C8** F4.
|
||||
- **B11 entregue em código (2026-07-21; deploy pendente com remodelagem):** escala **% dos válidos** no Mapa das Praças — seletor `Total (votos)` / `% dos válidos` (default %); domínio 0–100% fixo (`scaleMax` em `BahiaMap`); `validVotesByYear` no bundle; 2026 usa válidos 2022; compare desliga %; readout em % + votos absolutos; `Promise.all` nos loads TSE. Plano: [`escala-percentual-mapa-pracas.md`](../../../docs/plans/escala-percentual-mapa-pracas.md).
|
||||
- **Fill-in filtros-auto entregue em código (2026-07-21; deploy pendente com remodelagem):** debounce 1s + Enter imediato na busca de `/campanha/pracas`; remove Buscar; pending UI alinhado a `ActionPlanFilters`; `shouldUpdatePlazaSearchUrl` + no-op `buildPlazaFiltersKey`. Plano: [`filtros-auto-pracas.md`](../../../docs/plans/filtros-auto-pracas.md). Débitos pós-`/simplify` (sync back/forward, shell pending) → gatilhos no plano.
|
||||
|
|
@ -127,5 +128,5 @@ alwaysApply: false
|
|||
- 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 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 / B9 / B7 / B10 / B11 / filtros-auto entregues (2026-07-21; deploy pendente):** ver bullets no topo do Status. Próximos abertos Janela 1: **R6**, deploy+smoke remodelagem; Janela 3: **B6** (setStyle incremental pós-B10/B11), **B8**.
|
||||
- **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).
|
||||
- **A9 / B6 / B9 / B7 / B10 / B11 / B8 F1 / filtros-auto entregues (2026-07-21; deploy pendente):** ver bullets no topo do Status. Próximos abertos Janela 1: **R6**, deploy+smoke remodelagem, **B12**; Janela 3: **B8 F2** (polígonos no mapa).
|
||||
- **B8 registrado (2026-07-21); F1 entregue (2026-07-21):** polígonos das Praças-zona Salvador/Camaçari. F1: catálogo `plazaZoneNeighborhoods` + card na Praça. F2 pendente: dissolve malha IBGE → TopoJSON no mapa. Plano: [`docs/plans/poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# Polígonos das Praças-zona (Salvador / Camaçari)
|
||||
|
||||
Status: rascunho
|
||||
Status: F1 entregue em código (2026-07-21); F2 pendente
|
||||
Atualizado em: 2026-07-21
|
||||
Revisão 2026-07-21: F1 implementada — `plazaZoneNeighborhoods.ts`, fixture+int, `PlazaZoneNeighborhoodsCard` no overview da Praça-zona; mapa inalterado (F2).
|
||||
Revisão 2026-07-21 (pós-`/simplify` + capture-review-debts): débitos S1–S3 absorvidos como **F2 prep** (~½ dia, dentro do appetite F2); S4–S10 em Explicitamente fora / Adiado; JR1–JR7 em Já resolvido (não reabrir no simplify).
|
||||
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)
|
||||
|
|
@ -76,7 +78,7 @@ flowchart TD
|
|||
|
||||
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).
|
||||
- **`src/lib/plazaZoneNeighborhoods.ts`** — mapa estático `{ city: 'Salvador'|'Camaçari', zoneNumber, neighborhoods: string[] }` indexado por `plazaSlug`; helper `plazaZoneNeighborhoodEntryForSlug(slug)` + `plazaZoneNeighborhoodSourceLabel(source)`; 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). **F2 prep:** refatorar para autorar só `{ plazaSlug, source, neighborhoods[] }` e hidratar `city`/`zoneNumber` de `getPlazaCatalogEntry` (ver seção abaixo).
|
||||
- **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).
|
||||
|
|
@ -86,10 +88,17 @@ Componentes:
|
|||
|
||||
### 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 |
|
||||
| Fase | Entrega | Appetite |
|
||||
| -------- | ----------------------------------------------------------------------- | ------------- |
|
||||
| F1 | Catálogo + testes + bloco na Praça | ~1 dia |
|
||||
| F2 prep | Hidratação via `plazaCatalog` + canônico JSON→TS + teste ordem de slugs | ~½ dia |
|
||||
| F2 | Spike cobertura malha → script → TopoJSON → mapa | ~1,5–2,5 dias |
|
||||
|
||||
**F2 prep** (débitos pós-`/simplify`, capture-review-debts 2026-07-21 — não item paralelo no roadmap):
|
||||
|
||||
1. **S1 — Hidratar identidade do catálogo:** autorar entradas como `{ plazaSlug, source, neighborhoods[] }`; derivar `city` e `zoneNumber` de `getPlazaCatalogEntry(slug)` em `entry()` ou no read path (padrão `plazaElectionGeographyForSlug`). Reduz ~42 campos duplicados e risco de drift antes do dissolve.
|
||||
2. **S2 — Fonte única TS ↔ fixture:** o script `build-plaza-zone-geometries.mjs` (ou gerador irmão) emite `plazaZoneNeighborhoods.ts` a partir de `plaza-zone-neighborhoods.official.json` (ou o inverso com JSON como canônico de evidência). Elimina ~300 linhas espelhadas; `evidenceSha256` permanece no fixture.
|
||||
3. **S3 — Ordem de slugs:** int leve `plazaZoneNeighborhoods.map(e => e.plazaSlug)` vs slugs `kind === 'zona'` de `plazaCatalog` (alinha ao snapshot SHA de `plazaCatalog.int.spec.ts`).
|
||||
|
||||
## Dependências
|
||||
|
||||
|
|
@ -112,8 +121,29 @@ Componentes:
|
|||
- **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”.
|
||||
|
||||
## Já resolvido no simplify/critique (não reabrir)
|
||||
|
||||
- **JR1** — `PlazaZoneNeighborhoodsCard`: um lookup `plazaZoneNeighborhoodEntryForSlug` (sem duplo hit no `entryBySlug`).
|
||||
- **JR2** — export `neighborhoodsForPlazaSlug` removido (wrapper redundante).
|
||||
- **JR3** — guard do card: `!entry?.neighborhoods.length`.
|
||||
- **JR4** — `assertPlazaZoneNeighborhoodCatalogIntegrity` só no módulo (não exportado).
|
||||
- **JR5** — testes redundantes de sort e checksum duplicado removidos do int spec.
|
||||
- **JR6** — cobertura usa `zonePlazas.length` em vez de `21` fixo.
|
||||
- **JR7** — comentário no spec: sem teste de exclusividade Camaçari (fonte curada/aproximada).
|
||||
- Impeccable compacto na F1: sem snapshot `.impeccable/critique/` nem P0–P3 abertos nesta superfície; polish visual amplo → **R6**.
|
||||
|
||||
## Explicitamente fora (skips `/simplify` + descartes do triage)
|
||||
|
||||
- **S4 — Remover assert de integridade no import.** Fail-fast no import é aceitável aqui; int spec já cobre slugs zona. Não mover só para spec sem motivo de perf.
|
||||
- **S5 — Incluir `source` no SHA-256 canônico.** `deepEqual` fixture↔código já detecta mudança de `source`; refresh de `evidenceSha256` seria churn sem ganho.
|
||||
- **S7 — `PlazaZoneNeighborhoodCity` vs `ZONE_PLAZA_CITIES`.** Absorvido por S1 se F2 prep rodar; não tipo paralelo isolado.
|
||||
- **S8 — Proveniência em 3 camadas** (header TS, fixture `provenance`, `plazaZoneNeighborhoodSourceLabel`). Padrão aceitável para catálogo com copy UI distinta por `source`.
|
||||
- **S9 — Double gate** `view.kind === 'zona'` na página + card `null`. Defesa em profundidade; não simplificar só por DRY.
|
||||
- **S10 — `key={neighborhood}` no card.** Seguro com exclusividade Salvador testada; prefixar slug só se QA reportar colisão intra-zona.
|
||||
|
||||
## Adiado com gatilho
|
||||
|
||||
- **S6 — CSS compartilhado lista zona** (`PlazaMapPanel` zone breakdown ↔ `PlazaZoneNeighborhoodsCard`). **Gatilho:** F2 alterar `PlazaMapPanel`/`BahiaMap` ou R6 citar duplicação visual em ≥2 superfícies de zona.
|
||||
- **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).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap — Teqo
|
||||
|
||||
Atualizado em: 2026-07-21 (B6 entregue em código; B12 registrado — aproximar mapa ao filtro; A9+/B7/B9–B11/filtros-auto entregues; débitos B9 → C8 F4)
|
||||
Atualizado em: 2026-07-21 (B6 + B8 F1 entregues em código; débitos B8 F1 pós-/simplify absorvidos no plano F2 prep; B12 registrado — aproximar mapa ao filtro; A9+/B7/B9–B11/filtros-auto entregues; débitos B9 → C8 F4)
|
||||
|
||||
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).
|
||||
|
||||
|
|
@ -114,6 +114,7 @@ Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins da
|
|||
- **B10 (2026-07-21)** — **Hover/tap no Mapa das Praças** (destaque + `MapFeatureReadout`; desktop click navega; mobile 2º tap; SSA/CMS N>1 → `zoneBreakdown`; `plazasByIbgeCode` / `resolvePlazaMapNavigation`) — [plano](plans/hover-mapa-pracas.md).
|
||||
- **B11 (2026-07-21)** — **Escala % dos válidos no Mapa das Praças** (`validVotesByYear` no bundle; seletor `Total (votos)` / `% dos válidos`; domínio fixo 0–100%; 2026 usa válidos 2022; compare desliga %; readout em %) — [plano](plans/escala-percentual-mapa-pracas.md).
|
||||
- **B6 (2026-07-21)** — **`BahiaMap` setStyle incremental** (layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers em `bahiaMapStyle.ts`) — [plano](plans/escala-dry-pos-b3.md).
|
||||
- **B8 F1 (2026-07-21)** — **Catálogo bairros das Praças-zona** (`plazaZoneNeighborhoods`: Salvador TRE-BA RA 02/2017 + Camaçari curado; fixture+int; card **Bairros desta Praça** no overview de Praças `kind=zona`; sem migration) — [plano](plans/poligonos-pracas-zona.md). **F2 pendente** (F2 prep catálogo ~½ dia + TopoJSON + mapa). Débitos pós-`/simplify`: hidratação via `plazaCatalog`, canônico JSON→TS, teste ordem slugs → F2 prep no plano (S6 CSS compartilhado adiado até F2/R6).
|
||||
- **Fill-in filtros-auto (2026-07-21)** — **Filtros auto-aplicados na lista de Praças** (`PlazaFilters`: debounce 1s no `q`, Enter imediato, selects imediatos, remove Buscar; `useTransition` + pending a11y; `shouldUpdatePlazaSearchUrl` + no-op via `buildPlazaFiltersKey`; sem migration) — [plano](plans/filtros-auto-pracas.md). Débitos pós-`/simplify`: sync back/forward `search`↔`state.q` e shell pending compartilhado — gatilhos no plano.
|
||||
|
||||
## Supersedidos pela remodelagem (2026-07-20)
|
||||
|
|
@ -136,7 +137,7 @@ Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins da
|
|||
|
||||
- **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)
|
||||
- **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)
|
||||
- **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~~ (entregue 2026-07-21); F2 prep catálogo (~½ dia) + dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável (F2) · [plano](plans/poligonos-pracas-zona.md)
|
||||
- **B12** Aproximar o Mapa das Praças ao footprint filtrado (`fitToKeys` no `BahiaMap`; sem re-zoom em Ano/Escala) · Janela 1 · cortável · gatilho do B7 · [plano](plans/aproximar-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)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { PlazaCandidateComparisonTable } from '@/components/campaign/PlazaCandid
|
|||
import { PlazaLeadershipsPanel } from '@/components/campaign/PlazaLeadershipsPanel'
|
||||
import { PlazaPledgesPanel } from '@/components/campaign/PlazaPledgesPanel'
|
||||
import { PlazaStrategyCard } from '@/components/campaign/PlazaStrategyCard'
|
||||
import { PlazaZoneNeighborhoodsCard } from '@/components/campaign/PlazaZoneNeighborhoodsCard'
|
||||
import { PlazaTabNav } from '@/components/campaign/PlazaTabNav'
|
||||
import { PlazaUpdateFeed } from '@/components/campaign/PlazaUpdateFeed'
|
||||
import { PlazaUpdateForm } from '@/components/campaign/PlazaUpdateForm'
|
||||
|
|
@ -153,11 +154,15 @@ const OverviewTab = async ({
|
|||
isStaffView: boolean
|
||||
payloadUser: PayloadUser
|
||||
}) => {
|
||||
const zoneNeighborhoodsCard =
|
||||
view.kind === 'zona' ? <PlazaZoneNeighborhoodsCard plazaSlug={view.slug} /> : null
|
||||
|
||||
if (isStaffView && view.strategy) {
|
||||
const pledges = await loadPlazaPledges(payload, user, view.id)
|
||||
const leadershipVoteTotal = sumStaffPledgeEffectiveTotal(pledges)
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{zoneNeighborhoodsCard}
|
||||
<PlazaStrategyCard
|
||||
strategy={view.strategy}
|
||||
plazaSlug={view.slug}
|
||||
|
|
@ -173,19 +178,22 @@ const OverviewTab = async ({
|
|||
const ownPledge = leaderPledges.find((pledge) => pledge.plazaID === view.id) ?? null
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-4 rounded-xl border p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-base font-medium">Seus votos nesta Praça</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Informe quantos votos você está trazendo. Você pode atualizar o número quando quiser.
|
||||
</p>
|
||||
</div>
|
||||
<DeclareVotesForm
|
||||
plazaID={view.id}
|
||||
currentDeclaredVotes={ownPledge?.declaredVotes ?? null}
|
||||
formAction={declareVotesFormAction}
|
||||
/>
|
||||
</section>
|
||||
<div className="flex flex-col gap-6">
|
||||
{zoneNeighborhoodsCard}
|
||||
<section className="flex flex-col gap-4 rounded-xl border p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-base font-medium">Seus votos nesta Praça</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Informe quantos votos você está trazendo. Você pode atualizar o número quando quiser.
|
||||
</p>
|
||||
</div>
|
||||
<DeclareVotesForm
|
||||
plazaID={view.id}
|
||||
currentDeclaredVotes={ownPledge?.declaredVotes ?? null}
|
||||
formAction={declareVotesFormAction}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
39
src/components/campaign/PlazaZoneNeighborhoodsCard.tsx
Normal file
39
src/components/campaign/PlazaZoneNeighborhoodsCard.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
plazaZoneNeighborhoodEntryForSlug,
|
||||
plazaZoneNeighborhoodSourceLabel,
|
||||
} from '@/lib/plazaZoneNeighborhoods'
|
||||
|
||||
export const PlazaZoneNeighborhoodsCard = ({ plazaSlug }: { plazaSlug: string }) => {
|
||||
const entry = plazaZoneNeighborhoodEntryForSlug(plazaSlug)
|
||||
if (!entry?.neighborhoods.length) return null
|
||||
|
||||
const { neighborhoods, source } = entry
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="plaza-zone-neighborhoods-title"
|
||||
className="flex flex-col gap-4 rounded-xl border p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 id="plaza-zone-neighborhoods-title" className="text-base font-medium">
|
||||
Bairros desta Praça
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{neighborhoods.length}{' '}
|
||||
{neighborhoods.length === 1 ? 'bairro' : 'bairros'} ·{' '}
|
||||
{plazaZoneNeighborhoodSourceLabel(source)}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="grid gap-1.5 sm:grid-cols-2">
|
||||
{neighborhoods.map((neighborhood) => (
|
||||
<li
|
||||
key={neighborhood}
|
||||
className="rounded-md border px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
{neighborhood}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
321
src/lib/plazaZoneNeighborhoods.ts
Normal file
321
src/lib/plazaZoneNeighborhoods.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/**
|
||||
* Static catalog of neighborhoods per Praça-zona (Salvador ZE 1–19, Camaçari ZE 170/171).
|
||||
*
|
||||
* Salvador source (official):
|
||||
* - TRE-BA Resolução Administrativa nº 2/2017, Anexo I — circunscrição por bairro
|
||||
* https://www.tre-ba.jus.br/legislacao/compilada/resolucao/2017/resolucao-administrativa-no-2-de-10-de-maio-de-2017
|
||||
* (accessed 2026-07-21)
|
||||
*
|
||||
* Camaçari source (approximate):
|
||||
* - Localidades derivadas de locais de votação TRE-BA (ZE 170/171) + curadoria manual;
|
||||
* não há lista oficial zona↔bairro comparável a Salvador.
|
||||
* https://www.tre-ba.jus.br/servicos-eleitorais/titulo-e-local-de-votacao
|
||||
* (accessed 2026-07-21)
|
||||
*
|
||||
* Coverage is independently checked by tests/fixtures/plaza-zone-neighborhoods.official.json.
|
||||
*/
|
||||
|
||||
import { getPlazaCatalogEntry } from '@/lib/plazaCatalog'
|
||||
|
||||
export type PlazaZoneNeighborhoodSource = 'tre-ra-02-2017' | 'tre-voting-locations-curated'
|
||||
|
||||
export type PlazaZoneNeighborhoodCity = 'Salvador' | 'Camaçari'
|
||||
|
||||
export type PlazaZoneNeighborhoodEntry = {
|
||||
readonly plazaSlug: string
|
||||
readonly city: PlazaZoneNeighborhoodCity
|
||||
readonly zoneNumber: number
|
||||
readonly source: PlazaZoneNeighborhoodSource
|
||||
readonly neighborhoods: readonly string[]
|
||||
}
|
||||
|
||||
const sortNeighborhoods = (names: readonly string[]): readonly string[] =>
|
||||
[...names].sort((left, right) => left.localeCompare(right, 'pt-BR'))
|
||||
|
||||
const entry = (
|
||||
plazaSlug: string,
|
||||
city: PlazaZoneNeighborhoodCity,
|
||||
zoneNumber: number,
|
||||
source: PlazaZoneNeighborhoodSource,
|
||||
neighborhoods: readonly string[],
|
||||
): PlazaZoneNeighborhoodEntry => ({
|
||||
plazaSlug,
|
||||
city,
|
||||
zoneNumber,
|
||||
source,
|
||||
neighborhoods: sortNeighborhoods(neighborhoods),
|
||||
})
|
||||
|
||||
export const plazaZoneNeighborhoods: readonly PlazaZoneNeighborhoodEntry[] = [
|
||||
entry('salvador-ze-1', 'Salvador', 1, 'tre-ra-02-2017', [
|
||||
'Alto das Pombas',
|
||||
'Barra',
|
||||
'Calabar',
|
||||
'Canela',
|
||||
'Centro',
|
||||
'Centro Histórico',
|
||||
'Garcia',
|
||||
'Graça',
|
||||
'Vitória',
|
||||
]),
|
||||
entry('salvador-ze-2', 'Salvador', 2, 'tre-ra-02-2017', [
|
||||
'Amaralina',
|
||||
'Chapada do Rio Vermelho',
|
||||
'Engenho Velho da Federação',
|
||||
'Federação',
|
||||
'Ondina',
|
||||
'Rio Vermelho',
|
||||
]),
|
||||
entry('salvador-ze-3', 'Salvador', 3, 'tre-ra-02-2017', [
|
||||
'Baixa de Quintas',
|
||||
'Cidade Nova',
|
||||
'Fazenda Grande do Retiro',
|
||||
'IAPI',
|
||||
'Pau Miúdo',
|
||||
'Pero Vaz',
|
||||
'Santa Mônica',
|
||||
]),
|
||||
entry('salvador-ze-4', 'Salvador', 4, 'tre-ra-02-2017', [
|
||||
'Coutos',
|
||||
'Fazenda Coutos',
|
||||
'Ilha de Bom Jesus dos Passos',
|
||||
'Ilha de Maré',
|
||||
'Ilha dos Frades',
|
||||
'Nova Constituinte',
|
||||
'Paripe',
|
||||
'Periperi (parte ao norte da Rua das Pedrinhas)',
|
||||
'São Tomé',
|
||||
]),
|
||||
entry('salvador-ze-5', 'Salvador', 5, 'tre-ra-02-2017', [
|
||||
'Barreiras',
|
||||
'Cabula',
|
||||
'Engomadeira',
|
||||
'Mata Escura',
|
||||
'Resgate',
|
||||
'Retiro',
|
||||
'São Gonçalo',
|
||||
]),
|
||||
entry('salvador-ze-6', 'Salvador', 6, 'tre-ra-02-2017', [
|
||||
'Acupe',
|
||||
'Boa Vista de Brotas',
|
||||
'Brotas',
|
||||
'Candeal',
|
||||
'Engenho Velho de Brotas',
|
||||
]),
|
||||
entry('salvador-ze-7', 'Salvador', 7, 'tre-ra-02-2017', [
|
||||
'Barbalho',
|
||||
'Barris',
|
||||
'Comércio',
|
||||
'Cosme de Farias',
|
||||
'Luiz Anselmo',
|
||||
'Macaúbas',
|
||||
'Matatu',
|
||||
'Nazaré',
|
||||
'Santo Agostinho',
|
||||
'Santo Antônio',
|
||||
'Saúde',
|
||||
'Tororó',
|
||||
'Vila Laura',
|
||||
]),
|
||||
entry('salvador-ze-8', 'Salvador', 8, 'tre-ra-02-2017', [
|
||||
'Cajazeiras II',
|
||||
'Cajazeiras IV',
|
||||
'Cajazeiras V',
|
||||
'Cajazeiras VI',
|
||||
'Cajazeiras VII',
|
||||
'Cajazeiras VIII',
|
||||
'Campinas de Pirajá',
|
||||
'Marechal Rondon',
|
||||
'Moradas da Lagoa',
|
||||
'Palestina',
|
||||
'Pirajá',
|
||||
'Valéria',
|
||||
]),
|
||||
entry('salvador-ze-9', 'Salvador', 9, 'tre-ra-02-2017', [
|
||||
'Boa Viagem',
|
||||
'Bonfim',
|
||||
'Jardim Cruzeiro',
|
||||
'Mangueira',
|
||||
'Massaranduba',
|
||||
'Monte Serrat',
|
||||
'Ribeira',
|
||||
'Santa Luzia',
|
||||
'Uruguai',
|
||||
'Vila Ruy Barbosa',
|
||||
]),
|
||||
entry('salvador-ze-10', 'Salvador', 10, 'tre-ra-02-2017', [
|
||||
'Bairro da Paz',
|
||||
'Boca do Rio',
|
||||
'Costa Azul',
|
||||
'Imbuí',
|
||||
'Jardim Armação',
|
||||
'Patamares',
|
||||
'Piatã',
|
||||
'Pituaçu',
|
||||
'STIEP',
|
||||
]),
|
||||
entry('salvador-ze-11', 'Salvador', 11, 'tre-ra-02-2017', [
|
||||
'Águas Claras',
|
||||
'Calabetão',
|
||||
'Castelo Branco',
|
||||
'Dom Avelar',
|
||||
'Granjas Rurais Presidente Vargas',
|
||||
'Jardim Cajazeiras',
|
||||
'Jardim Santo Inácio',
|
||||
'Pau da Lima',
|
||||
'Porto Seco Pirajá',
|
||||
'Sete de Abril',
|
||||
'Vila Canária',
|
||||
]),
|
||||
entry('salvador-ze-12', 'Salvador', 12, 'tre-ra-02-2017', [
|
||||
'Aeroporto',
|
||||
'Alto do Coqueirinho',
|
||||
'Areia Branca',
|
||||
'Itapuã',
|
||||
'Itinga',
|
||||
'Jardim das Margaridas',
|
||||
'São Cristóvão',
|
||||
'Stella Maris',
|
||||
]),
|
||||
entry('salvador-ze-13', 'Salvador', 13, 'tre-ra-02-2017', [
|
||||
'Caminho das Árvores',
|
||||
'Itaigara',
|
||||
'Nordeste de Amaralina',
|
||||
'Pituba',
|
||||
'Santa Cruz',
|
||||
'Vale das Pedrinhas',
|
||||
]),
|
||||
entry('salvador-ze-14', 'Salvador', 14, 'tre-ra-02-2017', [
|
||||
'Arenoso',
|
||||
'Novo Horizonte',
|
||||
'São Marcos',
|
||||
'Sussuarana',
|
||||
'Tancredo Neves',
|
||||
]),
|
||||
entry('salvador-ze-15', 'Salvador', 15, 'tre-ra-02-2017', [
|
||||
'Arraial do Retiro',
|
||||
'Boa Vista de São Caetano',
|
||||
'Bom Juá',
|
||||
'Capelinha',
|
||||
'Lobato',
|
||||
'São Caetano',
|
||||
]),
|
||||
entry('salvador-ze-16', 'Salvador', 16, 'tre-ra-02-2017', [
|
||||
'Cabula VI',
|
||||
'Canabrava',
|
||||
'Centro Administrativo da Bahia',
|
||||
'Doron',
|
||||
'Narandiba',
|
||||
'Nova Sussuarana',
|
||||
'Pernambués',
|
||||
'Saboeiro',
|
||||
'São Rafael',
|
||||
'Saramandaia',
|
||||
'Vale dos Lagos',
|
||||
]),
|
||||
entry('salvador-ze-17', 'Salvador', 17, 'tre-ra-02-2017', [
|
||||
'Alto da Terezinha',
|
||||
'Alto do Cabrito',
|
||||
'Itacaranha',
|
||||
'Periperi (parte ao sul da Rua das Pedrinhas)',
|
||||
'Plataforma',
|
||||
'Praia Grande',
|
||||
'Rio Sena',
|
||||
'São João do Cabrito',
|
||||
]),
|
||||
entry('salvador-ze-18', 'Salvador', 18, 'tre-ra-02-2017', [
|
||||
"Caixa D'água",
|
||||
'Calçada',
|
||||
'Caminho de Areia',
|
||||
'Curuzu',
|
||||
'Lapinha',
|
||||
'Liberdade',
|
||||
'Mares',
|
||||
'Roma',
|
||||
]),
|
||||
entry('salvador-ze-19', 'Salvador', 19, 'tre-ra-02-2017', [
|
||||
'Boca da Mata',
|
||||
'Cajazeiras X',
|
||||
'Cajazeiras XI',
|
||||
'Cassange',
|
||||
'Fazenda Grande I',
|
||||
'Fazenda Grande II',
|
||||
'Fazenda Grande III',
|
||||
'Fazenda Grande IV',
|
||||
'Jaguaripe I',
|
||||
'Jardim Nova Esperança',
|
||||
'Mussurunga',
|
||||
'Nova Brasília',
|
||||
'Nova Esperança',
|
||||
'Novo Marotinho',
|
||||
'Trobogy',
|
||||
]),
|
||||
entry('camacari-ze-170', 'Camaçari', 170, 'tre-voting-locations-curated', [
|
||||
'Alto da Cruz',
|
||||
'Bairro do Natal',
|
||||
'Centro',
|
||||
'Camaçari de Dentro',
|
||||
'Dois de Julho',
|
||||
'Gleba A',
|
||||
'Gleba B',
|
||||
'Gleba C',
|
||||
'Gleba E',
|
||||
'Gleba H',
|
||||
'Gravatá',
|
||||
'Inocoop',
|
||||
'Jardim Limoeiro',
|
||||
'Lama Preta',
|
||||
'Limoeiro',
|
||||
'Mangueiral',
|
||||
'Nova Vitória',
|
||||
'Novo Horizonte',
|
||||
'Parque Verde',
|
||||
'Phoc I',
|
||||
'Phoc II',
|
||||
'Phoc III',
|
||||
'Polo Petroquímico',
|
||||
'Triângulo',
|
||||
]),
|
||||
entry('camacari-ze-171', 'Camaçari', 171, 'tre-voting-locations-curated', [
|
||||
'Abrantes',
|
||||
'Arembepe',
|
||||
'Barra do Jacuípe',
|
||||
'Barra do Pojuca',
|
||||
'Catu de Abrantes',
|
||||
'Guarajuba',
|
||||
'Itacimirim',
|
||||
'Jauá',
|
||||
'Monte Gordo',
|
||||
'Povoado de Areias',
|
||||
'Vila de Abrantes',
|
||||
]),
|
||||
]
|
||||
|
||||
const entryBySlug = new Map(plazaZoneNeighborhoods.map((record) => [record.plazaSlug, record]))
|
||||
|
||||
export const plazaZoneNeighborhoodEntryForSlug = (
|
||||
slug: string,
|
||||
): PlazaZoneNeighborhoodEntry | undefined => entryBySlug.get(slug)
|
||||
|
||||
export const plazaZoneNeighborhoodSourceLabel = (source: PlazaZoneNeighborhoodSource): string => {
|
||||
switch (source) {
|
||||
case 'tre-ra-02-2017':
|
||||
return 'Bairros conforme circunscrição oficial do TRE-BA (Resolução Administrativa nº 2/2017).'
|
||||
case 'tre-voting-locations-curated':
|
||||
return 'Lista aproximada a partir de locais de votação do TRE-BA; não há circunscrição oficial zona↔bairro em Camaçari.'
|
||||
}
|
||||
}
|
||||
|
||||
const assertPlazaZoneNeighborhoodCatalogIntegrity = (): void => {
|
||||
for (const record of plazaZoneNeighborhoods) {
|
||||
const catalogEntry = getPlazaCatalogEntry(record.plazaSlug)
|
||||
if (!catalogEntry || catalogEntry.kind !== 'zona') {
|
||||
throw new Error(`Missing zona Praça catalog entry for slug: ${record.plazaSlug}`)
|
||||
}
|
||||
if (catalogEntry.city !== record.city || catalogEntry.zoneNumber !== record.zoneNumber) {
|
||||
throw new Error(`Catalog mismatch for slug: ${record.plazaSlug}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertPlazaZoneNeighborhoodCatalogIntegrity()
|
||||
387
tests/fixtures/plaza-zone-neighborhoods.official.json
vendored
Normal file
387
tests/fixtures/plaza-zone-neighborhoods.official.json
vendored
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
{
|
||||
"provenance": {
|
||||
"salvador": {
|
||||
"description": "Circunscrição oficial por bairro das ZE 1–19, transcrita do Anexo I da Resolução Administrativa TRE-BA nº 2/2017.",
|
||||
"url": "https://www.tre-ba.jus.br/legislacao/compilada/resolucao/2017/resolucao-administrativa-no-2-de-10-de-maio-de-2017",
|
||||
"resolution": "RA 02/2017",
|
||||
"accessed": "2026-07-21"
|
||||
},
|
||||
"camacari": {
|
||||
"description": "Lista aproximada derivada de localidades nos locais de votação TRE-BA (ZE 170/171) e curadoria manual — não há circunscrição oficial zona↔bairro.",
|
||||
"url": "https://www.tre-ba.jus.br/servicos-eleitorais/titulo-e-local-de-votacao",
|
||||
"accessed": "2026-07-21"
|
||||
}
|
||||
},
|
||||
"zoneCount": 21,
|
||||
"assignments": [
|
||||
{
|
||||
"plazaSlug": "salvador-ze-1",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 1,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Alto das Pombas",
|
||||
"Barra",
|
||||
"Calabar",
|
||||
"Canela",
|
||||
"Centro",
|
||||
"Centro Histórico",
|
||||
"Garcia",
|
||||
"Graça",
|
||||
"Vitória"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-2",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 2,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Amaralina",
|
||||
"Chapada do Rio Vermelho",
|
||||
"Engenho Velho da Federação",
|
||||
"Federação",
|
||||
"Ondina",
|
||||
"Rio Vermelho"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-3",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 3,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Baixa de Quintas",
|
||||
"Cidade Nova",
|
||||
"Fazenda Grande do Retiro",
|
||||
"IAPI",
|
||||
"Pau Miúdo",
|
||||
"Pero Vaz",
|
||||
"Santa Mônica"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-4",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 4,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Coutos",
|
||||
"Fazenda Coutos",
|
||||
"Ilha de Bom Jesus dos Passos",
|
||||
"Ilha de Maré",
|
||||
"Ilha dos Frades",
|
||||
"Nova Constituinte",
|
||||
"Paripe",
|
||||
"Periperi (parte ao norte da Rua das Pedrinhas)",
|
||||
"São Tomé"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-5",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 5,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Barreiras",
|
||||
"Cabula",
|
||||
"Engomadeira",
|
||||
"Mata Escura",
|
||||
"Resgate",
|
||||
"Retiro",
|
||||
"São Gonçalo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-6",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 6,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Acupe",
|
||||
"Boa Vista de Brotas",
|
||||
"Brotas",
|
||||
"Candeal",
|
||||
"Engenho Velho de Brotas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-7",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 7,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Barbalho",
|
||||
"Barris",
|
||||
"Comércio",
|
||||
"Cosme de Farias",
|
||||
"Luiz Anselmo",
|
||||
"Macaúbas",
|
||||
"Matatu",
|
||||
"Nazaré",
|
||||
"Santo Agostinho",
|
||||
"Santo Antônio",
|
||||
"Saúde",
|
||||
"Tororó",
|
||||
"Vila Laura"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-8",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 8,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Cajazeiras II",
|
||||
"Cajazeiras IV",
|
||||
"Cajazeiras V",
|
||||
"Cajazeiras VI",
|
||||
"Cajazeiras VII",
|
||||
"Cajazeiras VIII",
|
||||
"Campinas de Pirajá",
|
||||
"Marechal Rondon",
|
||||
"Moradas da Lagoa",
|
||||
"Palestina",
|
||||
"Pirajá",
|
||||
"Valéria"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-9",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 9,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Boa Viagem",
|
||||
"Bonfim",
|
||||
"Jardim Cruzeiro",
|
||||
"Mangueira",
|
||||
"Massaranduba",
|
||||
"Monte Serrat",
|
||||
"Ribeira",
|
||||
"Santa Luzia",
|
||||
"Uruguai",
|
||||
"Vila Ruy Barbosa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-10",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 10,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Bairro da Paz",
|
||||
"Boca do Rio",
|
||||
"Costa Azul",
|
||||
"Imbuí",
|
||||
"Jardim Armação",
|
||||
"Patamares",
|
||||
"Piatã",
|
||||
"Pituaçu",
|
||||
"STIEP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-11",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 11,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Águas Claras",
|
||||
"Calabetão",
|
||||
"Castelo Branco",
|
||||
"Dom Avelar",
|
||||
"Granjas Rurais Presidente Vargas",
|
||||
"Jardim Cajazeiras",
|
||||
"Jardim Santo Inácio",
|
||||
"Pau da Lima",
|
||||
"Porto Seco Pirajá",
|
||||
"Sete de Abril",
|
||||
"Vila Canária"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-12",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 12,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Aeroporto",
|
||||
"Alto do Coqueirinho",
|
||||
"Areia Branca",
|
||||
"Itapuã",
|
||||
"Itinga",
|
||||
"Jardim das Margaridas",
|
||||
"São Cristóvão",
|
||||
"Stella Maris"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-13",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 13,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Caminho das Árvores",
|
||||
"Itaigara",
|
||||
"Nordeste de Amaralina",
|
||||
"Pituba",
|
||||
"Santa Cruz",
|
||||
"Vale das Pedrinhas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-14",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 14,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Arenoso",
|
||||
"Novo Horizonte",
|
||||
"São Marcos",
|
||||
"Sussuarana",
|
||||
"Tancredo Neves"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-15",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 15,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Arraial do Retiro",
|
||||
"Boa Vista de São Caetano",
|
||||
"Bom Juá",
|
||||
"Capelinha",
|
||||
"Lobato",
|
||||
"São Caetano"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-16",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 16,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Cabula VI",
|
||||
"Canabrava",
|
||||
"Centro Administrativo da Bahia",
|
||||
"Doron",
|
||||
"Narandiba",
|
||||
"Nova Sussuarana",
|
||||
"Pernambués",
|
||||
"Saboeiro",
|
||||
"São Rafael",
|
||||
"Saramandaia",
|
||||
"Vale dos Lagos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-17",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 17,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Alto da Terezinha",
|
||||
"Alto do Cabrito",
|
||||
"Itacaranha",
|
||||
"Periperi (parte ao sul da Rua das Pedrinhas)",
|
||||
"Plataforma",
|
||||
"Praia Grande",
|
||||
"Rio Sena",
|
||||
"São João do Cabrito"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-18",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 18,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Caixa D'água",
|
||||
"Calçada",
|
||||
"Caminho de Areia",
|
||||
"Curuzu",
|
||||
"Lapinha",
|
||||
"Liberdade",
|
||||
"Mares",
|
||||
"Roma"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "salvador-ze-19",
|
||||
"city": "Salvador",
|
||||
"zoneNumber": 19,
|
||||
"source": "tre-ra-02-2017",
|
||||
"neighborhoods": [
|
||||
"Boca da Mata",
|
||||
"Cajazeiras X",
|
||||
"Cajazeiras XI",
|
||||
"Cassange",
|
||||
"Fazenda Grande I",
|
||||
"Fazenda Grande II",
|
||||
"Fazenda Grande III",
|
||||
"Fazenda Grande IV",
|
||||
"Jaguaripe I",
|
||||
"Jardim Nova Esperança",
|
||||
"Mussurunga",
|
||||
"Nova Brasília",
|
||||
"Nova Esperança",
|
||||
"Novo Marotinho",
|
||||
"Trobogy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "camacari-ze-170",
|
||||
"city": "Camaçari",
|
||||
"zoneNumber": 170,
|
||||
"source": "tre-voting-locations-curated",
|
||||
"neighborhoods": [
|
||||
"Alto da Cruz",
|
||||
"Bairro do Natal",
|
||||
"Camaçari de Dentro",
|
||||
"Centro",
|
||||
"Dois de Julho",
|
||||
"Gleba A",
|
||||
"Gleba B",
|
||||
"Gleba C",
|
||||
"Gleba E",
|
||||
"Gleba H",
|
||||
"Gravatá",
|
||||
"Inocoop",
|
||||
"Jardim Limoeiro",
|
||||
"Lama Preta",
|
||||
"Limoeiro",
|
||||
"Mangueiral",
|
||||
"Nova Vitória",
|
||||
"Novo Horizonte",
|
||||
"Parque Verde",
|
||||
"Phoc I",
|
||||
"Phoc II",
|
||||
"Phoc III",
|
||||
"Polo Petroquímico",
|
||||
"Triângulo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plazaSlug": "camacari-ze-171",
|
||||
"city": "Camaçari",
|
||||
"zoneNumber": 171,
|
||||
"source": "tre-voting-locations-curated",
|
||||
"neighborhoods": [
|
||||
"Abrantes",
|
||||
"Arembepe",
|
||||
"Barra do Jacuípe",
|
||||
"Barra do Pojuca",
|
||||
"Catu de Abrantes",
|
||||
"Guarajuba",
|
||||
"Itacimirim",
|
||||
"Jauá",
|
||||
"Monte Gordo",
|
||||
"Povoado de Areias",
|
||||
"Vila de Abrantes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidenceSha256": "1879319cb12b85697a60e6903f633ef20d23bc031361f862991f26ffb6168187"
|
||||
}
|
||||
126
tests/int/plazaZoneNeighborhoods.int.spec.ts
Normal file
126
tests/int/plazaZoneNeighborhoods.int.spec.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { plazaCatalog } from '@/lib/plazaCatalog'
|
||||
import {
|
||||
plazaZoneNeighborhoodEntryForSlug,
|
||||
plazaZoneNeighborhoodSourceLabel,
|
||||
plazaZoneNeighborhoods,
|
||||
} from '@/lib/plazaZoneNeighborhoods'
|
||||
|
||||
type OfficialEvidence = {
|
||||
zoneCount: number
|
||||
assignments: Array<{
|
||||
plazaSlug: string
|
||||
city: 'Salvador' | 'Camaçari'
|
||||
zoneNumber: number
|
||||
source: 'tre-ra-02-2017' | 'tre-voting-locations-curated'
|
||||
neighborhoods: string[]
|
||||
}>
|
||||
evidenceSha256: string
|
||||
}
|
||||
|
||||
const officialEvidence = JSON.parse(
|
||||
readFileSync(new URL('../fixtures/plaza-zone-neighborhoods.official.json', import.meta.url), 'utf8'),
|
||||
) as OfficialEvidence
|
||||
|
||||
const canonicalEvidenceRows = (assignments: OfficialEvidence['assignments']): string =>
|
||||
[...assignments]
|
||||
.sort((left, right) => left.plazaSlug.localeCompare(right.plazaSlug, 'pt-BR'))
|
||||
.map(
|
||||
({ plazaSlug, city, zoneNumber, neighborhoods }) =>
|
||||
`${plazaSlug}\t${city}\t${zoneNumber}\t${[...neighborhoods]
|
||||
.sort((left, right) => left.localeCompare(right, 'pt-BR'))
|
||||
.join('|')}\n`,
|
||||
)
|
||||
.join('')
|
||||
|
||||
describe('Praça-zona neighborhood catalog (Salvador + Camaçari)', () => {
|
||||
const zonePlazas = plazaCatalog.filter((entry) => entry.kind === 'zona')
|
||||
|
||||
it('covers every zona Praça with a non-empty neighborhood list', () => {
|
||||
expect(plazaZoneNeighborhoods).toHaveLength(zonePlazas.length)
|
||||
expect(new Set(plazaZoneNeighborhoods.map((entry) => entry.plazaSlug)).size).toBe(
|
||||
zonePlazas.length,
|
||||
)
|
||||
|
||||
for (const plaza of zonePlazas) {
|
||||
const record = plazaZoneNeighborhoodEntryForSlug(plaza.slug)
|
||||
expect(record, plaza.slug).toBeDefined()
|
||||
expect(record?.city).toBe(plaza.city)
|
||||
expect(record?.zoneNumber).toBe(plaza.zoneNumber)
|
||||
expect(record?.neighborhoods.length, plaza.slug).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps Salvador neighborhoods exclusive across zones (Camaçari is curated — no cross-zone exclusivity test)', () => {
|
||||
const seen = new Map<string, string>()
|
||||
|
||||
for (const record of plazaZoneNeighborhoods.filter((entry) => entry.city === 'Salvador')) {
|
||||
for (const neighborhood of record.neighborhoods) {
|
||||
const previous = seen.get(neighborhood)
|
||||
expect(previous, `${neighborhood} in ZE ${record.zoneNumber}`).toBeUndefined()
|
||||
seen.set(neighborhood, record.plazaSlug)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('matches the independently transcribed official evidence fixture', () => {
|
||||
expect(officialEvidence.zoneCount).toBe(zonePlazas.length)
|
||||
expect(
|
||||
officialEvidence.assignments.map(
|
||||
({ plazaSlug, city, zoneNumber, source, neighborhoods }) => ({
|
||||
plazaSlug,
|
||||
city,
|
||||
zoneNumber,
|
||||
source,
|
||||
neighborhoods,
|
||||
}),
|
||||
),
|
||||
).toEqual(
|
||||
plazaZoneNeighborhoods.map(({ plazaSlug, city, zoneNumber, source, neighborhoods }) => ({
|
||||
plazaSlug,
|
||||
city,
|
||||
zoneNumber,
|
||||
source,
|
||||
neighborhoods: [...neighborhoods],
|
||||
})),
|
||||
)
|
||||
})
|
||||
|
||||
it('matches every assignment by a fixed evidence checksum', () => {
|
||||
const digest = createHash('sha256')
|
||||
.update(
|
||||
canonicalEvidenceRows(
|
||||
plazaZoneNeighborhoods.map(
|
||||
({ plazaSlug, city, zoneNumber, source, neighborhoods }) => ({
|
||||
plazaSlug,
|
||||
city,
|
||||
zoneNumber,
|
||||
source,
|
||||
neighborhoods: [...neighborhoods],
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.digest('hex')
|
||||
expect(digest).toBe(officialEvidence.evidenceSha256)
|
||||
})
|
||||
|
||||
it('exposes lookup helpers for zona slugs only', () => {
|
||||
expect(plazaZoneNeighborhoodEntryForSlug('salvador-ze-3')?.neighborhoods).toContain(
|
||||
'Santa Mônica',
|
||||
)
|
||||
expect(plazaZoneNeighborhoodEntryForSlug('camacari-ze-171')?.neighborhoods).toContain(
|
||||
'Arembepe',
|
||||
)
|
||||
expect(plazaZoneNeighborhoodEntryForSlug('feira-de-santana')).toBeUndefined()
|
||||
expect(plazaZoneNeighborhoodEntryForSlug('salvador-ze-1')?.source).toBe('tre-ra-02-2017')
|
||||
expect(plazaZoneNeighborhoodSourceLabel('tre-ra-02-2017')).toMatch(/Resolução Administrativa/)
|
||||
expect(plazaZoneNeighborhoodSourceLabel('tre-voting-locations-curated')).toMatch(/aproximada/)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue