feat(E10+): Escala e DRY pós-E10

- 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
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-29 23:16:47 -03:00
parent 7b6b3940d1
commit 342e528c87
14 changed files with 113 additions and 103 deletions

View file

@ -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<string, MunicipalityPotential>`); 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=<slug>`; 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` `<ul>` `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).

View file

@ -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`

View file

@ -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 `<p>` 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<string, string>` (435 strings fora do payload RSC do Início), `polygonRingsOf` exportado e os helpers de geometria retipados em `PolygonalFeature`/`BahiaFeature<P>`/`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)

View file

@ -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}
<TooltipProvider delayDuration={300}>{children}</TooltipProvider>
</div>
<CampaignBottomNav role={user.role} />
<Toaster position="top-center" />

View file

@ -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 = ({
</NativeSelect>
</Field>
) : null}
{regionFilterOptions.length ? (
<CampaignMobileMultiFilterField
id="municipality-filter-region"
label="Território"
emptyLabel="Todos"
options={regionFilterOptions}
selected={state.regions ?? []}
onToggle={(value) =>
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 (
<CampaignMobileMultiFilterField
key={definition.param}
id={`municipality-filter-${definition.param}`}
label={definition.label}
emptyLabel={definition.emptyLabel ?? 'Todos'}
options={options}
selected={selected}
onToggle={(value) =>
navigateWithSearch(
toggleMunicipalityMultiFilterValue(
state,
definition.param as MunicipalityMultiFilterParam,
value,
),
)
}
/>
)
}
const value = definition.param === 'coverage' ? (state.coverage ?? '') : ''
return (
<Field key={definition.param}>
@ -189,54 +218,6 @@ export const MunicipalityFilters = ({
</Field>
)
})}
{showStaffFilters ? (
<CampaignMobileMultiFilterField
id="municipality-filter-trend"
label="Tendência"
emptyLabel="Todas"
options={getMunicipalityFilterDefinition('trend').options ?? []}
selected={state.trends ?? []}
onToggle={(value) =>
navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'trend', value))
}
/>
) : null}
{showStaffFilters ? (
<CampaignMobileMultiFilterField
id="municipality-filter-class"
label="Classe"
emptyLabel="Todas"
options={getMunicipalityFilterDefinition('class').options ?? []}
selected={state.classes ?? []}
onToggle={(value) =>
navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'class', value))
}
/>
) : null}
{showStaffFilters ? (
<CampaignMobileMultiFilterField
id="municipality-filter-level"
label="Nível"
emptyLabel="Todos"
options={getMunicipalityFilterDefinition('level').options ?? []}
selected={state.levels ?? []}
onToggle={(value) =>
navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'level', value))
}
/>
) : null}
{showStaffFilters && advisorFilterOptions.length ? (
<CampaignMobileMultiFilterField
id="municipality-filter-advisor"
label="Assessores"
emptyLabel="Todos"
options={advisorFilterOptions}
selected={(state.advisors ?? []).map(String)}
onToggle={(value) =>
navigateWithSearch(toggleMunicipalityMultiFilterValue(state, 'advisor', value))
}
/>
) : null}
{showStaffFilters ? (
<MunicipalityFilterEstimateScenario
id="municipality-filter-estimate-scenario-mobile"

View file

@ -3,7 +3,7 @@
import type { PointerEvent, ReactElement, ReactNode, Ref } from 'react'
import { cloneElement, useEffect, useRef, useState } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
/**
* Hover/focus explanation with no extra chrome — wraps an existing control,
@ -92,19 +92,17 @@ export const CampaignHoverTooltip = ({
})
return (
<TooltipProvider delayDuration={300}>
<Tooltip open={!disabled && open} onOpenChange={(next) => setOpen(disabled ? false : next)}>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
ref={contentRef}
side={side}
align={align}
sideOffset={sideOffset}
className="max-w-xs text-left font-normal"
>
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip open={!disabled && open} onOpenChange={(next) => setOpen(disabled ? false : next)}>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
ref={contentRef}
side={side}
align={align}
sideOffset={sideOffset}
className="max-w-xs text-left font-normal"
>
{content}
</TooltipContent>
</Tooltip>
)
}

View file

@ -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 <Doc extends { id: number }>(
payload: Payload,
municipalities: Doc[],
): Promise<{
municipalities: Doc[]
pledgeAggregates: Map<number, MunicipalityPledgeAggregate>
}> => {
const pledgeAggregates = await aggregatePledgesByMunicipality(
payload,
municipalities.map((municipality) => municipality.id),
)
return { municipalities, pledgeAggregates }
}
export const loadMunicipalityScope = <ExtraSelect extends Record<string, true>>(
payload: Payload,
user: CampaignUser,

View file

@ -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<keyof typeof politicalTrendLabels>).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

View file

@ -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([

View file

@ -149,9 +149,18 @@ export const computeMunicipalityPotential = (slug: string): MunicipalityPotentia
}
}
let municipalityPotentialBySlugCache: Map<string, MunicipalityPotential> | null = null
/** `computeMunicipalityPotential` for every catalog municipality (435 slugs). */
export const computeAllMunicipalityPotentials = (): MunicipalityPotential[] =>
municipalityCatalog.map((entry) => computeMunicipalityPotential(entry.slug))
export const computeAllMunicipalityPotentials = (): Map<string, MunicipalityPotential> => {
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<string, MunicipalityPotential>
suggestedGoalBySlug: Map<string, SuggestedGoalByScenario>
} => {
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 }
}

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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)
})
})