Remove Tipo column and list kind filter from municipality list.

Drop the redundant type column, URL param, and filters while keeping municipality.kind for map and dossier; update tests and delivery docs.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-29 07:42:41 -03:00
parent c283cc5202
commit ee44581ba4
17 changed files with 52 additions and 151 deletions

View file

@ -7,6 +7,7 @@ alwaysApply: false
## Status
- **Fill-in remover coluna Tipo entregue em código (2026-07-29):** em `/campanha/municipios`, sumiram a coluna "Tipo", o filtro single-select no header (B16), o ramo mobile em `MunicipalityFilters`, a sort key `kind` e o param `?kind=` em `municipalityListUrl` / `buildMunicipalityListWhere` / `payloadSortFieldByKey` — bookmarks antigos ignoram `kind` no parse. `municipalityKindLabels` permanece para detalhe, mapa e dossiê. `MunicipalityHeaderFilter` ficou só com filtros multi (`singleRows`/`applyMunicipalityKindFilter` deletados). B17: 11 colunas staff; e2e do picker usa Território; B18 usa `?priority=alta`. Sem migration/action/Consent. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md).
- **B18 entregue em código (2026-07-28):** filtros salvos na lista de Municípios — o recorte ativo vira um bookmark nomeado no `localStorage` (`teqo:campaign:municipality-saved-filters`, `MAX_ENTRIES = 12`) e reaparece como submenu recolhível sob "Municípios" no sidebar. **Duas premissas do plano de 24/07 morreram na auditoria.** (1) `SidebarMenuSub`/`SidebarMenuSubItem`/`SidebarMenuSubButton`/`SidebarMenuAction` **não existiam** — o plano afirmava que estavam no kit sem uso, mas o Pass 2 W4b pôs knip `exports` em ERROR e o kit foi podado; os quatro primitivos voltaram do upstream, e **o re-add expôs um bug pré-existente no `SidebarMenuButton`**: `data-active={false}` renderiza como `data-active="false"` e o variante `data-active:` do Tailwind casa por **presença** do atributo, então todo item **não** ativo estava recebendo o realce do ativo (`isActive || undefined` nos dois botões). (2) `municipalityUi.ts` não existe mais (Pass 2 W1 o dividiu) e o estado da URL cresceu para 14 params — absorvido serializando o estado inteiro menos `page`/`compare` em `buildMunicipalitySavedFilterHref` (`compare` é a lente do mapa do Início, não recorte desta lista). Também corrigido no plano: **B17 persiste em cookie**, não `localStorage`. **A restrição que manda no desenho é o layout:** a sidebar está no layout de `(app)`, então o casamento "URL atual == filtro salvo" **não pode** usar o serializador do domínio (a lição dos 21 kB do B14 ✓) — virou `src/lib/listQueryMatch.ts`, comparador puro de `URLSearchParams` insensível à ordem e ignorando `page`, o que de passagem resolve estar na página 3 de um filtro salvo (caso que o plano não previa). Medido contra árvore limpa: `/campanha` **byte-idêntico**, `/campanha/municipios` +2 kB. **Identidade é o href canônico, não um id:** salvar o mesmo recorte duas vezes é upsert — o Popover pré-preenche o nome atual e o botão lê "Renomear"/"Atualizar nome". O nome padrão e o gate "tem filtro salvável?" saem os dois de `formatMunicipalityActiveFiltersSummary`, que o `Limpar` já usava, então o `municipalityListHasSavableFilters` que o plano pedia não precisou existir. Sincronização das duas ilhas (barra grava, sidebar lê) por `useSyncExternalStore` com cache de snapshot invalidado na escrita e no evento de outra aba — a armadilha conhecida do loop de render. **Três defeitos de comportamento achados na verificação, e os dois primeiros são a mesma família de bug que este repo já pagou várias vezes** (estado lido do render que agendou o trabalho / hidratação confundida com ação): (1) abrir o grupo quando `activeHref` **aparece** reabria o submenu em **todo reload**, porque o store responde vazio até o cliente ler o `localStorage` e o match surgia da hidratação — o gatilho passou a ser a **URL mudar**, guardada num `useRef`, e só então a preferência recolhida sobrevive a um F5 para quem mais usa a feature; (2) apagar o último filtro derrubava o foco para `<body>`, porque o botão que o detinha desmontava — o sucessor é resolvido **antes** da remoção, e o fallback tem de ser o link de nav, já que esvaziar o grupo desmonta o próprio disclosure; (3) SC 2.5.3 — o `aria-label` "Salvar este filtro" **não continha** o rótulo visível "Salvar filtro", então "clique em Salvar filtro" por voz não casava nada; removido (o caso de renomear pode manter porque a palavra visível vem primeiro). Mais acessibilidade: `aria-describedby` ligando a recusa ao input, `aria-label` no `<ul>` do submenu, `aria-current="page"` nos links ativos. Apagar é **desfazível** (toast com "Desfazer" restaurando nome e posição alfabética), que é por que nunca pede confirmação; o logout limpa o storage ao lado de `clearRecentVisits()`. Sem migration, collection, action ou `Consent`; nada de rota `/vistas/[id]`, sync multi-device ou saved views genéricas nas outras listas. **Cuidado operacional novo:** `pnpm build` limpa o `.next`, e com ele o `.next/e2e` que o dev server do Playwright usa — rodar e2e logo depois de um build estoura o timeout do `setup` enquanto a rota recompila (o e2e ganhou orçamento de 20 s nas navegações, mesmo motivo do `campaignColumnPicker`). Plano as-built: [`filtros-salvos-municipios.md`](../../../docs/plans/filtros-salvos-municipios.md).
- **Fill-in ✓ dimir resultado no compositor de giro (2026-07-28):** em `/campanha/atividades/giros`, `CampaignListPendingBoundary` + `CampaignListResults` dimam a proposta ao trocar TI; `TourRegionPicker` usa `useCampaignListTransition` (spinner do select permanece); `CalendarPhaseNote` dentro de Results; unit `tourRegionPending.unit.spec.ts`. Sem migration/action/`Consent`. Critique sem P0/P1. Plano: [`feedback-pendente-compositor-giro.md`](../../../docs/plans/feedback-pendente-compositor-giro.md).
- **Fill-in ícone de prioridade na lista entregue em código (2026-07-28):** em `/campanha/municipios`, o Badge destrutivo "Prioritária" na coluna do nome (tabela) e no cabeçalho do card mobile (`priority === 'alta'`, staff) virou `MunicipalityPriorityIndicator` — `FlagIcon` `text-primary`, `aria-label` e `CampaignHoverTooltip` (B23 ✓) com copy fixa **"Município prioritário"** (filtro/forms/StrategyCard/Dossier continuam no rótulo curto "Prioritária"). No card, `className="relative size-11"` no trigger para ficar acima do link esticado B42. Ilha client mínima; `MunicipalityList` segue RSC. Pin unit: staff vê `aria-label="Município prioritário"`; liderança não. Sem migration/action/Consent. Gate: tsc/lint/format/cycles; unit 770; knip P3 pré-existente; Aikido 0 findings no indicador. Impeccable B; harden/optimize out. Plano: [`icone-prioridade-lista-municipios.md`](../../../docs/plans/icone-prioridade-lista-municipios.md).

