diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 31cf8fc4..66e6b8c6 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,7 +7,11 @@ alwaysApply: false ## Status +- **B18 registrado (2026-07-24):** filtros **salvos com nome** em `/campanha/municipios` (`localStorage`) + acesso rápido de 2º nível sob Municípios no sidebar (hover desktop / expand mobile; sticky só no filtro salvo ativo). Soft B16/B17 (barra). Fatia concreta do “saved views” que FD2 adiava como genérico. Sem migration. Plano: [`filtros-salvos-municipios.md`](../../../docs/plans/filtros-salvos-municipios.md). +- **Avaliação: reorder de colunas por DnD (2026-07-24):** pedido para arrastar headers em `/campanha/municipios` → **fora de escopo** (não confundir com **B17** hide/show). Tecnicamente possível (~1–1,5d), mas conflita com sort B15 + filtros B16 no mesmo header, anti spreadsheet do PRODUCT, e não desbloqueia decisão de alocação. Gatilho em [`reordenar-colunas-lista-municipios.md`](../../../docs/plans/reordenar-colunas-lista-municipios.md). +- **B17 registrado (2026-07-24):** seletor **mostrar/ocultar** colunas em `/campanha/municipios` (desktop) — Popover + `localStorage`; `name` obrigatória; soft B15/B16; prepara viewport para E9. Distinto do reorder DnD (fora de escopo). Sem migration. Plano: [`seletor-colunas-lista-municipios.md`](../../../docs/plans/seletor-colunas-lista-municipios.md). - **A11 entregue em código (2026-07-24):** posição em votos (rank denso 435 + % da própria votação estadual) no `MunicipalityBaselineCard` e coluna/ordenação `?sort=votos` na lista (default desc; header=`2022`); helper puro [`municipalityVoteRank.ts`](../../../src/lib/municipalityVoteRank.ts); consome o contrato URL `sort`/`dir` de B15. Plano: [`ranking-votos-municipio.md`](../../../docs/plans/ranking-votos-municipio.md). +- **B16 registrado (2026-07-24):** filtros no header das colunas em `/campanha/municipios` — relocaciona params URL existentes (`region`/`kind`/`trend`/`coverage`/`priority`) para o `TableHead` ao lado do sort B15; barra slim (busca + Limpar [+ Cenário]); mobile mantém disclosure. Sem migration. Plano: [`filtros-no-header-lista-municipios.md`](../../../docs/plans/filtros-no-header-lista-municipios.md). - **E17 entregue em código (2026-07-24):** tabela comparativa dos 27 Territórios de Identidade no Início staff (`/campanha`) — `src/utilities/territoryOverview.ts` (rollup puro, client-safe: `computeTerritoryRollup` + `sortTerritoryRows`) + `src/utilities/loadTerritoryOverview.ts` (loader server-only, `overrideAccess: true` — leitura regional completa para advisor é contexto, não gestão; exposição só agregados TI-level) + `src/components/campaign/TerritoryOverviewTable.tsx` (tabela shadcn densa, ordenação client-side default `% da própria votação desc`, Metropolitano decomposto em Salvador 19 zonas × Demais RMS, linha→`/campanha/municipios?region=`). Somas/razões apenas (salvaguarda MAUP); sem migration/collection. Unit `territoryOverview.unit.spec.ts` (10 testes); tsc/lint/knip/unit 266/int 345/build verde; Aikido 0 findings. Plano: [`tabela-ti-inicio.md`](../../../docs/plans/tabela-ti-inicio.md). Primeira fatia de E12 (estende com captura/LQ/cobertura de meta por TI). - **B15 entregue (2026-07-24):** ordenar `/campanha/municipios` pelo header da coluna — contrato URL `?sort=`/`?dir=`, clique no header (desktop) + select mobile; A11 consome a key `votos`. Verificação (tsc/lint/test/build/Aikido) passou. `/simplify` concluído no mesmo dia: reduziu os comparadores de ordenação a um helper genérico, unificou os dois ramos `payload.find` nativo/derivado, moveu labels/opções de sort para `municipalityUi.ts`, simplificou o ícone de chevron e removeu o cast `as unknown` do mock de roteador nos testes. Plano: [`ordenacao-colunas-lista-municipios.md`](../../../docs/plans/ordenacao-colunas-lista-municipios.md). - **Fill-in registrado (2026-07-24):** seletor **Cenário** de `/campanha/municipios` → fileira de `MunicipalityFilters` (provider envolve filtros+resultados; overview sem seletor). Plano: [`cenario-junto-filtros-municipios.md`](../../../docs/plans/cenario-junto-filtros-municipios.md). diff --git a/docs/plans/cenario-junto-filtros-municipios.md b/docs/plans/cenario-junto-filtros-municipios.md index cf64ddc8..4d0f530c 100644 --- a/docs/plans/cenario-junto-filtros-municipios.md +++ b/docs/plans/cenario-junto-filtros-municipios.md @@ -31,6 +31,8 @@ Em `/campanha/municipios` (staff), o seletor **Cenário** (`VoteEstimateScenario A página hoje **embrulha o provider só em torno dos resultados**, de propósito — comentário em `municipios/page.tsx`: troca de cenário não deve re-renderizar os filtros. Pedido de produto (2026-07-24): o Cenário deve ficar **junto dos outros filtros** da página. +> **Nota 2026-07-24 (B16):** [filtros-no-header-lista-municipios.md](filtros-no-header-lista-municipios.md) relocaciona os selects de recorte (`region`/`kind`/…) para o header da tabela no desktop. O destino deste fill-in passa a ser a **barra slim** (busca + Limpar + Cenário), não a fileira completa atual — mesmo componente `MunicipalityFilters`, layout enxuto. Preferir aterrar Cenário **com ou depois** de B16 se ambos forem pegos na mesma janela. + Polish anterior (notebook 2026-07-23) já tinha movido o seletor do mapa → overview; este item completa o agrupamento mental “controles de recorte/leitura da lista”. ## Objetivos @@ -44,7 +46,7 @@ Polish anterior (notebook 2026-07-23) já tinha movido o seletor do mapa → ove ## Decisões travadas - **Fill-in com plano próprio (não reabrir A10; não absorver só em R6; não item de trilha B).** Relocação barata, paralelizável, cortável; A10 já entregue a semântica. (2026-07-24, classificação roadmap-item.) **Rejeitado:** fase informal de A10 (mistura entrega fechada com polish de layout); só R6 (atrasa quick win no critique largo); ID B novo (infla grafo por ~½ dia). -- **Provider envolve filtros + `CampaignListResults` (staff).** Necessário para o seletor viver em `MunicipalityFilters`. Custo: troca de cenário re-renderiza a árvore dos filtros — aceitável (controle local, sem navegação). **Rejeitado:** prop-drill `scenario`/`setScenario` sem provider; dois providers; manter seletor no overview *e* nos filtros (duplicata). +- **Provider envolve filtros + `CampaignListResults` (staff).** Necessário para o seletor viver em `MunicipalityFilters`. Custo: troca de cenário re-renderiza a árvore dos filtros — aceitável (controle local, sem navegação). **Rejeitado:** prop-drill `scenario`/`setScenario` sem provider; dois providers; manter seletor no overview _e_ nos filtros (duplicata). - **Cenário continua fora da URL e fora de `buildMunicipalityFiltersKey` / Limpar.** Alinhado à decisão A10 (Ano/Escala locais; só `compare` na URL do mapa). **Rejeitado:** `?estimate=` neste item; Limpar resetar cenário (mistura recorte URL com lente de leitura). - **Hint:** adaptar copy do overview (hoje `VOTE_ESTIMATE_SCENARIO_OVERVIEW_HINT`) para o contexto dos filtros (“Troca o total da visão geral e os votos na lista…”); mapa no Início permanece com o hint próprio. **Rejeitado:** remover o `CampaignInfoHint`. - **i18n e naming** (AGENTS.md): identificadores existentes (`VoteEstimateScenarioField`, `MunicipalityEstimateScenarioProvider`); strings “Cenário” / Pessimista / Média / Otimista. diff --git a/docs/plans/filtros-no-header-lista-municipios.md b/docs/plans/filtros-no-header-lista-municipios.md new file mode 100644 index 00000000..587e70fb --- /dev/null +++ b/docs/plans/filtros-no-header-lista-municipios.md @@ -0,0 +1,144 @@ +# B16 — Filtros no header das colunas da lista de Municípios + +Status: rascunho +Atualizado em: 2026-07-24 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Demais itens abertos, B16; superfície de coordenação) +Impeccable: B — encaixe em `MunicipalityList` / `MunicipalitySortableHead` / `MunicipalityFilters` em `/campanha/municipios`; sem rota nova +Appetite: ~1–1,5 dia eng; relocação dos selects de recorte para o header (desktop) + barra slim + mobile disclosure; sem migration, sem collection, sem Consent +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 2 — clareza sob pressão — e 8 — Feel the action) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · shells `MunicipalitySortableHead`, `CampaignListPendingBoundary`, shadcn `Table` / `NativeSelect` / `Popover` se precisar. + +Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (só composição header+filtro; sem redesign da lista/overview). + +Brief compacto: + +- **Persona / contexto:** Assessor / CG / Candidato na tabela densa de até 435 municípios; olho na coluna (TI, tendência, cobertura…) e quer **restringir aquele eixo** sem subir o olhar até a fileira de selects acima do overview. +- **Job principal:** filtrar pelo header da coluna correspondente **e** continuar ordenando pelo mesmo header (B15 intacto). +- **Estratégia de cor:** Restrained — indicador de filtro ativo sóbrio (ícone/weight), sem segunda fileira de chips coloridos. +- **Edit where you see:** não — filtro/sort são navegação URL; células B9 (Assessores / Tendência / votos estimados) permanecem Popovers de mutação. +- **Anti-goals:** spreadsheet / data-grid mode; TanStack Table / lib nova; inventar filtros numéricos novos (faixa de votos, frescor) neste item; esconder busca/Limpar; duplicar o mesmo select no topo **e** no header no desktop. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Não como métrica nova — **Dados: N/A** para fórmula/KPI. A superfície continua a **tabela/lista** já existente; este item só relocaciona as affordances de recorte (URL where) para o header. +- **Decisões desbloqueadas:** Staff: “neste TI / só zonas / só sem assessor / só tendência X / só prioritárias — e no topo por votos/frescor — o que atacar?” (mesmo recorte de hoje, gestual colado à coluna). +- **Forma escolhida:** **tabela / lista** (inalterada) + filtro no `TableHead` — **por quê:** o eixo do filtro já é a coluna. **Rejeitado:** segundo painel de facets; chips flutuantes sobre a tabela; chart de distribuição dos filtros. +- **Profile:** N/A (sem série/mapa novo); granularidade município; ≤435 no filtrado. +- **Anti-goals de dado:** sem % estadual; sem filtro que minta a paginação (where continua no loader); sem reordenar o mapa por este item. + +## Contexto + +Em `/campanha/municipios`, o estado canônico de lista já vive na URL via `MunicipalityListState` (`municipalityUi.ts`): `q`, `region`, `kind`, `coverage`, `priority`, `trend`, `page`, e desde **B15** `sort`/`dir` com headers clicáveis (`MunicipalitySortableHead`). Os filtros de recorte ainda estão numa **fileira separada** (`MunicipalityFilters`) acima do overview — o olho alterna entre barra e tabela. Sort e filtro não compartilham affordance espacial. + +Mapeamento atual coluna ↔ filtro URL (staff): + +| Coluna (desktop) | Sort (B15) | Filtro URL hoje | +| ------------------------ | --------------- | -------------------------------------------------------------------------- | +| Município | `name` | `q` (busca; não é select) + `priority` (badge/ícone na célula, sem coluna) | +| Território de identidade | `region` | `region` | +| Tipo | `kind` | `kind` | +| 2022 (`votos`) | `votos` | — | +| Assessores | — (editor B9) | — (filtro “Assessoria” alimenta a coluna **Cobertura**) | +| Tendência | `trend` | `trend` | +| Votos estimados | `expectedVotes` | — | +| Última atualização | `lastUpdateAt` | — | +| Cobertura | `coverage` | `coverage` | + +Pedido de produto (2026-07-24): avaliar juntar os filtros aos headers, **idealmente no header de cada coluna**, mantendo sort. + +Fill-ins vizinhos: [Cenário junto aos filtros](cenario-junto-filtros-municipios.md) (assume fileira de filtros); [ícone de prioridade](icone-prioridade-lista-municipios.md) (coluna do nome). **E9** herda a mesma lista/URL — o padrão de header deve sobreviver à fila. + +## Objetivos + +- Desktop (`md+`): para cada filtro URL já existente que tem coluna correspondente, o controle de filtro vive no `TableHead` daquela coluna, **ao lado** do sort B15 (sem perder `aria-sort` / toggle `dir`). +- Semântica URL intacta (`parse`/`build`/`resolveMunicipalityListUrl`); pending via `CampaignListPendingBoundary` / `commitNavigation` (Feel the action). +- Barra superior slim: **busca** (`q`) + **Limpar** (+ **Cenário** quando o fill-in de Cenário entregar; mobile: disclosure com os mesmos filtros). +- Mobile (cards): filtros **não** fingem header — permanecem no disclosure/barra (padrão B15 do select “Ordenar”). +- Guardrails: sem migration, sem collection, sem Consent, sem server action; `leader` continua redirecionado; staff-only filters só com `showStaffFilters` / `isStaffView`. + +## Decisões travadas + +- **Item de trilha B16 (não fill-in; não só R6; não reabrir B15).** Pattern de composição da lista que E9 consome; ~1–1,5d; B15 já entregue o contrato sort. (2026-07-24, roadmap-item.) **Rejeitado:** fill-in só (subestima conflito sort×filtro e o destino da barra); absorver em R6 (atrasa e dilui); fase informal de B15 (entrega fechada). +- **Só relocacionar filtros URL existentes — não inventar where novos.** v1: `region`, `kind`, `trend`, `coverage`, `priority` no header; `q` fica na barra (texto livre). **Rejeitado:** filtro por faixa de `expectedVotes` / frescor / rank 2022 neste item (rabbit hole de produto + UI numérica); filtro no header de Assessores (ambíguo com editor B9 — o where “com/sem assessor” fica no header **Cobertura**, label do select pode continuar “Assessoria” ou alinhar a “Cobertura” no craft). +- **Sort = clique no rótulo/chevron (B15); filtro = controle irmão no mesmo `TableHead`.** Dois alvos distintos (`CampaignTransitionAnchor` vs `NativeSelect`/`Popover`), `stopPropagation` no filtro. **Rejeitado:** clique único ciclando sort+filter; segunda linha de cells tipo Excel sob o header (altura + spreadsheet vibe); TanStack Table. +- **Desktop: removes selects duplicados da fileira superior** para os params que migraram ao header. Barra slim = busca + Limpar (+ Cenário). **Rejeitado:** manter barra completa + header (duplicata); eliminar a barra inteira (busca/Limpar/Cenário precisam de casa). +- **Mobile: disclosure com os filtros atuais** (e sort select B15). **Rejeitado:** inventar “headers” em cards. +- **Prioridade no header da coluna Município** (select Todos / Prioritárias), coexistindo com o fill-in do ícone Flag na célula. **Rejeitado:** coluna Prioridade nova só para o filtro. +- **i18n e naming:** identificadores ingleses (`MunicipalityColumnHead`, `MunicipalityHeaderFilter`, estender `MunicipalitySortableHead` ou composição); labels pt-BR inalterados na essência. + +## Questões em aberto + +- **Controle no header: `NativeSelect` compacto vs ícone Filter → Popover?** **Opções:** A) NativeSelect sob/ao lado do rótulo | B) ícone Filter + Popover com as opções | C) DropdownMenu. **Recomendação:** **B** se a largura da coluna apertar (TI é longo); **A** em colunas curtas (Tipo, Cobertura) — decidir no craft medindo a tabela real; default de implementação: Popover+lista para TI, NativeSelect para enums curtos. _(assumido — validar no craft)_ +- **Fill-in Cenário: aterrar antes ou depois de B16?** **Opções:** A) Cenário primeiro (barra ainda “cheia”) | B) B16 primeiro (barra slim; Cenário encaixa na slim) | C) mesmo PR. **Recomendação:** **B** ou **C** se o implementador pegar os dois — B16 redefine o destino da barra; o plano de Cenário já prevê fileira de `MunicipalityFilters` (atualizar destino → slim bar no craft). _(assumido)_ +- **Label do filtro `coverage` no header Cobertura: “Assessoria” vs “Cobertura”?** **Opções:** A) manter “Assessoria” (copy atual do select) | B) “Cobertura” alinhado ao header | C) “Com assessor”. **Recomendação:** **B** no header (contexto da coluna); opções internas `Com assessor` / `Sem assessor` inalteradas. + +## Abordagem proposta + +```mermaid +flowchart LR + URL["MunicipalityListState\nURL where + sort/dir"] + Slim["MunicipalityFilters slim\nq + Limpar + Cenário + mobile"] + Head["MunicipalityColumnHead\nsort + filter"] + List["MunicipalityList desktop"] + Loader["loadMunicipalityListPageBundle"] + Pending["CampaignListPendingBoundary"] + + URL --> Slim + URL --> Head + Slim --> Pending + Head --> Pending + Pending --> Loader + Loader --> List + Head --> List +``` + +Componentes: + +- **`MunicipalityColumnHead`** (novo ou extensão de `MunicipalitySortableHead` em `src/components/campaign/`): `TableHead` com (1) âncora de sort B15 e (2) slot opcional de filtro (`filterParam` + options). Cliente leve; reusa `buildMunicipalityListHref` / `commitNavigation` pattern / pending compartilhado. Depth: **não** extrair factory multi-lista (<3 call sites). +- **`MunicipalityList.tsx`**: passar state + wire dos filtros por coluna; coluna Assessores permanece `TableHead` estático (sem filtro/sort). +- **`MunicipalityFilters.tsx`**: desktop slim (busca + Limpar [+ Cenário]); em `md:hidden` (ou disclosure) manter os NativeSelects de `region`/`kind`/… espelhando a URL; remover duplicata desktop dos params migrados. +- **`municipalityUi.ts`**: sem mudança de contrato URL se os params forem os mesmos; helpers opcionais `municipalityHeaderFilterOptions` para labels/options por param (evitar duplicar arrays no JSX). +- **Migration:** Sem migration, sem collection, sem server action. + +## Dependências + +- **Dura:** nenhuma de item aberto — **B15 ✓** (sort no header + `sort`/`dir`) é pré-requisito já entregue. +- **Suaves:** fill-in Cenário (destino = barra slim); fill-in ícone prioridade (célula do nome); **E9** (herda o pattern). + +## Não escopo + +- Novos params de filtro (faixa de votos, frescor, rank) — produto futuro / E9 se a fila pedir. +- Sort na coluna Assessores; filtro “por assessor nomeado” (pessoa) — fora; coverage binário basta. +- Unificar headers filtráveis em apoiadores/lideranças — fill-in sob demanda (3º call site). +- Redesign do overview / mapa; R6 glossário. +- Lib de data-grid / reorder/resize de colunas — avaliação dedicada em [reordenar-colunas-lista-municipios.md](reordenar-colunas-lista-municipios.md) (fora de escopo com gatilho; não B17 agora). + +## Rabbit holes + +- **Spreadsheet mode.** Header filter+sort ≠ grade editável. **Mitigação:** só navegação URL + B9 Popovers existentes; ban TanStack/Editable grid neste item. +- **Duplicar pending / segundo `useTransition`.** **Mitigação:** reusar `useCampaignListPending` / `CampaignTransitionAnchor` como B15/filtros-auto. +- **Abstrair `FilterableSortableHead` genérico cedo.** **Mitigação:** composição local na lista de municípios; extrair no 2º consumidor. +- **Reescrever o fill-in Cenário como B16.** **Mitigação:** Cenário continua plano próprio; B16 só reserva o slot na slim bar. + +## Adiado com gatilho + +- **Filtro numérico / por faixa em Votos estimados ou 2022.** Revisitar quando: pedido explícito da mesa ou E9 exigir faceta na fila. +- **Filtro “por assessor” (ID de `campaignUser`).** Revisitar quando: coordenação pedir “só os meus” além do scope de advisor já imposto pelo access. +- **Shared head filtrável em `components/ui`.** Revisitar quando: 2ª lista `/campanha` pedir o mesmo padrão. + +## Referências + +- `docs/roadmap.md` (B16; B15 ✓; fill-ins Cenário / prioridade; E9) +- [ordenacao-colunas-lista-municipios.md](ordenacao-colunas-lista-municipios.md) (B15 — contrato sort/header) +- [filtros-auto-pracas.md](filtros-auto-pracas.md) (URL + pending + debounce busca) +- [cenario-junto-filtros-municipios.md](cenario-junto-filtros-municipios.md) (destino da barra) +- [icone-prioridade-lista-municipios.md](icone-prioridade-lista-municipios.md) (coluna nome) +- [fila-de-alocacao.md](fila-de-alocacao.md) (E9 — herda lista) +- `src/components/campaign/MunicipalityList.tsx`, `MunicipalitySortableHead.tsx`, `MunicipalityFilters.tsx` +- `src/utilities/municipalityUi.ts` — `MunicipalityListState`, href/where +- `src/components/campaign/CampaignListPending.tsx` — pending compartilhado +- AGENTS.md — naming EN / strings pt-BR; Campaign auth; sem migration neste item +- `PRODUCT.md` / `DESIGN.md` — Field Desk, Feel the action, anti spreadsheet diff --git a/docs/plans/filtros-salvos-municipios.md b/docs/plans/filtros-salvos-municipios.md new file mode 100644 index 00000000..dcb42e80 --- /dev/null +++ b/docs/plans/filtros-salvos-municipios.md @@ -0,0 +1,141 @@ +# B18 — Filtros salvos na lista de Municípios (+ acesso rápido no sidebar) + +Status: rascunho +Atualizado em: 2026-07-24 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Demais itens abertos, B18; superfície de coordenação) +Impeccable: B — encaixe em `MunicipalityFilters` / barra slim + `CampaignSidebar` (2º nível sob Municípios); sem rota nova +Appetite: ~1–1,5 dia eng; persistência local + Salvar com nome + submenu hover/sticky no sidebar; sem migration, sem collection, sem Consent +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 2 — clareza sob pressão — e 8 — Feel the action; exceção justificada ao “Auto-save, no Save button” porque nomear é confirmatório) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · shells `CampaignSidebar`, `MunicipalityFilters`, primitivos `SidebarMenuSub*` já em `src/components/ui/Sidebar.tsx`. + +Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (só salvar + submenu; sem redesign da nav completa nem sync multi-device). + +Brief compacto: + +- **Persona / contexto:** Alex (CG / Assessor / Candidato) monta o mesmo recorte várias vezes na semana (ex. “Prioritárias sem assessor · TI X · sort votos”) e precisa voltar a ele em um toque a partir da nav — sem refazer selects. +- **Job principal:** nomear o estado atual da URL da lista e reabri-lo como atalho de 2º nível sob **Municípios** no sidebar. +- **Estratégia de cor:** Restrained — submenu indentado com `border-l` do `SidebarMenuSub`; active sóbrio (`data-active`); sem chips coloridos de “views”. +- **Edit where you see:** não — bookmark de navegação URL; mutações B9 nas células seguem iguais. +- **Anti-goals:** collection/`campaignUser` prefs; saved views genéricas em todas as listas; segundo nível sempre visível (polui nav); sticky em qualquer `/campanha/municipios` (só no filtro salvo ativo); auto-gravar sem nome (vira “Visitados”). + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Não — **Dados: N/A**. Este item não inventa métrica; só persiste e reabre o **mesmo** recorte URL que a lista já resolve. +- **Decisões desbloqueadas:** Staff: “voltar ao recorte mental desta sessão (ex. prioritárias / TI / sem assessor) sem remontar filtros.” +- **Forma escolhida:** **lista/tabela existente** + links nomeados no sidebar — **por quê:** o dado já está na lista; o problema é atrito de remontagem. **Rejeitado:** dashboard de “minhas views”; chart de uso de filtros; segunda página de gestão de views. +- **Profile:** N/A. +- **Anti-goals de dado:** sem KPI de “quantos filtros salvos”; sem % estadual. + +Self-check dados: N/A (sem superfície de métrica nova). + +## Contexto + +Em `/campanha/municipios`, o estado canônico de recorte/ordem vive na URL via `MunicipalityListState` (`src/utilities/municipalityUi.ts`: `q`, `region`, `kind`, `coverage`, `priority`, `trend`, `sort`/`dir`, `page`, `compare`). Helpers já canonicalizam href (`resolveMunicipalityListUrl` / `buildMunicipalityListHref` / `buildMunicipalityListVisitHref`). **B15** ✓ sort; **B16** relocaciona filtros ao header e deixa barra slim; **B17** preferência de colunas em `localStorage`. + +O sidebar (`CampaignSidebar.tsx` + `nav.ts`) é **flat** — um `SidebarMenuButton` por item; primitivos `SidebarMenuSub` / `SidebarMenuSubButton` existem no shadcn local e **não são usados**. “Visitados recentemente” (`recentVisits.ts`) já prova o padrão client-only + limpeza no logout, mas é histórico automático — não nomeado. + +Pedido de produto (2026-07-24): **salvar** o filtro atual com um **nome**, e expor os salvos como **acesso rápido de 2º nível** sob Municípios: aparecem no **hover** (desktop) ou **clique/expansão** (mobile no drawer do sidebar); **permanecem visíveis** quando a URL atual é a de um filtro salvo; **somem** fora de Municípios e sem hover/expansão. + +FD2 ([field-desk-ux-pos-critique.md](field-desk-ux-pos-critique.md)) adiava “saved views genéricas”; este item é a fatia **concreta e única** (só lista de municípios), não um framework de views. + +## Objetivos + +- Staff em `/campanha/municipios`: affordance **Salvar filtro** (nome obrigatório) que grava o estado canônico atual da URL (sem `page`, sem `compare`). +- Lista de filtros salvos no **sidebar**, indentada sob **Municípios**, cada um linkando ao href canônico; item ativo quando a URL atual casa com o href salvo. +- Visibilidade do submenu: + - **mostrar** no hover do item Municípios (desktop); + - **mostrar** ao expandir Municípios no drawer mobile (clique); + - **mostrar (sticky)** quando a página atual é um filtro salvo (match de href); + - **esconder** em rotas que não são de municípios **e** sem hover/expansão. +- CRUD mínimo: criar + abrir + apagar (renomear = questão em aberto). +- Guardrails: sem migration, sem collection, sem Consent, sem server action de escrita; `leader` sem a página; access/loader inalterados; Feel the action no Salvar (pending no controle) e na navegação do atalho (`CampaignListPending` já cobre a lista). + +## Decisões travadas + +- **Item de trilha B18 (não fill-in; não fase de B16/B17; não absorver em Visitados).** Job distinto: bookmark **nomeado e intencional** vs histórico automático (Visitados) vs viewport de colunas (B17) vs relocação de selects (B16). Appetite ~1–1,5d; cortável. (2026-07-24, roadmap-item.) **Rejeitado:** fill-in só (subestima o contrato de submenu sticky/hover); estender `recentVisits` com “pin” (mistura dwell automático com nomeação); saved views genéricas multi-lista (FD2 já vetou; explode escopo). +- **Persistência = `localStorage` (não servidor).** Preferência/atalho por dispositivo; mesmo racional de Visitados e B17. Key estável namespaced (ex. `teqo:campaign:municipality-saved-filters`). **Rejeitado:** collection `savedFilter` / campo em `campaignUser` (migration + access + sync sem evidência de multi-device); `sessionStorage` (perde no ritual diário); cookie server-side. +- **Estado salvo = subset da URL da lista**, canonicalizado via `municipalityUi` (reusar `parseMunicipalityListParams` + href page=1): `q`, `region`, `kind`, `coverage`, `priority`, `trend`, `sort`, `dir`. **Excluir** `page` (sempre reabre na 1) e `compare` (lente do mapa no Início, não da lista). **Excluir** cenário A10 (fora da URL; lente local — alinhado ao fill-in Cenário / decisão A10). **Rejeitado:** gravar `page`/`compare`/cenário; gravar snapshot de IDs de municípios (vira lista morta). +- **Identidade:** `id` client-side (UUID) + `name` (pt-BR, trim, length cap) + `href` canônico (+ opcional `savedAt`). Active = comparação de href canônico com a URL atual (pathname `/campanha/municipios` + search normalizado). **Rejeitado:** rota dedicada `/campanha/municipios/vistas/[id]` (URL nova cara; share pior); dedup só por nome (dois nomes iguais OK se href diferente). +- **UI de salvar = submit explícito com nome** (Dialog/Popover + input + confirmar). Exceção justa ao princípio “Auto-save, no Save button”: nomear é confirmatório. **Rejeitado:** auto-save silencioso a cada mudança de filtro (vira Visitados); prompt nativo `window.prompt`. +- **Sidebar = 2º nível sob Municípios** com `SidebarMenuSub*` existente; hover (desktop) / expand (mobile); sticky só no match de filtro salvo — **não** sticky em `/campanha/municipios` genérico nem em `/campanha/municipios/[slug]`. Bottom nav mobile permanece flat (atalhos vivem no drawer do sidebar). **Rejeitado:** sempre listar salvos sob Municípios (polui); grupo separado no rodapé do sidebar; chips no dashboard. +- **Limite + logout:** `MAX_ENTRIES` curto (recomendação 12); `clearMunicipalitySavedFilters()` no logout junto com `clearRecentVisits()` (dispositivo compartilhado). **Rejeitado:** ilimitado; sync cross-device. +- **i18n e naming:** `MunicipalitySavedFilter`, `municipalitySavedFilters.ts`, `SaveMunicipalityFilterControl`, `MunicipalityNavSavedFilters`; strings “Salvar filtro”, “Filtros salvos”, “Apagar”; identificadores em inglês. + +## Questões em aberto + +- **Permitir salvar a lista “nua” (sem filtros, só sort default)?** **Opções:** A) só com ao menos um param de recorte (`q`/`region`/…) | B) qualquer estado, inclusive nua | C) nua só se `sort`/`dir` ≠ default. **Recomendação:** **A** — alinhado a Visitados (`buildMunicipalityListVisitLabel` retorna null sem filtro); atalho “Municípios” já cobre a lista nua. _(assumido — validar com produto)_ +- **Renomear no v1?** **Opções:** A) só criar/apagar | B) renomear no submenu (inline/Dialog). **Recomendação:** **A** — apagar+recriar basta no appetite; renomear = Adiado. _(assumido)_ +- **Onde mora o botão Salvar?** **Opções:** A) barra slim de `MunicipalityFilters` (ao lado de Limpar / Colunas) | B) menu “…” da lista | C) só no overview. **Recomendação:** **A** — mesmo lugar mental dos filtros; soft com B16/B17. _(assumido)_ +- **Dois salvos com o mesmo href: atualizar o existente ou criar duplicata?** **Opções:** A) upsert (pede confirmação se nome diferente) | B) sempre criar | C) bloquear com mensagem. **Recomendação:** **A** — um atalho por href; evita submenu duplicado. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + List["/campanha/municipios?…\nMunicipalityListState"] + Save["SaveMunicipalityFilterControl\nnome + confirmar"] + Store["municipalitySavedFilters.ts\nlocalStorage"] + Nav["CampaignSidebar\nMunicípios + SidebarMenuSub"] + Match["href canônico == URL atual\n→ sticky open + active"] + + List --> Save + Save --> Store + Store --> Nav + List --> Match + Match --> Nav + Nav -->|Link| List +``` + +Componentes: + +- **`src/utilities/municipalitySavedFilters.ts`** (client-safe, guarda `typeof window`): `STORAGE_KEY`, `MAX_ENTRIES`, tipos, `list` / `upsert` / `remove` / `clear`; validação de shape; no-op fora do browser. Depth: espelhar `recentVisits.ts`, não inventar service genérico de “prefs”. +- **Helpers em `municipalityUi.ts`:** `buildMunicipalitySavedFilterHref(state)` (= visit href / page=1 sem compare) + `municipalityListHasSavableFilters(state)` (gate do botão). Reusar parse/canonical já existentes. +- **`SaveMunicipalityFilterControl`** (client, em `src/components/campaign/`): Popover/Dialog com nome; desabilitado se não há filtro salvável ou se já é o ativo sem mudanças; pending imediato no confirmar; toast/inline “Salvo” curto; montar na barra slim (`MunicipalityFilters` pós-B16, ou fileira atual se B16 ainda não aterrissou). +- **`MunicipalityNavSavedFilters`** (client island no `CampaignSidebar`): lê storage pós-mount (sem mismatch de hidratação); renderiza `SidebarMenuSub` sob o item Municípios; hover/focus-within (desktop) + estado expandido (mobile); `forceOpen` quando `usePathname`+`useSearchParams` casam com algum `href` salvo; cada linha = `SidebarMenuSubButton asChild` + `Link` + ação apagar (`SidebarMenuAction` ou botão sr-only no hover); fechar drawer mobile ao navegar (mesmo padrão atual). +- **`CampaignSidebar` / item Municípios:** especializar só o item `href === '/campanha/municipios'` (não mudar `nav.ts` para árvore genérica — YAGNI <3 call sites). `isCampaignNavActive` do pai continua por pathname; sub-item ativo por match de search. +- **Logout:** chamar `clearMunicipalitySavedFilters()` ao lado de `clearRecentVisits()`. +- **Sem migration, sem collection, sem server action, sem Consent.** + +## Dependências + +- Soft: **B16** (barra slim = destino natural do Salvar) e **B17** (vizinho na mesma barra) — não bloqueiam; sem eles, Salvar pousa ao lado de Limpar na fileira atual. +- Soft: precedente Visitados ([visitados-recentemente.md](visitados-recentemente.md)) — padrão storage/logout. +- Nenhuma dependência dura de outro plano. Reusa `municipalityUi.ts`, `CampaignSidebar`, `SidebarMenuSub*`, `CampaignListPendingBoundary`. + +## Não escopo + +- Sync multi-device / preferências em `campaignUser` — exigiria migration + access. +- Saved views em Planos / Apoiadores / Demandas — FD2 “genéricas”; só Municípios aqui. +- Sticky do submenu em qualquer rota `/campanha/municipios*` — só match de filtro salvo. +- Incluir Cenário A10 ou `compare` do mapa no bookmark. +- Substituir Visitados recentemente / B14 município mais próximo. +- Reorder DnD de colunas — [fora de escopo](reordenar-colunas-lista-municipios.md). + +## Rabbit holes + +- **Framework de “nav tree” / collapsible genérico no `nav.ts`.** Se alguém “só preparar” para outros itens: explode o shell. **Mitigação:** especializar só Municípios neste item; 3º call site = gatilho de abstrair. +- **Rota `/vistas/[id]` + loader server.** Se alguém “só para deep-link nomeado”: migration mental + share frágil. **Mitigação:** href = query canônica da lista. +- **Optimistic list sem refresh ao abrir atalho.** Anti-goal Feel the action. **Mitigação:** `Link` + pending da região de resultados existente. +- **Spreadsheet / painel de gestão de views.** **Mitigação:** criar/apagar no fluxo; sem página admin de views. + +## Adiado com gatilho + +- **Renomear filtro salvo.** Revisitar quando: ≥2 atores pedirem em R6/sessão **ou** apagar+recriar gerar atrito medido. +- **Sync servidor / share de view entre assessores.** Revisitar quando: evidência de multi-device **e** pedido explícito de “mandar este recorte” (aí sim collection + access; não neste appetite). +- **Salvar também o viewport B17 (colunas visíveis) junto do filtro.** Revisitar quando: B17 estável **e** usuários pedirem “a view completa” (hoje misturaria bookmark de decisão com preferência de densidade). + +## Referências + +- `docs/roadmap.md` (Demais itens abertos · grafo · Janela 1–2 · cortes) +- `docs/plans/visitados-recentemente.md` — precedente localStorage + logout +- `docs/plans/filtros-no-header-lista-municipios.md` (B16) · `docs/plans/seletor-colunas-lista-municipios.md` (B17) +- `docs/plans/field-desk-ux-pos-critique.md` — “saved views genéricas” adiadas; este item é a fatia concreta +- `src/utilities/municipalityUi.ts` — estado URL / href canônico / visit label +- `src/utilities/recentVisits.ts` — espelho de storage +- `src/components/campaign/CampaignSidebar.tsx`, `nav.ts`, `MunicipalityFilters.tsx` +- `src/components/ui/Sidebar.tsx` — `SidebarMenuSub*` +- AGENTS.md — naming EN / copy pt-BR; campaign auth; sem Consent para preferência local do próprio usuário +- `PRODUCT.md` / `DESIGN.md` — Field Desk; Feel the action; Auto-save (exceção nomear) diff --git a/docs/plans/ordenacao-colunas-lista-municipios.md b/docs/plans/ordenacao-colunas-lista-municipios.md index 18b8fae6..e984651d 100644 --- a/docs/plans/ordenacao-colunas-lista-municipios.md +++ b/docs/plans/ordenacao-colunas-lista-municipios.md @@ -96,7 +96,7 @@ Componentes: ## Dependências - Nenhuma dura de outro item aberto. Reusa `campaignListUrl.ts`, bundle A9+ (`loadMunicipalityScope` / lista), B9 (células editáveis coexistindo), pending do hardening. -**Suaves (consumidores):** **A11 ✓** já adicionou keys `name`|`votos` + coluna/leitura sobre este contrato (2026-07-24); **B15** amplia as keys restantes (`region`, `kind`, `trend`, `expectedVotes`, `lastUpdateAt`, `coverage`) e unifica headers. **E9** depois adiciona `deficit` / risco-frescor na fila. + **Suaves (consumidores):** **A11 ✓** já adicionou keys `name`|`votos` + coluna/leitura sobre este contrato (2026-07-24); **B15** amplia as keys restantes (`region`, `kind`, `trend`, `expectedVotes`, `lastUpdateAt`, `coverage`) e unifica headers. **B16** ([filtros-no-header-lista-municipios.md](filtros-no-header-lista-municipios.md)) compõe filtro URL no mesmo `TableHead` sem alterar `sort`/`dir`. **E9** depois adiciona `deficit` / risco-frescor na fila. ## Não escopo @@ -117,6 +117,7 @@ Componentes: - **Coluna Assessores sortável (por contagem ou nome do 1º).** Revisitar quando: pedido explícito da mesa ou A11/E9 exigirem desempate por rede. - **Select "Ordenar por" no desktop** além do header. Revisitar quando: discovery mostrar que o header não é descoberto (improvável) ou A11 quiser atalho sem coluna ainda visível. +- **Reorder / resize de colunas (drag-and-drop).** Avaliado 2026-07-24 → **fora de escopo** com gatilho — [reordenar-colunas-lista-municipios.md](reordenar-colunas-lista-municipios.md). Não reabrir como B17 sem o gatilho desse plano. - **Shared `SortableTableHead` em `components/ui`.** Revisitar quando: 2ª lista de `/campanha` pedir o mesmo padrão. ## Referências diff --git a/docs/plans/reordenar-colunas-lista-municipios.md b/docs/plans/reordenar-colunas-lista-municipios.md new file mode 100644 index 00000000..1c6cf6e4 --- /dev/null +++ b/docs/plans/reordenar-colunas-lista-municipios.md @@ -0,0 +1,116 @@ +# Reordenar colunas da lista de Municípios (drag-and-drop) + +Status: fora de escopo (avaliado — não fazer agora) +Atualizado em: 2026-07-24 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fora de escopo — avaliação de produto/UX) +Impeccable: B — se revisitado: encaixe em `MunicipalityList` / headers desktop de `/campanha/municipios` (não mobile cards) +Appetite: ~1–1,5 dia eng **se** o gatilho disparar (DnD no header + persistência local + a11y); **0 eng agora** — só registro da avaliação +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípio 3 — Edit where you see / anti spreadsheet; princípio 6 — inteligência, não planilha chique) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'`. + +Na implementação (`implement-roadmap-item`), **só se o gatilho disparar:** craft compacto → critique → polish. Shape só se a affordance de drag vs sort+filtro (B15/B16) ainda estiver ambígua. + +Brief compacto (para revisitação): + +- **Persona / contexto:** Assessor / CG na tabela densa desktop; quer colunas “de decisão” (ex. 2022, cobertura, frescor) mais à esquerda sem mudar o sort da lista. +- **Job principal:** arrastar o header para mudar a **ordem visual** das colunas (não a ordem das linhas). +- **Estratégia de cor:** Restrained — affordance de grip sóbria; sem glow/pill de “modo edição”. +- **Edit where you see:** não — só layout; B9 Popovers e mutações intactos. +- **Anti-goals:** spreadsheet / data-grid mode; TanStack Table como plataforma; resize de colunas; hide/show de colunas neste item; DnD em cards mobile. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Não — **Dados: N/A**. Não cria métrica, série nem ranking; só permuta a ordem visual de colunas já existentes. +- **Decisões desbloqueadas:** Nenhuma decisão eleitoral nova. No máximo preferência de leitura (conforto visual) — insuficiente sozinha para priorizar eng agora. +- **Forma escolhida (se revisitado):** mesma **tabela/lista**; ordem de colunas = preferência local. **Rejeitado:** chart; segunda “view” custom; URL `?cols=` compartilhada (surpresa entre atores; não é estado de decisão). +- **Anti-goals de dado:** N/A (sem métrica nova). + +## Contexto + +Em `/campanha/municipios`, `MunicipalityList` (`src/components/campaign/MunicipalityList.tsx`) é Server Component com tabela desktop shadcn (`Table` / `TableHead`) e cards no mobile. Colunas staff atuais (ordem fixa no JSX): Município · TI · Tipo · 2022 (`votos`) · Assessores (B9) · Tendência (B9) · Votos estimados (B9) · Última atualização · Cobertura. + +**B15 ✓** ([ordenacao-colunas-lista-municipios.md](ordenacao-colunas-lista-municipios.md)) já entrega **reordenar as linhas** via clique no header (`?sort=`/`?dir=`). O rabbit hole explícito de B15: _não_ puxar TanStack / reorder de colunas / resize. + +**B16** ([filtros-no-header-lista-municipios.md](filtros-no-header-lista-municipios.md)) coloca filtro URL no mesmo `TableHead` ao lado do sort — headers ficam mais densos; Não escopo de B16 já lista “Lib de data-grid / reorder/resize de colunas”. + +Pedido de produto (2026-07-24): **avaliar** drag-hold-drop para reorganizar colunas. Não há evidência em `CUSTOMER.md` / sessão do CG pedindo ordem de colunas customizável; o job da mesa é “quem no topo / o que atacar”, já coberto por sort + filtros + (em breve) fila E9. + +Não há `@dnd-kit` (nem equivalente) no `package.json` hoje. + +## Objetivos (só se o gatilho disparar) + +- Desktop: drag no handle do header reordena colunas; clique no rótulo/chevron continua sort (B15); filtro no header (B16) não inicia drag. +- Persistência **local** da ordem (ex. `localStorage` keyed por role/vista); default = ordem canônica do código; botão “Restaurar ordem”. +- Mobile: sem reorder (cards); ordem canônica. +- Guardrails: sem migration, sem collection, sem Consent, sem server action; não mudar `MunicipalityListState` URL; `leader` não vê a tabela staff. + +## Decisões travadas + +- **Agora: fora de escopo — não implementar.** Avaliação 2026-07-24: custo/benefício ruim na Janela 1 (convenções → 05/08) frente a B16, E8, R6, onboarding. Preferência de _ordem_ visual ≠ decisão de alocação. **Rejeitado:** abrir item de trilha **só para DnD** agora (compete com path crítico de UX da lista e com inteligência); fill-in “barato” sem nomear o conflito gestual (subestima ~1–1,5d + a11y); absorver em R6 (R6 é critique/polish, não feature DnD). **Nota (mesmo dia):** o ID **B17** foi atribuído depois ao [seletor mostrar/ocultar colunas](seletor-colunas-lista-municipios.md) — job distinto (viewport densidade), sem DnD; este plano de reorder permanece fora de escopo. +- **Se revisitado: preferência local, não URL.** Ordem de colunas é preferência de ator/dispositivo; URL permanece filtro+sort. **Rejeitado:** `?cols=a,b,c` (share/back confusos; não é estado de decisão compartilhada); persistir em `campaignUser` (migration cara para preferência cosmética). +- **Se revisitado: handle de drag ≠ alvo de sort/filtro.** Grip explícito (ou long-press no header) separado do `CampaignTransitionAnchor` de sort e do controle de filtro B16. **Rejeitado:** arrastar o rótulo inteiro (colide com clique de sort); pointer-events no filtro iniciando drag. +- **Coluna Município (nome) fixa à esquerda (v1).** Identidade da linha não some no scroll horizontal. **Rejeitado:** todas as colunas permutáveis sem âncora (desorientação). +- **i18n e naming:** identificadores EN (`MunicipalityColumnOrder`, `columnOrderKeys`); strings pt-BR (“Restaurar ordem das colunas”, aria do grip). + +## Questões em aberto (para quando o gatilho disparar) + +- **Lib DnD?** **Opções:** A) `@dnd-kit/sortable` | B) HTML5 drag nativo | C) reordenar só via menu “Mover coluna” (sem drag). **Recomendação:** **C** primeiro se o pedido for “customizar ordem” sem gesto Excel; **A** só se discovery insistir em drag. _(assumido)_ +- **Ordem default após E9?** A fila pode ter outro conjunto de colunas — este plano cobre só `/campanha/municipios`; E9 decide o próprio layout. **Recomendação:** não unificar ordem entre lista e fila até 2º call site. + +## Abordagem proposta (somente se revisitado) + +```mermaid +flowchart LR + Canonical["columnOrderKeys canônico\nmunicipalityUi / list module"] + Pref["localStorage preferência"] + Head["TableHeader client island\ngrip + sort + filter"] + Body["TableBody cells na mesma ordem"] + Canonical --> Pref + Pref --> Head + Pref --> Body +``` + +Componentes (depth check): + +- **Preferência + keys** em módulo client-safe junto de `municipalityUi` (ou helper dedicado se crescer) — lista ordenada de keys de coluna; default canônico; parse/serialize; sem Payload. +- **Ilha cliente no header** (hoje a lista é Server Component): ou extrair só o `` + map de células por key, ou um wrapper fino que recebe rows já serializadas. **Não** virar a página inteira client-only. +- **Sem migration / sem server action.** + +## Dependências + +- **Duras (se construir):** **B15 ✓** (sort no header); **B16** entregue ou no mínimo o contrato de composição header (sort + filtro) estável — senão o grip compete com alvos em movimento. +- **Suaves:** E9 (não compartilhar ordem); fill-ins de prioridade/Cenário (não bloqueiam). + +## Não escopo + +- Resize de colunas; pin múltiplo; export CSV da vista; TanStack Table como shell da lista. +- **Hide/show de colunas** — aberto como **B17** ([seletor-colunas-lista-municipios.md](seletor-colunas-lista-municipios.md)); este plano é só _ordem_. +- Reordenar linhas por drag (já é sort B15). +- Preferência sync cross-device / admin Payload. +- Aplicar a mesma feature em apoiadores/lideranças/TI — só no 3º call site. + +## Rabbit holes + +- **Spreadsheet mode.** Drag de coluna + B9 editável + B16 filtro = grade tipo Excel. **Mitigação:** grip só de layout; ban resize/hide; anti-goal PRODUCT. +- **Conflito gestual sort × drag × filtro.** **Mitigação:** alvos separados; sem drag no mobile. +- **Hydration mismatch** (localStorage vs SSR). **Mitigação:** default canônico no SSR; aplicar preferência após mount (flash aceitável) ou `suppressHydrationWarning` localizado no header only. +- **Lib DnD + bundle.** **Mitigação:** preferir menu “Mover” antes de adicionar `@dnd-kit`. + +## Adiado com gatilho + +- **Implementar reorder de colunas.** Revisitar quando **qualquer** de: (1) nota de sessão / R6 / discovery com ≥2 atores da mesa pedindo ordem custom; (2) B16 + **B17** + E9 estáveis e a ordem fixa for citada como atrito medido (não só “seria legal”); (3) produto decidir menu “Mover coluna” sem DnD como polish <0,5d. Até lá: **não** implementar — B17 já cobre densidade via hide/show. + +## Referências + +- `docs/roadmap.md` (Fora de escopo; B15 ✓; B16; **B17** seletor; E9) +- [seletor-colunas-lista-municipios.md](seletor-colunas-lista-municipios.md) — B17 hide/show (job distinto; compartilhará ids de coluna se reorder voltar) +- [ordenacao-colunas-lista-municipios.md](ordenacao-colunas-lista-municipios.md) — B15; rabbit hole reorder/TanStack +- [filtros-no-header-lista-municipios.md](filtros-no-header-lista-municipios.md) — B16; Não escopo data-grid/reorder +- [edicao-rapida-lista-pracas.md](edicao-rapida-lista-pracas.md) — B9; anti spreadsheet +- `src/components/campaign/MunicipalityList.tsx`, `MunicipalitySortableHead.tsx` +- `src/utilities/municipalityUi.ts` +- `PRODUCT.md` — Edit where you see / Intelligence serves organization +- AGENTS.md — naming EN / strings pt-BR; Campaign auth diff --git a/docs/plans/seletor-colunas-lista-municipios.md b/docs/plans/seletor-colunas-lista-municipios.md new file mode 100644 index 00000000..116b4427 --- /dev/null +++ b/docs/plans/seletor-colunas-lista-municipios.md @@ -0,0 +1,129 @@ +# B17 — Seletor de colunas na lista de Municípios + +Status: rascunho +Atualizado em: 2026-07-24 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Demais itens abertos, B17; superfície de coordenação) +Impeccable: B — encaixe em `MunicipalityList` / barra slim de `/campanha/municipios`; sem rota nova +Appetite: ~0,5–1 dia eng; Popover de colunas + persistência local + render condicional desktop; sem migration, sem collection, sem Consent +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 2 — clareza sob pressão — e 8 — Feel the action) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · shells `MunicipalityList`, `CampaignListPendingBoundary`, shadcn `Popover` / `Checkbox` / `Button`. + +Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (só seletor + hide/show; sem redesign da lista/overview nem reorder). + +Brief compacto: + +- **Persona / contexto:** Alex (CG / Assessor / Candidato) na tabela desktop densa (hoje **9** colunas staff: Município, TI, Tipo, 2022, Assessores, Tendência, Votos estimados, Última atualização, Cobertura); olho compete entre eixos e a tela aperta em laptop de campo. +- **Job principal:** ligar/desligar colunas secundárias para ver só o recorte mental da sessão (ex. nome + 2022 + votos estimados) sem perder sort/filtro/URL. +- **Estratégia de cor:** Restrained — botão “Colunas” sóbrio na barra; checkboxes padrão; sem segunda fileira de chips. +- **Edit where you see:** não — seletor é preferência de viewport; células B9 continuam mutáveis nas colunas que permanecerem visíveis. +- **Anti-goals:** spreadsheet / data-grid / TanStack Table; reorder drag-and-drop de colunas; esconder a coluna Município; meter preferência na URL (quebra share); reinventar cards mobile. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Não como métrica nova — **Dados: N/A** para fórmula/KPI. A superfície continua a **tabela/lista** já existente; este item só controla **quais colunas** do viewport desktop aparecem. +- **Decisões desbloqueadas:** Staff: “nesta sessão quero comparar só concentração 2022 × votos estimados × tendência — esconder TI/Tipo/Cobertura para caber na tela.” +- **Forma escolhida:** **tabela / lista** (inalterada) + seletor de visibilidade — **por quê:** o dado já está na tabela; o problema é densidade do viewport. **Rejeitado:** segunda view “compacta” fixa; chart de colunas; export CSV só das colunas visíveis neste item. +- **Profile:** N/A (sem série/mapa novo); granularidade município; ≤435 no filtrado; colunas já carregadas no VM (hide é só UI). +- **Anti-goals de dado:** sem inventar coluna/métrica; sem omitir coluna no loader só porque está oculta (paginação/sort/filtro independem do viewport). + +Self-check dados: N/A (sem superfície de métrica nova). + +## Contexto + +Em `/campanha/municipios`, `MunicipalityList` (`src/components/campaign/MunicipalityList.tsx`) renderiza tabela desktop (`md+`) com headers fixos + cards mobile com subset curado. Estado canônico de **recorte/ordem** vive na URL (`MunicipalityListState` em `municipalityUi.ts`: filtros + B15 `sort`/`dir`). **B16** relocaciona filtros para o `TableHead` e deixa a barra slim (busca + Limpar [+ Cenário]). **E9** (fila de alocação) vai acrescentar colunas derivadas na mesma lista — densidade só aumenta. + +Não há hoje controle de quais colunas aparecem. Pedido de produto (2026-07-24): **avaliar** seletor para ativar/desativar colunas. + +Vizinhos: [B15 ordenação](ordenacao-colunas-lista-municipios.md) ✓ · [B16 filtros no header](filtros-no-header-lista-municipios.md) · [E9 fila](fila-de-alocacao.md) · fill-ins [Cenário](cenario-junto-filtros-municipios.md) / [ícone prioridade](icone-prioridade-lista-municipios.md). + +## Objetivos + +- Desktop (`md+`), staff: controle **Colunas** (Popover) listando as colunas toggable com checkboxes; toggles refletem imediatamente no `Table` (sem RSC round-trip). +- Coluna **Município** (`name`) sempre visível (não aparece como desligável, ou aparece desabilitada). +- Preferência persistida **localmente** no browser (sobrevive reload da mesma máquina); defaults = todas as colunas atuais ligadas. +- Sort/filtro URL intactos: se a coluna ativa de sort/filtro for ocultada, o estado URL permanece (lista continua ordenada/filtrada); affordance de sort some com o header — sem mentir a ordem. +- Mobile (cards): **fora do seletor** — cards já são subset curado; não inventar “headers” em cards. +- Guardrails: sem migration, sem collection, sem Consent, sem server action; `leader` sem a página; access/loader inalterados (`overrideAccess: false`); dados do VM continuam completos (hide ≠ select omit). + +## Decisões travadas + +- **Item de trilha B17 (não fill-in; não só R6; não fase informal de B16).** Pattern de viewport da lista que **E9** consome quando a fila engordar colunas; ~0,5–1d; distinto de sort (B15) e filtro-no-header (B16). (2026-07-24, roadmap-item — avaliação de produto.) **Rejeitado:** fill-in só (subestima o contrato de ids de coluna que E9 precisa respeitar); absorver em B16 (B16 já tem appetite e job diferentes); absorver em E9 (atrasa o quick win de densidade pré-fila e mistura preferência de UI com métricas derivadas); só R6 (atrasa e dilui). +- **Persistência = `localStorage` (não URL).** Preferência de viewport por dispositivo/ator local; não é estado de decisão compartilhado. Key estável namespaced (ex. `campanha:municipality-list:visible-columns`). **Rejeitado:** `?cols=` na URL (polui share/back; compete com sort/filter; B15 já rejeitou cookie/URL para preferência pessoal no sort — aqui URL seria ainda pior); só memória da sessão (perde no ritual diário do CG que reabre a página); cookie server-side / preferência em `campaignUser` (migration + sync multi-device sem evidência). +- **IDs de coluna estáveis em inglês**, alinhados às sort keys quando existir par: `name` | `region` | `kind` | `votos` | `advisors` | `trend` | `expectedVotes` | `lastUpdateAt` | `coverage`. Labels pt-BR = headers atuais. **Rejeitado:** ids derivados do label pt-BR; TanStack column defs genéricas. +- **`name` obrigatória.** Demais toggable. **Rejeitado:** permitir tabela sem nome (inútil); forçar `votos` sempre on (a âncora A11 é default de sort, não de viewport — sessão pode focar só estimativas). +- **Affordance = botão “Colunas” → Popover + Checkbox** na barra slim (ao lado de busca/Limpar/Cenário quando existirem), não no `TableHead`. **Rejeitado:** DropdownMenu novo (não há no shadcn do repo — Popover+Checkbox bastam); menu por coluna (descuberta pior); TanStack Table ColumnVisibility API. +- **Hide é só render** — loader/VM/paginação/sort inalterados. **Rejeitado:** omitir campos no `select` Payload conforme colunas (acopla preferência client a query; quebra sort em coluna oculta). +- **i18n e naming:** `MunicipalityColumnId`, `MunicipalityColumnVisibility`, `MunicipalityColumnPicker`; strings “Colunas”, “Mostrar colunas”, labels dos headers em pt-BR. + +## Questões em aberto + +- **Default inicial: todas ligadas vs. preset “mesa” (nome + 2022 + votos estimados + tendência)?** **Opções:** A) todas on (zero surpresa) | B) preset compacto | C) preset compacto só na 1ª visita com CTA “Restaurar todas”. **Recomendação:** **A** — preferência começa igual ao hoje; quem apertar a tela desliga. _(assumido — validar com produto)_ +- **Coluna oculta que é o `sort` ativo: manter chevron invisível ou forçar reexibir a coluna?** **Opções:** A) ocultar header; sort URL permanece; live region/`sortSummary` já anuncia a ordem | B) ao ocultar coluna sorted, resetar sort para `name` | C) impedir uncheck da coluna sorted. **Recomendação:** **A** — sortSummary já existe; não mutar URL por preferência de viewport. _(assumido)_ +- **Aterrar antes ou depois de B16?** **Opções:** A) B17 depois de B16 (barra slim já definida) | B) B17 agora na fileira atual | C) mesmo PR. **Recomendação:** **A** ou **C** se o implementador pegar os dois — o botão Colunas mora na slim bar que B16 cria; sem B16, pousa ao lado de Limpar na fileira atual. Soft dep, não dura. + +## Abordagem proposta + +```mermaid +flowchart LR + Picker["MunicipalityColumnPicker\nPopover + Checkbox"] + Storage["localStorage\nvisible column ids"] + List["MunicipalityList\ncondicional por id"] + URL["URL sort/filter\nintacta"] + + Picker --> Storage + Storage --> List + URL --> List +``` + +Componentes: + +- **`MunicipalityColumnId` + defaults/labels** (em `src/utilities/municipalityUi.ts` ou módulo irmão `municipalityListColumns.ts` se o arquivo de UI já estiver gordo): união das colunas desktop staff; `MANDATORY_MUNICIPALITY_LIST_COLUMNS = ['name']`; labels reusando headers / `municipalityListSortLabels` onde couber (`advisors` sem sort key). +- **`useMunicipalityColumnVisibility`** (hook client, `src/components/campaign/…` ou colado no picker): lê/escreve `localStorage` com parse fail-closed → default todas on; `toggle(id)` ignora mandatory; SSR-safe (default all-on até hydrate — flash mínimo aceitável, ou `useSyncExternalStore`). +- **`MunicipalityColumnPicker`** (`'use client'`): `Button` “Colunas” + `Popover` com lista de `Checkbox` + label; `aria-label` “Mostrar ou ocultar colunas”; estado imediato no controle (Feel the action — sem pending de RSC). +- **`MunicipalityList`**: receber `visibleColumns` via island wrapper **ou** tornar o trecho da tabela um client child fino que consome o hook — preferir **wrapper client só da tabela desktop** (`MunicipalityListTable`) para a lista RSC continuar a montar props; cards mobile inalterados. Condicionar cada `TableHead`/`TableCell` ao set visível. +- **Barra**: plugar o picker em `MunicipalityFilters` (slim pós-B16) ou slot explícito na page ao lado dos filtros — depth: reusar a barra, não inventar segunda toolbar. +- **Migration**: Sem migration, sem collection, sem server action. + +Depth check: reusa `Popover`/`Checkbox`/`Button` e keys de `municipalityUi`; sem lib de tabela; sem preferência server-side. + +## Dependências + +- **Suaves:** B15 ✓ (ids alinhados a sort keys); B16 (destino da barra slim — soft). Nenhuma dura de outro plano aberto. +- **Dependentes suaves:** **E9** (fila) deve reusar os mesmos `MunicipalityColumnId` ao acrescentar colunas derivadas (novas ids entram no picker com default on). + +## Não escopo + +- Reordenação / resize / pin de colunas (spreadsheet) — reorder DnD avaliado e **fora de escopo**: [reordenar-colunas-lista-municipios.md](reordenar-colunas-lista-municipios.md). +- Preferência syncada em `campaignUser` / multi-device. +- Seletor nas listas de apoiadores / lideranças / planos (fill-in sob demanda no 2º call site). +- Alterar o subset dos cards mobile. +- Omitir campos no loader/VM; export CSV filtrado por colunas visíveis. +- Colunas novas da fila E9 (só o contrato de ids para quando chegarem). + +## Rabbit holes + +- **TanStack Table / data-grid.** Explode B9 + RSC + pending. **Mitigação:** hide condicional no Table shadcn atual. +- **URL `?cols=`.** Polui share e compete com B15/B16. **Mitigação:** localStorage only. +- **Shared ColumnVisibility service genérico.** Classitis com 1 call site. **Mitigação:** módulo da lista de municípios; extrair no 3º consumidor. +- **Hydration mismatch agressivo.** **Mitigação:** default all-on no SSR + sync no client; ou `useSyncExternalStore` com `getServerSnapshot` = all-on. + +## Adiado com gatilho + +- **Preset “mesa” compacto como default.** Revisitar quando: evidência de campo (onboarding / R6) de que 9 colunas atrapalham na 1ª visita **e** o seletor não é descoberto. +- **Preferência por `campaignUser` (server).** Revisitar quando: o mesmo ator reclamar em 2+ dispositivos **e** houver appetite para migration de preferências UI. +- **Picker em outras listas `/campanha`.** Revisitar quando: 2º call site real (não especulativo). +- **Ids/novas colunas de E9 no picker.** Entram no item E9 (default on); este item só deixa o contrato pronto. + +## Referências + +- `docs/roadmap.md` (Demais itens abertos · B17; grafo; cortes) +- `src/components/campaign/MunicipalityList.tsx` — headers/células desktop a condicionar +- `src/utilities/municipalityUi.ts` — sort keys / labels / state URL (não misturar visibility na URL) +- `src/components/campaign/MunicipalityFilters.tsx` — destino do botão na barra +- `src/components/ui/Popover.tsx`, `Checkbox.tsx`, `button.tsx` +- Planos vizinhos: [ordenacao-colunas-lista-municipios.md](ordenacao-colunas-lista-municipios.md), [filtros-no-header-lista-municipios.md](filtros-no-header-lista-municipios.md), [fila-de-alocacao.md](fila-de-alocacao.md), [reordenar-colunas-lista-municipios.md](reordenar-colunas-lista-municipios.md) (fora de escopo — ordem ≠ visibilidade) +- AGENTS.md — naming EN / strings pt-BR; Campaign auth staff-only +- `PRODUCT.md` / `DESIGN.md` — Field Desk, clareza sob pressão, anti spreadsheet diff --git a/docs/roadmap.md b/docs/roadmap.md index 5a32034c..f3c277ca 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-24 (A11 ✓ posição em votos + sort=votos; B15 ✓ ordenação por coluna na lista; fill-in ícone+tooltip de prioridade na lista — [plano](plans/icone-prioridade-lista-municipios.md); fill-in Cenário junto aos filtros — [plano](plans/cenario-junto-filtros-municipios.md); E4R ✓; E17 ✓; B14; janela 1: smoke, R6, E8) +Atualizado em: 2026-07-24 (B18 filtros salvos + atalho sidebar — [plano](plans/filtros-salvos-municipios.md); B17; B16; reorder DnD fora de escopo; A11 ✓; B15 ✓; fill-ins; E4R ✓; E17 ✓; B14; janela 1: smoke, R6, E8) Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). ## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026) @@ -84,11 +84,14 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · absorve os débitos de produto/UX remanescentes de FD2 ([field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md)): glossário inline (O3 — hipótese ainda sem evidência), triagem em lote, empty states de coordenador · gatilho: antes de 16/08 - **B14** município mais próximo (geolocalização → atalho no Início staff) · pede permissão **1× por sessão** se ainda não concedida; matching client-side sobre centroides IBGE; Salvador multi-zona → lista filtrada até B8 F2 · ~1d · Janela 1–2 · sem deps duras · cortável · [plano](plans/municipio-mais-proximo.md) - ~~**B15** ordenar lista de municípios pelo header da coluna~~ — **entregue 2026-07-24** (`?sort=`/`?dir=` na URL + clique no header (desktop) / select compacto (mobile); ordenação global no filtrado; **A11** consome o contrato (key `votos`)) · [plano](plans/ordenacao-colunas-lista-municipios.md) +- **B16** filtros no header das colunas da lista de municípios — relocaciona `region`/`kind`/`trend`/`coverage`/`priority` para o `TableHead` (desktop) ao lado do sort B15; barra slim (busca + Limpar [+ Cenário]); mobile mantém disclosure · ~1–1,5d · Janela 1–2 · soft: B15 ✓ · cortável · [plano](plans/filtros-no-header-lista-municipios.md) +- **B17** seletor de colunas (mostrar/ocultar) na lista de municípios — Popover “Colunas” + `localStorage`; desktop only; `name` obrigatória; soft: B15 ✓ (ids) / B16 (barra slim); prepara viewport para **E9** · ~0,5–1d · Janela 1–2 · cortável · [plano](plans/seletor-colunas-lista-municipios.md) +- **B18** filtros salvos na lista de municípios — nomear o estado URL atual (`localStorage`); acesso rápido de 2º nível sob Municípios no sidebar (hover desktop / expand mobile; sticky só no filtro salvo ativo) · ~1–1,5d · Janela 1–2 · soft: B16/B17 (barra) · cortável · [plano](plans/filtros-salvos-municipios.md) ### Fill-ins abertos - **Ícone de prioridade na lista** em `/campanha/municipios` — trocar Badge “Prioritária” por ícone Flag + tooltip “Município prioritário” (hover) na coluna do nome · ~0,25d · Impeccable B · [plano](plans/icone-prioridade-lista-municipios.md) -- **Cenário junto aos filtros** em `/campanha/municipios` — mover `VoteEstimateScenarioField` para a fileira de `MunicipalityFilters`; overview só consome o contexto · ~0,25–0,5d · Impeccable B · [plano](plans/cenario-junto-filtros-municipios.md) +- **Cenário junto aos filtros** em `/campanha/municipios` — mover `VoteEstimateScenarioField` para a fileira/barra slim de `MunicipalityFilters` (com **B16**, a barra deixa de carregar os selects de coluna); overview só consome o contexto · ~0,25–0,5d · Impeccable B · soft: B16 · [plano](plans/cenario-junto-filtros-municipios.md) - **O0+** escala/DRY pós-Onda 0 · [plano](plans/escala-dry-pos-onda0.md) - **RS+** auth read leve + shells de senha · [plano](plans/escala-dry-pos-reset-senha-perfil.md) - **C10 / C11** escala apoiadores/planos · reavaliar nas superfícies remodeladas · gatilhos originais intactos: base nominal crescendo ([plano](plans/escala-dry-pos-c9.md)) / volume de planos medido ([plano](plans/escala-dry-pos-c7.md)) @@ -117,7 +120,10 @@ flowchart TD D4 -.thread.-> D5 B8F2["B8 F2 Polígonos
zonas de Salvador"] B14n["B14 Município mais próximo"] - B15n["B15 Ordenar lista
por coluna"] + B15n["B15 ✓ Ordenar lista
por coluna"] + B16n["B16 Filtros no header
da lista"] + B17n["B17 Seletor de colunas
da lista"] + B18n["B18 Filtros salvos
+ atalho sidebar"] E4R["E4R ✓ Import planilha
(seed estratégia)"] A11n["A11 ✓ Posição em votos"] E17n["E17 ✓ Tabela TI no Início"] @@ -138,7 +144,14 @@ flowchart TD E4R -.metas seedadas.-> E8i B15n -.contrato sort/dir.-> A11n + B15n -.header sort.-> B16n + B15n -.ids de coluna.-> B17n + B16n -.barra slim.-> B17n + B16n -.barra slim.-> B18n + B17n -.vizinho barra.-> B18n A11n -.coluna/ordenação.-> E9i + B16n -.pattern header.-> E9i + B17n -.viewport colunas.-> E9i E17n -.primeira fatia.-> E12i E8i --> E9i --> E11i C12i --> E11i @@ -160,11 +173,11 @@ flowchart TD B8F2 -.ZE Salvador.-> B14n ``` -Paralelizáveis agora: **E8** (A10 ✓ e remodelagem em produção), **C12** (paralelo a E8), **E16** (compõe o existente), **B14** (atalho geo no Início; soft B8 F2 só para ZE Salvador), fill-ins (ícone de prioridade na lista, Cenário junto aos filtros, O0+, RS+). **D3** só após smoke + folga e só se o fluxo sede-digita (C12) não absorver os deltas do ZAP (não compete com E8/E9). +Paralelizáveis agora: **E8** (A10 ✓ e remodelagem em produção), **C12** (paralelo a E8), **E16** (compõe o existente), **B14** (atalho geo no Início; soft B8 F2 só para ZE Salvador), **B16** (filtros no header; soft B15 ✓ já entregue), **B17** (seletor de colunas; soft B15/B16 — pousa na barra atual ou slim), **B18** (filtros salvos + submenu Municípios; soft B16/B17 — botão na barra), fill-ins (ícone de prioridade na lista, Cenário junto aos filtros — encaixa na barra slim de B16 —, O0+, RS+). **D3** só após smoke + folga e só se o fluxo sede-digita (C12) não absorver os deltas do ZAP (não compete com E8/E9). ### Sequência por janela (só pendentes) -**Janela 1 — agora → 05/08 (convenções):** smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + default `sort=votos`) entregues 2026-07-24; gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E8** pode começar em paralelo; **B14** (atalho geo) se sobrar folga de campo no onboarding; Onda 0 jurídica em paralelo (externa). +**Janela 1 — agora → 05/08 (convenções):** smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + default `sort=votos`) entregues 2026-07-24; **B16** (filtros no header) / **B17** (seletor de colunas) / **B18** (filtros salvos + atalho sidebar) se folga de UX da lista no onboarding; gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E8** pode começar em paralelo; **B14** (atalho geo) se sobrar folga de campo no onboarding; Onda 0 jurídica em paralelo (externa). **Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; **E8** conta da cadeira → **E9** fila de alocação, com **C12** registro-fundação em paralelo (migrations cedo, longe do congelamento); **E16** dossiê do município (pedido O6 — compõe o existente, melhora com E8); D2 se sobrar folga. @@ -176,7 +189,7 @@ Paralelizáveis agora: **E8** (A10 ✓ e remodelagem em produção), **C12** (pa **Não cortáveis:** Onda 0 (jurídico/Consent); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); C2 dados reais; assimetria declarado×estimado (relação de campo); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha). -**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** níveis (manter `priority` alta/normal); **B8 F2** polígonos (mapa continua agregado no município; manter F1 bairros); **D2** push (adiar); **A6**; **B14** município mais próximo (lista/busca e Recentes continuam); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); **E16** dossiê (último dos extras — pedido explícito de campo); débitos/fill-ins. +**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** níveis (manter `priority` alta/normal); **B8 F2** polígonos (mapa continua agregado no município; manter F1 bairros); **D2** push (adiar); **A6**; **B14** município mais próximo (lista/busca e Recentes continuam); **B18** filtros salvos (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B17** seletor de colunas (tabela completa continua; scroll horizontal); **B16** filtros no header (barra de filtros atual continua a funcionar); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); **E16** dossiê (último dos extras — pedido explícito de campo); débitos/fill-ins. ## Bloqueadores atuais @@ -220,6 +233,7 @@ Paralelizáveis agora: **E8** (A10 ✓ e remodelagem em produção), **C12** (pa - WhatsApp Business API / disparo em massa / blast a apoiadores ou eleitores (Res. TSE 23.610 art. 33; Meta veda WABA político). **Exceto** o programa **D3–D5**: bridge não oficial **1:1 interno** staff↔lideranças já no CRM (ver [whatsapp-interno-campanha.md](plans/whatsapp-interno-campanha.md)) — risco ToS/operacional explícito nos planos; não é substituto de WABA - Previsão estatística de votos neste ciclo - Import **automático/recorrente** de planilhas de projeção como feature de produto (atualização contínua segue via UI). O seed **único** E4R foi aprovado em 2026-07-24 (evidência O5 — a planilha é a fonte de verdade da mesa) — [plano](plans/import-planilha-projecao.md) +- **Reordenar colunas** da tabela de `/campanha/municipios` por drag-and-drop — avaliado 2026-07-24: tecnicamente possível (~1–1,5d), mas **não agora**. Distinto de **B17** (mostrar/ocultar colunas — aberto). B15 já reordena **linhas** (sort no header); B16 densifica o mesmo header com filtros; PRODUCT anti spreadsheet/data-grid; preferência de _ordem_ visual ≠ decisão de alocação; sem evidência de campo. Gatilho: ≥2 atores pedirem em sessão/R6 **ou** B16+B17+E9 estáveis com atrito medido de ordem — [plano](plans/reordenar-colunas-lista-municipios.md) _(fonte: avaliação roadmap-item 2026-07-24; rabbit hole B15 / Não escopo B16/B17)_ ## Fontes