From b4f2e2fa0a26ce2f82ad6dd25137e5e71505ba59 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:03:07 +0000 Subject: [PATCH 1/9] =?UTF-8?q?B129=20=E2=80=94=20Remove=20municipality=20?= =?UTF-8?q?list=20overview=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove MunicipalityListOverview from /campanha/municipios and drop the overview field/computation from loadMunicipalityListPageBundle. Filters, scenario selector, and per-row goal coverage for sorting remain unchanged. Co-authored-by: Francisco Solla --- .../remover-overview-lista-municipios-impl.md | 53 ++++++ .../remover-overview-lista-municipios.md | 25 +++ .../campanha/(app)/municipios/page.tsx | 19 +- .../municipality/MunicipalityList.tsx | 2 +- .../municipality/MunicipalityListOverview.tsx | 177 ------------------ .../municipality/municipalityPageData.ts | 50 +---- tests/int/municipalityPageData.int.spec.ts | 16 +- 7 files changed, 91 insertions(+), 251 deletions(-) create mode 100644 docs/plans/remover-overview-lista-municipios-impl.md delete mode 100644 src/components/campaign/municipality/MunicipalityListOverview.tsx diff --git a/docs/plans/remover-overview-lista-municipios-impl.md b/docs/plans/remover-overview-lista-municipios-impl.md new file mode 100644 index 00000000..3289ab98 --- /dev/null +++ b/docs/plans/remover-overview-lista-municipios-impl.md @@ -0,0 +1,53 @@ +# Impl: B129 — Remover overview da lista de municípios + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #266 +Intenção: docs/plans/remover-overview-lista-municipios.md +Appetite restante: remoção cirúrgica (sem redesign) + +## Leitura da intenção + +- **Outcome:** `/campanha/municipios` mostra filtros → lista, sem strip de métricas agregadas. +- **O que NÃO negociar:** filtros + cenário na barra; overview de apoiadores e dashboard intactos; leader lockdown. +- **O que reavaliar:** manter prefetch `loadStatewideSuggestedGoals` — sim, ainda aquece cache para `loadMunicipalityGoalCoverageBundle` usado nas linhas/colunas. + +## Abordagem recomendada + +**Opções consideradas:** + +- **A — Esconder com CSS/feature flag:** rejeitada — morto no loader e no bundle é mais honesto e barato. +- **B — Remover UI, manter `overview` no bundle:** rejeitada — viola “dead code dies immediately”. +- **C — Remover UI + tipo/campo/computação do loader:** **recomendada**. + +**Rejeitadas:** A, B. + +### Componentes / mudanças + +- **`municipios/page.tsx`:** remover import/render de `MunicipalityListOverview` e `shameHref`. +- **`MunicipalityListOverview.tsx`:** deletar arquivo. +- **`municipalityPageData.ts`:** remover `MunicipalityListOverviewData`, campo `overview` do bundle, rollup/overview-only fields; manter `staffScope` → `pledgeAggregates` + `goalCoverageByMunicipalityID` para sort/células. +- **Tests:** remover asserts de `bundle.overview`; manter asserts de lista/filtros/classe. +- **Migration:** sem migration. +- **Access / Consent:** sem mudança. + +## Fases verificáveis + +1. **Server** — limpar loader + deletar componente morto. +2. **UI** — página sem overview; comentário em `MunicipalityList.tsx`. +3. **Gates** — `pnpm gate:fast`; push via `pnpm push`. + +## Rabbit holes / Não escopo + +- Redesenhar `loading.tsx` (fora do aceite). +- Tocar `SupporterListOverview` ou dashboard. + +## Riscos e mitigação + +- **Sort por déficit depende de goal coverage:** mitigado mantendo `loadMunicipalityGoalCoverageBundle` no loader. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (int `municipalityPageData`) diff --git a/docs/plans/remover-overview-lista-municipios.md b/docs/plans/remover-overview-lista-municipios.md index 7c16feea..c654b71f 100644 --- a/docs/plans/remover-overview-lista-municipios.md +++ b/docs/plans/remover-overview-lista-municipios.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD # Remover overview da lista de municípios Status: registrado @@ -74,3 +75,27 @@ Na lista de municípios (`/campanha/municipios`), o painel de visão geral acima - `src/app/(campaign)/campanha/(app)/municipios/page.tsx` - `docs/plans/overview-lista-nucleos.md` · `docs/plans/cenario-junto-filtros-municipios.md` - E9 / “coluna da vergonha” — contexto em `docs/plans/inteligencia-campanha.md` (não reabrir fila de alocação aqui) +======= +# B129 — Remover overview da lista de municípios + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #266 + +## Outcome + +Tirar o painel de visão geral (“Média nos municípios filtrados” e demais agregados) de `/campanha/municipios` em todos os viewports, para a lista voltar a ser o conteúdo útil logo após os filtros. + +## Aceite (produto) + +- Overview da lista de municípios ausente em desktop e mobile. +- Filtros e seletor de cenário na barra de filtros permanecem. +- Atalho E9 “prioritárias sem responsável” some com o bloco (filtros Prioridade + Assessoria cobrem o recorte). +- Fora: overview de apoiadores, Início/dashboard, redesign da lista. + +## Direção no codebase (hipótese) + +- Remover `MunicipalityListOverview` da página e deletar o componente. +- Retirar `overview` de `loadMunicipalityListPageBundle`; manter `staffScope` só para agregados de linha (pledges, cobertura de meta por município). +- Atualizar int tests que assertam `bundle.overview`. +>>>>>>> 0c052371 (B129 — Remove municipality list overview panel) diff --git a/src/app/(campaign)/campanha/(app)/municipios/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/page.tsx index fd8cbcbd..8d2e8453 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/page.tsx +++ b/src/app/(campaign)/campanha/(app)/municipios/page.tsx @@ -10,7 +10,6 @@ import { RecentVisitTracker } from '@/components/campaign/dashboard/RecentVisitT import { MunicipalityEstimateScenarioProvider } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext' import { MunicipalityFilters } from '@/components/campaign/municipality/MunicipalityFilters' import { MunicipalityList } from '@/components/campaign/municipality/MunicipalityList' -import { MunicipalityListOverview } from '@/components/campaign/municipality/MunicipalityListOverview' import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' @@ -60,7 +59,11 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie municipalities: listMunicipalities, totalDocs, totalPages, +<<<<<<< HEAD overview, +======= + scopeTotal, +>>>>>>> c432f439 (B129 — Remove municipality list overview panel) filterFacets, } = pageBundle const resolvedUrl = resolveMunicipalityListUrl(rawSearchParams, totalPages) @@ -109,20 +112,10 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie /> ) - // E9 coluna da vergonha: same scope, filtered down to priority municipalities - // with nobody answering for them. `null` when that is already the filter. - const shameHref = - state.priority === 'alta' && state.coverage === 'sem_assessor' - ? null - : buildMunicipalityListHref({ ...state, priority: 'alta', coverage: 'sem_assessor' }, 1) - - // The overview and the table's filter header stay mounted even with zero - // results — only the rows are replaced by the empty state. + // The table's filter header stays mounted even with zero results — only the + // rows are replaced by the empty state. const listBody = ( <> - {isStaffView && overview ? ( - - ) : null} ( diff --git a/src/components/campaign/municipality/MunicipalityListOverview.tsx b/src/components/campaign/municipality/MunicipalityListOverview.tsx deleted file mode 100644 index 5a8f1c1a..00000000 --- a/src/components/campaign/municipality/MunicipalityListOverview.tsx +++ /dev/null @@ -1,177 +0,0 @@ -'use client' - -import { CircleAlertIcon } from 'lucide-react' -import { useEffect, useRef, useState } from 'react' - -import { useMunicipalityEstimateScenario } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext' -import { CampaignTransitionAnchor } from '@/components/campaign/shared/CampaignListPending' -import { CampaignMetricStrip } from '@/components/campaign/shared/CampaignMetricStrip' -import { campaignPrioritySurfaceClassName } from '@/components/campaign/shell/CampaignPageShell' -import { VoteEstimateScenarioStrip } from '@/components/campaign/votePledge/VoteEstimateScenarioStrip' -import { formatElectionNumber } from '@/lib/electionFormat' -import { cn } from '@/lib/utils' -import { - formatVoteEstimateEndpointsLabel, - hasAnyVoteEstimate, - voteEstimateScenarioLabels, -} from '@/lib/voteEstimate' -import { - formatGoalCoverageDeficitLabel, - formatGoalCoverageRatioLabel, - goalCoverageProgressPercent, -} from '@/utilities/municipality/goalCoverage' -import type { MunicipalityListOverviewData } from '@/utilities/municipality/municipalityPageData' - -const SCENARIO_FLASH_MS = 420 - -/** - * E9 "coluna da vergonha": the assessoria ratio hides the only gap the - * coordination can close today, so the priority municipalities with nobody - * answering for them are named as their own number — and the number is the - * link that filters the list down to exactly those rows (`shameHref` is - * `null` when that filter is already applied, so the count doesn't pretend to - * navigate somewhere new). - */ -const AssessoriaGapDetail = ({ - priorityWithoutAdvisorCount, - shameHref, -}: { - priorityWithoutAdvisorCount: number - shameHref: string | null -}) => { - if (priorityWithoutAdvisorCount === 0) { - return