View file

@ -1,7 +1,7 @@
# Remover coluna Tipo da lista de Municípios
Status: rascunho
Atualizado em: 2026-07-25
Status: entregue
Atualizado em: 2026-07-29
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins)
Impeccable: B — encaixe em `MunicipalityList` / filtros da lista (`/campanha/municipios`); sem rota nova
Appetite: ~0,25–0,5 dia eng; remove coluna + filtro/sort `kind` da URL da lista; sem migration
@ -105,8 +105,8 @@ Depth check: só delete/estreitamento nos módulos profundos já donos do contra
- `docs/roadmap.md` (Fill-ins abertos)
- `src/components/campaign/municipality/MunicipalityList.tsx` — coluna `kind` (~L262–270)
- `src/utilities/municipalityListUrl.ts` — contrato URL / sort / where
- `src/utilities/municipalityListFilters.ts` — definition + `applyMunicipalityKindFilter`
- `src/utilities/municipality/municipalityListUrl.ts` — contrato URL / sort / where (Pass 2 W2)
- `src/utilities/municipality/municipalityListFilters.ts` — definitions (sem `kind` desde 2026-07-29)
- `src/components/campaign/municipality/MunicipalityHeaderFilter.tsx` — single-select Tipo
- `src/components/campaign/municipality/MunicipalityFilters.tsx` — mobile
- `src/utilities/municipalityLabels.ts` — `municipalityKindLabels` (permanece para detalhe)

View file

