Merge branch 'cursor/e61261e7'

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-25 20:02:44 -03:00
commit b29a332b9f
9 changed files with 212 additions and 27 deletions

View file

@ -7,11 +7,11 @@ alwaysApply: false
## Status
- **B20 entregue (2026-07-25):** Municípios prioritários no Início — `pickDashboardPriorityMunicipalities` (`src/utilities/dashboardPriorityMunicipalities.ts`) ordena `priority=alta` por déficit E9/`central`, amostra 8; `CampaignDashboard` exibe Badge com `highPriorityCount`, CTA “Ver todas” → `?priority=alta`, chips com `list-none`. Unit `dashboardPriorityMunicipalities.unit.spec.ts`. Plano: [`municipios-prioritarios-inicio.md`](../../../docs/plans/municipios-prioritarios-inicio.md).
- **B28 registrado (2026-07-25):** e-mail + celular na lista `/campanha/liderancas` — colunas com copy-on-click + WhatsApp (`wa.me`) no padrão de `/campanha/assessores` (B19 ✓); `email` no view model da lista; extrair primitivo compartilhado (2º call site); **sem** toggle Editar in-list (Contact update adiado). Appetite ~0,5–0,75d; Janela 1; cortável; Impeccable B. Plano: [`email-celular-lista-liderancas.md`](../../../docs/plans/email-celular-lista-liderancas.md).
- **B27 registrado (2026-07-25):** combobox + chips + auto-save no popover de Assessores de `/campanha/municipios` (pedido de produto). Hoje: lista de checkboxes de todas as contas elegíveis + botão "Salvar assessores" (`MunicipalityListAdvisorsControl`, B9) → replace do array + `revalidateMunicipalityListPaths`. Plano: chips removíveis acima de um `Command` com busca acento-insensível; gravação **por delta** em endpoint JSON espelho do `expected-votes/` (sem revalidar 435 linhas), reusando o lock `municipality-advisors:{id}` e o cálculo puro do B19 (`nextAdvisorIdsAfterMembership` sobe para `src/lib/`) — mas **não** `setAdvisorMunicipalityMembership`, cujo `assertTargetAdvisor` recusa coordenador/candidato e o próprio ator (legais nesta tela). Gatilho da célula intacto (contrato de tooltip do B23); UI segue coordenador-only (candidato = adiado com gatilho, servidor já permite desde B19 ✓). Appetite ~0,75d; Janela 1–2; cortável; Impeccable B. Plano: [`combobox-assessores-lista-municipios.md`](../../../docs/plans/combobox-assessores-lista-municipios.md).
- **Fill-in registrado (2026-07-25):** renomear coluna "Cobertura da meta" → "Cobertura" em `/campanha/municipios` — header curto (padrão A11); só lista + label de sort `deficit`; glossário/KPI strips intactos. Soft B16 ✓ (liberou o nome). Appetite ~0,25d; Janela 1; cortável; Impeccable B. Plano: [`renomear-coluna-cobertura-meta.md`](../../../docs/plans/renomear-coluna-cobertura-meta.md).
- **Fill-in registrado (2026-07-25):** remover coluna "Tipo" de `/campanha/municipios` — município e ZE tratados iguais na coordenação; filtrar ZE = filtrar Salvador (19 slugs). Remove coluna + filtro/sort UI + `?kind=` do contrato URL; schema/catálogo `kind` intacto (mapa, B8, dossiê). Appetite ~0,25–0,5d; Janela 1; cortável; Impeccable B. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md).
- **B20 registrado (2026-07-25):** Municípios prioritários no Início (`/campanha`) — a seção hoje faz `.slice(0, 6)` sem sort sobre ~50 `priority=alta` (E4R) e renderiza `<ul>` com bullets. Plano: atalho das 8 com maior déficit de cobertura (mesmo critério E9/`central`) + contagem + “Ver todas” → `?priority=alta`; markup `list-none`. Appetite ~0,5d; Janela 1; cortável; Impeccable B. Plano: [`municipios-prioritarios-inicio.md`](../../../docs/plans/municipios-prioritarios-inicio.md).
- **Pass 2 — Consolidação de engenharia entregue (2026-07-25, W0–W5 + D3):** tudo que foi one-shotted desde o Pass 1 virou infraestrutura compartilhada e arquitetura documentada. **W1 sistema de listas:** colunas como dado em `shared/CampaignTable.tsx` (seams `mandatory`/`defaultVisible` deixam **B17 trivial**; serialização canônica congelada deixa **B18 trivial**), o god module `municipalityUi.ts` (765 linhas/59 exports) dividido em `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`, 8 superfícies migradas com URLs byte-idênticas (pins do W0 verdes) e **B16+ absorvido** (`useOptimistic` no popover, facet por slugs, hrefs via serializador canônico com pin de byte-igualdade, facets no mesmo `Promise.all`); fora por decisão D5: planos (cards), `TerritoryOverviewTable`, comparação, LeaderContacts, preview de import. **W2 fronteiras:** `slug`/`phone`/`wordStartFilter`/`voteEstimate` → `lib/`, chaves de consent em `lib/campaignConsentKeys.ts`, 21 loaders `server-only`, `lib/campaignRoles.ts` client-safe (sidebar sem o barrel), `votePledgeViews` (contract) vs `votePledgeData` (loaders), ciclo supporter quebrado, `components/campaign` em **15 subpastas de domínio**; decisão D1: **sem `src/domains/`**, sem ports-and-adapters (`utilities/` adiado com gatilho: 3º módulo novo de um domínio num mês). **W4a:** `electionInsights.ts` (941 linhas, 2 exports vivos) **deletado** — sobreviventes em `electionFormat.ts`/`voteTrend.ts`; **E10 nasce em módulo novo**. **W4b:** knip `exports`/`types`/`enumMembers` em **ERROR** no CI, 0 findings (kit ui sem peças mortas; `duplicates` off — únicos hits eram os 6 aliases deliberados `canUpdateX = canReadX`). **W4c:** tabs de detalhe extraídas + factory `detailTabUi`. **W4d:** `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções comentadas). **W4e:** tipos de fonte única (cenários, trend, strategy VM nomeado). **W4f:** copy Praça→Município varrida (~70 strings; apoiadores desambiguado: cidade do contato = "Cidade"). **D3:** `submitWhatsapp` resolve `Consent.key='whatsapp-inscricao'` fail-closed; migration `20260725_170000` tagueia o doc vivo — **revisar o SQL antes de push/deploy**. Artefatos novos: `docs/ARCHITECTURE.md` + `.cursor/rules/codebase-map.mdc` (atualizados no mesmo PR que mover coisas). Suites: 46 unit / 48 int / 7 e2e; gates verdes por entrega. Tracker: [`IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2.
- **Correções e débitos pós-B16 (2026-07-25):** a revisão paralela achou três defeitos funcionais, **corrigidos na própria entrega**: as facetas omitiam só o param multi, então "Prioritária" escondia os não-prioritários do popover de Município e "Sem assessor" esvaziava o de Assessores (cada faceta passou a omitir o grupo do seu popover — `slugs`+`priority`, `advisors`+`coverage`, com int test); "todas as tendências" tinha duas codificações (as três marcadas deixavam `trend=` inútil na URL com o funil inativo — agora `parseMunicipalityListParams` canonicaliza o conjunto completo para ausente e `narrowingMunicipalityTrends` foi deletado); e o Território havia sumido do mobile ao virar multi (voltou como `MobileMultiFilterField` alimentado pela faceta, via novo prop `regionFilterOptions`). O `/simplify` da entrega (quality / performance / reuse em paralelo) aplicou o cleanup barato no próprio branch — setter único sobre params crus em `municipalityUi.ts` (fim de três casts e da escada de `if`), resumo de filtros de três funções para uma, remoção do array `municipalitySlugFilterOptions` (435 objetos que iam no bundle sem leitor), linha de opção única no Popover, facetas reusando o escopo já carregado e `getEligibleAdvisorOptions` de volta a coordenador-only. O que sobrou é maior que cleanup e virou o fill-in **B16+**: `useOptimistic` no lugar do par `{baseKey,next}` manual, payload dos rótulos de município (~20 KB por navegação) e hrefs do Popover a partir de uma base `URLSearchParams` em vez de duas serializações por opção — [`escala-dry-pos-b16.md`](../../../docs/plans/escala-dry-pos-b16.md). Adiados com gatilho no plano: gêmeo `MunicipalityFilterHead` (3º header com filtro), `ui/Command` no Popover (2º popover de busca), predicado de busca unificado, multi-seleção mobile honesta. Fora do `/campanha`: o `next build` inteiro morre quando o global `metadata` vem vazio (`/abaixo-assinado/[id]` chama `stripTrailingSlash` no `undefined`) — registrado em "Site público" no roadmap.
- **B16 entregue em código (2026-07-25):** filtros no header das colunas em `/campanha/municipios` — funil + Popover (`MunicipalityHeaderFilter`) ao lado do sort do B15, com multi-seleção OR em Território (`region`), Município (`slug`, com busca acento-insensível e o check "Prioritária" no mesmo popover), Assessores (`advisor`, que também hospeda o toggle com/sem assessor) e Tendência (`trend`); "Limpar" nos multi grandes; mobile mantém os selects empilhados. Header sticky resolvendo contra o scroller do `<main>` (sem scroller interno na tabela) e empty state que troca só as linhas de conteúdo — visão geral e linha de filtros continuam montadas, senão o usuário fica preso em zero resultados. Facetas cross-filtradas (as opções de um filtro respeitam os demais, mantendo o que já está selecionado visível) reaproveitando o escopo já carregado em `loadMunicipalityListFilterFacets`. A pedido do usuário, a coluna "Assessoria" saiu: a ordenação `coverage` passou para "Assessores", o filtro com/sem assessor virou opção do popover, e o alerta "sem responsável" do E9 (prioridade alta sem ninguém) migrou para a célula de assessores, onde antes só haveria um "Sem assessor" apagado. Plano: [`filtros-no-header-lista-municipios.md`](../../../docs/plans/filtros-no-header-lista-municipios.md).

