From 248f896d0553f24be73ac497822614763f8f8ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Tue, 21 Jul 2026 12:54:23 -0300 Subject: [PATCH] =?UTF-8?q?Deliver=20auto-applied=20filters=20on=20Pra?= =?UTF-8?q?=C3=A7as=20list=20and=20close=20filtros-auto=20fill-in.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the Buscar step with debounced search, immediate select navigation, and pending UI; update roadmap, plan, and notebook. --- .cursor/rules/projects/nucleos-eleitorais.mdc | 3 +- docs/plans/filtros-auto-pracas.md | 16 ++- docs/roadmap.md | 9 +- src/components/campaign/PlazaFilters.tsx | 102 +++++++++++++----- src/utilities/plazaUi.ts | 5 + tests/unit/plazaUi.unit.spec.ts | 16 +++ 6 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 tests/unit/plazaUi.unit.spec.ts diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 922ed854..d4236e8e 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,7 @@ alwaysApply: false ## Status +- **Fill-in filtros-auto entregue em código (2026-07-21; deploy pendente com remodelagem):** debounce 1s + Enter imediato na busca de `/campanha/pracas`; remove Buscar; pending UI alinhado a `ActionPlanFilters`; `shouldUpdatePlazaSearchUrl` + no-op `buildPlazaFiltersKey`. Plano: [`filtros-auto-pracas.md`](../../../docs/plans/filtros-auto-pracas.md). Débitos pós-`/simplify` (sync back/forward, shell pending) → gatilhos no plano. - **B10 entregue em código (2026-07-21; deploy pendente com remodelagem):** hover/tap no Mapa das Praças — destaque + `MapFeatureReadout`; desktop click navega; mobile 2º tap; SSA/CMS N>1 foca `zoneBreakdown`; `plazasByIbgeCode` / `resolvePlazaMapNavigation`. Plano: [`hover-mapa-pracas.md`](../../../docs/plans/hover-mapa-pracas.md). Débito perf hover O(n) → **B6** [`escala-dry-pos-b3.md`](../../../docs/plans/escala-dry-pos-b3.md). - **B9 entregue em código (2026-07-21; deploy pendente com remodelagem):** edição rápida na lista — Assessores / Tendência / `expectedVotes` via `PlazaList*Control` + `listFormActions`; sem migration. Plano: [`edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md). Débitos `/simplify` → **A9+** F2 + **C8** F4. - **A9 entregue em código (2026-07-21; deploy pendente com remodelagem):** `plaza.expectedVotes` staff-only, fallback `expectedVotes ?? effectiveTotal` em mapa 2026/overview/dashboard, UI `/editar` + leitura lista/detalhe; migration `20260721_133444_add_plaza_expected_votes`. Plano: [`estimativa-votos-praca.md`](../../../docs/plans/estimativa-votos-praca.md). Fill-in pós-`/simplify`: **A9+** [`escala-dry-pos-a9.md`](../../../docs/plans/escala-dry-pos-a9.md) (loader compartilhado + revalidate escopada pós-B9). @@ -123,6 +124,6 @@ alwaysApply: false - Em 2026-07-17 o roadmap foi repriorizado orientado ao calendário eleitoral (Res. TSE 23.760/2026: propaganda a partir de 16/08, 1º turno 04/10), com ordem de execução, trilhas paralelas (A dados eleitorais/território, B superfícies de coordenação, C operação de campo, D plataforma) e grafo de dependências. Decisões de sequenciamento: lançar o MVP antes da migração multi-município; multi-município antes de zonas-por-municipio/eventos/demandas; baseline TSE independente de zonas-por-municipio (dependência rebaixada a suave); WhatsApp Business API movido a fora de escopo (vedação Meta + Res. 23.610 art. 33). - Backlog consolidado do produto (admin RBAC, mapa, eventos, demandas, apoiadores, listas globais, site, white-label): [`docs/roadmap.md`](../../../docs/roadmap.md). - **B7 entregue (2026-07-21):** mapa em `/campanha/pracas` usa `buildPlazaListWhere` (lista/overview/mapa alinhados). Plano: [`docs/plans/mapa-pracas-filtrado.md`](../../../docs/plans/mapa-pracas-filtrado.md). Débito perf triplicado → **A9+**. -- **A9 / B9 / B7 / B10 entregues (2026-07-21; deploy pendente):** ver bullets no topo do Status. Próximos abertos Janela 1: **B11**, **R6**, filtros-auto, deploy+smoke remodelagem; Janela 3: **B6** (setStyle incremental pós-B10), **B8**. +- **A9 / B9 / B7 / B10 / filtros-auto entregues (2026-07-21; deploy pendente):** ver bullets no topo do Status. Próximos abertos Janela 1: **B11**, **R6**, deploy+smoke remodelagem; Janela 3: **B6** (setStyle incremental pós-B10), **B8**. - **B8 registrado (2026-07-21):** polígonos das Praças-zona Salvador/Camaçari. Pesquisa: SSA tem lista oficial TRE-BA zona↔bairro (RA 02/2017); CMS **não** — só locais de votação por ZE 170/171 + curadoria; TSE/TRE não publicam polígonos de ZE. Plano: F1 catálogo+UI na Praça; F2 dissolve malha de bairros (IBGE/GeoSalvador) → TopoJSON. Janela 3; F2 cortável. Plano: [`docs/plans/poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md). - **B11 registrado (2026-07-21):** opção de escala no Mapa das Praças — cor por % dos votos válidos da Praça (0–100% fixos; denominador `electionTally.votosValidos`; 2026 usa válidos 2022); absoluto permanece. Plano: [`docs/plans/escala-percentual-mapa-pracas.md`](../../../docs/plans/escala-percentual-mapa-pracas.md). diff --git a/docs/plans/filtros-auto-pracas.md b/docs/plans/filtros-auto-pracas.md index 3cd3db90..843c378d 100644 --- a/docs/plans/filtros-auto-pracas.md +++ b/docs/plans/filtros-auto-pracas.md @@ -1,12 +1,14 @@ # Filtros auto-aplicados na lista de Praças -Status: rascunho +Status: entregue em código (2026-07-21; deploy pendente com remodelagem) Atualizado em: 2026-07-21 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins) Impeccable: B — encaixe em `PlazaFilters` (`/campanha/pracas`); sem rota nova Appetite: ~0,5 dia eng; só client em `PlazaFilters` (+ pending a11y); sem migration Responsável: — +_Revisão 2026-07-21 (pós-implementação + `/simplify` + capture-review-debts): debounce 1s + Enter imediato + pending UI; `commitNavigation` com `buildPlazaFiltersKey` no-op; `shouldUpdatePlazaSearchUrl` em `plazaUi.ts`; testes `tests/unit/plazaUi.unit.spec.ts`. Débitos S1–S2 registrados em Adiado (não reabrir no simplify)._ + ## Design (Impeccable) Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `CampaignSearchInput`, `PlazaFilters`). @@ -78,7 +80,7 @@ Componentes: ## Dependências - **Dura:** nenhuma de outro plano (R2 já entregue). -- **Suave:** B7 ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)) — debounce aumenta frequência de troca de filtro; mapa filtrado fica mais útil, mas este item funciona com o mapa ainda “all accessible”. +- **Suave:** B7 ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)) ✓ entregue — cada replace de filtro também refresca o mapa filtrado. - Reusa: `plazaUi.ts` (`buildPlazaListHref`, `PlazaListState`), padrão pending de `ActionPlanFilters.tsx`, dwell do `RecentVisitTracker` (já re-arma o timer a cada `href` — debounce 1s + dwell ~2s evita visitar intermediários). ## Não escopo @@ -94,8 +96,18 @@ Componentes: - **Debounce via lib (`use-debounce` etc.).** Dependência nova por 10 linhas. **Mitigação:** `setTimeout` + cleanup. - **Otimistic list client-side.** Duplicaria `buildPlazaListWhere` no browser. **Mitigação:** URL + RSC como hoje. +## Já resolvido no simplify/critique (não reabrir) + +- Alias `plazaSearchParamFromInput` removido; `normalizedText` + `shouldUpdatePlazaSearchUrl` em `plazaUi.ts`. +- No-op de navegação via `buildPlazaFiltersKey` (não dois `buildPlazaListHref`). +- `hasActiveFilters` inclui texto local de busca (Limpar visível antes do debounce). +- `clearDebounce` duplicado no `onSubmit` removido. +- Impeccable `detect.mjs` no alvo: 0 findings (sem critique formal nesta sessão). + ## Adiado com gatilho +- **Sync `search` local ↔ `state.q` no back/forward do browser.** `key={buildPlazaFiltersKey(state)}` remonta na maioria dos casos; edge case: campo pode mostrar texto stale após voltar/avançar. **Gatilho:** QA ou R6 reportar desync em `/campanha/pracas`. +- **Shell compartilhado de pending UI** (`data-pending` + `aria-busy` + `aria-live` "Atualizando resultados…"). Hoje só `ActionPlanFilters` + `PlazaFilters` (`SupporterFilters` ainda sem pending). **Gatilho:** 3ª lista de campanha com o mesmo padrão pending, ou R6/critique citar duplicação em ≥2 superfícies. - **Mesmo padrão em `SupporterFilters` (e outras listas com Buscar).** Revisitar quando produto pedir ou quando R6/critique citar “Buscar” como atrito repetido em ≥2 listas. - **Extrair `useDebouncedSearchParam`.** Revisitar no 3º call site idêntico (Praças + apoiadores + 1). diff --git a/docs/roadmap.md b/docs/roadmap.md index 4126f179..b074919a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-21 (B9 + B10 entregues; débitos B9 → A9+/C8 F4; débitos B10 → B6) +Atualizado em: 2026-07-21 (fill-in filtros-auto entregue em código; B9 + B10 entregues; débitos B9 → A9+/C8 F4; débitos B10 → B6) 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). @@ -81,11 +81,11 @@ flowchart TD C2prod --> C5["C5 GOTV (validar)"] ``` -Paralelizáveis agora: **B11**, fill-ins da lista (filtros-auto, A9+, …). ~~**A9** / **B9** / **B7** / **B10**~~ entregues 2026-07-21. **B6** absorve o hot path de hover pós-B10 (Janela 3 / gatilho de densidade). +Paralelizáveis agora: **B11**, fill-ins da lista (A9+, …). ~~**A9** / **B9** / **B7** / **B10** / **filtros-auto**~~ entregues 2026-07-21. **B6** absorve o hot path de hover pós-B10 (Janela 3 / gatilho de densidade). ### Sequência por janela -**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **B11** escala % dos válidos; R6 critique/polish; Onda 0 jurídica em paralelo. +**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**filtros-auto** lista de Praças~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **B11** escala % dos válidos; R6 critique/polish; Onda 0 jurídica em paralelo. **Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; D2 se sobrar folga. @@ -108,6 +108,7 @@ Paralelizáveis agora: **B11**, fill-ins da lista (filtros-auto, A9+, …). ~~** - **B7 (2026-07-21)** — **Mapa das Praças filtrado pela lista** (`buildPlazaListWhere` em `loadPlazaMapBundle`; `rawSearchParams` na página; empty → omitir painel; int `plazaMapData.int.spec.ts`) — [plano](plans/mapa-pracas-filtrado.md). - **B9 (2026-07-21)** — **Edição rápida na lista de Praças** (Assessores / Tendência / `expectedVotes` via Popovers em `PlazaList*Control`; `listFormActions`; sem migration) — [plano](plans/edicao-rapida-lista-pracas.md). Débitos `/simplify`: **A9+** F2 (revalidate) + **C8** F4 (DRY formActions). - **B10 (2026-07-21)** — **Hover/tap no Mapa das Praças** (destaque + `MapFeatureReadout`; desktop click navega; mobile 2º tap; SSA/CMS N>1 → `zoneBreakdown`; `plazasByIbgeCode` / `resolvePlazaMapNavigation`) — [plano](plans/hover-mapa-pracas.md). Débito perf hover O(n) → **B6** [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md). +- **Fill-in filtros-auto (2026-07-21)** — **Filtros auto-aplicados na lista de Praças** (`PlazaFilters`: debounce 1s no `q`, Enter imediato, selects imediatos, remove Buscar; `useTransition` + pending a11y; `shouldUpdatePlazaSearchUrl` + no-op via `buildPlazaFiltersKey`; sem migration) — [plano](plans/filtros-auto-pracas.md). Débitos pós-`/simplify`: sync back/forward `search`↔`state.q` e shell pending compartilhado — gatilhos no plano. ## Supersedidos pela remodelagem (2026-07-20) @@ -135,7 +136,7 @@ Paralelizáveis agora: **B11**, fill-ins da lista (filtros-auto, A9+, …). ~~** - **C5** operação dia D / GOTV _(validar com produto)_ · design [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) · depende de C2 dados reais - **D2** push + sino in-app · soft: chave `campanha-notificacoes-push` (Onda 0) · [plano](plans/notifications.md) - **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · gatilho: antes de 16/08 · (opcional: mover filtros acima do mapa) -- **Fill-ins:** filtros auto na lista de Praças (debounce 1s no `q`, selects já imediatos; remove Buscar) · [plano](plans/filtros-auto-pracas.md) · **A9+** loader compartilhado lista/mapa + revalidate escopada pós-B9 ([plano](plans/escala-dry-pos-a9.md)) · **C8** F4 DRY `listFormActions`↔`/editar` ([plano](plans/escala-dry-pos-c6.md)) · O0+ ([plano](plans/escala-dry-pos-onda0.md)) · RS+ auth read leve + shells de senha ([plano](plans/escala-dry-pos-reset-senha-perfil.md)) · higiene PascalCase +- **Fill-ins:** **A9+** loader compartilhado lista/mapa + revalidate escopada pós-B9 ([plano](plans/escala-dry-pos-a9.md)) · **C8** F4 DRY `listFormActions`↔`/editar` ([plano](plans/escala-dry-pos-c6.md)) · O0+ ([plano](plans/escala-dry-pos-onda0.md)) · RS+ auth read leve + shells de senha ([plano](plans/escala-dry-pos-reset-senha-perfil.md)) · higiene PascalCase ## Bloqueadores atuais diff --git a/src/components/campaign/PlazaFilters.tsx b/src/components/campaign/PlazaFilters.tsx index 82c75d46..f413020c 100644 --- a/src/components/campaign/PlazaFilters.tsx +++ b/src/components/campaign/PlazaFilters.tsx @@ -1,21 +1,26 @@ 'use client' import { useRouter } from 'next/navigation' -import { useState } from 'react' +import { useEffect, useRef, useState, useTransition } from 'react' import { CampaignSearchInput } from '@/components/campaign/CampaignSearchInput' import { Button } from '@/components/ui/button' import { Field, FieldLabel } from '@/components/ui/field' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' import { bahiaIdentityTerritories } from '@/lib/bahiaTerritories' +import { normalizedText } from '@/utilities/campaignListUrl' import { + buildPlazaFiltersKey, buildPlazaListHref, plazaKindLabels, plazaListCoverageLabels, politicalTrendLabels, + shouldUpdatePlazaSearchUrl, type PlazaListState, } from '@/utilities/plazaUi' +const SEARCH_DEBOUNCE_MS = 1000 + type PlazaFiltersProps = { state: PlazaListState showStaffFilters: boolean @@ -24,31 +29,83 @@ type PlazaFiltersProps = { export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => { const router = useRouter() const [search, setSearch] = useState(state.q ?? '') + const [isPending, startTransition] = useTransition() + const debounceRef = useRef | null>(null) - const navigate = (next: PlazaListState) => { - router.replace(buildPlazaListHref({ ...next, page: 1 }, 1), { scroll: false }) + useEffect( + () => () => { + if (debounceRef.current) clearTimeout(debounceRef.current) + }, + [], + ) + + const clearDebounce = () => { + if (debounceRef.current) { + clearTimeout(debounceRef.current) + debounceRef.current = null + } + } + + const commitNavigation = (patch: Partial) => { + clearDebounce() + const merged: PlazaListState = { + ...state, + ...patch, + page: 1, + q: normalizedText(patch.q !== undefined ? patch.q : search), + } + if (buildPlazaFiltersKey(merged) === buildPlazaFiltersKey({ ...state, page: 1 })) return + + startTransition(() => { + router.replace(buildPlazaListHref(merged, 1), { scroll: false }) + }) + } + + const scheduleSearchNavigation = (value: string) => { + clearDebounce() + if (!shouldUpdatePlazaSearchUrl(value, state.q)) return + + debounceRef.current = setTimeout(() => { + debounceRef.current = null + commitNavigation({ q: value }) + }, SEARCH_DEBOUNCE_MS) } const hasActiveFilters = Boolean( - state.q || state.region || state.kind || state.coverage || state.priority || state.trend, + state.q || + normalizedText(search) || + state.region || + state.kind || + state.coverage || + state.priority || + state.trend, ) return (
{ event.preventDefault() - navigate({ ...state, q: search.trim() || undefined }) + commitNavigation({ q: search }) }} > +