@ -134,7 +134,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
- ~~**Limitar o mapa da Bahia a pan/zoom dentro do estado**~~ — **✓ (entregue 2026-07-26)** `BahiaMap` trava pan com `maxBounds` (`BAHIA_BOUNDS.pad(0.1)`) + `maxBoundsViscosity: 1` e zoom-out com `minZoom` via `getBoundsZoom(BAHIA_BOUNDS, false, L.point(16, 16))` recalculado no `ResizeObserver` existente; único arquivo tocado; sem migration/props · [plano](plans/limitar-mapa-a-bahia.md)
- ~~**Renomear coluna Cobertura da meta → Cobertura**~~ — **✓ (entregue 2026-07-28)** em `/campanha/municipios`: `municipalityListSortLabels.deficit` → "Cobertura" (B17 ✓ propaga header, picker e sort); card mobile via `municipalityColumnLabels.goalCoverage`; `whereItAppears` do conceito E18 atualizado; strips/card/título do glossário e `/campanha/territorios` intactos · [plano](plans/renomear-coluna-cobertura-meta.md)
- **Remover coluna Tipo** em `/campanha/municipios` — pedido de produto (2026-07-25): a coluna "Tipo" (Município × Zona eleitoral) não informa a coordenação (ambos tratados iguais); filtrar ZE = filtrar Salvador (19 slugs). Remove coluna + filtro/sort UI + param `?kind=` do contrato URL da lista; **schema/catálogo `kind` intacto** (mapa, B8, dossiê) · ~0,25–0,5d · Impeccable B · soft: B15 ✓/B16 ✓ (header); B17 ganha uma coluna a menos · cortável · [plano](plans/remover-coluna-tipo-municipios.md)
- ~~**Remover coluna Tipo** em `/campanha/municipios`~~ — **✓ (entregue 2026-07-29)** coluna "Tipo" + filtro/sort no header e mobile + param `?kind=` do contrato URL da lista removidos; `municipality.kind` e `municipalityKindLabels` intactos (mapa, B8, dossiê); bookmarks `?kind=` viram no-op no parse; testes B17/B18 migrados para Território/`?priority=alta`; módulos em `src/utilities/municipality/` · [plano](plans/remover-coluna-tipo-municipios.md)
- ~~**Remover lista "Municípios por zona eleitoral" do Início**~~ — **✓ (entregue 2026-07-28)** bloco do painel + `zoneBreakdown` do bundle removidos; ressalva de aproximação permanece via `hasZoneMunicipalities` (B8+ F3 ✓) · [plano](plans/remover-lista-zonas-mapa-inicio.md)
- ~~**Dimir o resultado no compositor de giro**~~ — **✓ (entregue 2026-07-28)** em `/campanha/atividades/giros`: `CampaignListPendingBoundary` + `CampaignListResults` dimam a proposta (`data-pending`/`aria-busy`/`aria-live`), `TourRegionPicker` usa `useCampaignListTransition` (spinner do select permanece); `CalendarPhaseNote` moveu para dentro de Results; unit `tourRegionPending.unit.spec.ts` · sem migration · [plano](plans/feedback-pendente-compositor-giro.md)
- ~~**Ícone de prioridade na lista**~~ ✓ (entregue 2026-07-28) em `/campanha/municipios` — Badge “Prioritária” trocado por Flag + `CampaignHoverTooltip` “Município prioritário” na coluna do nome (tabela + card mobile) · ~0,25d · Impeccable B · [plano](plans/icone-prioridade-lista-municipios.md)

View file

