mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
B129 — Remove municipality list overview panel
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 <fsolla@users.noreply.github.com>
This commit is contained in:
parent
f176412e2b
commit
b4f2e2fa0a
7 changed files with 91 additions and 251 deletions
53
docs/plans/remover-overview-lista-municipios-impl.md
Normal file
53
docs/plans/remover-overview-lista-municipios-impl.md
Normal file
|
|
@ -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`)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<MunicipalityListOverview view={overview} shameHref={shameHref} />
|
||||
) : null}
|
||||
<MunicipalityList
|
||||
municipalities={listMunicipalities}
|
||||
advisorNamesById={advisorNamesById}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export type MunicipalityListProps = {
|
|||
columnVisibility: CampaignColumnVisibility
|
||||
}
|
||||
|
||||
/** Replaces only the rows: the filter header row and the overview stay put. */
|
||||
/** Replaces only the rows: the filter header row stays put. */
|
||||
const MunicipalityListEmptyState = ({ state }: { state: MunicipalityListState }) => (
|
||||
<Empty className="min-h-56">
|
||||
<EmptyHeader>
|
||||
|
|
|
|||
|
|
@ -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 <p className="text-xs text-muted-foreground">Nenhuma prioritária sem responsável</p>
|
||||
}
|
||||
|
||||
const label = `${formatElectionNumber(priorityWithoutAdvisorCount)} ${
|
||||
priorityWithoutAdvisorCount === 1 ? 'prioritária' : 'prioritárias'
|
||||
} sem responsável`
|
||||
|
||||
if (!shameHref) {
|
||||
return <p className="text-xs font-medium text-destructive tabular-nums">{label}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<CampaignTransitionAnchor
|
||||
href={shameHref}
|
||||
scroll={false}
|
||||
className="inline-flex min-h-11 items-center gap-1 text-xs font-medium text-destructive tabular-nums underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<CircleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
{label}
|
||||
</CampaignTransitionAnchor>
|
||||
)
|
||||
}
|
||||
|
||||
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<ReturnType<typeof setTimeout> | 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 = (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{hasScenarioTotals ? (
|
||||
<VoteEstimateScenarioStrip
|
||||
values={scenarioTotals}
|
||||
activeScenario={scenario}
|
||||
labelMode="endpoints"
|
||||
markerMode="active-only"
|
||||
stretch
|
||||
className="max-w-none"
|
||||
/>
|
||||
) : null}
|
||||
{endpointsLabel ? (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">{endpointsLabel}</p>
|
||||
) : null}
|
||||
{view.missingEstimateCount > 0 ? (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">
|
||||
{formatElectionNumber(view.missingEstimateCount)}{' '}
|
||||
{view.missingEstimateCount === 1 ? 'declaração' : 'declarações'} sem estimativa
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Visão geral dos municípios filtrados"
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
'rounded-xl transition-[box-shadow,background-color] duration-300',
|
||||
campaignPrioritySurfaceClassName,
|
||||
isFlashing ? 'ring-2 ring-primary/35' : undefined,
|
||||
)}
|
||||
>
|
||||
<CampaignMetricStrip
|
||||
className="rounded-none ring-0"
|
||||
metrics={[
|
||||
{
|
||||
label: `${scenarioLabel} nos municípios filtrados`,
|
||||
value: formatElectionNumber(activeTotal),
|
||||
detail: heroDetail,
|
||||
emphasize: true,
|
||||
valueAriaLabel: `${scenarioLabel} nos municípios filtrados: ${formatElectionNumber(activeTotal)}${
|
||||
endpointsLabel ? `; ${endpointsLabel}` : ''
|
||||
}${
|
||||
view.missingEstimateCount > 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: (
|
||||
<AssessoriaGapDetail
|
||||
priorityWithoutAdvisorCount={view.priorityWithoutAdvisorCount}
|
||||
shameHref={shameHref}
|
||||
/>
|
||||
),
|
||||
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),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<VoteEstimateScenario, number>
|
||||
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<VoteEstimateScenario, MunicipalityGoalCoverage>
|
||||
}
|
||||
|
||||
/** 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<Municipality, 'slug' | 'region' | 'advisors'>
|
|||
/**
|
||||
* 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<number, MunicipalityPledgeAggregate>()
|
||||
let goalCoverageByMunicipalityID = new Map<
|
||||
number,
|
||||
Record<VoteEstimateScenario, MunicipalityGoalCoverage>
|
||||
>()
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue