From 342e528c87d64e04ebdd7a4b256e9fe170f03666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Wed, 29 Jul 2026 23:16:47 -0300 Subject: [PATCH] =?UTF-8?q?feat(E10+):=20Escala=20e=20DRY=20p=C3=B3s-E10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - F1: loadMunicipalityScopeFromDocs evita segunda query em municipalityPageData - F2: computeAllMunicipalityPotentials memoizado por processo - F3: TooltipProvider movido para o layout de (app) - F4: DRY nos filtros multi do mobile em MunicipalityFilters --- .cursor/rules/projects/nucleos-eleitorais.mdc | 1 + docs/plans/escala-dry-pos-e10.md | 4 +- docs/roadmap.md | 2 +- src/app/(campaign)/campanha/(app)/layout.tsx | 3 +- .../municipality/MunicipalityFilters.tsx | 107 +++++++----------- .../campaign/shared/CampaignHoverTooltip.tsx | 28 +++-- .../municipality/campaignMunicipalityScope.ts | 22 +++- .../municipality/municipalityListFilters.ts | 7 ++ .../municipality/municipalityPageData.ts | 11 +- .../municipality/municipalityPotential.ts | 18 ++- .../municipality/municipalityTriggers.ts | 2 +- .../territory/loadTerritoryOverview.ts | 7 +- tests/int/campaignSuggestions.int.spec.ts | 2 +- tests/unit/municipalityPotential.unit.spec.ts | 2 +- 14 files changed, 113 insertions(+), 103 deletions(-) diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 6f18d66f..f8a4bc2b 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,7 @@ alwaysApply: false ## Status +- **E10+ entregue em código (2026-07-29):** Escala e DRY pós-E10 — F1: `loadMunicipalityScopeFromDocs` no `municipalityPageData.ts` evita a segunda query de `municipality` quando `isPagedByPayload === false`; F2: `computeAllMunicipalityPotentials` memoizado por processo (retorna cache `Map`); F3: `TooltipProvider` movido para o layout de `(app)`, restaurando o agrupamento de delay do Radix para os tooltips das listas; F4: DRY nos filtros multi do mobile em `MunicipalityFilters` via `.map` sobre `municipalityFilterDefinitions` (que ganhou `emptyLabel`). Sem migration/UI nova. Plano: [`escala-dry-pos-e10.md`](../../../docs/plans/escala-dry-pos-e10.md). - **B60 entregue em código (2026-07-29):** etapa de busca de município nos wizards UX-1 — `WizardMunicipalitySearchStep` (debounce B47, `POST /campanha/home-search` modo `wizard-municipality`, só municípios scoped); select = `wizardActionHref` com `?municipio=`; loader compartilhado `searchStaffMunicipalityHits` (extraído de B48); `HomeSearchMunicipalityVoteTrailing` + `HomeSearchHitRow` link; stub B61 `WizardMunicipalitySelectedStub` com caption sticky; `WizardMunicipalityPlaceholderStep` removido. Sem migration/Consent. Unit `campaignActionRoutes`; int `searchStaffMunicipalityHits`; e2e wizard → `?municipio=cairu`. Plano: [`busca-municipio-wizard.md`](../../../docs/plans/busca-municipio-wizard.md). Desbloqueia **B61** / **B63** / **B64** / **B70**. - **B52 entregue em código (2026-07-29):** quarto grupo da busca global do Início — **Dobradinhas** via `searchHomeStateDeputies` + `HomeSearchStateDeputyGroup` (após Atividades); `POST /campanha/home-search` agrega `stateDeputies`; match word-start em nome ou partido; secundário `partido · N municípios`; assessor vê hits (staff read); `municipalityIdsByStateDeputyIds` em `stateDeputyData`. Int `homeSearchStateDeputies.int.spec.ts`. WA = **B55** (sem telefone na entidade). Plano: [`busca-global-resultados-dobradinhas.md`](../../../docs/plans/busca-global-resultados-dobradinhas.md). Próximo na cadeia busca: **B53** demandas. - **B72 entregue em código (2026-07-29):** gap horizontal da strip de ações do Início (UX-1) — `CampaignHomeActionStrip` `
    ` `gap-6` → `gap-4` (16px); `w-[4.75rem]`, círculo 48px, `snap-x`, scrollbar oculta e pan B67 ✓ inalterados. Sem migration/action/URL. Unit strip 11/11 em `campaignHomeActionButton.unit.spec.tsx`. Plano: [`espacamento-strip-acoes-inicio.md`](../../../docs/plans/espacamento-strip-acoes-inicio.md). diff --git a/docs/plans/escala-dry-pos-e10.md b/docs/plans/escala-dry-pos-e10.md index 2e0012d5..5cd3f447 100644 --- a/docs/plans/escala-dry-pos-e10.md +++ b/docs/plans/escala-dry-pos-e10.md @@ -1,7 +1,7 @@ # Escala e DRY pós-E10 (classificação territorial relativa) -Status: rascunho -Atualizado em: 2026-07-25 +Status: entregue (2026-07-29) +Atualizado em: 2026-07-29 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, **E10+**, fill-in de engenharia) Impeccable: A — N/A (sem superfície UI nova; as quatro fases preservam o comportamento visível de E10) Appetite: ~0,75–1 dia eng; quatro fases independentes, nenhuma com migration, collection ou `Consent` diff --git a/docs/roadmap.md b/docs/roadmap.md index fd836089..8806a661 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -173,7 +173,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - ~~**Selecionados no topo nos filtros do header**~~ — **✓ (entregue 2026-07-28)** opções já marcadas sobem ao topo nos Popovers multi-seleção do sistema de listas (municípios, lideranças, territórios, dobradinhas) e nos `CampaignMobileMultiFilterField`; helper puro `orderFilterOptionsSelectedFirst` (`src/lib/listFilterOptions.ts`); snapshot congelado enquanto o Popover está aberto; hairline entre grupos (oculto durante busca); single-select gateado por `row.checkbox` · Impeccable B · [plano](plans/selecionados-no-topo-filtros-header.md) - ~~**Cenário junto aos filtros**~~ — **✓ (entregue 2026-07-29)** `VoteEstimateScenarioField` na barra slim de `MunicipalityFilters` (desktop/mobile); overview só consome contexto; provider envolve filtros + resultados · ~0,25–0,5d · Impeccable B · [plano](plans/cenario-junto-filtros-municipios.md) - ~~**B16+** escala/DRY pós-B16~~ — **absorvido pelo Pass 2 W1-D1 (2026-07-25)**: `useOptimistic`, facet por slugs, hrefs via serializador canônico, facets no mesmo `Promise.all` · [plano](plans/escala-dry-pos-b16.md) -- **E10+** escala/DRY pós-E10 — quatro fases do `/simplify` da classificação territorial: **F1** a lista lê `municipality` 2× por request (`listQuery` + `loadMunicipalityScope`, mesmo `where`, select superset) — **fecha o "Adiado com gatilho" do A11**, cujo gatilho ("3º path derivado no loader") disparou com `classe` sendo o 4º (`votos`, `deficit`, `frescor`); **F2** `computeAllMunicipalityPotentials()` recomputa 435 linhas puras do artefato por request (memo de processo, como `bahiaElectionAggregates`/`municipalityVoteRank`); **F3** um `TooltipProvider` por tooltip (~30 na lista) anula o agrupamento de delay do Radix — hoist para o layout de `(app)`; **F4** os 4 multi-filtros do mobile saem da mesma `municipalityFilterDefinitions` que os single-select vizinhos já usam · ~0,75–1d eng · Impeccable A · sem migration · cortável · [plano](plans/escala-dry-pos-e10.md) +- ~~**E10+** escala/DRY pós-E10~~ — **✓ (entregue 2026-07-29)** quatro fases do `/simplify` da classificação territorial: **F1** a lista lê `municipality` 2× por request (`listQuery` + `loadMunicipalityScope`, mesmo `where`, select superset) — **fecha o "Adiado com gatilho" do A11**, cujo gatilho ("3º path derivado no loader") disparou com `classe` sendo o 4º (`votos`, `deficit`, `frescor`); **F2** `computeAllMunicipalityPotentials()` recomputa 435 linhas puras do artefato por request (memo de processo, como `bahiaElectionAggregates`/`municipalityVoteRank`); **F3** um `TooltipProvider` por tooltip (~30 na lista) anula o agrupamento de delay do Radix — hoist para o layout de `(app)`; **F4** os 4 multi-filtros do mobile saem da mesma `municipalityFilterDefinitions` que os single-select vizinhos já usam · ~0,75–1d eng · Impeccable A · sem migration · cortável · [plano](plans/escala-dry-pos-e10.md) - ~~**B13+** escala/DRY pós-B13~~ — **✓ (entregue 2026-07-29)** quatro fases do `/simplify` pós-B13: **F1** `tests/unit/mapLqConcordance.unit.spec.ts` pina o LQ do painel contra `computeMunicipalityTerritorialClass` (rede de segurança do E15, sem extrair helper); **F2** `actorRole` no mapa do **Quadro** (`quadro/page.tsx` → `MunicipalityMapPanelDynamic`) e copy de escopo no readout (quantis/LQ "no seu escopo" para assessor; posição "na cidade inteira"; bolha E10 "(lista)"); **F3** uma varredura federal por ano no `build:election-aggregates` via `candidateVotesByCityZoneFromFederalSlice` / `campoFederalVotesByCityZoneFromFederalSlice` (`party` no select do slice); **F4** `competitiveRankByYear` no bundle RSC como tupla `[rank, candidates]` + `readFederalCompetitivePlacement` (artefato JSON inalterado) · sem migration · [plano](plans/escala-dry-pos-b13.md) - ~~**B8+** escala/DRY pós-B8 F2~~ — **✓ (entregue 2026-07-27)** as quatro fases do `/simplify` dos polígonos das ZE. **F1:** a política de escala de chão nasceu em `scripts/lib/topology.mjs` (primeiro módulo de `scripts/lib/` — **B5 F2** pousa nele) declarando tolerância de Visvalingam em **m² de chão** e derivando os dígitos de quantização do bbox de cada topologia; `SIMPLIFY_QUANTILE`/`QUANTIZE_DIGITS` morreram nos dois scripts. A auditoria corrigiu a alavanca do plano: quantização sozinha quase não corta (mesmo na grade estadual as zonas ficariam em 203 pontos/feature), o peso absoluto é que manda — as zonas caíram de **9.120 → 574 pontos de arco (480 → 30,2/feature) e 72.310 → 7.687 bytes**, `bahia-municipalities` saiu **byte-idêntico** e territórios mudou 1 byte (grade 102,730 → 102,822 m). Tolerância própria por malha, com o número medido no comentário: a tolerância municipal chegaria a 17,9/feature nas zonas mas comeria 5% da área de Salvador (306 → 291 km²), que é justamente a grandeza que prova que nenhum bairro sumiu. `MAX_TOPO_BYTES` ganhou ao lado `MAX_POINTS_PER_FEATURE = 60`. **F2:** `featureMapKey` passou a ser exercido sobre features **reais** das três malhas, cada uma no laço que já a varre inteira (417 `codarea` + 27 `code` + 19 `slug`) — o `/simplify` corrigiu a primeira escrita, que asseria a cidade dentro do laço das zonas e portanto cobria **uma** feature municipal 19 vezes; o teste de precedência sintético caiu e o comentário passou a declarar o invariante real (as três propriedades de chave são mutuamente exclusivas; `ibgeCode` na zona deliberadamente não é chave). **F3:** `.catch()` na metade das zonas — o mapa segue sem elas (com o motivo logado), com Salvador como polígono sem dado nem interação — e a ressalva de aproximação saiu do gate `!comparisonActive` para um `

    ` condicionado a `bundle.hasZoneMunicipalities`, campo novo publicado pelo loader a partir de `kind === 'zona'`: o `/simplify` **rejeitou** o predicado `isZoneMapKey` que lia isso da forma da chave (7 dígitos ⇒ município), porque era uma segunda codificação da regra de `mapKeyForMunicipality`, e gatear em `zoneBreakdown` recriaria o acoplamento que o fill-in da lista (✓ 2026-07-28) deletou. **F4:** dois acessores órfãos deletados, a união de navegação de 3 estados colapsada em `string | undefined`, `MunicipalitiesByMapKey` virou `Record` (435 strings fora do payload RSC do Início), `polygonRingsOf` exportado e os helpers de geometria retipados em `PolygonalFeature`/`BahiaFeature

    `/`BahiaMeshFeature`. **Um item do plano foi rejeitado por medição:** `city`/`zoneNumber` de `MunicipalityZoneNeighborhoodEntry` **ficam** — entram nas linhas hasheadas por `canonicalEvidenceRows` e são colunas do fixture de proveniência da RA 02/2017, então não são código morto (só o `zoneNumber` do **artefato** TopoJSON saiu) · sem migration · [plano](plans/escala-dry-pos-b8f2.md) - **E12+** escala/DRY pós-E12 — **✓ (entregue 2026-07-29)** F1: `src/lib/metropolitanoTerritoryPeers.ts` (peers/sub-linhas Metropolitano unificados para rollup, classe agregada e benchmark T4); F2 adiada — `benchmarkBySlug` já cobre · ~0,25d · Impeccable A · [plano](plans/escala-dry-pos-e12.md) diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index f85559f0..299d5ed1 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -19,6 +19,7 @@ import { SidebarTrigger, } from '@/components/ui/Sidebar' import { Toaster } from '@/components/ui/Toaster' +import { TooltipProvider } from '@/components/ui/tooltip' import { deviceLabelFromUserAgent } from '@/lib/deviceLabel' import { getCampaignUser } from '@/utilities/campaignAuth' import { campaignUserShellView } from '@/utilities/campaignUserProfile' @@ -84,7 +85,7 @@ export default async function CampaignAppLayout({ children }: { children: React. data-slot="campaign-content-scroll" className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 pb-24 md:p-6 md:pb-6 print:h-auto print:overflow-visible print:p-0" > - {children} + {children} diff --git a/src/components/campaign/municipality/MunicipalityFilters.tsx b/src/components/campaign/municipality/MunicipalityFilters.tsx index 1d571e1f..4e9fd10a 100644 --- a/src/components/campaign/municipality/MunicipalityFilters.tsx +++ b/src/components/campaign/municipality/MunicipalityFilters.tsx @@ -16,11 +16,12 @@ import { buildMunicipalityFilterHref, clearMunicipalityListFilters, formatMunicipalityActiveFiltersSummary, - getMunicipalityFilterDefinition, + getMunicipalityMultiFilterValues, municipalityFilterDefinitions, toggleMunicipalityExclusiveFilterValue, toggleMunicipalityMultiFilterValue, type MunicipalityFilterOption, + type MunicipalityMultiFilterParam, } from '@/utilities/municipality/municipalityListFilters' import { municipalityListSortOptions, @@ -75,8 +76,8 @@ export const MunicipalityFilters = ({ const mobileFilterDefinitions = municipalityFilterDefinitions.filter((definition) => { if (definition.staffOnly && !showStaffFilters) return false - // Header popovers own multi/name filters; mobile keeps exclusive selects. - return definition.selection === 'single' || definition.selection === 'toggle' + if (definition.param === 'name') return false + return true }) return ( @@ -145,19 +146,47 @@ export const MunicipalityFilters = ({ ) : null} - {regionFilterOptions.length ? ( - - navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'region', value)) - } - /> - ) : null} {mobileFilterDefinitions.map((definition) => { + if (definition.selection === 'multi') { + let options = definition.options ?? [] + if (definition.param === 'region') options = regionFilterOptions + if (definition.param === 'advisor') options = advisorFilterOptions + + if (options.length === 0) return null + + let selected: string[] = [] + if (definition.param !== 'region' && definition.param !== 'advisor') { + selected = getMunicipalityMultiFilterValues( + state, + definition.param as MunicipalityMultiFilterParam, + ) + } else if (definition.param === 'region') { + selected = state.regions ?? [] + } else if (definition.param === 'advisor') { + selected = (state.advisors ?? []).map(String) + } + + return ( + + navigateWithSearch( + toggleMunicipalityMultiFilterValue( + state, + definition.param as MunicipalityMultiFilterParam, + value, + ), + ) + } + /> + ) + } + const value = definition.param === 'coverage' ? (state.coverage ?? '') : '' return ( @@ -189,54 +218,6 @@ export const MunicipalityFilters = ({ ) })} - {showStaffFilters ? ( - - navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'trend', value)) - } - /> - ) : null} - {showStaffFilters ? ( - - navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'class', value)) - } - /> - ) : null} - {showStaffFilters ? ( - - navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'level', value)) - } - /> - ) : null} - {showStaffFilters && advisorFilterOptions.length ? ( - - navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'advisor', value)) - } - /> - ) : null} {showStaffFilters ? ( - setOpen(disabled ? false : next)}> - {trigger} - - {content} - - - + setOpen(disabled ? false : next)}> + {trigger} + + {content} + + ) } diff --git a/src/utilities/municipality/campaignMunicipalityScope.ts b/src/utilities/municipality/campaignMunicipalityScope.ts index a0f8e890..0d68da96 100644 --- a/src/utilities/municipality/campaignMunicipalityScope.ts +++ b/src/utilities/municipality/campaignMunicipalityScope.ts @@ -75,12 +75,7 @@ const loadScope = async ( } const municipalities = await municipalitiesPromise - const pledgeAggregates = await aggregatePledgesByMunicipality( - payload, - municipalities.map((municipality) => municipality.id), - ) - - return { municipalities, pledgeAggregates } + return loadMunicipalityScopeFromDocs(payload, municipalities) } /** @@ -112,6 +107,21 @@ const loadScopeCached = cache( * in the `cache()` key via its sorted key list — a widened call never receives * the base select's cached rows (P3-E pin: cache-key separation). */ +export const loadMunicipalityScopeFromDocs = async ( + payload: Payload, + municipalities: Doc[], +): Promise<{ + municipalities: Doc[] + pledgeAggregates: Map +}> => { + const pledgeAggregates = await aggregatePledgesByMunicipality( + payload, + municipalities.map((municipality) => municipality.id), + ) + + return { municipalities, pledgeAggregates } +} + export const loadMunicipalityScope = >( payload: Payload, user: CampaignUser, diff --git a/src/utilities/municipality/municipalityListFilters.ts b/src/utilities/municipality/municipalityListFilters.ts index 20945282..917c5ae7 100644 --- a/src/utilities/municipality/municipalityListFilters.ts +++ b/src/utilities/municipality/municipalityListFilters.ts @@ -54,6 +54,8 @@ export type MunicipalityFilterDefinition = { label: string /** Shown for exclusive single-select clear row; omitted for toggle/multi. */ allLabel?: string + /** Shown for empty state in multi-selects. */ + emptyLabel?: string staffOnly: boolean selection: MunicipalityFilterSelectionMode /** Absent for the params whose options are cross-filtered server-side. */ @@ -70,12 +72,14 @@ export const municipalityFilterDefinitions: MunicipalityFilterDefinition[] = [ { param: 'region', label: 'Território', + emptyLabel: 'Todos', staffOnly: false, selection: 'multi', }, { param: 'advisor', label: 'Assessores', + emptyLabel: 'Todos', staffOnly: true, selection: 'multi', }, @@ -93,6 +97,7 @@ export const municipalityFilterDefinitions: MunicipalityFilterDefinition[] = [ { param: 'trend', label: 'Tendência', + emptyLabel: 'Todas', staffOnly: true, selection: 'multi', options: (Object.keys(politicalTrendLabels) as Array).map( @@ -102,6 +107,7 @@ export const municipalityFilterDefinitions: MunicipalityFilterDefinition[] = [ { param: 'class', label: 'Classe', + emptyLabel: 'Todas', staffOnly: true, selection: 'multi', options: ( @@ -114,6 +120,7 @@ export const municipalityFilterDefinitions: MunicipalityFilterDefinition[] = [ { param: 'level', label: 'Nível', + emptyLabel: 'Todos', staffOnly: true, selection: 'multi', // Static options, like `trend`: the ladder is a closed set, so offering diff --git a/src/utilities/municipality/municipalityPageData.ts b/src/utilities/municipality/municipalityPageData.ts index d6986ea7..dd5a7be1 100644 --- a/src/utilities/municipality/municipalityPageData.ts +++ b/src/utilities/municipality/municipalityPageData.ts @@ -16,7 +16,10 @@ import { type VoteEstimateScenario } from '@/lib/voteEstimate' import type { CampaignUser, Municipality } from '@/payload-types' import { isCampaignLeader, isCampaignStaff } from '@/utilities/campaignAccess' import { createEntityNotFoundError } from '@/utilities/entityNotFound' -import { loadMunicipalityScope } from '@/utilities/municipality/campaignMunicipalityScope' +import { + loadMunicipalityScope, + loadMunicipalityScopeFromDocs, +} from '@/utilities/municipality/campaignMunicipalityScope' import { centralDeficitSortValue, type MunicipalityGoalCoverage, @@ -354,7 +357,11 @@ export const loadMunicipalityListPageBundle = async ( // Request-scoped shared load (docs + pledge aggregates in one place). const staffScopePromise = isStaff - ? loadMunicipalityScope(payload, user, where) + ? isPagedByPayload + ? loadMunicipalityScope(payload, user, where) + : listQuery.then((result) => + loadMunicipalityScopeFromDocs(payload, result.docs as Municipality[]), + ) : Promise.resolve(null) const [listResult, scopeCount, staffScope, filterFacets] = await Promise.all([ diff --git a/src/utilities/municipality/municipalityPotential.ts b/src/utilities/municipality/municipalityPotential.ts index 18d19a52..5f98eca2 100644 --- a/src/utilities/municipality/municipalityPotential.ts +++ b/src/utilities/municipality/municipalityPotential.ts @@ -149,9 +149,18 @@ export const computeMunicipalityPotential = (slug: string): MunicipalityPotentia } } +let municipalityPotentialBySlugCache: Map | null = null + /** `computeMunicipalityPotential` for every catalog municipality (435 slugs). */ -export const computeAllMunicipalityPotentials = (): MunicipalityPotential[] => - municipalityCatalog.map((entry) => computeMunicipalityPotential(entry.slug)) +export const computeAllMunicipalityPotentials = (): Map => { + if (municipalityPotentialBySlugCache) return municipalityPotentialBySlugCache + + municipalityPotentialBySlugCache = new Map() + for (const entry of municipalityCatalog) { + municipalityPotentialBySlugCache.set(entry.slug, computeMunicipalityPotential(entry.slug)) + } + return municipalityPotentialBySlugCache +} /** Fallback pessimistic haircut (as a share) when `campaignGoals.margin` is unset. */ const DEFAULT_PESSIMISTIC_HAIRCUT = 0.1 @@ -350,12 +359,11 @@ export const computeStatewideSuggestedGoals = ( potentialBySlug: Map suggestedGoalBySlug: Map } => { - const potentials = computeAllMunicipalityPotentials() + const potentialBySlug = computeAllMunicipalityPotentials() const { suggestedGoalBySlug } = deriveSuggestedGoalsByScenario( - potentials.map((potential) => potential.slug), + Array.from(potentialBySlug.keys()), goals, ) - const potentialBySlug = new Map(potentials.map((potential) => [potential.slug, potential])) return { potentialBySlug, suggestedGoalBySlug } } diff --git a/src/utilities/municipality/municipalityTriggers.ts b/src/utilities/municipality/municipalityTriggers.ts index 388f430b..cef88c49 100644 --- a/src/utilities/municipality/municipalityTriggers.ts +++ b/src/utilities/municipality/municipalityTriggers.ts @@ -125,7 +125,7 @@ const isoDaysAgo = (now: Date, days: number): string => let projectedValidVotesUpperCutMemo: number | null = null const projectedValidVotesUpperCut = (): number => { if (projectedValidVotesUpperCutMemo !== null) return projectedValidVotesUpperCutMemo - const values = computeAllMunicipalityPotentials() + const values = Array.from(computeAllMunicipalityPotentials().values()) .map((potential) => potential.projectedValidVotes) .filter((value) => value > 0) .sort((left, right) => left - right) diff --git a/src/utilities/territory/loadTerritoryOverview.ts b/src/utilities/territory/loadTerritoryOverview.ts index 3e090898..b36b0f1b 100644 --- a/src/utilities/territory/loadTerritoryOverview.ts +++ b/src/utilities/territory/loadTerritoryOverview.ts @@ -7,6 +7,7 @@ import { getMunicipalityFederalBaseline } from '@/lib/bahiaElectionAggregates' import { slugsForMetropolitanoSubRowLabel } from '@/lib/metropolitanoTerritoryPeers' import { DEFAULT_VOTE_ESTIMATE_SCENARIO, type VoteEstimateScenario } from '@/lib/voteEstimate' import type { CampaignUser, Municipality, User } from '@/payload-types' +import { loadMunicipalityScopeFromDocs } from '@/utilities/municipality/campaignMunicipalityScope' import { loadMunicipalityGoalCoverageBundle } from '@/utilities/municipality/municipalityGoalAccount' import { fieldCeiling, ownVotes2022 } from '@/utilities/municipality/municipalityPotential' import { computeAggregateTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass' @@ -15,7 +16,6 @@ import { type TerritoryMunicipalityInput, type TerritoryOverviewRow, } from '@/utilities/territory/territoryOverview' -import { aggregatePledgesByMunicipality } from '@/utilities/votePledgeData' import { emptyMunicipalityPledgeAggregate, resolveMunicipalityStaffVoteTotal, @@ -82,10 +82,7 @@ export const loadTerritoryOverview = cache( }) const docs = result.docs as MunicipalityDoc[] - const aggregates = await aggregatePledgesByMunicipality( - payload, - docs.map((doc) => doc.id), - ) + const { pledgeAggregates: aggregates } = await loadMunicipalityScopeFromDocs(payload, docs) const { coverageByMunicipalityID } = await loadMunicipalityGoalCoverageBundle( payload, diff --git a/tests/int/campaignSuggestions.int.spec.ts b/tests/int/campaignSuggestions.int.spec.ts index 94204601..a1736edf 100644 --- a/tests/int/campaignSuggestions.int.spec.ts +++ b/tests/int/campaignSuggestions.int.spec.ts @@ -191,7 +191,7 @@ describe('loadMunicipalitySuggestions (E11)', () => { // The evaluator's own cut convention, recomputed here so a drifting // artifact fails this line, not the loop below. - const values = computeAllMunicipalityPotentials() + const values = Array.from(computeAllMunicipalityPotentials().values()) .map((potential) => potential.projectedValidVotes) .filter((value) => value > 0) .sort((left, right) => left - right) diff --git a/tests/unit/municipalityPotential.unit.spec.ts b/tests/unit/municipalityPotential.unit.spec.ts index 2479bbb0..7c7dee32 100644 --- a/tests/unit/municipalityPotential.unit.spec.ts +++ b/tests/unit/municipalityPotential.unit.spec.ts @@ -360,6 +360,6 @@ describe('computeMunicipalityPotential / computeAllMunicipalityPotentials (real await import('@/utilities/municipality/municipalityPotential') const potentials = computeAllMunicipalityPotentials() - expect(potentials).toHaveLength(municipalityCatalog.length) + expect(potentials.size).toBe(municipalityCatalog.length) }) })