mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
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:
parent
7b6b3940d1
commit
342e528c87
14 changed files with 113 additions and 103 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue