diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 11ad9beb..af449483 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -60,7 +60,7 @@ alwaysApply: false - **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). - **B21 entregue (2026-07-25):** nova rota staff `/campanha/territorios` com os 27 Territórios de Identidade no sistema de listas — sort/filtros/busca canônicos na URL, sub-linhas do Metropolitano achatadas sem perder a hierarquia, tabela mobile com primeira coluna sticky e drill para `/campanha/municipios?region=`. O chrome de header rico foi extraído para `shared/CampaignSortableHead` + `CampaignHeaderFilterPopover`, com os wrappers de municípios migrados sem mudança de contrato; B29/B33 agora só consomem essa peça. O painel de TI saiu do Início por decisão desta sessão. Sem migration/collection/action. Plano: [`pagina-territorios-identidade.md`](../../../docs/plans/pagina-territorios-identidade.md). - **B28 entregue em código (2026-07-26):** e-mail + celular na lista `/campanha/liderancas` — colunas **E-mail** e **Celular** (copy-on-click, toast, "—" vazio) mais uma coluna de **Ações** com botão WhatsApp (`wa.me`, desabilitado com `aria-label` honesto sem celular válido), ordem final Nome → E-mail → Celular → Status → Municípios → Organizações → Acesso ao app → Ações, no mesmo padrão visual/interativo de `/campanha/assessores` (**B19 ✓**). `whatsAppHrefForPhone` promovido de `AdvisorsTable.tsx` para `src/lib/phone.ts` (pura, client-safe); novo `shared/CampaignCopyableCell.tsx` (+ `campaignReadCellClassName` exportado) nasce **já no 2º call site** — `AdvisorsTable.tsx` migrado no mesmo PR para consumi-lo, eliminando o `copyText`/`READ_CELL_CLASS` duplicados. `leadershipData.ts`: `contactNameAndPhone` virou `contactSummary` (+ `email`), `LeadershipRowViewModel.email` populado sem query nova (`depth: 1` já trazia o `Contact` inteiro), e `loadLeadershipDetail` parou de reparsear `doc.contact` — reusa `row.email` de `toLeadershipRows`. Sem migration/collection/Consent/URL novo; access inalterado. Gate verde: tsc/lint/format/check:cycles limpos, 393 unit+int, build produção ok; `pnpm exec knip` mantém o erro pré-existente ao carregar `payload.config.ts` (P3, não relacionado); `pnpm test:e2e` reproduziu só o flake pré-existente já ledgerado em `campaignMunicipalities.e2e.spec.ts` (checkbox de consentimento do `LeaderContactsPanel` antes da hidratação) — nenhuma rota tocada por este item falhou. Aikido 0 achados nos 5 arquivos novos/editados. Plano: [`email-celular-lista-liderancas.md`](../../../docs/plans/email-celular-lista-liderancas.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 "Cobertura da meta" → "Cobertura" entregue em código (2026-07-28):** coluna `goalCoverage` e sort `deficit` em `/campanha/municipios` passam a mostrar **"Cobertura"** via SSoT pós-B17 (`municipalityListSortLabels.deficit` → `municipalityColumnLabels.goalCoverage`); `
` do card mobile cita o mesmo registro (sem literal). B22 ✓ mantém a definição longa no hover; título E18, strips, Conta da cadeira e coluna de territórios seguem "Cobertura da meta". Pins em `campaignComponents.unit.spec.ts` e `municipalityList.unit.spec.ts`. Sem migration. Plano as-built: [`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). - **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. diff --git a/docs/plans/renomear-coluna-cobertura-meta.md b/docs/plans/renomear-coluna-cobertura-meta.md index 6d31d244..d829cc42 100644 --- a/docs/plans/renomear-coluna-cobertura-meta.md +++ b/docs/plans/renomear-coluna-cobertura-meta.md @@ -1,7 +1,7 @@ # Renomear coluna "Cobertura da meta" → "Cobertura" -Status: rascunho -Atualizado em: 2026-07-25 +Status: entregue (2026-07-28) +Atualizado em: 2026-07-28 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins) Impeccable: B — encaixe no header/card de `/campanha/municipios`; sem rota nova Appetite: ~0,25 dia eng (ou menos); só strings + pins de teste; sem migration @@ -31,11 +31,11 @@ No E8 a coluna antiga "Cobertura" (= tem assessor) foi renomeada para "Assessori Pedido de produto (2026-07-25): **coluna = "Cobertura"**. Mesmo padrão do A11 (`2022` em vez de "Concentração 2022"): título curto na tabela; definição no hover (**B22** consome `oneLiner` de `cobertura-da-meta` no E18). -Onde a string aparece hoje na lista: +Onde a string aparecia na lista (pré-B17 o header citava `children`; pós-B17 ✓ o SSoT é `municipalityListSortLabels.deficit` → `municipalityColumnLabels.goalCoverage` → header/picker/sort): -- Header desktop: `MunicipalityList.tsx` coluna `id: 'goalCoverage'` → `MunicipalitySortableHead` children. -- Card mobile: `
Cobertura da meta
` no mesmo arquivo. -- Sort select / summary: `municipalityListSortLabels.deficit = 'Cobertura da meta'`. +- Header desktop: `MunicipalityList.tsx` coluna `id: 'goalCoverage'` → `MunicipalitySortableHead` `sortKey="deficit"` (label do registro de sort). +- Card mobile: `
` via `municipalityColumnLabels.goalCoverage`. +- Sort select / summary: `municipalityListSortLabels.deficit`. Fora da lista (não escopo deste fill-in): strip do dashboard/overview, card "Conta da cadeira", dossiê, título do conceito em `/campanha/conceitos` — o nome canônico da métrica continua "Cobertura da meta". @@ -70,7 +70,7 @@ flowchart LR Componentes: -- **`MunicipalityList.tsx`**: children do `MunicipalitySortableHead` `sortKey="deficit"` → `"Cobertura"`; `
` do card mobile idem. +- **`MunicipalityList.tsx`**: `
` do card mobile usa `municipalityColumnLabels.goalCoverage` (header desktop segue o mesmo registro via B17). - **`municipalityListUrl.ts`**: `municipalityListSortLabels.deficit = 'Cobertura'`; comentário do sort `coverage` já documenta a intenção — confirmar que continua verdadeiro. - **Testes:** `tests/unit/campaignComponents.unit.spec.ts` (`toContain('Cobertura')` / deixar de exigir o sufixo "da meta" na lista); opcional pin em `municipalityList.unit.spec.ts` (`deficit` → `"Cobertura"`). - **B22:** ao implementar, o mapa de `description` da coluna `goalCoverage` já aponta para `campaignConceptOneLiner('cobertura-da-meta')` — o header curto + tooltip longo é o desenho desejado; **não** editar o plano B22 em massa neste fill-in. @@ -80,13 +80,14 @@ Depth check: só strings nos módulos que já donam a coluna — sem helper novo ## Dependências -- Nenhuma dura. Soft: **B16 ✓** (liberou o nome "Cobertura"); **A11 ✓** (precedente de header curto); **B22** (tooltip carrega a definição — soft, não bloqueia este rename). +- Nenhuma dura. Soft: **B16 ✓** (liberou o nome "Cobertura"); **A11 ✓** (precedente de header curto); **B22 ✓** (tooltip carrega a definição); **B17 ✓** (picker/header compartilham `deficit`). ## Não escopo - Título do conceito / heading em `/campanha/conceitos` — **E18 ✓** (e2e `campaignConcepts.e2e.spec.ts` permanece). - Labels do `CampaignMetricStrip` (dashboard + overview) e do `MunicipalityGoalAccountCard` / dossiê. -- Seletor de colunas / explicação no header / tooltip de célula — **B17** / **B22** / **B23**. +- Label da coluna homônima em `/campanha/territorios` (continua "Cobertura da meta" em `territoryListSortLabels.cobertura`). +- Seletor de colunas / explicação no header / tooltip de célula — consumidores de B17 ✓ / B22 ✓ / B23 ✓ (não reimplementar). - Semântica ou sort key `deficit` / fórmula E8. ## Rabbit holes @@ -98,7 +99,7 @@ Depth check: só strings nos módulos que já donam a coluna — sem helper novo ## Adiado com gatilho - **Encurtar "Cobertura da meta" nos strips / card de detalhe.** Revisitar quando: critique/R6 ou CG pedir paridade explícita com a lista. -- Nenhum outro neste item. +- **Demais `
` do card mobile ainda literais** (Classe, Nível, Tendência, …). Revisitar quando: fill-in remover coluna Tipo ou passada de paridade mobile na lista de municípios. ## Referências @@ -114,3 +115,7 @@ Depth check: só strings nos módulos que já donam a coluna — sem helper novo - [remover-coluna-tipo-municipios.md](remover-coluna-tipo-municipios.md) — precedente fill-in Impeccable B na mesma lista - AGENTS.md — naming; E8 - `PRODUCT.md` / `DESIGN.md` — Field Desk + +## Revisão (2026-07-28) + +Auditoria `implement-roadmap-item`: header não usa mais `children` do `MunicipalitySortableHead` — B17 centraliza rótulos em `municipalityListSortLabels`/`municipalityColumnLabels`. Entrega: `deficit: 'Cobertura'`, mobile `
` via `municipalityColumnLabels.goalCoverage`, `whereItAppears` do conceito E18 atualizado (lista curta; título longo intacto). TI fora de escopo. diff --git a/docs/roadmap.md b/docs/roadmap.md index f145ed79..5e86375c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -133,7 +133,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re ### Fill-ins abertos - ~~**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** em `/campanha/municipios` — pedido de produto (2026-07-25): header curto na fila (padrão A11 `2022`); definição no hover (**B22**) / glossário (**E18 ✓**). Só lista (header + card mobile + label de sort `deficit`); strips/card/conceitos mantêm o nome longo da métrica. Soft: **B16 ✓** (saiu "Assessoria", liberou o nome) · ~0,25d · Impeccable B · cortável · [plano](plans/renomear-coluna-cobertura-meta.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 lista "Municípios por zona eleitoral" do Início** — pedido de produto (2026-07-26): com **B8 F2 ✓** cada ZE de Salvador tem polígono próprio no mapa; a lista de 19 linhas sob o painel (`zoneBreakdown`) é densidade morta. Remove o bloco em `MunicipalityMapPanel` + o campo do bundle; mantém a ressalva de aproximação atrelada à malha — **B8+** F3 ✓ já a moveu para fora do gate da lista, condicionada a `bundle.hasZoneMunicipalities` (campo próprio, que sobrevive ao drop de `zoneBreakdown`), então resta apagar o `