@ -8,12 +8,10 @@ import { Button } from '@/components/ui/button'
import { Field, FieldLabel } from '@/components/ui/field'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import {
applyMunicipalityKindFilter,
buildMunicipalityFilterHref,
clearMunicipalityListFilters,
formatMunicipalityActiveFiltersSummary,
getMunicipalityFilterDefinition,
getMunicipalitySingleFilterValue,
municipalityFilterDefinitions,
toggleMunicipalityExclusiveFilterValue,
toggleMunicipalityMultiFilterValue,
@ -130,10 +128,7 @@ export const MunicipalityFilters = ({
/>
) : null}
{mobileFilterDefinitions.map((definition) => {
const value =
definition.param === 'kind' || definition.param === 'coverage'
? (getMunicipalitySingleFilterValue(state, definition.param) ?? '')
: ''
const value = definition.param === 'coverage' ? (state.coverage ?? '') : ''
return (
<Field key={definition.param}>
<FieldLabel htmlFor={`municipality-filter-${definition.param}`}>
@ -150,10 +145,6 @@ export const MunicipalityFilters = ({
? toggleMunicipalityExclusiveFilterValue(state, 'coverage', selected)
: { ...state, coverage: undefined },
)
return
}
if (definition.param === 'kind') {
navigateWithSearch(applyMunicipalityKindFilter(state, selected))
}
}}
className="min-h-11 w-full"

View file

@ -8,7 +8,6 @@ import {
} from '@/components/campaign/shared/CampaignHeaderFilterPopover'
import { municipalityPriorityLabels } from '@/utilities/municipality/municipalityLabels'
import {
applyMunicipalityKindFilter,
buildMunicipalityFilterHref,
buildMunicipalityFilterOptionHref,
clearMunicipalityAdvisorFilters,
@ -16,7 +15,6 @@ import {
clearMunicipalityNameFilters,
getMunicipalityFilterDefinition,
getMunicipalityMultiFilterValues,
getMunicipalitySingleFilterValue,
isMunicipalityColumnFilterActive,
municipalityFilterOptionsForSlugs,
toggleMunicipalityExclusiveFilterValue,
@ -48,7 +46,6 @@ const multiParamByFilter = {
trend: 'trend',
class: 'class',
level: 'level',
kind: null,
coverage: null,
} as const satisfies Record<MunicipalityFilterParam, MunicipalityMultiFilterParam | null>
@ -123,35 +120,6 @@ export const MunicipalityHeaderFilter = ({
onChoose: () => commit(row.next),
}))
const singleRows: CampaignHeaderFilterRow[] =
definition.selection === 'single'
? [
...(definition.allLabel
? [
{
value: '',
label: definition.allLabel,
href: buildMunicipalityFilterHref(
applyMunicipalityKindFilter(viewState, undefined),
),
selected: !getMunicipalitySingleFilterValue(viewState, 'kind'),
onChoose: () => commit(applyMunicipalityKindFilter(viewState, undefined)),
},
]
: []),
...options.map((option) => {
const next = applyMunicipalityKindFilter(viewState, option.value)
return {
value: option.value,
label: option.label,
href: buildMunicipalityFilterHref(next),
selected: getMunicipalitySingleFilterValue(viewState, 'kind') === option.value,
onChoose: () => commit(next),
}
}),
]
: []
const multiRows: CampaignHeaderFilterRow[] = multiParam
? options.map((option) => ({
value: option.value,
@ -171,7 +139,7 @@ export const MunicipalityHeaderFilter = ({
active={isActive}
closeOnChoose={definition.selection !== 'multi'}
exclusiveRows={sharedExclusiveRows}
optionRows={definition.selection === 'single' ? singleRows : multiRows}
optionRows={multiRows}
clear={
hasSelection
? {

View file

@ -46,7 +46,6 @@ import {
formatTerritorialClassWhy,
municipalityColumnDescriptions,
municipalityGeographyParts,
municipalityKindLabels,
territorialClassBadgeVariant,
territorialClassLabels,
type MunicipalityListColumnId,
@ -311,19 +310,6 @@ const municipalityListColumns = ({
cellClassName: 'max-w-56 whitespace-normal text-muted-foreground',
cell: (municipality) => <TerritoryLink region={municipality.region} />,
},
{
id: 'kind',
label: municipalityColumnLabels.kind,
head: (
<MunicipalitySortableHead
state={state}
sortKey="kind"
filterParam="kind"
description={municipalityColumnDescriptions.kind}
/>
),
cell: (municipality) => municipalityKindLabels[municipality.kind],
},
{
id: 'votos',
label: municipalityColumnLabels.votos,

View file

@ -189,14 +189,13 @@ export const formatMunicipalityGeographyLabel = (municipality: {
}
/**
* The 12 real column ids in `/campanha/municipios` — 11 staff columns plus
* The 11 real column ids in `/campanha/municipios` — 10 staff columns plus
* `lastUpdateAt`, which only the non-staff (leader) view renders. Closed union
* so `municipalityColumnDescriptions` below can't silently drop a column.
*/
export type MunicipalityListColumnId =
| 'name'
| 'region'
| 'kind'
| 'votos'
| 'classe'
// Matches the column's own `id` and its `?level=` filter param; the SORT key
@ -228,7 +227,6 @@ const CLASS_COLUMN_DESCRIPTION =
export const municipalityColumnDescriptions: Record<MunicipalityListColumnId, string> = {
name: 'Nome do município e prioridade da campanha.',
region: 'Território de Identidade (Bahia) a que o município pertence.',
kind: 'Município inteiro ou zona eleitoral de Salvador (ZE 1–19).',
votos: formatMunicipalityConcentrationHint(),
classe: CLASS_COLUMN_DESCRIPTION,
level: campaignConceptOneLiner('nivel-de-envolvimento'),

View file

@ -8,7 +8,6 @@ import { EMPTY_ENGAGEMENT_LEVEL_LABEL, formatEngagementLevelLabel } from '@/lib/
import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
import { truncatedNamesLabel } from '@/utilities/campaignListUrl'
import {
municipalityKindLabels,
municipalityListCoverageLabels,
municipalityPriorityLabels,
politicalTrendLabels,
@ -31,7 +30,6 @@ import type { MunicipalityTerritorialClass } from '@/utilities/municipality/muni
export type MunicipalityFilterParam =
| 'name'
| 'region'
| 'kind'
| 'coverage'
| 'trend'
| 'class'
@ -75,16 +73,6 @@ export const municipalityFilterDefinitions: MunicipalityFilterDefinition[] = [
staffOnly: false,
selection: 'multi',
},
{
param: 'kind',
label: 'Tipo',
allLabel: 'Todos',
staffOnly: false,
selection: 'single',
options: (
Object.keys(municipalityKindLabels) as Array<keyof typeof municipalityKindLabels>
).map((kind) => ({ value: kind, label: municipalityKindLabels[kind] })),
},
{
param: 'advisor',
label: 'Assessores',
@ -149,11 +137,6 @@ export const getMunicipalityFilterDefinition = (
param: MunicipalityFilterParam,
): MunicipalityFilterDefinition => municipalityFilterDefinitionByParam[param]
export const getMunicipalitySingleFilterValue = (
state: MunicipalityListState,
param: 'kind' | 'coverage',
): string | undefined => state[param]
export type MunicipalityMultiFilterParam =
| 'region'
| 'slug'
@ -177,16 +160,6 @@ export const getMunicipalityMultiFilterValues = (
const withMunicipalityListPageReset = (state: MunicipalityListState): MunicipalityListState =>
parseMunicipalityListParams(municipalityListStateToRawParams({ ...state, page: 1 }, 1))
/** Exclusive single-value set/clear for `kind`. Empty (or invalid) clears. */
export const applyMunicipalityKindFilter = (
state: MunicipalityListState,
value: string | undefined,
): MunicipalityListState =>
parseMunicipalityListParams({
...municipalityListStateToRawParams({ ...state, page: 1 }, 1),
kind: value || undefined,
})
/** Toggle exclusivity: clicking the active value clears (coverage). */
export const toggleMunicipalityExclusiveFilterValue = (
state: MunicipalityListState,
@ -337,8 +310,6 @@ export const isMunicipalityColumnFilterActive = (
case 'advisor':
// The Assessores popover also owns the com/sem assessor toggle.
return Boolean(state.advisors?.length || state.coverage)
case 'kind':
return Boolean(state.kind)
case 'coverage':
return Boolean(state.coverage)
case 'trend':
@ -363,7 +334,6 @@ export const formatMunicipalityActiveFiltersSummary = (
)
}
if (state.regions?.length) parts.push(truncatedNamesLabel([...state.regions]))
if (state.kind) parts.push(municipalityKindLabels[state.kind])
if (state.advisors?.length) {
parts.push(state.advisors.length === 1 ? '1 assessor' : `${state.advisors.length} assessores`)
}

View file

@ -29,14 +29,11 @@ import {
import type { MunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass'
import { parseTerritoryRegionsParam } from '@/utilities/territory/territoryRegionParam'
import type { Municipality } from '@/payload-types'
export const municipalityPageSize = 25
export type MunicipalityListSortKey =
| 'name'
| 'region'
| 'kind'
| 'trend'
| 'expectedVotes'
| 'lastUpdateAt'
@ -80,7 +77,6 @@ const DEFAULT_MUNICIPALITY_LIST_SORT_KEY: MunicipalityListSortKey = 'deficit'
export const municipalityListSortLabels: Record<MunicipalityListSortKey, string> = {
name: 'Município',
region: 'Território',
kind: 'Tipo',
trend: 'Tendência',
expectedVotes: 'Votos estimados',
lastUpdateAt: 'Última atualização',
@ -95,8 +91,8 @@ export const municipalityListSortLabels: Record<MunicipalityListSortKey, string>
}
/**
* B17 — the name each column answers to in the column picker. Ten of the
* twelve quote the sort label, and the header renders that same record
* B17 — the name each column answers to in the column picker. Nine of the
* eleven quote the sort label, and the header renders that same record
* (`MunicipalitySortableHead` falls back to it when given no children, which
* is why those heads pass none), so renaming a header renames the menu entry
* with it. The two that differ do so on purpose: a header sits above its own
@ -106,7 +102,6 @@ export const municipalityListSortLabels: Record<MunicipalityListSortKey, string>
export const municipalityColumnLabels: Record<MunicipalityListColumnId, string> = {
name: municipalityListSortLabels.name,
region: municipalityListSortLabels.region,
kind: municipalityListSortLabels.kind,
/** Header is the bare year, under the "2022" group of the table. */
votos: 'Votação 2022',
classe: municipalityListSortLabels.classe,
@ -129,7 +124,6 @@ export type MunicipalityListState = {
slugs?: string[]
/** Multi-select (OR) campaignUser advisor IDs. */
advisors?: number[]
kind?: Municipality['kind']
coverage?: 'com_assessor' | 'sem_assessor'
priority?: 'alta'
/**
@ -163,7 +157,6 @@ const municipalityListParamNames = [
'region',
'slug',
'advisor',
'kind',
'coverage',
'priority',
'trend',
@ -279,7 +272,6 @@ export const municipalityListStateToRawParams = (
region: state.regions,
slug: state.slugs,
advisor: state.advisors?.map(String),
kind: state.kind,
coverage: state.coverage,
priority: state.priority,
trend: state.trends,
@ -298,7 +290,6 @@ export const parseMunicipalityListParams = (
const regions = parseRegionsParam(params.region)
const slugs = parseSlugsParam(params.slug)
const advisors = parseAdvisorsParam(params.advisor)
const rawKind = firstValue(params.kind)
const rawCoverage = firstValue(params.coverage)
const rawPriority = firstValue(params.priority)
const trends = parseExhaustiveEnumParam<PoliticalTrendStatus>(
@ -325,7 +316,6 @@ export const parseMunicipalityListParams = (
...(regions.length ? { regions } : {}),
...(slugs.length ? { slugs } : {}),
...(advisors.length ? { advisors } : {}),
...(rawKind === 'municipio' || rawKind === 'zona' ? { kind: rawKind } : {}),
...(rawCoverage === 'com_assessor' || rawCoverage === 'sem_assessor'
? { coverage: rawCoverage }
: {}),
@ -353,7 +343,6 @@ export const buildMunicipalityListWhere = (state: MunicipalityListState): Where
if (state.regions?.length) filters.push({ region: { in: state.regions } })
if (state.slugs?.length) filters.push({ slug: { in: state.slugs } })
if (state.advisors?.length) filters.push({ advisors: { in: state.advisors } })
if (state.kind) filters.push({ kind: { equals: state.kind } })
if (state.coverage) {
filters.push({
advisors: { exists: state.coverage === 'com_assessor' },
@ -402,7 +391,6 @@ export const serializeCanonicalMunicipalityListSearchParams = (
for (const region of canonicalState.regions ?? []) params.append('region', region)
for (const slug of canonicalState.slugs ?? []) params.append('slug', slug)
for (const advisor of canonicalState.advisors ?? []) params.append('advisor', String(advisor))
if (canonicalState.kind) params.set('kind', canonicalState.kind)
if (canonicalState.coverage) params.set('coverage', canonicalState.coverage)
if (canonicalState.priority) params.set('priority', canonicalState.priority)
for (const trend of canonicalState.trends ?? []) params.append('trend', trend)

View file

@ -195,7 +195,6 @@ const loadMunicipalityListFilterFacets = async (
const payloadSortFieldByKey: Partial<Record<MunicipalityListSortKey, string>> = {
name: 'name',
region: 'region',
kind: 'kind',
lastUpdateAt: 'lastUpdateAt',
trend: 'politicalTrend.status',
// E9 (deficit, frescor), E10 (classe) and E14 (nivel) are absent on purpose:

View file

@ -36,20 +36,20 @@ test.describe('Seletor de colunas', () => {
await campaign.login(page, email, password)
await page.goto('/campanha/municipios')
const kindHeader = page.getByRole('columnheader', { name: /Tipo/ })
await expect(kindHeader).toBeVisible()
const regionHeader = page.getByRole('columnheader', { name: /Território/ })
await expect(regionHeader).toBeVisible()
const picker = page.getByRole('button', { name: /Mostrar ou ocultar colunas/ })
await picker.click()
await page.getByRole('checkbox', { name: 'Tipo' }).click()
await page.getByRole('checkbox', { name: 'Território' }).click()
// Closing flushes the batched cookie write and refreshes the RSC.
await page.keyboard.press('Escape')
await expect(kindHeader).toHaveCount(0, REFRESH)
await expect(regionHeader).toHaveCount(0, REFRESH)
await expect(page.getByRole('button', { name: /1 oculta/ })).toBeVisible()
await page.reload()
await expect(page.getByRole('columnheader', { name: /Tipo/ })).toHaveCount(0, REFRESH)
await expect(page.getByRole('columnheader', { name: /Território/ })).toHaveCount(0, REFRESH)
// The cookie carries one key per list: another list is untouched. The
// territories list is the honest neighbour here — its 27 rows come from
@ -63,7 +63,7 @@ test.describe('Seletor de colunas', () => {
await page.getByRole('button', { name: 'Restaurar todas' }).click()
await page.keyboard.press('Escape')
await expect(page.getByRole('columnheader', { name: /Tipo/ })).toBeVisible(REFRESH)
await expect(page.getByRole('columnheader', { name: /Território/ })).toBeVisible(REFRESH)
})
test('lists the mandatory column without letting it be unchecked', async ({ campaign, page }) => {

View file

@ -9,7 +9,7 @@ import { expect, test } from './fixtures/campaignE2EFixtures.js'
*/
/** 416 whole municipalities — deep enough that `?page=2` is a real page. */
const SAVED_RECORTE = '/campanha/municipios?kind=municipio'
const SAVED_RECORTE = '/campanha/municipios?priority=alta'
const SAVED_NAME = 'Recorte do teste'
const RENAMED = 'Recorte renomeado'

View file

@ -31,7 +31,9 @@ describe('loadMunicipalityMapBundle — list URL filters', () => {
it('paints each zone under its own map key, never pooled under the city code', async () => {
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
const bundle = await loadMunicipalityMapBundle(payload, coordinator, { kind: 'zona' })
const bundle = await loadMunicipalityMapBundle(payload, coordinator, {
slug: zoneMunicipalities.map((entry) => entry.slug),
})
expect(bundle).not.toBeNull()
expect(bundle!.hasZoneMunicipalities).toBe(true)
@ -73,10 +75,9 @@ describe('loadMunicipalityMapBundle — list URL filters', () => {
expect(Object.keys(included!.municipalitiesByMapKey)).toEqual([expectedMapKey])
expect(Object.keys(included!.valuesByYear['2022'] ?? {})).toEqual([expectedMapKey])
const excluded =
administered.kind === 'zona'
? await loadMunicipalityMapBundle(payload, advisor, { kind: 'municipio' })
: await loadMunicipalityMapBundle(payload, advisor, { kind: 'zona' })
const outsideSlug = municipalityCatalog.find((entry) => entry.slug !== administered.slug)!.slug
const excluded = await loadMunicipalityMapBundle(payload, advisor, { slug: [outsideSlug] })
expect(excluded).toBeNull()
})

View file

@ -395,10 +395,11 @@ describe('loadMunicipalityListPageBundle', () => {
expect(included.overview).not.toBeNull()
expect(included.municipalities.some((row) => row.slug === administered.slug)).toBe(true)
const excluded =
administered.kind === 'zona'
? await loadMunicipalityListPageBundle(payload, advisor, { kind: 'municipio' })
: await loadMunicipalityListPageBundle(payload, advisor, { kind: 'zona' })
const outsideSlug = municipalityCatalog.find((entry) => entry.slug !== administered.slug)!.slug
const excluded = await loadMunicipalityListPageBundle(payload, advisor, {
slug: [outsideSlug],
})
expect(excluded.overview?.municipalityCount).toBe(0)
expect(excluded.municipalities).toHaveLength(0)
})

View file

@ -22,7 +22,7 @@ let cookieJar = ''
const COLUMNS = [
{ id: 'name', label: 'Município', mandatory: true },
{ id: 'kind', label: 'Tipo' },
{ id: 'region', label: 'Território' },
{ id: 'trend', label: 'Tendência' },
]
@ -73,7 +73,7 @@ describe('CampaignColumnPicker', () => {
render(picker([]))
openMenu()
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
// Reading the next label takes longer than any debounce worth having:
// a second between checkboxes must still be one refresh, not two.
await vi.advanceTimersByTimeAsync(1_000)
@ -84,7 +84,7 @@ describe('CampaignColumnPicker', () => {
closeMenu()
expect(refresh).toHaveBeenCalledTimes(1)
expect(cookieJar).toContain('campaign_columns=municipios:kind~trend')
expect(cookieJar).toContain('campaign_columns=municipios:region~trend')
} finally {
vi.useRealTimers()
}
@ -96,14 +96,14 @@ describe('CampaignColumnPicker', () => {
try {
render(picker([]))
openMenu()
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
await vi.advanceTimersByTimeAsync(3_000)
// The idle timer is durability, not the commit: a slow reader who takes
// more than the interval between checkboxes must not pay a route render
// per column, which is the failure the 400 ms debounce was rejected for.
expect(cookieJar).toContain('campaign_columns=municipios:kind')
expect(cookieJar).toContain('campaign_columns=municipios:region')
expect(refresh).not.toHaveBeenCalled()
closeMenu()
@ -114,11 +114,11 @@ describe('CampaignColumnPicker', () => {
})
it('does not refresh a session that ends where it started', () => {
render(picker(['kind']))
render(picker(['region']))
openMenu()
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
closeMenu()
expect(refresh).not.toHaveBeenCalled()
@ -130,10 +130,10 @@ describe('CampaignColumnPicker', () => {
// Radix reports no close event here, so without the unmount flush this
// choice would be lost to a sidebar navigation with no feedback at all.
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
unmount()
expect(cookieJar).toContain('campaign_columns=municipios:kind')
expect(cookieJar).toContain('campaign_columns=municipios:region')
})
it('never lets the mandatory column be unchecked', () => {
@ -150,7 +150,7 @@ describe('CampaignColumnPicker', () => {
const { rerender } = render(picker([]))
openMenu()
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Território' }))
closeMenu()
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1))
@ -158,8 +158,8 @@ describe('CampaignColumnPicker', () => {
openMenu()
fireEvent.click(screen.getByRole('checkbox', { name: 'Tendência' }))
// … and the RSC payload of the FIRST one lands inside it. Adopting it here
// would uncheck nothing and re-commit `kind` alone, dropping `trend`.
rerender(picker(['kind']))
// would uncheck nothing and re-commit `region` alone, dropping `trend`.
rerender(picker(['region']))
expect(screen.getByRole('checkbox', { name: 'Tendência' }).getAttribute('aria-checked')).toBe(
'false',
@ -167,19 +167,19 @@ describe('CampaignColumnPicker', () => {
closeMenu()
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(2))
expect(cookieJar).toContain('campaign_columns=municipios:kind~trend')
expect(cookieJar).toContain('campaign_columns=municipios:region~trend')
})
it('adopts the server payload once nothing is pending', async () => {
const { rerender } = render(picker([]))
rerender(picker(['kind']))
rerender(picker(['region']))
openMenu()
await waitFor(() =>
expect(screen.getByRole('checkbox', { name: 'Tipo' }).getAttribute('aria-checked')).toBe(
'false',
),
expect(
screen.getByRole('checkbox', { name: 'Território' }).getAttribute('aria-checked'),
).toBe('false'),
)
expect(refresh).not.toHaveBeenCalled()
})

View file

@ -267,7 +267,7 @@ describe('campaign visual foundation', () => {
// the mobile card's own class tooltip (B42) add triggers of their own.
const [, tbodyHtml = ''] = html.split('<tbody')
const theadHtml = html.slice(html.indexOf('<thead'), html.indexOf('</thead>'))
expect(theadHtml.match(/data-slot="tooltip-trigger"/g)).toHaveLength(11)
expect(theadHtml.match(/data-slot="tooltip-trigger"/g)).toHaveLength(10)
// B41: horizontal scroll + sticky Município (Territory pattern).
expect(html).toMatch(/data-slot="table-container"[^>]*class="[^"]*overflow-x-auto/)
@ -521,7 +521,7 @@ describe('campaign visual foundation', () => {
// B22: the leader view's 5 columns (name/region/kind/votos/lastUpdateAt)
// also carry header explanations.
const [theadHtml] = html.split('<tbody')
expect(theadHtml.match(/data-slot="tooltip-trigger"/g)).toHaveLength(5)
expect(theadHtml.match(/data-slot="tooltip-trigger"/g)).toHaveLength(4)
})
/**
@ -553,7 +553,7 @@ describe('campaign visual foundation', () => {
// The staff `<thead>` already carries 11 header-explanation tooltips
// (B22), regardless of row data. This baseline isolates the advisors
// CELL tooltip, the one this test actually exercises.
const HEADER_TOOLTIP_COUNT = 11
const HEADER_TOOLTIP_COUNT = 10
const baseMunicipality = {
id: 1,
name: 'Seabra',

View file

@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import { formatMunicipalityConcentrationHint } from '@/utilities/municipality/municipalityLabels'
import {
applyMunicipalityKindFilter,
buildMunicipalityFilterHref,
buildMunicipalityFilterOptionHref,
formatMunicipalityActiveFiltersSummary,
@ -256,13 +255,13 @@ describe('municipality list header filters (B16+)', () => {
page: '3',
sort: 'votos',
dir: 'asc',
kind: 'zona',
priority: 'alta',
})
const withRegion = toggleMunicipalityMultiFilterValue(state, 'region', 'Irecê')
expect(withRegion.regions).toEqual(['Irecê'])
expect(withRegion.page).toBe(1)
expect(buildMunicipalityFilterHref(withRegion)).toBe(
'/campanha/municipios?region=Irec%C3%AA&kind=zona&sort=votos&dir=asc',
'/campanha/municipios?region=Irec%C3%AA&priority=alta&sort=votos&dir=asc',
)
const withTwo = toggleMunicipalityMultiFilterValue(withRegion, 'region', 'Recôncavo')
@ -329,7 +328,6 @@ describe('municipality list header filters (B16+)', () => {
expect(
toggleMunicipalityMultiFilterValue(state, 'region', 'NotATerritory').regions,
).toBeUndefined()
expect(applyMunicipalityKindFilter(state, 'bairro').kind).toBeUndefined()
expect(
toggleMunicipalityExclusiveFilterValue(state, 'coverage', 'maybe').coverage,
).toBeUndefined()