+ {isPending ? 'Atualizando resultados…' : ''} +

setSearch(event.target.value)} + onChange={(event) => { + const value = event.target.value + setSearch(value) + scheduleSearchNavigation(value) + }} /> Território de identidade @@ -56,8 +113,7 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => id="plaza-filter-region" value={state.region ?? ''} onChange={(event) => - navigate({ - ...state, + commitNavigation({ region: (event.target.value || undefined) as PlazaListState['region'], }) } @@ -77,8 +133,7 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => id="plaza-filter-kind" value={state.kind ?? ''} onChange={(event) => - navigate({ - ...state, + commitNavigation({ kind: (event.target.value || undefined) as PlazaListState['kind'], }) } @@ -100,8 +155,7 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => id="plaza-filter-coverage" value={state.coverage ?? ''} onChange={(event) => - navigate({ - ...state, + commitNavigation({ coverage: (event.target.value || undefined) as PlazaListState['coverage'], }) } @@ -125,8 +179,7 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => id="plaza-filter-trend" value={state.trend ?? ''} onChange={(event) => - navigate({ - ...state, + commitNavigation({ trend: (event.target.value || undefined) as PlazaListState['trend'], }) } @@ -148,8 +201,7 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => id="plaza-filter-priority" value={state.priority ?? ''} onChange={(event) => - navigate({ - ...state, + commitNavigation({ priority: event.target.value === 'alta' ? 'alta' : undefined, }) } @@ -161,24 +213,24 @@ export const PlazaFilters = ({ state, showStaffFilters }: PlazaFiltersProps) => ) : null} -
- - {hasActiveFilters ? ( + {hasActiveFilters ? ( +
- ) : null} -
+
+ ) : null}
) diff --git a/src/utilities/plazaUi.ts b/src/utilities/plazaUi.ts index 292e5c17..76b5d780 100644 --- a/src/utilities/plazaUi.ts +++ b/src/utilities/plazaUi.ts @@ -158,6 +158,11 @@ export const buildPlazaFiltersKey = (state: PlazaListState): string => export const buildPlazaListHref = (state: PlazaListState, page: number): string => buildListHref(state, buildPlazaListSearchParams, '/campanha/pracas', page) +export const shouldUpdatePlazaSearchUrl = ( + input: string, + currentQ: string | undefined, +): boolean => normalizedText(input) !== currentQ + export const resolvePlazaListUrl = ( params: PlazaListSearchParams, totalPages?: number, diff --git a/tests/unit/plazaUi.unit.spec.ts b/tests/unit/plazaUi.unit.spec.ts new file mode 100644 index 00000000..a53b8ed6 --- /dev/null +++ b/tests/unit/plazaUi.unit.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' + +import { shouldUpdatePlazaSearchUrl } from '@/utilities/plazaUi' + +describe('shouldUpdatePlazaSearchUrl', () => { + it('returns false when canonical q matches the current URL q', () => { + expect(shouldUpdatePlazaSearchUrl('salvador', 'salvador')).toBe(false) + expect(shouldUpdatePlazaSearchUrl(' salvador ', 'salvador')).toBe(false) + }) + + it('returns true when q differs or is newly set', () => { + expect(shouldUpdatePlazaSearchUrl('salv', 'salvador')).toBe(true) + expect(shouldUpdatePlazaSearchUrl('salvador', undefined)).toBe(true) + expect(shouldUpdatePlazaSearchUrl('', 'salvador')).toBe(true) + }) +})