Nenhuma prioritária sem responsável

- } - - const label = `${formatElectionNumber(priorityWithoutAdvisorCount)} ${ - priorityWithoutAdvisorCount === 1 ? 'prioritária' : 'prioritárias' - } sem responsável` - - if (!shameHref) { - return

{label}

- } - - return ( - - - ) -} - -export const MunicipalityListOverview = ({ - view, - shameHref, -}: { - view: MunicipalityListOverviewData - shameHref: string | null -}) => { - const { scenario } = useMunicipalityEstimateScenario() - const [isFlashing, setIsFlashing] = useState(false) - const skipFlashRef = useRef(true) - const flashTimeoutRef = useRef | undefined>(undefined) - - useEffect(() => { - if (skipFlashRef.current) { - skipFlashRef.current = false - return - } - const reduceMotion = - typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches - if (reduceMotion) return - setIsFlashing(true) - clearTimeout(flashTimeoutRef.current) - flashTimeoutRef.current = setTimeout(() => setIsFlashing(false), SCENARIO_FLASH_MS) - return () => clearTimeout(flashTimeoutRef.current) - }, [scenario]) - - const scenarioTotals = view.staffVoteTotalByScenario - const activeTotal = scenarioTotals[scenario] - const endpointsLabel = formatVoteEstimateEndpointsLabel(scenarioTotals) - const scenarioLabel = voteEstimateScenarioLabels[scenario] - const hasScenarioTotals = hasAnyVoteEstimate(scenarioTotals) - const goalCoverage = view.goalCoverageByScenario[scenario] - - const heroDetail = ( -
- {hasScenarioTotals ? ( - - ) : null} - {endpointsLabel ? ( -

{endpointsLabel}

- ) : null} - {view.missingEstimateCount > 0 ? ( -

- {formatElectionNumber(view.missingEstimateCount)}{' '} - {view.missingEstimateCount === 1 ? 'declaração' : 'declarações'} sem estimativa -

- ) : null} -
- ) - - return ( -
- 0 - ? `; ${formatElectionNumber(view.missingEstimateCount)} sem estimativa` - : '' - }`, - }, - { - label: 'Declarações de votos', - value: view.pledgeCount ? formatElectionNumber(view.pledgeCount) : 'Nenhuma', - }, - { - label: 'Cobertura de assessoria', - value: `${view.withAdvisorCount} de ${view.municipalityCount} municípios`, - detail: ( - - ), - progress: - view.municipalityCount > 0 - ? Math.round((view.withAdvisorCount / view.municipalityCount) * 100) - : undefined, - }, - { - label: 'Cobertura da meta', - value: formatGoalCoverageRatioLabel(goalCoverage), - detail: formatGoalCoverageDeficitLabel(goalCoverage), - progress: goalCoverageProgressPercent(goalCoverage), - }, - ]} - /> -
- ) -} diff --git a/src/utilities/municipality/municipalityPageData.ts b/src/utilities/municipality/municipalityPageData.ts index dd5a7be1..5dea27d4 100644 --- a/src/utilities/municipality/municipalityPageData.ts +++ b/src/utilities/municipality/municipalityPageData.ts @@ -51,33 +51,13 @@ import { type MunicipalityListViewModel, } from '@/utilities/municipality/municipalityViewModels' import { loadStateDeputySummaries } from '@/utilities/stateDeputyData' -import { - rollupMunicipalityStaffVotes, - type MunicipalityPledgeAggregate, -} from '@/utilities/votePledgeViews' +import { type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews' export const MunicipalityNotFoundError = createEntityNotFoundError( 'Municipality', 'Município não encontrado.', ) -export type MunicipalityListOverviewData = { - municipalityCount: number - staffVoteTotalByScenario: Record - pledgeCount: number - missingEstimateCount: number - withAdvisorCount: number - /** - * E9 "coluna da vergonha": priority municipalities in scope with nobody - * answering for them — the one gap the coordination can close today, so it - * gets named as a number and a filtered link instead of hiding inside the - * assessoria ratio. - */ - priorityWithoutAdvisorCount: number - /** E8 "conta da cadeira" — meta × comprometido do escopo filtrado, por cenário. */ - goalCoverageByScenario: Record -} - /** Values still reachable under the OTHER active filters (column-filter options). */ type MunicipalityListFilterFacets = { /** Catalog order. */ @@ -91,7 +71,6 @@ export type MunicipalityListPageBundle = { totalDocs: number totalPages: number scopeTotal: number - overview: MunicipalityListOverviewData | null filterFacets: MunicipalityListFilterFacets } @@ -106,8 +85,8 @@ type MunicipalityFacetRow = Pick /** * E10's class filter is the only one that isn't a Payload constraint (the * class is derived from the committed TSE artifact, not stored), so every - * place that would otherwise trust `where` — the page query, the overview - * scope and the facets — has to apply this predicate itself. Returns `null` + * place that would otherwise trust `where` — the page query and the facets — + * has to apply this predicate itself. Returns `null` when no class is selected, * when no class is selected, which is also the signal to keep the cheap * database-paginated path. */ @@ -320,7 +299,6 @@ export const loadMunicipalityListPageBundle = async ( totalDocs: 0, totalPages: 0, scopeTotal: 0, - overview: null, filterFacets: emptyMunicipalityListFilterFacets, } } @@ -389,24 +367,17 @@ export const loadMunicipalityListPageBundle = async ( isStaff ? loadStatewideSuggestedGoals(payload, user) : null, ]) - let overview: MunicipalityListOverviewData | null = null let pledgeAggregates = new Map() let goalCoverageByMunicipalityID = new Map< number, Record >() - // Computed even for an empty filtered scope: the overview stays on screen - // (zeroed) next to the empty state instead of disappearing with the rows. if (staffScope) { pledgeAggregates = staffScope.pledgeAggregates - // Never mutate the scope: `loadMunicipalityScope` is request-cached and - // shared with the facets. Filtering here is what keeps the overview - // (E8 coverage, E9 shame column) counting the filtered scope. const scopedMunicipalities = classMatches ? staffScope.municipalities.filter((municipality) => classMatches(municipality.slug)) : staffScope.municipalities - const rollup = rollupMunicipalityStaffVotes(scopedMunicipalities, pledgeAggregates) const goalCoverageBundle = await loadMunicipalityGoalCoverageBundle( payload, user, @@ -414,20 +385,6 @@ export const loadMunicipalityListPageBundle = async ( pledgeAggregates, ) goalCoverageByMunicipalityID = goalCoverageBundle.coverageByMunicipalityID - overview = { - municipalityCount: scopedMunicipalities.length, - staffVoteTotalByScenario: { ...rollup.staffVoteTotalByScenario }, - pledgeCount: rollup.pledgeCount, - missingEstimateCount: rollup.missingEstimateCount, - withAdvisorCount: scopedMunicipalities.filter( - (municipality) => (municipality.advisors ?? []).length > 0, - ).length, - priorityWithoutAdvisorCount: scopedMunicipalities.filter( - (municipality) => - municipality.priority === 'alta' && (municipality.advisors ?? []).length === 0, - ).length, - goalCoverageByScenario: goalCoverageBundle.aggregateByScenario, - } } let pageDocs: Municipality[] @@ -468,7 +425,6 @@ export const loadMunicipalityListPageBundle = async ( totalDocs, totalPages, scopeTotal: scopeCount.totalDocs, - overview, filterFacets, } } diff --git a/tests/int/municipalityPageData.int.spec.ts b/tests/int/municipalityPageData.int.spec.ts index d0edfa7b..755ba7d1 100644 --- a/tests/int/municipalityPageData.int.spec.ts +++ b/tests/int/municipalityPageData.int.spec.ts @@ -27,17 +27,13 @@ describe('loadMunicipalityListPageBundle', () => { payload = await getPayload({ config: await config }) }) - it('keeps a zeroed overview when the filtered municipality set is empty', async () => { + it('returns an empty list when the filtered municipality set is empty', async () => { const coordinator = await campaignFixtures().createCampaignUser('coordinator') const bundle = await loadMunicipalityListPageBundle(payload, coordinator, { q: 'zzznomatch' }) - // The overview stays on screen next to the empty state instead of unmounting. expect(bundle.municipalities).toHaveLength(0) - expect(bundle.overview).not.toBeNull() - expect(bundle.overview!.municipalityCount).toBe(0) - expect(bundle.overview!.staffVoteTotalByScenario.central).toBe(0) - expect(bundle.overview!.goalCoverageByScenario.central.goal).toBe(0) + expect(bundle.totalDocs).toBe(0) }) it('rolls up staffVoteTotal from expectedVotes when no pledge overrides apply', async () => { @@ -81,8 +77,6 @@ describe('loadMunicipalityListPageBundle', () => { q: municipality.name, }) - expect(bundle.overview).not.toBeNull() - expect(bundle.overview!.staffVoteTotalByScenario.central).toBeGreaterThanOrEqual(1_500) const row = bundle.municipalities.find((item) => item.slug === municipality.slug) expect(row?.votePosition2022).not.toBeNull() expect(row!.votePosition2022!.totalUnits).toBe(435) @@ -282,7 +276,7 @@ describe('loadMunicipalityListPageBundle', () => { /** * E10: the class is derived from the committed TSE artifact, so the filter * cannot be a Payload constraint. The bundle must still agree with itself — - * rows, `totalDocs` and the overview all counting the filtered scope. + * rows and `totalDocs` counting the filtered scope. */ it('filters by the derived territorial class and keeps the overview honest', async () => { const fixtures = campaignFixtures() @@ -326,7 +320,6 @@ describe('loadMunicipalityListPageBundle', () => { }) expect(filtered.municipalities.map((row) => row.slug)).toEqual([first.slug]) expect(filtered.totalDocs).toBe(1) - expect(filtered.overview?.municipalityCount).toBe(1) expect(filtered.municipalities[0]!.territorialClass).toBe(firstClass) expect(filtered.municipalities[0]!.territorialClassFactors.length).toBeGreaterThan(0) @@ -411,7 +404,6 @@ describe('loadMunicipalityListPageBundle', () => { const included = await loadMunicipalityListPageBundle(payload, advisor, { q: administered.name, }) - expect(included.overview).not.toBeNull() expect(included.municipalities.some((row) => row.slug === administered.slug)).toBe(true) const outsideSlug = municipalityCatalog.find((entry) => entry.slug !== administered.slug)!.slug @@ -419,7 +411,6 @@ describe('loadMunicipalityListPageBundle', () => { const excluded = await loadMunicipalityListPageBundle(payload, advisor, { slug: [outsideSlug], }) - expect(excluded.overview?.municipalityCount).toBe(0) expect(excluded.municipalities).toHaveLength(0) }) @@ -498,7 +489,6 @@ describe('loadMunicipalityListPageBundle', () => { const bundle = await loadMunicipalityListPageBundle(payload, leader, { q: municipality.name }) - expect(bundle.overview).toBeNull() expect(bundle.municipalities).toHaveLength(0) expect(bundle.scopeTotal).toBe(0) }) From 76597292c99561aeca9eb0ae1a72b52ee6630622 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:05:41 +0000 Subject: [PATCH 2/9] =?UTF-8?q?B129=20=E2=80=94=20Simplify=20cleanup=20aft?= =?UTF-8?q?er=20overview=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix duplicated JSDoc, trim stale int test, defer perf follow-ups in impl plan. Co-authored-by: Francisco Solla --- .../remover-overview-lista-municipios-impl.md | 1 + .../municipality/municipalityPageData.ts | 3 +- tests/int/municipalityPageData.int.spec.ts | 37 +------------------ 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/docs/plans/remover-overview-lista-municipios-impl.md b/docs/plans/remover-overview-lista-municipios-impl.md index 3289ab98..e4791b21 100644 --- a/docs/plans/remover-overview-lista-municipios-impl.md +++ b/docs/plans/remover-overview-lista-municipios-impl.md @@ -41,6 +41,7 @@ Appetite restante: remoção cirúrgica (sem redesign) - Redesenhar `loading.tsx` (fora do aceite). - Tocar `SupporterListOverview` ou dashboard. +- **Defer:** escopo de `loadMunicipalityGoalCoverageBundle` ao page slice quando `isPagedByPayload` (gatilho: perf review pós-B129); skip `aggregateByScenario` no loader de lista (gatilho: 2º consumidor de rows-only). ## Riscos e mitigação diff --git a/src/utilities/municipality/municipalityPageData.ts b/src/utilities/municipality/municipalityPageData.ts index 5dea27d4..48a8367e 100644 --- a/src/utilities/municipality/municipalityPageData.ts +++ b/src/utilities/municipality/municipalityPageData.ts @@ -87,8 +87,7 @@ type MunicipalityFacetRow = Pick * class is derived from the committed TSE artifact, not stored), so every * place that would otherwise trust `where` — the page query and the facets — * has to apply this predicate itself. Returns `null` when no class is selected, - * when no class is selected, which is also the signal to keep the cheap - * database-paginated path. + * which is also the signal to keep the cheap database-paginated path. */ const territorialClassFilterPredicate = ( state: MunicipalityListState, diff --git a/tests/int/municipalityPageData.int.spec.ts b/tests/int/municipalityPageData.int.spec.ts index 755ba7d1..ffc2527c 100644 --- a/tests/int/municipalityPageData.int.spec.ts +++ b/tests/int/municipalityPageData.int.spec.ts @@ -9,8 +9,6 @@ import { territorialClassSortWeight } from '@/lib/territorialClassSortWeight' import config from '@/payload.config' import { loadMunicipalityListPageBundle } from '@/utilities/municipality/municipalityPageData' import { computeMunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass' -import { aggregatePledgesByMunicipality } from '@/utilities/votePledgeData' -import { rollupMunicipalityStaffVotes } from '@/utilities/votePledgeViews' import { installCampaignFixtures } from '../helpers/campaignFixtures' @@ -36,43 +34,12 @@ describe('loadMunicipalityListPageBundle', () => { expect(bundle.totalDocs).toBe(0) }) - it('rolls up staffVoteTotal from expectedVotes when no pledge overrides apply', async () => { + it('includes 2022 vote position on list rows', async () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') const municipality = await fixtures.getMunicipality() fixtures.touchMunicipality(municipality.id) - await payload.update({ - collection: 'municipality', - id: municipality.id, - data: { expectedVotes: { pessimistic: null, central: 1_500, optimistic: null } }, - depth: 0, - overrideAccess: true, - }) - - const contact = await fixtures.createContact() - const leadership = await fixtures.createLeadership({ - contact: contact.id, - municipalities: [municipality.id], - supportStatus: 'engajado', - }) - await fixtures.createVotePledge({ - leadership: leadership.id, - municipality: municipality.id, - declaredVotes: 80, - estimatedVotes: { pessimistic: null, central: 120, optimistic: null }, - }) - - const refreshedMunicipality = await payload.findByID({ - collection: 'municipality', - id: municipality.id, - depth: 0, - overrideAccess: true, - }) - const aggregates = await aggregatePledgesByMunicipality(payload, [municipality.id]) - const rollup = rollupMunicipalityStaffVotes([refreshedMunicipality], aggregates) - expect(rollup.staffVoteTotal).toBe(1_500) - const bundle = await loadMunicipalityListPageBundle(payload, coordinator, { q: municipality.name, }) @@ -278,7 +245,7 @@ describe('loadMunicipalityListPageBundle', () => { * cannot be a Payload constraint. The bundle must still agree with itself — * rows and `totalDocs` counting the filtered scope. */ - it('filters by the derived territorial class and keeps the overview honest', async () => { + it('filters by the derived territorial class and keeps totals honest', async () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') From 8dc78f48a634cf3f287256863ed204cd401f3d08 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:16:19 +0000 Subject: [PATCH 3/9] =?UTF-8?q?B129=20=E2=80=94=20Fix=20conflict=20markers?= =?UTF-8?q?=20left=20in=20intention=20plan=20after=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remover-overview-lista-municipios.md | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/docs/plans/remover-overview-lista-municipios.md b/docs/plans/remover-overview-lista-municipios.md index c654b71f..7c16feea 100644 --- a/docs/plans/remover-overview-lista-municipios.md +++ b/docs/plans/remover-overview-lista-municipios.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD # Remover overview da lista de municípios Status: registrado @@ -75,27 +74,3 @@ Na lista de municípios (`/campanha/municipios`), o painel de visão geral acima - `src/app/(campaign)/campanha/(app)/municipios/page.tsx` - `docs/plans/overview-lista-nucleos.md` · `docs/plans/cenario-junto-filtros-municipios.md` - E9 / “coluna da vergonha” — contexto em `docs/plans/inteligencia-campanha.md` (não reabrir fila de alocação aqui) -======= -# B129 — Remover overview da lista de municípios - -Status: aprovado -Atualizado em: 2026-08-02 -Issue: #266 - -## Outcome - -Tirar o painel de visão geral (“Média nos municípios filtrados” e demais agregados) de `/campanha/municipios` em todos os viewports, para a lista voltar a ser o conteúdo útil logo após os filtros. - -## Aceite (produto) - -- Overview da lista de municípios ausente em desktop e mobile. -- Filtros e seletor de cenário na barra de filtros permanecem. -- Atalho E9 “prioritárias sem responsável” some com o bloco (filtros Prioridade + Assessoria cobrem o recorte). -- Fora: overview de apoiadores, Início/dashboard, redesign da lista. - -## Direção no codebase (hipótese) - -- Remover `MunicipalityListOverview` da página e deletar o componente. -- Retirar `overview` de `loadMunicipalityListPageBundle`; manter `staffScope` só para agregados de linha (pledges, cobertura de meta por município). -- Atualizar int tests que assertam `bundle.overview`. ->>>>>>> 0c052371 (B129 — Remove municipality list overview panel) From 7676d7c5e11955cebd74808fca7e995718246df3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:25:51 +0000 Subject: [PATCH 4/9] =?UTF-8?q?B129=20=E2=80=94=20Format=20municipios=20pa?= =?UTF-8?q?ge=20after=20rebase=20conflict=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(campaign)/campanha/(app)/municipios/page.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/municipios/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/page.tsx index 8d2e8453..dd6d067f 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/page.tsx +++ b/src/app/(campaign)/campanha/(app)/municipios/page.tsx @@ -55,17 +55,7 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie const canMoveEngagementLevel = isCampaignUnrestricted(user) const pageBundle = await loadMunicipalityListPageBundle(payload, user, rawSearchParams) - const { - municipalities: listMunicipalities, - totalDocs, - totalPages, -<<<<<<< HEAD - overview, -======= - scopeTotal, ->>>>>>> c432f439 (B129 — Remove municipality list overview panel) - filterFacets, - } = pageBundle + const { municipalities: listMunicipalities, totalDocs, totalPages, filterFacets } = pageBundle const resolvedUrl = resolveMunicipalityListUrl(rawSearchParams, totalPages) if (resolvedUrl.redirectHref) redirect(resolvedUrl.redirectHref) const { state } = resolvedUrl From 5bb8bcc62b0282526fba1c5a7dfdf293f613fb02 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:34:23 +0000 Subject: [PATCH 5/9] =?UTF-8?q?B129=20=E2=80=94=20Align=20municipality=20E?= =?UTF-8?q?2E=20with=20page=20chrome=20and=20scoped=20trend=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/campaignMunicipalities.e2e.spec.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index 1e74fece..17815532 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -153,7 +153,10 @@ test.describe('Municípios — jornadas por papel', () => { ) await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() - await page.getByRole('button', { name: 'Editar tendência política' }).click() + const trendButton = page.getByRole('button', { + name: new RegExp(`Editar tendência política em ${municipality.name}`), + }) + await trendButton.click() const trendPopover = page.locator('[data-slot="popover-content"]') await expect(trendPopover).toBeVisible() await expect(trendPopover.getByRole('button', { name: 'Salvar' })).toHaveCount(0) @@ -165,13 +168,11 @@ test.describe('Municípios — jornadas por papel', () => { trendPopover.getByLabel('Justificativa').blur(), ]) - await expect(page.getByRole('button', { name: 'Editar tendência política' })).toContainText( - 'Favorável', - ) + await expect(trendButton).toContainText('Favorável') await page.keyboard.press('Escape') await page.reload() - await page.getByRole('button', { name: 'Editar tendência política' }).click() + await trendButton.click() const reopened = page.locator('[data-slot="popover-content"]') await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel') await expect(reopened.getByLabel('Justificativa')).toHaveValue(note) @@ -428,6 +429,11 @@ test.describe('Municípios — FAB ações rápidas mobile (B126)', () => { await page.goto( `${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`, ) +<<<<<<< HEAD +======= + await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() + +>>>>>>> 29b49e63 (B129 — Align municipality E2E with page chrome and scoped trend control) const fab = page.getByRole('button', { name: 'Ações rápidas' }) await expect(fab).toBeVisible() await expect(page.getByLabel('Buscar na campanha')).toHaveCount(0) @@ -478,7 +484,11 @@ test.describe('Municípios — FAB overlay polish (B126)', () => { await campaign.login(page, coordinator.email!, coordinator.password) await page.goto(`${campaign.baseURL}/campanha/municipios`) +<<<<<<< HEAD await expect(page.getByRole('button', { name: 'Ações rápidas' })).toBeVisible() +======= + await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() +>>>>>>> 29b49e63 (B129 — Align municipality E2E with page chrome and scoped trend control) await page.getByRole('button', { name: 'Ações rápidas' }).click() const overlay = page.locator('#CampaignQuickActionsOverlay') From 5ded385ed9e958ee3a24ae795e2c7e8eff9b6ea7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:44:45 +0000 Subject: [PATCH 6/9] =?UTF-8?q?B129=20=E2=80=94=20Stabilize=20campaignPage?= =?UTF-8?q?Chrome=20E2E=20helper=20for=20title=20+=20subtitle=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/fixtures/campaignE2EFixtures.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts index e8cd3b7a..8147f765 100644 --- a/tests/e2e/fixtures/campaignE2EFixtures.ts +++ b/tests/e2e/fixtures/campaignE2EFixtures.ts @@ -562,6 +562,10 @@ export const test = base.extend({ }) export const campaignPageChrome = (page: Page, title: string) => - page.locator('[data-slot="campaign-page-chrome"]').filter({ visible: true }).getByText(title) + page + .locator('[data-slot="campaign-page-chrome"]') + .filter({ visible: true }) + .getByText(title) + .first() export { expect } From 4cb21576c2f4cfc6379eb13124f4802f4fd19702 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:46:39 +0000 Subject: [PATCH 7/9] =?UTF-8?q?B129=20=E2=80=94=20Anchor=20trend-control?= =?UTF-8?q?=20E2E=20regex=20per=20codebase=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/campaignMunicipalities.e2e.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index 17815532..c94bd825 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -154,7 +154,7 @@ test.describe('Municípios — jornadas por papel', () => { await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() const trendButton = page.getByRole('button', { - name: new RegExp(`Editar tendência política em ${municipality.name}`), + name: new RegExp(`^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), }) await trendButton.click() const trendPopover = page.locator('[data-slot="popover-content"]') @@ -172,7 +172,12 @@ test.describe('Municípios — jornadas por papel', () => { await page.keyboard.press('Escape') await page.reload() - await trendButton.click() + await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() + await page + .getByRole('button', { + name: new RegExp(`^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), + }) + .click() const reopened = page.locator('[data-slot="popover-content"]') await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel') await expect(reopened.getByLabel('Justificativa')).toHaveValue(note) From a92701db682775006c47614dc9d7db3414335735 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 19:47:08 +0000 Subject: [PATCH 8/9] =?UTF-8?q?B129=20=E2=80=94=20Format=20campaignMunicip?= =?UTF-8?q?alities=20E2E=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/campaignMunicipalities.e2e.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index c94bd825..93a02f42 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -154,7 +154,9 @@ test.describe('Municípios — jornadas por papel', () => { await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() const trendButton = page.getByRole('button', { - name: new RegExp(`^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), + name: new RegExp( + `^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, + ), }) await trendButton.click() const trendPopover = page.locator('[data-slot="popover-content"]') @@ -175,7 +177,9 @@ test.describe('Municípios — jornadas por papel', () => { await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() await page .getByRole('button', { - name: new RegExp(`^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), + name: new RegExp( + `^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, + ), }) .click() const reopened = page.locator('[data-slot="popover-content"]') From 16c2358479ce9dcf5ff88472c0ee2ba4765ae27f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 20:07:35 +0000 Subject: [PATCH 9/9] =?UTF-8?q?B129=20=E2=80=94=20Restore=20anchored=20tre?= =?UTF-8?q?nd-control=20E2E=20regex=20after=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/campaignMunicipalities.e2e.spec.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index 93a02f42..b41e4b9b 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -154,9 +154,7 @@ test.describe('Municípios — jornadas por papel', () => { await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() const trendButton = page.getByRole('button', { - name: new RegExp( - `^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, - ), + name: new RegExp(`^Editar tendência política em ${municipality.name}`), }) await trendButton.click() const trendPopover = page.locator('[data-slot="popover-content"]') @@ -174,14 +172,7 @@ test.describe('Municípios — jornadas por papel', () => { await page.keyboard.press('Escape') await page.reload() - await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() - await page - .getByRole('button', { - name: new RegExp( - `^Editar tendência política em ${municipality.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, - ), - }) - .click() + await trendButton.click() const reopened = page.locator('[data-slot="popover-content"]') await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel') await expect(reopened.getByLabel('Justificativa')).toHaveValue(note) @@ -438,11 +429,8 @@ test.describe('Municípios — FAB ações rápidas mobile (B126)', () => { await page.goto( `${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`, ) -<<<<<<< HEAD -======= await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() ->>>>>>> 29b49e63 (B129 — Align municipality E2E with page chrome and scoped trend control) const fab = page.getByRole('button', { name: 'Ações rápidas' }) await expect(fab).toBeVisible() await expect(page.getByLabel('Buscar na campanha')).toHaveCount(0) @@ -493,11 +481,7 @@ test.describe('Municípios — FAB overlay polish (B126)', () => { await campaign.login(page, coordinator.email!, coordinator.password) await page.goto(`${campaign.baseURL}/campanha/municipios`) -<<<<<<< HEAD - await expect(page.getByRole('button', { name: 'Ações rápidas' })).toBeVisible() -======= await expect(campaignPageChrome(page, 'Municípios')).toBeVisible() ->>>>>>> 29b49e63 (B129 — Align municipality E2E with page chrome and scoped trend control) await page.getByRole('button', { name: 'Ações rápidas' }).click() const overlay = page.locator('#CampaignQuickActionsOverlay')