View file

@ -1,7 +1,8 @@
# Municípios prioritários no Início
Status: rascunho
Status: entregue (2026-07-25)
Atualizado em: 2026-07-25
Revisão 2026-07-25: implementado — `dashboardPriorityMunicipalities.ts`, loader + `CampaignDashboard` (Badge, Ver todas, list-none); auditoria corrigiu `coverageByMunicipalityID` no texto do plano.
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Demais itens abertos, B20)
Impeccable: B — encaixe na seção existente do dashboard staff (`CampaignDashboard` em `/campanha`); sem rota nova
Appetite: ~0,5 dia eng; semântica da seção + ordenação + link “Ver todas” + markup sem bullets; sem migration
@ -87,7 +88,7 @@ flowchart LR
Componentes:
- **`getCampaignDashboardData`** (`src/utilities/campaignDashboardData.ts`): após o bundle E8, filtrar `priority === 'alta'`, ordenar por `goalCoverageBundle.byMunicipalityId` (ou equivalente já exposto) no cenário `central` — maior `deficit` primeiro; `null`/sem meta no fim; `slice(0, DASHBOARD_PRIORITY_SAMPLE_LIMIT)`; continuar populando `highPriorityCount` (agora consumido na UI). Extrair helper puro testável se o sort não couber inline (&lt;15 linhas → pode ficar no próprio arquivo; ≥ complexidade → `pickDashboardPriorityMunicipalities` no mesmo módulo ou ao lado de `goalCoverage`).
- **`getCampaignDashboardData`** (`src/utilities/campaignDashboardData.ts`): após o bundle E8, filtrar `priority === 'alta'`, ordenar por `goalCoverageBundle.coverageByMunicipalityID` no cenário `central` — maior `deficit` primeiro; `null`/sem meta no fim; `slice(0, DASHBOARD_PRIORITY_SAMPLE_LIMIT)`; continuar populando `highPriorityCount` (agora consumido na UI). Helper puro `pickDashboardPriorityMunicipalities` em `src/utilities/dashboardPriorityMunicipalities.ts`.
- **`CampaignDashboard`** (`src/components/campaign/dashboard/CampaignDashboard.tsx`): header da seção com título + `Badge`/`span` com `highPriorityCount`; `ul` com `m-0 flex list-none flex-wrap gap-2 p-0 [&>li]:mt-0`; chips `Button outline` + `Link` como hoje; CTA `Link` “Ver todas” via `buildMunicipalityListHref` com `priority: 'alta'` (defaults de sort da lista — `deficit` desc — alinhados a E9). Esconder CTA se `highPriorityCount <= amostra`.
- **Unit test** do helper de pick/sort (fixture 10 municipíos, assert ordem e limite).
- **Migration:** Sem migration, sem collection, sem server action.
@ -115,12 +116,15 @@ Componentes:
- **Priorizar “alta sem assessor” na amostra.** Revisitar quando: CG pedir explicitamente no Início **ou** a coluna da vergonha deixar de ser descoberta na lista (evidência de sessão/R6).
- **Expor `highPriorityCount` no metric strip.** Revisitar quando: houver 2º consumidor além do header da seção (hoje seria vaidade).
- **Extrair `sortByNullableValue` para `lib/`.** Revisitar no 3º call site de ordenação por valor nullable (E9 lista + B20 já compartilham `centralDeficitSortValue` em `goalCoverage.ts`).
- **Paralelizar `loadMunicipalityGoalCoverageBundle` com os finds de pledge/update no loader do Início.** Revisitar se TTFB medido do `/campanha` staff virar gargalo.
## Referências
- `docs/roadmap.md` (B20; E9 fila; E4R seed ~50 alta)
- `src/utilities/campaignDashboardData.ts` — loader + `slice(0, 6)` atual + `highPriorityCount` morto
- `src/components/campaign/dashboard/CampaignDashboard.tsx` — seção + `ul` sem `list-none`
- `src/utilities/campaignDashboardData.ts` — `pickDashboardPriorityMunicipalities` + `highPriorityCount`
- `src/components/campaign/dashboard/CampaignDashboard.tsx` — Badge, “Ver todas”, `list-none`
- `src/utilities/dashboardPriorityMunicipalities.ts` — pick/sort (E9 `central` déficit)
- `src/utilities/municipalityListUrl.ts` — `buildMunicipalityListHref` / filtro `priority`
- `src/utilities/municipalityPageData.ts` — `applyDerivedMunicipalitySort` (precedente déficit)
- `src/app/(campaign)/campanha/(app)/assessores/[id]/page.tsx` — strip `list-none` de referência

File diff suppressed because one or more lines are too long

View file

@ -19,6 +19,7 @@ import {
formatGoalCoverageRatioLabel,
goalCoverageProgressPercent,
} from '@/utilities/goalCoverage'
import { buildMunicipalityListHref } from '@/utilities/municipalityListUrl'
const dateTimeFormatter = new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'short',
@ -154,10 +155,23 @@ export const CampaignDashboard = ({
{view.priorityMunicipalities.length ? (
<section aria-labelledby="dashboard-priority-title" className="flex flex-col gap-3">
<h2 id="dashboard-priority-title" className="text-base font-medium">
Municípios prioritários
</h2>
<ul className="flex flex-wrap gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<h2 id="dashboard-priority-title" className="text-base font-medium">
Municípios prioritários
</h2>
<Badge variant="secondary">{view.highPriorityCount}</Badge>
</div>
{view.highPriorityCount > view.priorityMunicipalities.length ? (
<Button asChild variant="ghost" className="min-h-11">
<Link href={buildMunicipalityListHref({ page: 1, priority: 'alta' }, 1)}>
Ver todas
<ArrowRightIcon data-icon="inline-end" aria-hidden="true" />
</Link>
</Button>
) : null}
</div>
<ul className="m-0 flex list-none flex-wrap gap-2 p-0 [&>li]:mt-0">
{view.priorityMunicipalities.map((municipality) => (
<li key={municipality.slug}>
<Button asChild variant="outline" className="min-h-11">

View file

@ -5,6 +5,10 @@ import type { Payload } from 'payload'
import type { VoteEstimateScenario } from '@/lib/voteEstimate'
import type { CampaignUser, VotePledge } from '@/payload-types'
import { loadMunicipalityScope } from '@/utilities/campaignMunicipalityScope'
import {
type DashboardPriorityMunicipality,
pickDashboardPriorityMunicipalities,
} from '@/utilities/dashboardPriorityMunicipalities'
import type { MunicipalityGoalCoverage } from '@/utilities/goalCoverage'
import { loadMunicipalityGoalCoverageBundle } from '@/utilities/municipalityGoalAccount'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
@ -29,7 +33,7 @@ export type StaffDashboardView = {
contactName: string
declaredVotes: number
}>
priorityMunicipalities: Array<{ name: string; slug: string }>
priorityMunicipalities: DashboardPriorityMunicipality[]
recentUpdates: Array<{
id: number
municipalityName: string
@ -139,6 +143,11 @@ export const getCampaignDashboardData = async (
resolveNames(payload, 'campaignUser', authorIDs, { name: true }),
])
const dashboardPriority = pickDashboardPriorityMunicipalities(
municipalities.docs,
goalCoverageBundle.coverageByMunicipalityID,
)
return {
kind: 'staff',
role:
@ -147,9 +156,6 @@ export const getCampaignDashboardData = async (
withAdvisorCount: municipalities.docs.filter(
(municipality) => (municipality.advisors ?? []).length > 0,
).length,
highPriorityCount: municipalities.docs.filter(
(municipality) => municipality.priority === 'alta',
).length,
staffVoteTotalByScenario: { ...staffVoteTotalByScenario },
declaredVotesTotal,
pledgeCount,
@ -171,10 +177,8 @@ export const getCampaignDashboardData = async (
declaredVotes: pledge.declaredVotes,
}
}),
priorityMunicipalities: municipalities.docs
.filter((municipality) => municipality.priority === 'alta')
.slice(0, 6)
.map((municipality) => ({ name: municipality.name, slug: municipality.slug })),
highPriorityCount: dashboardPriority.highPriorityCount,
priorityMunicipalities: dashboardPriority.municipalities,
recentUpdates: recentUpdatesResult.docs.map((update) => {
const municipality = municipalityById.get(relationshipId(update.municipality) ?? -1)
const author = authorsById.get(relationshipId(update.author) ?? -1)

View file

@ -0,0 +1,48 @@
import type { VoteEstimateScenario } from '@/lib/voteEstimate'
import type { Municipality } from '@/payload-types'
import { centralDeficitSortValue, type MunicipalityGoalCoverage } from '@/utilities/goalCoverage'
export const DASHBOARD_PRIORITY_SAMPLE_LIMIT = 8
export type DashboardPriorityMunicipality = {
name: string
slug: string
}
export type DashboardPriorityMunicipalitiesPick = {
highPriorityCount: number
municipalities: DashboardPriorityMunicipality[]
}
type DashboardPriorityMunicipalitySource = Pick<Municipality, 'id' | 'name' | 'slug' | 'priority'>
const municipalityNameCompare = (
left: DashboardPriorityMunicipalitySource,
right: DashboardPriorityMunicipalitySource,
): number => left.name.localeCompare(right.name, 'pt-BR')
/** B20: `priority === 'alta'` sample ordered like E9 `deficit` / `central` desc. */
export const pickDashboardPriorityMunicipalities = (
municipalities: DashboardPriorityMunicipalitySource[],
coverageByMunicipalityID: ReadonlyMap<
number,
Record<VoteEstimateScenario, MunicipalityGoalCoverage>
>,
sampleLimit = DASHBOARD_PRIORITY_SAMPLE_LIMIT,
): DashboardPriorityMunicipalitiesPick => {
const highPriority = municipalities.filter((municipality) => municipality.priority === 'alta')
const municipalitiesSample = [...highPriority]
.sort((left, right) => {
const leftValue = centralDeficitSortValue(coverageByMunicipalityID.get(left.id)?.central)
const rightValue = centralDeficitSortValue(coverageByMunicipalityID.get(right.id)?.central)
if (leftValue === null && rightValue === null) return municipalityNameCompare(left, right)
if (leftValue === null) return 1
if (rightValue === null) return -1
if (leftValue === rightValue) return municipalityNameCompare(left, right)
return rightValue - leftValue
})
.slice(0, sampleLimit)
.map((municipality) => ({ name: municipality.name, slug: municipality.slug }))
return { highPriorityCount: highPriority.length, municipalities: municipalitiesSample }
}

View file

@ -37,6 +37,11 @@ export type MunicipalityGoalCoverage = {
deficit: number
}
/** E9 list + B20 dashboard: rank key for `central` when goal > 0; null sorts last. */
export const centralDeficitSortValue = (
coverage: MunicipalityGoalCoverage | null | undefined,
): number | null => (coverage && coverage.goal > 0 ? coverage.deficit : null)
/** Read-only zero coverage — never mutate; use for view models outside the staff scope. */
const emptyMunicipalityGoalCoverage: Readonly<MunicipalityGoalCoverage> = {
goal: 0,

View file

@ -14,7 +14,7 @@ import { type VoteEstimateScenario } from '@/lib/voteEstimate'
import type { CampaignUser, Municipality } from '@/payload-types'
import { isCampaignLeader, isCampaignStaff } from '@/utilities/campaignAccess'
import { loadMunicipalityScope } from '@/utilities/campaignMunicipalityScope'
import type { MunicipalityGoalCoverage } from '@/utilities/goalCoverage'
import { centralDeficitSortValue, type MunicipalityGoalCoverage } from '@/utilities/goalCoverage'
import { loadMunicipalityGoalCoverageBundle } from '@/utilities/municipalityGoalAccount'
import {
buildMunicipalityListWhere,
@ -226,10 +226,9 @@ const applyDerivedMunicipalitySort = (
// server has no scenario to sort by (moving it to the URL is the
// "Cenário junto aos filtros" fill-in). A município with no goal at all
// has no deficit to rank — `null` sends it to the end either way.
return sortByNullableValue(docs, dir, (municipality) => {
const coverage = goalCoverageByMunicipalityID.get(municipality.id)?.central
return coverage && coverage.goal > 0 ? coverage.deficit : null
})
return sortByNullableValue(docs, dir, (municipality) =>
centralDeficitSortValue(goalCoverageByMunicipalityID.get(municipality.id)?.central),
)
case 'frescor': {
// One clock read for the whole sort: the accessor runs per comparison,
// and a "now" that drifts mid-sort is not a consistent ordering key.

View file

@ -0,0 +1,110 @@
import { describe, expect, it } from 'vitest'
import type { VoteEstimateScenario } from '@/lib/voteEstimate'
import type { Municipality } from '@/payload-types'
import {
DASHBOARD_PRIORITY_SAMPLE_LIMIT,
pickDashboardPriorityMunicipalities,
} from '@/utilities/dashboardPriorityMunicipalities'
import type { MunicipalityGoalCoverage } from '@/utilities/goalCoverage'
import { stub } from '../helpers/stub'
const centralCoverage = (goal: number, committed: number): MunicipalityGoalCoverage => ({
goal,
committed,
coverageRatio: goal > 0 ? committed / goal : null,
deficit: goal - committed,
})
const coverageMap = (
entries: Array<[number, MunicipalityGoalCoverage]>,
): Map<number, Record<VoteEstimateScenario, MunicipalityGoalCoverage>> =>
new Map(
entries.map(([id, central]) => [
id,
{
pessimistic: centralCoverage(0, 0),
central,
optimistic: centralCoverage(0, 0),
},
]),
)
const municipality = (id: number, slug: string, priority: Municipality['priority']): Municipality =>
stub<Municipality>({
id,
name: slug,
slug,
priority,
})
describe('pickDashboardPriorityMunicipalities', () => {
it('orders alta by central deficit desc and caps at the sample limit', () => {
const municipalities = [
municipality(1, 'alpha', 'alta'),
municipality(2, 'beta', 'alta'),
municipality(3, 'gamma', 'alta'),
municipality(4, 'delta', 'alta'),
municipality(5, 'epsilon', 'alta'),
municipality(6, 'zeta', 'alta'),
municipality(7, 'eta', 'alta'),
municipality(8, 'theta', 'alta'),
municipality(9, 'iota', 'alta'),
municipality(10, 'kappa', 'alta'),
municipality(11, 'normal', 'normal'),
]
const coverageByMunicipalityID = coverageMap([
[1, centralCoverage(100, 10)],
[2, centralCoverage(100, 20)],
[3, centralCoverage(100, 30)],
[4, centralCoverage(100, 40)],
[5, centralCoverage(100, 50)],
[6, centralCoverage(100, 60)],
[7, centralCoverage(100, 70)],
[8, centralCoverage(100, 80)],
[9, centralCoverage(100, 90)],
[10, centralCoverage(100, 95)],
[11, centralCoverage(500, 0)],
])
const { municipalities: result, highPriorityCount } = pickDashboardPriorityMunicipalities(
municipalities,
coverageByMunicipalityID,
)
expect(highPriorityCount).toBe(10)
expect(result).toHaveLength(DASHBOARD_PRIORITY_SAMPLE_LIMIT)
expect(result.map((m) => m.slug)).toEqual([
'alpha',
'beta',
'gamma',
'delta',
'epsilon',
'zeta',
'eta',
'theta',
])
})
it('sends municipalities without a positive goal to the end', () => {
const municipalities = [
municipality(1, 'with-goal', 'alta'),
municipality(2, 'no-goal', 'alta'),
municipality(3, 'missing-coverage', 'alta'),
]
const coverageByMunicipalityID = coverageMap([
[1, centralCoverage(100, 50)],
[2, centralCoverage(0, 0)],
])
const { municipalities: result } = pickDashboardPriorityMunicipalities(
municipalities,
coverageByMunicipalityID,
3,
)
expect(result.map((m) => m.slug)).toEqual(['with-goal', 'missing-coverage', 'no-goal'])
})
})