diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index e14abcd1..c77ac9ef 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -8,6 +8,7 @@ alwaysApply: false ## Status - **MVP Ciclo 1 entregue e enviado em `main` (2026-07-18):** ondas 1–8 (auth/RBAC, domínio+UI de Núcleo/Liderança/Inteligência/Atualizações/Convites, dashboard, hardening) + refactors de composição Next.js, primitivos compartilhados, locks PostgreSQL, fixtures E2E e migration consolidada `20260718_010733_consolidate_campaign_schema`. +- **Ciclo 2 entregue, mesclado e deployado (2026-07-18):** A1 território multi-município/bairro, A3 baseline TSE 2022 (modelo + import), B1 overview da lista, C1 compartilhar página e D1 PWA — detalhes no item datado abaixo. - Fluxos operacionais cobertos: login isolado, CRUD/arquivo de núcleos por slug, designação de coordenador (`geral`), CRM de lideranças, estimativas sugerir/confirmar com versão UUID, feed de atualizações, convites WhatsApp (`wa.me` + hash de uso único), dashboard com agregados só de núcleos ativos. - Pendência operacional (não de código): Consent `lideranca-autopreenchimento` + lote jurídico LGPD antes de dados reais; smoke pós-deploy Vercel. Ver Onda 0 em [`docs/roadmap.md`](../../../docs/roadmap.md). - Em 2026-07-17, a listagem de Núcleos passou a aplicar filtros automaticamente pelo estado da URL, sem botão de confirmação, com comboboxes territoriais estritos compartilhados com o formulário. @@ -27,6 +28,11 @@ alwaysApply: false - Em 2026-07-18, o follow-up Phase 4C fixou o Playwright em dois workers por padrão, adicionou guardas automáticos de erros do navegador/console/5xx, fortaleceu privacidade HTML+Flight por papel e tornou o login por teclado uma interação real com Enter e cookie verificado. - Em 2026-07-18, a projeção do dashboard omite `supportStatus` ausente por redação de acesso e lança invariante genérico se um valor não-vazio inválido chegar à fronteira; agregação e badge são exaustivos no enum. - Em 2026-07-18, os designs UX Pilot dos próximos ciclos foram versionados em `docs/design-refs/latest/` (11 pares `.png`+`.html`) e mapeados aos itens do roadmap — tabela "Referências de design (UX Pilot)" em `docs/roadmap.md`, com seção "Referência visual (UX Pilot)" em cada plano. Decisão: a UX/estrutura dos designs é a referência; a paleta NÃO — os arquivos vieram na paleta antiga (vermelho escuro/navy/dourado) e toda implementação usa os tokens claros do tema `data-theme='campaign'`. Sem design nesta leva: dobradinha, mapa, demandas, detalhe do plano de ação, opt-in de push e fill-ins. +- **Em 2026-07-18, o Ciclo 2 foi mesclado e deployado: A1, A3, B1, C1 e D1.** + - **B1 Overview da lista** — painel agregado entre filtros e lista em `/campanha/nucleos` (`NucleusListOverview`, `loadNucleusListOverviewData`): estimativa de votos confirmada + % + sugestões pendentes (staff), cobertura de coordenador e prévia de 3 atualizações, tudo sobre o conjunto **filtrado** inteiro (`buildNucleusListWhere`, `pagination: false`), oculto quando vazio. `lideranca` vê o overview com select reduzido, sem contagem de sugestões e só as próprias atualizações. + - **C1 Compartilhar núcleo** — `ShareNucleusDialogShell` no header do detalhe; destinatários carregados sob demanda por Server Action de leitura (`loadNucleusShareRecipients`), não no DTO da página. Loader confirma acesso ao núcleo (`overrideAccess: false`) e então lê nome+telefone com `overrideAccess: true` (evita N+1 de field access); lideranças só `engajado`; seção de lideranças omitida para o papel `lideranca`; `canReadCampaignUserPhone` passou a liberar telefone de `geral` para qualquer usuário de campanha. Compartilhar envia só o link — não cria convite nem concede acesso. + - **D1 PWA** — manifest + SW servidos por route handlers sob `/campanha` (script em `campaignPwa.ts`), cache `campanha-` com `skipWaiting`/`claim`, navegação network-first com fallback `/campanha/offline`, RSC/Flight e `/campanha/convite/*` jamais cacheados; ícones em `public/campaign-icons/`; `InstallPwaToast` (Android `beforeinstallprompt`, iOS Drawer, dispensa em `sessionStorage`); logout limpa Cache API no cliente antes da action. + - **A3 Baseline TSE 2022 Fase 1** — collections `electionTally`/`electionCandidateVote`/`electionCandidate` (grupo `Dados Eleitorais`, leitura para `campaignUser`/`users` via `canReadElectionData`, escrita admin-only), migration `20260718_195854_add_election_results`, seed local `pnpm db:seed:tse` (idempotente por escopo year/office/turn, delete+bulk insert via drizzle, guard de banco local). Sem PII (sem CPF/título); identidade cross-ano por `identityKey` hash. UI fica no A4. ## Decisões @@ -78,6 +84,7 @@ alwaysApply: false - A fronteira pública de convites exporta somente as três Server Actions; helpers privilegiados de SQL, autorização, token, consentimento e recuperação ficam em módulos `server-only`, com limites arquiteturais cobertos por teste. - Em 2026-07-17, `Contact.city` passou a opcional porque Núcleos podem representar um Território de Identidade sem município/localidade; os fluxos públicos continuam exigindo cidade em seus próprios schemas. O cadastro de liderança mascara o celular no cliente, normaliza 11 dígitos no servidor e preserva valores/erros após falhas. - Em 2026-07-17, decisão de produto: `Município` e `Bairro` do Núcleo passam a aceitar múltiplas entradas (arrays planos `cities`/`neighborhoods`), e `region` vira `regions` derivado no servidor a partir dos municípios, permitindo cruzar Territórios de Identidade. Bairros exigem município único. Modelo A (arrays planos) escolhido em vez de array estruturado por município. Plano: [`docs/plans/territorio-multi-municipio-bairro.md`](../../../docs/plans/territorio-multi-municipio-bairro.md); item do roadmap: A1. +- Em 2026-07-18, decisão de produto (revisão A2): no formulário de território, sugestões cruzadas **opt-in** via chips `{rótulo} +` (município/TI → união de ZEs faltantes; TI → municípios irmãos; ZE → municípios oficiais), em vez do auto-preenchimento forçado + Zonas TSE read-only da versão 2026-07-17 do plano. TI a partir do município continua derivado (A1). Cadastro estático município↔zona (`bahiaTseZones`) permanece. Plano: [`docs/plans/zonas-por-municipio.md`](../../../docs/plans/zonas-por-municipio.md); item do roadmap: A2. - Em 2026-07-18, **A1 entregue:** `regions`/`cities`/`neighborhoods` como `text` + `hasMany` (tabela `electoral_nucleus_texts`); migration `20260718_190559_territorio_multi_municipio_bairro` com backfill; filtros da lista permanecem single-select na URL e consultam com `{ equals }` (associação ao array — não o operador substring `contains`); UI de `/campanha` com chips multi + bairros só com 1 município; dashboard e criação de liderança atualizados (`Contact.city` só quando `cities.length === 1`). - O DTO paginado de atualizações contém somente filtro/paginação/feed; a visão geral chama um loader dedicado que busca `limit: 3` e apenas os autores necessários à prévia. - A lista paginada de lideranças seleciona e serializa somente os campos da linha; avaliação interna e dados completos de contato são carregados apenas para a liderança selecionada. O fechamento usa `onCloseAutoFocus` no Sheet e `finalFocus` no Drawer, focaliza a linha de origem (ou o título do Núcleo) antes de navegar para a URL canônica, sem RAF/timeout. diff --git a/.cursor/skills/local-database/SKILL.md b/.cursor/skills/local-database/SKILL.md index 33e0c99a..87f886ec 100644 --- a/.cursor/skills/local-database/SKILL.md +++ b/.cursor/skills/local-database/SKILL.md @@ -59,6 +59,18 @@ pnpm db:seed:posts `REVALIDATE_SECRET` must be set in the Vercel production env. See the "Posts & Tags" section in `AGENTS.md` for the full rationale (returns `401` on a bad secret, `500` if the secret is unset). +## Seed TSE 2022 election results (`pnpm db:seed:tse`) + +```bash +pnpm db:seed:tse +TSE_CACHE_DIR=./data/tse pnpm db:seed:tse # reuse downloaded zips +``` + +- `scripts/seed-tse-results.mjs` downloads the TSE open-data zips for the 2022 general election (provenance URLs + SHA-256 in the script header), parses the Bahia scope, and imports into `electionTally` / `electionCandidateVote` / `electionCandidate` via a drizzle transaction. +- Same safety guard as `pnpm dev`: it refuses a non-local `DATABASE_URL` unless `ALLOW_REMOTE_DB=true`. +- **Idempotent by scope** — each `(year, office, turn)` scope is replaced wholesale (delete + bulk insert), so re-running never duplicates. +- No revalidation step: campaign pages are dynamic with auth (no ISR tags). Downloads are cached under `data/tse/` (gitignored). + ## Prepare / reset the test database The test DB needs the schema applied via migrations: diff --git a/.cursor/skills/roadmap-item/SKILL.md b/.cursor/skills/roadmap-item/SKILL.md new file mode 100644 index 00000000..c1c03dfd --- /dev/null +++ b/.cursor/skills/roadmap-item/SKILL.md @@ -0,0 +1,110 @@ +--- +name: roadmap-item +description: Adiciona uma ideia, tarefa ou funcionalidade ao docs/roadmap.md do Teqo no local correto (trilha, janela do calendário eleitoral, grafo de dependências, paralelismo) e cria o plano detalhado correspondente em docs/plans/. Usar quando o usuário pedir para adicionar algo ao roadmap, registrar uma ideia/feature/débito, criar um plano para um item, ou disser "adiciona ao roadmap", "cria um plano para", "registra essa ideia". +--- + +# Adicionar item ao roadmap + criar plano + +Esta skill transforma uma ideia solta em: (1) um item posicionado corretamente no `docs/roadmap.md` e (2) um arquivo de plano em `docs/plans/.md` linkado a partir do item. O roadmap é o registro canônico **em ordem de execução, com dependências e paralelismo explícitos** — um item mal posicionado é pior que um item ausente. + +## Checklist do fluxo + +``` +- [ ] 1. Ler docs/roadmap.md inteiro (nunca editar sem ler a versão atual) +- [ ] 2. Classificar a ideia e checar duplicidade/absorção +- [ ] 3. Explorar o código relevante para fundamentar o plano +- [ ] 4. Decidir posicionamento: seção, ID, dependências, janela, paralelismo +- [ ] 5. Criar docs/plans/.md a partir do template +- [ ] 6. Editar o roadmap em TODOS os pontos de consistência +- [ ] 7. Verificar links, mermaid e consistência cruzada +``` + +## Passo 1 — Ler o roadmap atual + +Leia `docs/roadmap.md` por completo antes de qualquer edição. A estrutura que importa: + +- **Âncoras do calendário eleitoral** — datas fixas (convenções 20/07–05/08, registro 15/08, propaganda 16/08, congelamento ~20/09, 1º turno 04/10) que definem as janelas. +- **Trilhas com IDs**: A (dados eleitorais e território), B (superfícies de coordenação), C (operação de campo), D (plataforma e engajamento). IDs são sequenciais por trilha (próximo livre, ex.: `C6`, `D3`). +- **Grafo mermaid de dependências** — setas cheias = dependência dura; tracejadas = suave. +- **Tabelas por janela** (Janela 1–4) com colunas Ordem / Item / Plano / Depende de / Paralelizável com. A coluna "Ordem" é numeração global contínua. +- Seções paralelas: **Onda 0** (caminho crítico p/ produção), **Fill-ins**, **Bloqueadores atuais**, **Site público**, **Admin Payload**, **Plataforma white-label**, **Fora de escopo**, **Cortes seguros**. + +## Passo 2 — Classificar e checar duplicidade + +Determine o que a ideia é: + +| Tipo | Destino no roadmap | +| --------------------------------------------- | --------------------------------------------------------- | +| Feature de `/campanha` com escopo próprio | Nova linha numa trilha A/B/C/D + tabela de janela + grafo | +| Feature do site público | Lista da seção "Site público" | +| Melhoria do admin Payload | Lista da seção "Admin Payload" | +| Tarefa pequena que não bloqueia nada | Lista "Fill-ins" | +| Débito/risco que impede algo de ir a produção | Tabela "Bloqueadores atuais" (e possivelmente Onda 0) | +| Fase 2 / multi-tenant | "Plataforma white-label" | +| Decisão de NÃO fazer | "Fora de escopo (por enquanto)" com racional e fonte | + +**Antes de criar item novo, cheque se já existe.** Faça grep no roadmap e em `docs/plans/*.md` por termos da ideia. Três desfechos possíveis: + +- **Já coberto** → não crie nada; aponte o item/plano existente ao usuário. +- **É uma fase de um plano existente** → adicione ao plano existente (nova fase/seção), não crie item paralelo. Precedente: "Gap vs 2022" foi absorvido como Fase 4 do baseline em vez de item próprio. +- **Realmente novo** → siga adiante. + +## Passo 3 — Explorar o código antes de escrever o plano + +Um plano competente cita arquivos, utilities e padrões **reais** — não abstrações. Antes de escrever, localize no código: + +- Onde a feature se pluga (páginas em `src/app/(campaign)/...`, `src/app/(frontend)/...`). +- O que já existe para reusar (`src/utilities/*`, `src/components/campaign/*`, `src/components/ui/*`, `src/lib/*`). +- Se exige collection nova ou campo novo → então exige migration (`pnpm migrate:create`) e grupo `admin.group` correto. +- Se toca pessoa → é join com `Contact`, nunca cadastro paralelo. Se toca opt-in/PII → `Consent` por chave estável, falhando fechado. +- Precedente análogo: encontre o plano existente em `docs/plans/` mais parecido e espelhe o nível de detalhe dele. + +## Passo 4 — Decidir o posicionamento + +Responda explicitamente, nesta ordem: + +1. **Dependências duras**: sem quais itens este não funciona? (ex.: precisa de `cities[]` → depende de A1). Dependências **suaves** melhoram mas não bloqueiam — marque como tracejadas. +2. **Dependentes**: quais itens existentes passam a depender dele? Atualize-os também. +3. **Paralelismo**: sem seta de entrada = paralelizável a qualquer momento → entra na lista de paralelizáveis abaixo do grafo. +4. **Janela**: a mais cedo possível dado (a) as dependências e (b) a âncora de calendário que o item serve. Item que serve a propaganda de rua precisa estar pronto antes de 16/08; nada de migration arriscada depois de ~20/09 (congelamento). +5. **Prioridade dentro da janela**: caminho crítico (jurídico/Onda 0) > base de dados > operação de campo > inteligência > engajamento/plataforma. Quick wins de campo podem furar fila se forem baratos e paralelizáveis. +6. **Cortável?** Decida se entra na lista de "Cortes seguros" (e em que posição) ou na lista de "não cortáveis" — todo item de trilha precisa estar em uma das duas, com racional. +7. **Bloqueador externo?** Se depende de jurídico/LGPD (novo texto de `Consent`), o texto entra no **lote jurídico da Onda 0** — nunca criar rodada jurídica separada (decisão travada: fatiar multiplica lead time). + +Se a priorização depender de decisão de produto que você não tem como inferir, posicione com a sua melhor recomendação e marque o item como _(proposto — validar com produto)_, como foi feito com C5. + +## Passo 5 — Criar o plano em `docs/plans/` + +- **Slug**: kebab-case em português, descritivo, curto (padrão existente: `cadastro-nominal-apoiadores.md`, `zonas-por-municipio.md`). Idioma do conteúdo: pt-BR; identificadores de código citados: inglês. +- **Estrutura**: siga [plan-template.md](plan-template.md) à risca — mesmas seções, mesma ordem. Leia o template antes de escrever. +- **Qualidade mínima** (o que separa um plano útil de um genérico): + - "Decisões travadas" registra decisões com data e fonte, não desejos. Cada uma deve ser defensável ("por quê" incluído). + - "Questões em aberto" sempre traz **Recomendação** para cada questão — nunca deixar pergunta sem posição. + - "Abordagem proposta" tem diagrama mermaid + lista de componentes com caminhos de arquivo reais e assinaturas concretas. + - "Não escopo" é explícito e cita para qual outro plano/item cada exclusão vai. + - "Referências" lista os arquivos-fonte reais que o implementador vai abrir. +- Se existir design em `docs/design-refs/latest/`, inclua a seção "Referência visual (UX Pilot)" com o aviso padrão de paleta (a estrutura vale, a paleta não — usar tokens do tema `data-theme='campaign'`). + +## Passo 6 — Editar o roadmap (todos os pontos de consistência) + +Um item novo de trilha toca **até 7 lugares** no roadmap. Pule apenas os que não se aplicam: + +1. **`Atualizado em:`** (linha 3) — nova data + nota curta do que mudou. +2. **Grafo mermaid** — nó novo na subgraph da trilha (`X9["X9 Nome curto"]`) + setas de/para. Setas cheias `-->` para dependência dura, `-.rótulo.->` para suave. +3. **Lista de paralelizáveis** (parágrafo logo abaixo do grafo) — se o item não tem seta de entrada. +4. **Tabela da janela** — linha nova com Ordem (renumere as seguintes se inserir no meio), Item, link `[detalhes](plans/.md)`, Depende de, Paralelizável com. Atualize a coluna "Paralelizável com" dos itens vizinhos afetados. +5. **Tabela de referências de design** — se houver design correspondente em `docs/design-refs/latest/`. +6. **Cortes seguros / não cortáveis** — insira na posição certa da lista de cortes, ou justifique como não cortável. +7. **Onda 0** — se o item exigir novo texto jurídico/`Consent`, adicione o texto ao lote jurídico único. + +Para itens fora das trilhas (site público, admin, fill-in, bloqueador, fora de escopo): edite só a lista/tabela da seção correspondente + `Atualizado em`, mantendo o padrão da seção — bullets com _(fonte)_ em itálico no final, ou linha de tabela com Status e Fonte. + +## Passo 7 — Verificação final + +- Todos os links relativos resolvem (`plans/.md` a partir de `docs/`, `../roadmap.md` a partir de `docs/plans/`). +- O mermaid continua válido (nomes de nó únicos, subgraphs fechadas — renderize mentalmente ou cole num validador se a edição foi grande). +- Consistência tripla: o que a **tabela de janela** diz em "Depende de" = setas do **grafo** = seção "Dependências" do **plano**. Divergência aqui é o bug mais comum. +- O plano referencia o roadmap (`Item do roadmap:` no cabeçalho) e o roadmap referencia o plano (link na tabela). +- Não inventou ID duplicado nem quebrou a numeração de "Ordem". + +Ao final, resuma para o usuário: ID atribuído, janela, dependências assumidas, e as decisões de posicionamento que merecem validação de produto. diff --git a/.cursor/skills/roadmap-item/plan-template.md b/.cursor/skills/roadmap-item/plan-template.md new file mode 100644 index 00000000..c214b6ba --- /dev/null +++ b/.cursor/skills/roadmap-item/plan-template.md @@ -0,0 +1,84 @@ +# Template de plano (`docs/plans/.md`) + +Estrutura extraída dos planos existentes (`overview-lista-nucleos.md`, `visitados-recentemente.md`, etc.). Manter as seções nesta ordem; omitir "Referência visual" apenas quando não houver design. + +````markdown +# + +Status: rascunho +Atualizado em: +Item do roadmap: [docs/roadmap.md](../roadmap.md) () +Responsável: — + +## Referência visual (UX Pilot) + + + +Design: [`.png`](../design-refs/latest/.png) · [`.html`](../design-refs/latest/.html) + +![](../design-refs/latest/.png) + +Como usar: + +- **Adotar a estrutura:** . +- **Fora deste plano:** . +- **Ajustar cores e código:** o HTML/PNG usa a paleta antiga (vermelho escuro `#8E0E23`, navy `#1B2B4B`, dourado `#C8874B`) e Tailwind via CDN. Implementar com os componentes shadcn existentes (`src/components/ui`) e os tokens do tema `data-theme='campaign'` (`src/app/(frontend)/styles.css`): fundo branco, primário `#C51414`, superfícies neutras claras. + +## Contexto + + + +## Objetivos + +- + +## Decisões travadas + +- **.** +- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (), strings visíveis em pt-BR. + +## Questões em aberto + +- **?** **Recomendação:** . <"Definir com + produto" quando aplicável — mas nunca pergunta sem recomendação.> + +## Abordagem proposta + +\```mermaid +flowchart LR + +\``` + +Componentes: + +- **``** (em `src/`): +- +- ****: `, o que adiciona, + se tem backfill. Se não houver: "Sem migration, sem collection, sem server action."> + +## Dependências + +- + +## Não escopo + +- + +## Referências + +- `docs/roadmap.md` () +- +- AGENTS.md — +```` + +Notas: + +- No bloco mermaid acima, remover as barras invertidas dos fences internos (`\``` → ` ``` `). +- Nível de detalhe alvo: ~100–130 linhas, como os planos existentes. Menos que isso costuma significar que o Passo 3 (exploração do código) foi pulado. diff --git a/AGENTS.md b/AGENTS.md index 346932d1..23b8f849 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si - Every action that writes to more than one collection (see `src/app/(frontend)/actions/*.ts`) wraps the writes in a Payload transaction (`payload.db.beginTransaction/commitTransaction/rollbackTransaction`) and passes `req: { transactionID }` to every `payload.create`/`payload.update` call. Follow this pattern for any new multi-collection write. - LGPD consent is tracked via a `Consent` collection (richText, versioned, optional stable `key`) referenced by relationship from `Signature`/`Subscription`/campaign `leadership`. New campaign opt-in flows resolve Consent by stable `key` (e.g. `lideranca-autopreenchimento`) and fail closed when missing — don't invent a parallel consent mechanism or hardcode document IDs for new work. - Cache invalidation: collections/globals call `revalidateDocumentById` (`src/utilities/documents.ts`) / `revalidateGlobal` (`src/utilities/globals.ts`) in an `afterChange` hook. Add the same hook to any new collection/global that backs a public page. Collections whose listing is cached under a shared tag also call the listing helper (`revalidateCollectionListing`, e.g. `revalidatePostsListing()` → `revalidateTag('posts')`) — see "Posts & Tags" below. -- Admin UI is organized via `admin.group` (e.g. `'Abaixo-assinados'`, `'Contatos'`, `'Configurações'`, `'Paginas'`) — group new collections consistently instead of leaving them ungrouped. +- Admin UI is organized via `admin.group` (e.g. `'Abaixo-assinados'`, `'Contatos'`, `'Configurações'`, `'Paginas'`, `'Campanha'`, `'Dados Eleitorais'`) — group new collections consistently instead of leaving them ungrouped. - i18n: Payload admin defaults to `pt` (not `pt-BR` — see `payload.config.ts`, `payload/i18n/pt`). - Language & naming conventions: ALL code identifiers are in English — variable, function, parameter, type/interface names, local constants, and Next.js dynamic route segment folder names used as param keys (e.g. `[type]/[category]/[slug]`, not `[tipo]/[categoria]`). Portuguese is allowed ONLY in: user-visible string literals (JSX text, button/labels), image `alt` text, SEO/metadata values (title/description/keywords), Payload admin config text (`labels`/`singular`/`plural`/`admin.description`/field `label`), and intentional URL slug/enum VALUES kept in Portuguese for SEO (e.g. the `post.type` enum values `noticia|campanha|artigo|evento`, tag/category slugs like `saude`, `eleitoral`). Never translate those data/URL values or admin labels. Renaming a route segment folder changes only the param KEY, never the public URL (segment values come from the data). @@ -29,6 +29,7 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si - `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit. - To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data). - To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below. +- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard; no revalidate needed — campaign pages are dynamic). See "Election baseline data (TSE 2022)" below. ## Known Gaps (as of 2026-07-18 — resolve before relying on this file for onboarding new devs) @@ -39,7 +40,7 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap. 3. **`Post`/`Tag` ship the news system, but there is still no `Pages` collection for institutional content.** Partly resolved: the `post`/`tag` collections back the news/publications system, and the home "Últimas notícias" list plus the `/[type]`, `/[type]/[category]`, and article routes are live (see "Posts & Tags" below). Still hardcoded/pending: the home hero heading + subtitle copy (`HomePage` global still exposes only a single `image` field — the text lives in `src/app/(frontend)/(home)/page.tsx`), and there is no `Pages` collection yet for institutional content such as the bio and propostas. 4. **Campaign LGPD consent text is not yet configured in production.** Invites and self-service leadership flows fail closed until an admin creates `Consent.key = 'lideranca-autopreenchimento'` with counsel-approved copy (see deploy checklist). -**Recently resolved (2026-07-18):** the `/campanha` nuclei MVP shipped (auth/RBAC, nuclei, leaderships, vote estimates, updates, WhatsApp invites, dashboard) with consolidated migration `20260718_010733_consolidate_campaign_schema`. Earlier (2026-07-15): local Postgres via `docker-compose.yml` (Postgres 17, `teqo_test`); Payload migrations baselined; dev/test database guards; `post`/`tag` news routes deployed (see "Posts & Tags" below). +**Recently resolved (2026-07-18):** the `/campanha` nuclei MVP shipped (auth/RBAC, nuclei, leaderships, vote estimates, updates, WhatsApp invites, dashboard) with consolidated migration `20260718_010733_consolidate_campaign_schema`. Later the same day, cycle 2 was merged and deployed: multi-municipality/neighborhood territory arrays (migration `20260718_190559_territorio_multi_municipio_bairro`), TSE 2022 election baseline collections + `pnpm db:seed:tse` (migration `20260718_195854_add_election_results`), the filtered nuclei-list overview panel, the nucleus share dialog, and the installable `/campanha` PWA. Earlier (2026-07-15): local Postgres via `docker-compose.yml` (Postgres 17, `teqo_test`); Payload migrations baselined; dev/test database guards; `post`/`tag` news routes deployed (see "Posts & Tags" below). ## Posts & Tags (news / publications) @@ -93,12 +94,21 @@ The first `/campanha` operational cycle is centered on `electoralNucleus`; a Nú - **Collections:** `electoralNucleus` (territory, coordinators, TSE zone references, intelligence and vote estimates), `leadership` (unique `Contact`↔nucleus join with support status), `nucleusUpdate` (immutable field reports), and `campaignInvite` (single-use WhatsApp invite hashes). All are in admin group `Campanha`. - **Canonical nucleus URLs:** nuclei use a unique canonical `slug` generated from `name` and route through `/campanha/nucleos/[slug]`. IDs stay internal. Names are immutable after creation so existing shared links remain durable without an alias table. -- **Territory (current cycle):** Bahia is implicit. Nuclei use single `region` / `city` / `neighborhood` / `locality` / `territoryNotes` validated against official Bahia identity territories (`src/lib/bahiaTerritories.ts`). Multi-município/bairro arrays remain a planned follow-up (`docs/plans/territorio-multi-municipio-bairro.md`). +- **Territory:** Bahia is implicit. Since 2026-07-18 nuclei use `hasMany` arrays `regions` / `cities` / `neighborhoods` (plus `locality` / `territoryNotes`), validated against official Bahia identity territories (`src/lib/bahiaTerritories.ts`): `regions` is derived server-side from the selected cities, and neighborhoods require exactly one city. List filters stay single-select and query array membership with `{ equals }`. Migration `20260718_190559_territorio_multi_municipio_bairro` backfilled the old scalar fields (`docs/plans/territorio-multi-municipio-bairro.md`). - **Role scope:** `geral` sees/manages all nuclei, assigns coordinators, and confirms estimates; `coordenador` sees assigned nuclei and manages their leaderships, updates, invites, and estimates; `lideranca` sees only nuclei linked through an engaged leadership record, creates own updates, and suggests estimates. - **Sensitive fields:** `leadership.supportStatus`, internal notes, consent notes, and strategic nucleus intelligence are staff-only and are excluded from leadership view models. Revoking `engajado` revokes nucleus access immediately. Dashboard aggregates omit Payload-redacted missing statuses and fail closed on invalid enum values. - **People and consent:** leaderships always reuse `Contact`; multi-collection writes are transactional. Self-service updates accept a strict whitelist derived from the invite token. Campaign consent is looked up only by the stable key `lideranca-autopreenchimento`. - **Invites:** only SHA-256 token hashes are persisted. Absolute invite URLs require a valid `NEXT_PUBLIC_SITE_URL` (exact HTTPS DNS origin in production). Public invite pages are dynamic, `no-store`, `noindex,nofollow`, `no-referrer`, and have no third-party application scripts. Login invites are allowed only for engaged leaderships. - **Vote estimates:** proposals never overwrite the confirmed value; each proposal carries a server-generated UUID version compared under advisory lock on confirm. Only a coordinator assigned to the nucleus or `geral` may confirm. +- **List overview (B1, 2026-07-18):** `/campanha/nucleos` renders an aggregate panel between the filters and the list (`NucleusListOverview` + `loadNucleusListOverviewData`): confirmed vote estimate total/%, coordinator coverage, and a 3-update preview, computed over the entire filtered set (`buildNucleusListWhere`, `pagination: false`, `overrideAccess: false`). Hidden when the filtered set is empty; `lideranca` gets a reduced select without pending-suggestion counts. +- **Share dialog (C1, 2026-07-18):** the nucleus detail header has a "Compartilhar" dialog (WhatsApp `wa.me` per recipient + copy link) that sends only the URL — it never creates invites or grants access. Recipients (gerais, assigned coordinators, `engajado` leaderships) are loaded on open via a read-only Server Action; the loader gates on nucleus access (`overrideAccess: false`) then reads name+phone privileged (`overrideAccess: true`) to avoid N+1 field-access checks. `canReadCampaignUserPhone` allows any campaign user to read `geral` phones. +- **PWA (D1, 2026-07-18):** `/campanha` is installable — manifest and service worker are served by route handlers under `/campanha` (script built in `src/utilities/campaignPwa.ts`), scoped to the vertical only (public site and `/admin` have no SW). Cache is named `campanha-` and swapped on deploy (`skipWaiting`/`claim`); navigation is network-first with an `/campanha/offline` fallback; RSC/Flight responses and `/campanha/convite/*` are never cached. `InstallPwaToast` prompts install on mobile (Android `beforeinstallprompt`, iOS step-by-step drawer, `sessionStorage` dismissal), and logout wipes the Cache API client-side before the server action runs. Push handlers are placeholders for D2. + +## Election baseline data (TSE 2022) + +Public TSE 2022 results back the electoral baseline (roadmap item A3; UI lands with A4). Three collections in admin group `Dados Eleitorais`: `electionTally` (per city×zone totals: eligible voters, turnout, valid/blank/null votes), `electionCandidateVote` (nominal votes per candidate×city×zone), and `electionCandidate` (candidate registry with a cross-year `identityKey` hash — no CPF or voter ID is ever persisted). Read access requires an authenticated `campaignUser` or `users` account (`canReadElectionData`); writes are Payload-admin-only, and the import runs via CLI with `overrideAccess: true`. + +`pnpm db:seed:tse` (`scripts/seed-tse-results.mjs`) downloads the TSE open-data zips (provenance URLs + SHA-256 in the script header), parses the Bahia scope, and imports via `payload.db.drizzle` in a transaction — idempotent by replace-per-scope `(year, office, turn)`, only `voteType: 'nominal'` rows in v1. It refuses a non-local `DATABASE_URL` (same guard family as the other seeds; override with `ALLOW_REMOTE_DB=true`) and caches downloads under `data/tse/` (gitignored). No revalidate step is needed: campaign pages are dynamic with auth, not ISR-cached. ### Local verification and deploy checklist @@ -134,7 +144,7 @@ src/ │ ├── (frontend)/ # Frontend routes + server actions (src/app/(frontend)/actions/*.ts) │ ├── (payload)/ # Payload admin routes │ └── (campaign)/ # Internal campaign-management area (auth barrier, see "Campaign auth") -├── collections/ # Users, CampaignUser, CampaignInvite, ElectoralNucleus, Leadership, NucleusUpdate, Media, Petition, Signature, Consent, Contact, Subscription, Post, Tag +├── collections/ # Users, CampaignUser, CampaignInvite, ElectoralNucleus, Leadership, NucleusUpdate, ElectionTally, ElectionCandidateVote, ElectionCandidate, Media, Petition, Signature, Consent, Contact, Subscription, Post, Tag ├── globals/ # Global configs (SiteSettings, HomePage, Metadata) ├── components/ # Custom React components (campaign/*, ui from shadcn) ├── lib/ # cities.ts, bahiaTerritories.ts, formData.ts, zod schemas (lib/schemas/*) diff --git a/README.md b/README.md index 7160e382..f57aba68 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Teqo then becomes a configurable base product for other politicians in Brazil: 6. Start the dev server: `pnpm dev` 7. Open: `http://localhost:3000` -Useful scripts: `pnpm db:start` / `pnpm db:stop` (local Postgres), `pnpm db:pull` (refresh local content from prod), `pnpm db:seed:posts` (import news posts/tags fresh from the live jorgesolla.com.br site into the local db; idempotent by slug, refuses a non-local database). +Useful scripts: `pnpm db:start` / `pnpm db:stop` (local Postgres), `pnpm db:pull` (refresh local content from prod), `pnpm db:seed:posts` (import news posts/tags fresh from the live jorgesolla.com.br site into the local db; idempotent by slug, refuses a non-local database), `pnpm db:seed:tse` (import TSE 2022 Bahia election results into the local db; idempotent per year/office/turn scope, refuses a non-local database). ### Content & cache revalidation diff --git a/docs/plans/baseline-eleitoral-tse.md b/docs/plans/baseline-eleitoral-tse.md index 68f47d80..d1293715 100644 --- a/docs/plans/baseline-eleitoral-tse.md +++ b/docs/plans/baseline-eleitoral-tse.md @@ -171,15 +171,17 @@ Cada insight é uma **derivação de leitura** (sem escrita, sem `Consent`, sem ## Arquivos a criar/alterar **Fase 1 (A3) — feitos:** + - Criar: `src/collections/ElectionTally.ts`, `ElectionCandidateVote.ts`, `ElectionCandidate.ts`; `src/lib/electionResults.ts`, `electionResultsParse.ts`, `electionResultsBuild.ts`, `electionResultsCsv.ts`, `electionResultsZip.ts`, `electionCandidateIdentity.ts`; `src/utilities/electionResultsImport.ts`; `scripts/seed-tse-results.mjs`; migration `20260718_195854_add_election_results`; fixtures `tests/fixtures/tse/*`; testes unit/int. - Alterar: `src/payload.config.ts`, `src/utilities/campaignAccess.ts` (`canReadElectionData` / `canMutateElectionData`), `package.json` (`db:seed:tse`), `scripts/seed-loader.mjs` (stub `server-only`), `.gitignore` (`/data/tse/`). **Fases 2–5 (A4+) — ainda a criar:** + - `src/lib/electionInsights.ts`, `src/utilities/nucleusElectoralBaseline.ts`, `src/components/campaign/NucleusElectoralBaseline.tsx`, `NucleusInsights.tsx`, `scripts/reconcile-running-again.mjs`; alterar páginas do núcleo / overview. ## Dependências -1. `zonas-por-municipio.md` — **dependência suave, não bloqueante** (revisado em 2026-07-17). A agregação funciona sem ele: `citiesForTerritory` já existe em `bahiaTerritories.ts` e as zonas de cada cidade saem das próprias rows de `electionTally`. O que aquele plano melhora é a **qualidade do input**: com auto-preenchimento, `electoralNucleus.tseZones` deixa de depender de digitação manual (zonas erradas produzem baseline errado; zonas ausentes caem no fallback correto "todas as zonas da cidade"). Recomendado implementá-lo antes ou em paralelo, mas este plano pode ser executado de forma independente. +1. `zonas-por-municipio.md` — **dependência suave, não bloqueante** (revisado em 2026-07-17; UX A2 revisada 2026-07-18 para chips opt-in). A agregação funciona sem ele: `citiesForTerritory` já existe em `bahiaTerritories.ts` e as zonas de cada cidade saem das próprias rows de `electionTally`. O que aquele plano melhora é a **qualidade do input**: com sugestões município/TI→ZE, `electoralNucleus.tseZones` deixa de depender só de digitação manual (zonas erradas produzem baseline errado; zonas ausentes caem no fallback correto "todas as zonas da cidade"). Recomendado implementá-lo antes ou em paralelo, mas este plano pode ser executado de forma independente. 2. `territorio-multi-municipio-bairro.md` (torna `cities[]`) — `getNucleusElectoralBaseline` já desenhado para iterar sobre `cities[]`; funciona igualmente com o modelo single atual (array de 1). ## Não escopo diff --git a/docs/plans/compartilhar-pagina.md b/docs/plans/compartilhar-pagina.md index 5828273f..eb81cd3a 100644 --- a/docs/plans/compartilhar-pagina.md +++ b/docs/plans/compartilhar-pagina.md @@ -1,10 +1,22 @@ # Compartilhar página do núcleo -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos", linha 51) +Status: **implementado** (2026-07-18) +Atualizado em: 2026-07-18 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha C → C1) Responsável: — +## Como foi implementado (2026-07-18) + +Entregue com `ShareNucleusDialogShell` + `ShareNucleusDialog` (`src/components/campaign/`), `getNucleusShareRecipients` (`src/utilities/nucleusShareRecipients.ts`) e a Server Action `loadNucleusShareRecipients` (`src/app/(campaign)/campanha/(app)/nucleos/[slug]/shareRecipientsActions.ts`). Botão no header do detalhe, template de mensagem e `wa.me` + copiar link conforme o plano. Desvios e refinamentos em relação ao texto original: + +- **Carregamento sob demanda, não no `Promise.all` da página.** Os destinatários são buscados por uma Server Action de leitura só quando o diálogo abre (shell lazy no padrão dos demais diálogos), evitando serializar telefones no HTML da página. +- **Leituras privilegiadas atrás do gate do núcleo.** Em vez de depender do field access por linha (`overrideAccess: false`, que causaria N+1 checagens de telefone), o loader confirma primeiro o acesso do ator ao núcleo (`overrideAccess: false`) e só então lê as três seções com `overrideAccess: true`, retornando apenas `{ id, name, phone }`. +- **Seção "Com liderança" restrita a `supportStatus = 'engajado'`** e omitida para o papel `lideranca` (o loader retorna lista vazia; o componente oculta seções vazias). +- **`canReadCampaignUserPhone` ampliado:** telefone de usuários `geral` é legível por qualquer usuário de campanha autenticado (para contato), mantendo a regra de escopo por núcleo para os demais. +- **UI: Dialog central único** (não Sheet/Drawer responsivo); destinatário sem telefone não aparece; estados de loading/erro/retry e "copiado" implementados. + +Testes: `tests/int/campaignNucleusShareRecipients.int.spec.ts`, `tests/int/campaignUserPhone.int.spec.ts`, `tests/unit/campaignNucleusShare.unit.spec.ts`. O restante do documento é o plano original, mantido como registro. + ## Referência visual (UX Pilot) Design: [`Compartilhar-Nucleo.png`](../design-refs/latest/Compartilhar-Nucleo.png) · [`Compartilhar-Nucleo.html`](../design-refs/latest/Compartilhar-Nucleo.html) diff --git a/docs/plans/notifications.md b/docs/plans/notifications.md index 3ad28d51..54c4fc26 100644 --- a/docs/plans/notifications.md +++ b/docs/plans/notifications.md @@ -78,7 +78,7 @@ Componentes: ## Dependências -- PWA de `/campanha` — push exige o service worker registrado. Ver [plans/pwa-campanha.md](./pwa-campanha.md). **O sino in-app não depende do PWA** e pode ser entregue antes, se fizer sentido faseá-lo. +- PWA de `/campanha` — push exige o service worker registrado. **Entregue em 2026-07-18** ([plans/pwa-campanha.md](./pwa-campanha.md)); o SW em `src/utilities/campaignPwa.ts` já reserva os handlers `push`/`notificationclick`. **O sino in-app não depende do PWA** e pode ser entregue antes, se fizer sentido faseá-lo. - `Consent.key = 'campanha-notificacoes-push'` criada por admin após aprovação jurídica (mesmo padrão fail-closed do `lideranca-autopreenchimento`; texto no lote jurídico único do roadmap). - Migration para as collections `Notification` e `PushSubscription` (`pnpm migrate:create`). diff --git a/docs/plans/overview-lista-nucleos.md b/docs/plans/overview-lista-nucleos.md index 7973022b..31225979 100644 --- a/docs/plans/overview-lista-nucleos.md +++ b/docs/plans/overview-lista-nucleos.md @@ -1,10 +1,23 @@ # Overview na lista de núcleos -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos", linha 50) +Status: **implementado** (2026-07-18) +Atualizado em: 2026-07-18 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B → B1) Responsável: — +## Como foi implementado (2026-07-18) + +Entregue conforme a abordagem proposta: `loadNucleusListOverviewData` (`src/utilities/nucleusListOverviewPageData.ts`) + `buildNucleusListOverviewViewModel` (`src/utilities/nucleusListOverviewViewModels.ts`) + componente server `NucleusListOverview` (`src/components/campaign/NucleusListOverview.tsx`), integrado em `src/app/(campaign)/campanha/(app)/nucleos/page.tsx` entre filtros e lista. Sem collection, migration ou server action; todas as queries com `user` + `overrideAccess: false`. Questões em aberto resolvidas: + +- **`lideranca` vê o overview** — sim, com `select` reduzido (sem `proposedVoteEstimate`) e sem o badge de sugestões pendentes; as atualizações da prévia são só as próprias (escopo do access control) e o autor é resolvido localmente sem query extra. +- **Preview de atualizações: 3** (`nucleusListOverviewPreviewLimit`), com autor, badge de tipo, data relativa e link para `?tab=updates`. +- **Pendente = `proposedVoteEstimate != null`**, espelhando o dashboard; o total soma apenas confirmadas. +- **Bloco Cobertura incluído** (% de núcleos com coordenador no conjunto filtrado). +- **Overview oculto** quando o conjunto filtrado é vazio (loader retorna `null`). +- **Eventos/Demandas**: apenas o ponto de extensão comentado no componente, sem UI órfã. + +Testes: `tests/int/campaignNucleusListOverview.int.spec.ts`, `tests/unit/campaignNucleusListOverview.unit.spec.ts`. O restante do documento é o plano original, mantido como registro. + ## Referência visual (UX Pilot) Design: [`Lista-Nucleos-Overview.png`](../design-refs/latest/Lista-Nucleos-Overview.png) · [`Lista-Nucleos-Overview.html`](../design-refs/latest/Lista-Nucleos-Overview.html) diff --git a/docs/plans/pwa-campanha.md b/docs/plans/pwa-campanha.md index b93191a0..b3b63246 100644 --- a/docs/plans/pwa-campanha.md +++ b/docs/plans/pwa-campanha.md @@ -1,10 +1,25 @@ # PWA / offline só em `/campanha` -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos", linha 62) +Status: **implementado** (2026-07-18) +Atualizado em: 2026-07-18 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D → D1) Responsável: — +## Como foi implementado (2026-07-18) + +Entregue conforme a abordagem proposta, com as questões em aberto resolvidas assim: + +- **Servir SW e manifest:** opção (a) — route handlers em `src/app/(campaign)/campanha/manifest.webmanifest/route.ts` e `src/app/(campaign)/campanha/sw.js/route.ts`; constantes e script do SW gerados em `src/utilities/campaignPwa.ts`. +- **Estratégia de cache:** `network-first` para navegação (com fallback à página `/campanha/offline`) e para GETs same-origin sob `/campanha` — não SWR. Precache mínimo: `/campanha/login`, `/campanha/offline` e os ícones (`public/campaign-icons/`, cache-first). **Nunca cacheia:** requests RSC/Flight (payloads personalizados jamais vão ao Cache Storage) e as páginas de convite `/campanha/convite/*` (no-store por design). +- **Versionamento/atualização:** nome do cache = `campanha-` (`VERCEL_GIT_COMMIT_SHA`/`VERCEL_DEPLOYMENT_ID`, `dev` local); `skipWaiting` + `clients.claim` no activate, que também expurga caches de builds antigos. Sem UX de "nova versão" — o deploy troca o cache. +- **Ícones:** PNG 192/512 + maskable 512 + apple-touch em `public/campaign-icons/`; `theme_color #c51414`, `background_color #ffffff`, `display: standalone`, `lang: pt-BR`. +- **Logout invalida cache:** `clearCampaignPwaCaches` (`src/utilities/campaignPwaClient.ts`) roda no cliente antes de `logoutCampaign` (Cache API wipe + postMessage ao SW). +- **Registro:** `RegisterServiceWorker` no root layout `(campaign)`, só em produção, com falha silenciosa. +- **Toast de instalação:** `InstallPwaToast` no layout `(app)` conforme a seção abaixo — sonner persistente, `beforeinstallprompt` no Android, passo a passo iOS em `Drawer`, dispensa em `sessionStorage`. +- **Métricas de adoção:** adiadas. + +Handlers `push`/`notificationclick` seguem como placeholder para o D2 ([notifications.md](notifications.md) — push destravado). Testes: `tests/unit/campaignPwa.unit.spec.ts`, `tests/e2e/campaign-pwa.e2e.spec.ts`. O restante do documento é o plano original, mantido como registro. + ## Referência visual (UX Pilot) Design: [`Notificacoes-PWA.png`](../design-refs/latest/Notificacoes-PWA.png) · [`Notificacoes-PWA.html`](../design-refs/latest/Notificacoes-PWA.html) — **compartilhado com [notifications.md](notifications.md)** (a central de notificações pertence àquele plano). diff --git a/docs/plans/territorio-multi-municipio-bairro.md b/docs/plans/territorio-multi-municipio-bairro.md index 0266d1dc..0083f578 100644 --- a/docs/plans/territorio-multi-municipio-bairro.md +++ b/docs/plans/territorio-multi-municipio-bairro.md @@ -60,12 +60,12 @@ O rascunho dizia `contains`, mas em Payload `contains` é substring. A associaç ## Questões em aberto — resolvidas na implementação -| Questão | Resolução | -| --- | --- | -| Tipo do campo Payload | `text` + `hasMany` para os três campos | -| Admin Payload UI | inputs nativos hasMany; combobox rico só em `/campanha` | -| Limite máximo | 27 municípios/TIs, 30 bairros | -| Filtro cross-territory | aparece ao filtrar por qualquer TI tocado (`equals`) | +| Questão | Resolução | +| ---------------------- | ------------------------------------------------------- | +| Tipo do campo Payload | `text` + `hasMany` para os três campos | +| Admin Payload UI | inputs nativos hasMany; combobox rico só em `/campanha` | +| Limite máximo | 27 municípios/TIs, 30 bairros | +| Filtro cross-territory | aparece ao filtrar por qualquer TI tocado (`equals`) | ## Dependências @@ -74,4 +74,4 @@ O rascunho dizia `contains`, mas em Payload `contains` é substring. A associaç ## Não escopo -Array estruturado por município; mapa/PostGIS; auto-preenchimento A2; mudar `Contact.city` além da regra de derivação na criação de liderança. +Array estruturado por município; mapa/PostGIS; sugestões cruzadas A2 (`bahiaTseZones` + chips opt-in); mudar `Contact.city` além da regra de derivação na criação de liderança. diff --git a/docs/plans/zonas-por-municipio.md b/docs/plans/zonas-por-municipio.md index 102cc915..bbf90d9e 100644 --- a/docs/plans/zonas-por-municipio.md +++ b/docs/plans/zonas-por-municipio.md @@ -1,4 +1,4 @@ -# Zonas TSE por município (auto-preenchimento) +# Zonas TSE por município + sugestões cruzadas de território Status: rascunho Atualizado em: 2026-07-18 @@ -7,7 +7,11 @@ Responsável: — ## Premissa atualizada (A1 entregue) -A1 ([territorio-multi-municipio-bairro.md](territorio-multi-municipio-bairro.md)) renomeou o território do núcleo para arrays `regions` / `cities` / `neighborhoods` (`text` + `hasMany`). Este plano **nasce contra `cities[]`**: o auto-preenchimento de zonas passa a ser a **união** das zonas oficiais de todos os municípios selecionados (não “a zona do município”). Bairros continuam a exigir exatamente um município (regra A1), então os modos com bairro permanecem escopados a um único município. +A1 ([territorio-multi-municipio-bairro.md](territorio-multi-municipio-bairro.md)) renomeou o território do núcleo para arrays `regions` / `cities` / `neighborhoods` (`text` + `hasMany`). Este plano **nasce contra `cities[]`**: sugestões e validações leem o conjunto de municípios/territórios selecionados. Bairros continuam a exigir exatamente um município (regra A1). + +## Revisão de produto (2026-07-18) + +A versão anterior deste plano (2026-07-17) definia quatro modos com auto-preenchimento forçado e Zonas TSE em somente leitura quando o município cobria o conjunto exato. A decisão de produto de 2026-07-18 **substitui esse modelo** por **sugestões opt-in** (chips `{rótulo} +` acima dos inputs): o usuário mantém o input livre e clica para completar o que falta. O cadastro estático município↔zona e o lift de estado compartilhado permanecem; o que muda é a UX e a validação server-side (não há mais “zonas devem ser exatamente o conjunto oficial”). ## Referência visual (UX Pilot) @@ -15,119 +19,128 @@ Design: [`Formulario-Territorio.png`](../design-refs/latest/Formulario-Territori ![Formulário de território do núcleo — seção Zonas TSE](../design-refs/latest/Formulario-Territorio.png) -Como usar (parte deste plano — seção "Zonas Eleitorais (TSE)"): +Como usar (parte deste plano — seção "Zonas Eleitorais (TSE)" + chips de sugestão): -- **Adotar a estrutura:** banner informativo "Zonas sugeridas pelo cadastro oficial do TSE — confira antes de salvar", lista de zonas como linhas com checkbox + número ("ZE 12"), badge "sugerida" e o município de origem como legenda — bom formato para o modo multi-município (união das zonas, cada uma rastreável ao município). -- **Divergências a resolver na implementação:** o design mostra checkboxes editáveis e um link "Adicionar zona manualmente" para todos os casos; o plano define 4 modos — no modo 1 (municípios sem bairro) as zonas são **somente leitura** (sem checkbox, sem adicionar manual); checkbox/edição vale para os modos 2 e 3 (restrito ao conjunto permitido); entrada manual livre só no modo 4. Usar o visual das linhas do design, trocando checkbox por badge estático no modo read-only. -- **Manter do design:** a zona desmarcada "ZE 7 (zona adicional)" ilustra bem o modo 2/3 (subconjunto permitido). -- **Ajustar cores:** paleta antiga no HTML/PNG; implementar com `TseZoneInput` + tokens do tema `campaign` (chips TSE `#F1F3F5`/`#3F4854`). +- **Adotar a estrutura:** banner “Zonas sugeridas pelo cadastro oficial do TSE — confira antes de salvar”; chips/linhas com badge “sugerida”; entrada manual ainda disponível. O design já aponta para **sugestão opt-in**, não para lock read-only. +- **Divergência vs design (aceita nesta revisão):** o HTML mostra checkboxes de zonas individuais pré-marcadas; a UX canônica deste plano são chips `{Município} +` / `{Território} +` **acima** do `TseZoneInput` (abaixo do título da seção), que ao clicar fazem união das ZEs faltantes. No campo Municípios, chips `{Município irmão} +` acima do combobox. Não implementar a lista checkbox do design como fonte de verdade. +- **Ajustar cores:** paleta antiga no HTML/PNG; implementar com `TseZoneInput` / `NucleusTerritoryFields` + tokens do tema `campaign` (chips TSE `#F1F3F5`/`#3F4854`; chips de sugestão com primário `#C51414` ou outline secundário — seguir o padrão de Badge existente). ## Contexto -Hoje o campo **Zonas TSE** do formulário do núcleo (`TseZoneInput` em `src/components/campaign/TseZoneInput.tsx`) é sempre de preenchimento manual livre: o usuário digita números de 1 a 999, separados por vírgula/espaço/Enter, e o servidor só valida unicidade e intervalo (`parseTseZoneNumbers` em `src/utilities/tseZone.ts`, `validateNucleusTerritoryAndZones` em `src/collections/ElectoralNucleus.ts`). Não há nenhuma relação entre o(s) município(s) selecionado(s) e as zonas informadas — o usuário precisa saber (ou adivinhar) quais zonas cobrem aquele território. +Hoje o campo **Zonas TSE** (`TseZoneInput` em `src/components/campaign/TseZoneInput.tsx`) é preenchimento manual livre: números 1–999, validados por `parseTseZoneNumbers` (`src/utilities/tseZone.ts`) e `validateNucleusTerritoryAndZones` (`src/collections/ElectoralNucleus.ts`). Não há relação município↔zona no app. -O TSE publica, como dado aberto, a correspondência oficial entre município e zona eleitoral (cada par município×zona aparece no conjunto "Eleitorado por município e zona", por UF). A decisão de produto (2026-07-17) é usar esse cadastro oficial para **preencher automaticamente** as Zonas TSE do núcleo a partir do município selecionado, sem edição manual nesse caso. O território de identidade já é auto-selecionado hoje quando o município muda (`territoryForCity` em `src/lib/bahiaTerritories.ts`, já wired em `NucleusTerritoryFields`); este plano preserva esse comportamento e o documenta como parte do mesmo fluxo. +O bloco de território (`NucleusTerritoryFields` em `src/components/campaign/NucleusTerritoryFields.tsx`) já deriva `regions` no cliente via `territoriesForCities` quando há municípios (e o servidor rederiva). Falta o restante do grafo de sugestões: completar ZEs a partir de município/TI, sugerir municípios irmãos do mesmo TI, e sugerir municípios a partir de uma ZE adicionada. -O cadastro de zonas é relativamente estável, mas pode mudar entre eleições; por isso o mapeamento entra como **dado estático versionado com proveniência**, no mesmo estilo de `src/lib/bahiaTerritories.ts` e seu fixture `tests/fixtures/bahia-identity-territories.official.json`. +O TSE publica a correspondência município×zona (“Eleitorado por município e zona”, UF BA). A decisão de produto (2026-07-17, UX revisada 2026-07-18) é versionar esse cadastro como dado estático (estilo `src/lib/bahiaTerritories.ts`) e usá-lo para **oferecer sugestões clicáveis**, sem forçar o conjunto no save. + +Exemplo canônico (TI oficial `Vale do Jiquiriçá`): + +1. Usuário adiciona município **Itiruçu** → `regions` ganha automaticamente **Vale do Jiquiriçá** (já existe). +2. Se alguma ZE de Itiruçu ainda não está em `tseZones` → chip **「Itiruçu +」** acima do input de ZEs; clique faz união das ZEs oficiais de Itiruçu. +3. Se o TI está selecionado (derivado ou manual) e falta ao menos uma ZE da união dos municípios do TI → chip **「Vale do Jiquiriçá +」** (mesma posição). +4. Municípios do mesmo TI ainda não selecionados (ex. **Maracás**) → chips **「Maracás +」** acima do input de Municípios. +5. Usuário adiciona uma ZE → municípios oficiais dessa ZE que ainda não estão em `cities` → chips **「{Município} +」** acima do input de Municípios. ## Objetivos -- Mapeamento estático **município (BA) → lista de Zonas TSE** com proveniência oficial TSE, em novo arquivo `src/lib/bahiaTseZones.ts`, validado por fixture independente `tests/fixtures/bahia-tse-zones.official.json` e teste `tests/int/bahiaTseZones.int.spec.ts` (mesmo padrão de `bahiaTerritories`). -- No formulário do núcleo (criação e edição), quatro modos para o campo Zonas TSE, regidos por uma regra única: **se a geografia selecionada define o conjunto exato de zonas que o núcleo cobre, auto-preencher em somente leitura; se define um superconjunto (zonas permitidas, mas o núcleo pode cobrir um subconjunto), editar restrito; se não define nada, livre**. - 1. **Município selecionado e bairro vazio** → o núcleo cobre o município inteiro, conjunto exato conhecido → auto-preencher com todas as zonas oficiais daquele município, em ordem crescente, **somente leitura** (sem adicionar/remover). - 2. **Bairro selecionado** (implica município selecionado) → o núcleo cobre um subconjunto do município (não sabemos qual zona cobre qual bairro) → editável, **restrito às zonas do município**. - 3. **Apenas território de identidade selecionado** (sem município) → o núcleo cobre um subconjunto do território → editável, **restrito à união das zonas dos municípios daquele território** (`citiesForTerritory` ∘ `tseZonesForCity`). - 4. **Sem território e sem município** → preenchimento manual livre (1–999), comportamento atual. -- Ao selecionar o município, o **território de identidade** continua sendo auto-selecionado (já existe; manter e documentar). Ao selecionar só o território, o município permanece vazio (modo 3). -- Validação **server-side** espelhando exatamente os quatro modos, para que o estado read-only do cliente não seja a única barreira (hook `validateNucleusTerritoryAndZones` + zod `validateGeographyAndZones`). -- Sem nova collection, sem alteração de schema, sem migration, sem server action nova, sem `Consent`. O campo `tseZones` (array de `{ zoneNumber, label }`) já existe. +- Mapeamento estático **município (BA) ↔ Zonas TSE** em `src/lib/bahiaTseZones.ts`, com fixture independente `tests/fixtures/bahia-tse-zones.official.json` e teste `tests/int/bahiaTseZones.int.spec.ts` (espelha `bahiaTerritories`). +- Helpers bidirecionais: `tseZonesForCity`, `tseZonesForTerritory` (união), `citiesForTseZone` (reverso), `isTseZoneOfCity` / `isTseZoneOfTerritory`. +- No formulário de criar/editar núcleo, **três faixas de sugestão opt-in** (chips `{rótulo} +`), sempre acima do input alvo e abaixo do título/label do campo: + 1. **→ Zonas TSE:** por município selecionado com ZEs faltantes; por TI selecionado (derivado ou manual) com ao menos uma ZE da união faltando. + 2. **→ Municípios:** irmãos do(s) TI(s) tocado(s) ainda não selecionados; municípios oficiais de cada ZE já selecionada ainda não em `cities`. +- Clique em sugestão **une** o conjunto faltante ao estado atual (nunca substitui nem remove o que o usuário já escolheu). +- `regions` continua derivado dos municípios quando `cities` não vazio (A1; sem mudança de regra). +- Inputs de município e ZE permanecem **sempre editáveis** (livre 1–999 para ZE; combobox estrito para município). Sem modo read-only forçado. +- Validação server-side: intervalo/unicidade de ZEs (já existe) + geografia Bahia (já existe). **Não** exigir que `tseZones` coincida com o conjunto oficial. Opcional: aviso suave no cliente se houver ZE fora do conjunto sugerido pela geografia atual — sem bloquear save. +- Sem nova collection, sem migration, sem server action, sem `Consent`. ## Decisões travadas -- **Mapeamento como dado estático versionado.** Novo `src/lib/bahiaTseZones.ts` derivado do conjunto "Eleitorado por município e zona" do TSE (UF BA), com cabeçalho documentando URL oficial, versão/data e SHA-256 do download — espelhando o estilo de `src/lib/bahiaTerritories.ts`. O fixture `tests/fixtures/bahia-tse-zones.official.json` é gerado de forma independente e serve de evidência para o teste, nunca lendo `bahiaTseZones.ts`. -- **Sem migration.** O campo `tseZones` já é `array<{ zoneNumber, label }>`. Toda a mudança é UI + dado estático + validação. Nenhuma escrita nova além do que já existe. -- **Regra de auto-preenchimento é autoritativa no servidor (em criação).** O cliente mostra read-only/construído, mas o servidor revalida em **create**: (1) cidade + sem bairro → zonas enviadas devem ser exatamente o conjunto oficial do município; (2) cidade + bairro → cada zona enviada deve pertencer ao conjunto do município; (3) só território → cada zona enviada deve pertencer à união das zonas dos municípios do território; (4) sem território/cidade → livre 1–999. Divergência = `APIError` 400 / issue zod, igual ao padrão atual de território incompatível. Em **update**, a regra só se aplica quando o campo `tseZones` é efetivamente alterado na requisição — para preservar zonas manuais legadas de núcleos existentes (ver "Questões em aberto → núcleos existentes"). -- **Território de identidade auto-selecionado já existe.** `NucleusTerritoryFields` já chama `territoryForCity(nextCity)` no `onValueChange` do município. Este plano **não** reimplementa isso; só garante que a subida de estado do novo fluxo não quebre esse comportamento e o documenta como parte do mesmo conjunto de regras. -- **Estado compartilhado entre território e zonas.** Hoje `NucleusTerritoryFields` e `TseZoneInput` são siblings com estado independente em `NucleusFormFields`. Para reagir à mudança de município/bairro, o estado de `region/city/neighborhood/tseZones` precisa ser **levantado** para um único componente cliente que os coordene e emita os hidden inputs. Reusar os componentes existentes como filhos controlados. -- **Read-only não significa oculto.** Nos modos read-only (1), as zonas auto-preenchidas continuam visíveis como badges (mesmo visual do `TseZoneInput`), mas sem o input de texto e sem o botão de remover. Indicar `aria-readonly` e um texto de ajuda explicando que as zonas derivam do município. -- **Transição modo 1 → modo 2 (digitar bairro).** Mantém todas as zonas do município selecionadas como ponto de partida, agora editáveis (o usuário pode remover as que não se aplicam ao bairro). Decisão de produto 2026-07-17. -- **Restrição por território (modo 3).** `tseZonesForTerritory(territory)` = união ordenada e deduplicada de `tseZonesForCity(m)` para cada `m` em `citiesForTerritory(territory)`. O input aceita somente zonas desse conjunto; zonas fora são rejeitadas com a mesma mensagem de "Zona TSE inválida" já usada por `parseTseZoneNumbers`. -- **Subcampo `label` das zonas.** No auto-preenchimento, `label` fica vazio (é opcional). No modo (3) manual, continua sem label (comportamento atual). Não inventar descrição automática. -- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (`bahiaTseZones`, `tseZonesForCity`, `NucleusTerritoryAndZonesFields` ou similar), strings visíveis em pt-BR. -- **Bahia implícita.** O mapeamento é só para BA (todo núcleo é baiano). Sem UF no dado. -- **Ordem de execução: depois de [`territorio-multi-municipio-bairro.md`](territorio-multi-municipio-bairro.md)** (decisão de sequenciamento 2026-07-17). Aquele plano muda `city` → `cities[]`; implementar este plano antes exigiria retrabalho imediato dos quatro modos. Este plano deve nascer contra o modelo de arrays: os modos abaixo leem "município" como "conjunto de municípios selecionados" e o auto-preenchimento passa a ser a **união** de `tseZonesForCity(m)` para cada `m` em `cities`. Modo 1 = `cities` não vazio e sem bairro (união read-only); modo 2 = bairro selecionado (implica `cities.length === 1`, editável restrito às zonas desse município); modo 3 = só território(s) (editável restrito à união das zonas dos municípios dos territórios); modo 4 = sem geografia (livre). A validação server-side espelha a mesma leitura. +- **Sugestão opt-in, nunca auto-gravação forçada de ZEs/municípios irmãos.** Adicionar Itiruçu **não** preenche `tseZones` sozinho; só aparece o chip até o usuário clicar (ou digitar). Municípios irmãos e municípios-por-ZE idem. (Decisão de produto 2026-07-18; revisa o auto-preenchimento forçado de 2026-07-17.) +- **TI a partir do município continua automático** via A1 (`territoriesForCities` / rederivação no servidor). Isso **não** é chip de sugestão — é derivação. Remover municípios restaura edição manual de `regions`. +- **Mapeamento como dado estático versionado.** `src/lib/bahiaTseZones.ts` a partir do “Eleitorado por município e zona” (UF BA), cabeçalho com URL, versão/data e SHA-256 — estilo `bahiaTerritories.ts`. Fixture independente nunca lido a partir do módulo TypeScript. +- **Sem migration.** Campo `tseZones` já é `array<{ zoneNumber, label }>`. +- **Estado compartilhado.** Hoje `NucleusTerritoryFields` e `TseZoneInput` são siblings com estado independente em `NucleusForm`. Subir estado de `regions` / `cities` / `neighborhoods` / `tseZones` para um coordenador cliente (ex. `NucleusTerritoryAndZonesFields`) que emite hidden inputs e calcula sugestões. +- **União, não replace.** Clique em 「Itiruçu +」 = `tseZones ∪ tseZonesForCity('Itiruçu')`. Clique em 「Vale do Jiquiriçá +」 = união de todas as ZEs de `citiesForTerritory('Vale do Jiquiriçá')`. Clique em 「Maracás +」 = adiciona o município (respeitando `MAX_NUCLEUS_CITIES`). +- **Quando mostrar o chip de ZE por município:** município ∈ `cities` e existe `z ∈ tseZonesForCity(m)` com `z ∉ tseZones` atuais. Chip some quando o conjunto estiver completo. +- **Quando mostrar o chip de ZE por TI:** TI ∈ `displayRegions` (derivado ou manual) e existe ao menos uma ZE em `tseZonesForTerritory(ti)` ausente de `tseZones`. Se o município já tem chip próprio, o chip do TI ainda aparece quando a união do TI tem ZEs além das dos municípios já selecionados (ex.: só Itiruçu selecionado → chip do município + chip do TI para puxar o resto do Vale). +- **Quando mostrar município irmão:** cidade ∈ `citiesForTerritory(ti)` para algum `ti` em `displayRegions`, cidade ∉ `cities`, e ainda há cota em `MAX_NUCLEUS_CITIES`. +- **Quando mostrar município a partir de ZE:** cidade ∈ `citiesForTseZone(z)` para algum `z` em `tseZones`, cidade ∉ `cities`. Zona compartilhada entre muitos municípios é o caso normal — ver teto de UI em “Questões em aberto”. +- **Ordem dos chips de sugestão:** alfabética por rótulo (pt-BR), ZEs internas em ordem crescente (já padrão de `parseTseZoneNumbers`). +- **`label` das zonas** continua vazio no preenchimento por sugestão (opcional no schema). +- **Bahia implícita.** Mapeamento só BA. +- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (`bahiaTseZones`, `tseZonesForCity`, `citiesForTseZone`, `TerritorySuggestionChips`, `NucleusTerritoryAndZonesFields`), strings visíveis em pt-BR (`「Itiruçu +」`, aria-labels “Adicionar zonas TSE de Itiruçu”). +- **Ordem de execução:** depois de A1 (já entregue). Nasce contra `cities[]`. ## Questões em aberto -- **Versão do cadastro TSE.** Decidido: usar a versão mais recente do cadastro TSE validada em 2026 (conjunto "Eleitorado por município e zona", UF BA), com data e SHA-256 documentados no cabeçalho de `bahiaTseZones.ts` e no fixture. Reavaliar a cada eleição. -- **Núcleos existentes com zonas manuais divergentes.** Decidido: **só aplicar a novos**. Sem migration de dados; núcleos já criados mantêm suas zonas manuais. No formulário de edição, ao carregar um núcleo existente, pré-preencher com as **zonas armazenadas** (não as oficiais); o auto-preenchimento só dispara quando o usuário **altera** o município/território. A validação server-side estrita (zonas == oficiais do município, etc.) vale em **create**; em **update**, só revalidar quando `tseZones` vier alterado na requisição — aceitar zonas legadas divergentes caso contrário. Sub-questão a fechar na implementação: como detectar "tseZones alterado" de forma robusta (comparar com `originalDoc.tseZones`). -- **Cidade com zero zonas no dataset?** Não deveria ocorrer, mas o helper deve falhar fechado: se `tseZonesForCity(city)` retornar vazio para uma cidade válida, tratar como dado faltante e permitir modo manual com aviso, em vez de travar o formulário. O mesmo vale para `tseZonesForTerritory(territory)`. -- **Zona que cobre vários municípios pequenos.** Isso é normal (uma zona, vários municípios). O mapeamento é município→suas zonas, então cada município lista a zona compartilhada; não há ambiguidade para o auto-preenchimento. No modo 3 (território), a união deduplica naturalmente zonas compartilhadas entre municípios do território. Confirmar com o fixture. -- **Ordem e dedup.** `tseZonesForCity` e `tseZonesForTerritory` retornam números únicos em ordem crescente (mesmo padrão de `parseTseZoneNumbers`). -- **Edição de bairro após auto-preenchimento.** Decidido: manter todas as zonas do município selecionadas como ponto de partida (modo 1 → modo 2), agora editáveis. -- **Cache/ISR.** O dado é estático no bundle; sem `unstable_cache`, sem tag. Confirmar com produto. -- **Relação com o item "Import do cadastro oficial de zonas TSE e/ou polígonos GeoJSON" (roadmap linha 59).** Este plano entrega o mapeamento município→zonas e território→zonas (cadastro tabular). Polígonos GeoJSON para o mapa continuam no item da linha 59. Definir se este plano substitui a parte "cadastro" daquele item ou se mantém ambos — recomendação: este plano cobre o cadastro tabular; o item da linha 59 fica restrito a polígonos/GeoJSON. +- **Versão do cadastro TSE.** **Recomendação:** versão mais recente validada em 2026 do “Eleitorado por município e zona” BA, data + SHA-256 no cabeçalho e no fixture. Reavaliar a cada eleição. +- **Teto de chips visíveis.** Um TI tem até ~30 municípios; uma ZE compartilhada pode listar vários. **Recomendação:** mostrar até **8** chips por faixa; se houver mais, chip “+N municípios” / “+N sugestões” que expande a lista (ou abre o combobox com filtro pré-aplicado). Definir N com produto se 8 for baixo em uso real. +- **Prioridade quando as duas fontes sugerem o mesmo município** (irmão do TI e município da ZE). **Recomendação:** deduplicar por nome; uma única chip. +- **Cidade com zero zonas no dataset.** **Recomendação:** falhar fechado no helper (array vazio) e **não** mostrar chip 「{cidade} +」; permitir input manual sem travar. Log/teste garante cobertura dos 417 municípios na fixture. +- **ZE digitada fora do conjunto da geografia atual.** **Recomendação:** não bloquear save; opcional `FieldDescription` âmbar “Zona X não aparece no cadastro dos municípios/TIs selecionados” — só UX, sem `APIError`. +- **Núcleos existentes.** **Recomendação:** carregar zonas/municípios armazenados; sugestões aparecem só para o que ainda falta. Sem backfill. +- **Bairros e sugestões.** Bairro não altera o conjunto oficial município→zona. **Recomendação:** chips de ZE por município/TI ignoram bairros (mesmo com bairro, sugerir o conjunto municipal completo; o usuário remove ZEs irrelevantes manualmente). +- **Cache/ISR.** Dado estático no bundle; sem `unstable_cache`. ## Abordagem proposta ```mermaid flowchart LR - Form["NucleusFormFields (client)
estado: region/city/neighborhood/tseZones"] - Geo["Geografia selecionada"] - Auto1["Modo 1: cidade, sem bairro
tseZonesForCity → read-only"] - Auto2["Modo 2: cidade + bairro
restrito a tseZonesForCity (editável)"] - Auto3["Modo 3: só território
restrito a tseZonesForTerritory (editável)"] - Auto4["Modo 4: sem território/cidade
manual livre 1-999"] - Region["territoryForCity(city)
→ região auto (se cidade)"] - Hidden["hidden inputs:
region, city, neighborhood, tseZones"] - Server["validateNucleusTerritoryAndZones
+ validateGeographyAndZones
revalida os 4 modos (create)"] + Coord["NucleusTerritoryAndZonesFields
estado: regions/cities/
neighborhoods/tseZones"] + Data["bahiaTseZones + bahiaTerritories"] + SugZE["Chips → Zonas
cidade+ / TI+"] + SugCity["Chips → Municípios
irmão TI+ / cidade da ZE+"] + Regions["territoriesForCities
regions derivados"] + Hidden["hidden inputs"] + Server["validateNucleusTerritoryAndZones
geografia + parse ZE
sem igualdade forçada"] - Form --> Geo - Geo --> Auto1 - Geo --> Auto2 - Geo --> Auto3 - Geo --> Auto4 - Form --> Region - Auto1 --> Hidden - Auto2 --> Hidden - Auto3 --> Hidden - Auto4 --> Hidden + Coord --> Data + Data --> SugZE + Data --> SugCity + Coord --> Regions + SugZE --> Coord + SugCity --> Coord + Coord --> Hidden Hidden --> Server ``` -Arquivos: +Componentes: -- **`src/lib/bahiaTseZones.ts`** (novo): export `bahiaMunicipalityTseZones` (record/map de cidade→`number[]` ordenado e único), `tseZonesForCity(city): number[]`, `tseZonesForTerritory(territory): number[]` (união deduplicada e ordenada de `tseZonesForCity` sobre `citiesForTerritory(territory)`), e `isTseZoneOfCity`/`isTseZoneOfTerritory` para validação server-side. Cabeçalho com proveniência (URL, versão validada em 2026, SHA-256), no estilo de `bahiaTerritories.ts`. -- **`tests/fixtures/bahia-tse-zones.official.json`** (novo): evidência independente (não gerada a partir de `bahiaTseZones.ts`) com `provenance` (URL do "Eleitorado por município e zona" BA, data, SHA-256 do download) e a tabela município→zonas. Espelha `bahia-identity-territories.official.json`. -- **`tests/int/bahiaTseZones.int.spec.ts`** (novo): valida que `bahiaTseZones.ts` cobre exatamente os 417 municípios de `CitiesByState.BA`, que cada zona está em 1–999, que `tseZonesForTerritory` bate com a união manual sobre `citiesForTerritory`, e que o conteúdo bate com o fixture. Espelha `bahiaTerritories.int.spec.ts`. -- **`src/components/campaign/NucleusForm.tsx` / `NucleusTerritoryFields.tsx` / `TseZoneInput.tsx`**: subir o estado de `region/city/neighborhood/tseZones` para um componente cliente coordenador (ex.: `NucleusTerritoryFields` passar a ser controlado, ou um wrapper novo em `NucleusFormFields`). `TseZoneInput` ganha um modo `readOnly` que renderiza só os badges (sem input/remover) e um modo `allowedZones` que restringe a entrada aos números permitidos (do município ou do território). Seleção de cidade dispara `tseZonesForCity` + `territoryForCity`; seleção de só território dispara `tseZonesForTerritory`; limpar cidade/território reverte o modo conforme a regra. Ao carregar núcleo existente, pré-preencher com as **zonas armazenadas** (não as oficiais). -- **`src/lib/schemas/nucleus.ts`** (`validateGeographyAndZones`) e **`src/collections/ElectoralNucleus.ts`** (`validateNucleusTerritoryAndZones`): adicionar a regra de consistência geografia↔zonas usando `tseZonesForCity`/`isTseZoneOfCity` e `tseZonesForTerritory`/`isTseZoneOfTerritory`, conforme os quatro modos. Em create, validar tudo; em update, só quando `tseZones` vier alterado (preservar zonas legadas). Mesma mensagem em pt-BR e mesmo padrão de `APIError` 400 já usado para território incompatível. -- **`src/utilities/nucleusUi.ts`** (`parseTseZoneNumbers` / `parseSharedNucleusFormData`): quando aplicável, validar no parse que as zonas enviadas respeitam o modo (defesa em profundidade antes do zod/hook), usando o conjunto permitido derivado da geografia enviada (cidade ou território). -- **Sem migration, sem collection, sem server action.** Todo o fluxo é leitura/escrita no formulário existente + dado estático. +- **`src/lib/bahiaTseZones.ts`** (novo): `bahiaMunicipalityTseZones` (cidade → `number[]` ordenado único), `tseZonesForCity(city)`, `tseZonesForTerritory(territory)` (= união sobre `citiesForTerritory`), `citiesForTseZone(zoneNumber)` (reverso, nomes canônicos ordenados), predicados `isTseZoneOfCity` / `isTseZoneOfTerritory`. Cabeçalho com proveniência TSE. +- **`tests/fixtures/bahia-tse-zones.official.json`** + **`tests/int/bahiaTseZones.int.spec.ts`**: cobertura dos 417 municípios de `CitiesByState.BA`, zonas em 1–999, reverso consistente com o mapa direto, união por TI bate com `citiesForTerritory`, conteúdo = fixture. +- **`src/lib/territorySuggestions.ts`** (novo, puro, testável): funções que, dado `{ cities, regions, tseZones }`, retornam `{ zoneSuggestions: { kind: 'city'|'territory', label: string, zonesToAdd: number[] }[], citySuggestions: { kind: 'sibling'|'zone', label: string, city: string }[] }` já filtradas (só o que falta) e ordenadas. Sem React. +- **`TerritorySuggestionChips`** (em `src/components/campaign/TerritorySuggestionChips.tsx`): lista de botões/Badges `{label} +` com `aria-label` descritivo; `onAccept(suggestion)`. +- **`NucleusTerritoryAndZonesFields`** (novo ou evolução de `NucleusTerritoryFields` + `TseZoneInput` em `src/components/campaign/`): coordenador cliente; monta chips acima de Municípios e acima de Zonas TSE; ao aceitar, atualiza estado e esconde chips resolvidos; `TseZoneInput` vira controlado (`value`/`onChange`) em vez de só `defaultValues`. +- **`NucleusForm.tsx`**: passa a usar o coordenador no lugar dos dois siblings soltos. +- **`src/lib/schemas/nucleus.ts` / `ElectoralNucleus.ts` / `nucleusUi.ts`**: **não** adicionar regra de igualdade zonas==oficiais. Manter validação atual de geografia e parse de ZEs. (A revisão 2026-07-18 remove a validação estrita prevista na versão anterior deste plano.) +- **Sem migration, sem collection, sem server action.** ## Dependências -- **[`territorio-multi-municipio-bairro.md`](territorio-multi-municipio-bairro.md) — pré-requisito de sequenciamento** (decisão 2026-07-17): este plano nasce contra `cities[]` (união de zonas), evitando retrabalho. Ver "Decisões travadas". -- Fora isso, reusa `territoryForCity`/`citiesForTerritory` (`src/lib/bahiaTerritories.ts`), `CitiesByState.BA` (`src/lib/cities`), `parseTseZoneNumbers` (`src/utilities/nucleusUi.ts`), `NucleusTerritoryFields`/`TseZoneInput` existentes, e o padrão de fixture/teste de `bahiaTerritories`. -- O plano [`baseline-eleitoral-tse.md`](baseline-eleitoral-tse.md) **não** depende deste para funcionar (resolve zonas pelas rows de `electionTally`), mas se beneficia da qualidade do input: zonas auto-preenchidas eliminam erro de digitação que distorceria o baseline por núcleo. -- **Polígonos GeoJSON / mapa** (roadmap linha 59) continuam fora deste plano. +- **A1** ([territorio-multi-municipio-bairro.md](territorio-multi-municipio-bairro.md)) — dura, já entregue: `cities[]` / `regions` derivados / `territoriesForCities` / `citiesForTerritory`. +- Reusa `bahiaTerritories.ts`, `CitiesByState.BA`, `parseTseZoneNumbers`, `NucleusTerritoryFields` / `TseZoneInput` / `StrictCombobox`, padrão de fixture de `bahiaTerritories`. +- **[baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)** — dependência suave na outra direção: A4 se beneficia de `tseZones` bem preenchidos; o baseline não bloqueia este plano (`electionTally` já resolve cidade↔zona). +- Polígonos GeoJSON / B4 continuam fora. ## Não escopo -- Importar polígonos/GeoJSON de zonas ou integrar o mapa (roadmap linha 59 e "Mapa / PostGIS" linha 52). -- Alterar o schema `tseZones` ou criar migration — o campo já existe. -- Mapear bairro→zona (dado não existe oficialmente no TSE; continua ambíguo por design). -- Estender o mapeamento para outros estados (todo núcleo é BA). -- Previsão estatística de votos por zona (roadmap linha 60) — aqui só preenchemos o cadastro. -- Alterar a lista de núcleos, filtros ou dashboard — só o formulário de criar/editar núcleo. +- Auto-preenchimento forçado / Zonas TSE read-only (modelo descartado em 2026-07-18). +- Importar polígonos/GeoJSON de zonas (B4 / [mapa-bahia-geometrias.md](mapa-bahia-geometrias.md)). +- Alterar schema `tseZones` ou criar migration. +- Mapear bairro→zona (dado oficial inexistente). +- Estender mapeamento para outros estados. +- UI de baseline/insights (A4/A5) — só melhora a qualidade do input de `tseZones`. +- Alterar lista de núcleos, filtros ou dashboard. ## Referências -- `docs/roadmap.md` (item novo; linha 59 para o relacionado "Import do cadastro oficial de zonas TSE") -- `src/components/campaign/NucleusForm.tsx`, `NucleusTerritoryFields.tsx`, `TseZoneInput.tsx` — formulário a alterar -- `src/lib/bahiaTerritories.ts` e `tests/fixtures/bahia-identity-territories.official.json`, `tests/int/bahiaTerritories.int.spec.ts` — padrão de dado estático + fixture + teste a espelhar +- `docs/roadmap.md` (Trilha A → A2; Janela 1 ordem 3) +- `src/components/campaign/NucleusForm.tsx`, `NucleusTerritoryFields.tsx`, `TseZoneInput.tsx` — formulário a coordenar +- `src/lib/bahiaTerritories.ts`, `tests/fixtures/bahia-identity-territories.official.json`, `tests/int/bahiaTerritories.int.spec.ts` — padrão a espelhar - `src/collections/ElectoralNucleus.ts` — `validateNucleusTerritoryAndZones` - `src/lib/schemas/nucleus.ts` — `validateGeographyAndZones` -- `src/utilities/nucleusUi.ts` — `parseTseZoneNumbers`, `parseSharedNucleusFormData` -- Portal de Dados Abertos do TSE — conjunto "Eleitorado por município e zona" (UF BA): https://dadosabertos.tse.jus.br/ -- AGENTS.md — naming conventions, padrão de leitura/escrita, "Bahia implícita no Núcleo" +- `src/utilities/tseZone.ts` / `nucleusUi.ts` — parse de ZEs +- `docs/design-refs/latest/Formulario-Territorio.{png,html}` — referência visual (banner + “sugerida”) +- Portal de Dados Abertos do TSE — “Eleitorado por município e zona” (UF BA): https://dadosabertos.tse.jus.br/ +- AGENTS.md — naming, Bahia implícita, overrideAccess nas queries (este plano não introduz queries novas) diff --git a/docs/roadmap.md b/docs/roadmap.md index b2b1e0f9..a9397d1d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-18 (código do MVP de Núcleos enviado; priorização eleitoral mantida) +Atualizado em: 2026-07-18 (MVP + Ciclo 2 deployados; A2 revisado: sugestões cruzadas opt-in no formulário de território, em vez de auto-preenchimento forçado) Registro canônico no repositório dos planos futuros e débitos conhecidos. Status operacional do ciclo atual de Núcleos fica em [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc); este arquivo lista o que ainda é futuro ou bloqueador, **em ordem de execução**, com dependências e paralelismo explícitos. @@ -29,7 +29,7 @@ Registro canônico no repositório dos planos futuros e débitos conhecidos. Sta ## Onda 0 — Caminho crítico para `/campanha` em produção -O MVP de Núcleos está **entregue em código** (ondas 1–8 + refactors; commit em `main` em 2026-07-18). A migration `20260718_010733_consolidate_campaign_schema` aplica automaticamente no `pnpm build` da Vercel. O que ainda separa a vertical de uso operacional com dados reais é jurídico + smoke pós-deploy: +O MVP de Núcleos está **entregue e deployado** (ondas 1–8 + refactors; 2026-07-18), junto com A1 (território multi-município/bairro), A3 (baseline TSE 2022 — modelo + import), B1 (overview da lista), C1 (compartilhar página) e D1 (PWA). As migrations (`20260718_010733_consolidate_campaign_schema`, `20260718_190559_territorio_multi_municipio_bairro`, `20260718_195854_add_election_results`) aplicam automaticamente no `pnpm build` da Vercel. O que ainda separa a vertical de uso operacional com dados reais é jurídico + smoke pós-deploy: 1. **Lote jurídico único de LGPD/Consent** _(externo — assessoria jurídica eleitoral; é o caminho crítico da vertical inteira)_. Uma única rodada cobrindo: - Base do art. 11 da LGPD + texto versionado de `Consent.key = 'lideranca-autopreenchimento'` (bloqueador do MVP de Núcleos; o app falha fechado sem a chave). @@ -39,14 +39,22 @@ O MVP de Núcleos está **entregue em código** (ondas 1–8 + refactors; commit - Avaliação de necessidade de RIPD (tratamento em larga escala + dado sensível). - Racional: fatiar em rodadas separadas multiplica o lead time externo; quatro textos + aviso numa rodada só. 2. **Smoke pós-deploy** _(após o build Vercel aplicar a migration)_: conferir `NEXT_PUBLIC_SITE_URL` HTTPS exato, login `/campanha`, criar núcleo de teste, e só então cadastrar o Consent de liderança. Checklist completo no AGENTS.md. -3. **Ativação com dados reais assim que (1) liberar a chave de liderança.** Decisão de sequenciamento: **não** esperar a migração multi-município. Prós: coordenadores estruturam núcleos reais durante as convenções (20/07–05/08); a migration multi-município é in-place com backfill seguro e pode rodar com dados reais pequenos. Contras: uma migration a mais pós-lançamento. O contra é barato; o pró é tempo de campanha, que não volta. +3. **Ativação com dados reais assim que (1) liberar a chave de liderança.** (A decisão de 2026-07-17 de não esperar a migração multi-município ficou superada: A1 foi entregue e deployado antes da ativação — coordenadores já estruturam núcleos reais durante as convenções, 20/07–05/08, com o território definitivo.) 4. **Onboarding do time real** (usuários `geral`/`coordenador`, primeiros núcleos, treinamento básico de campo). ## Campanha (`/campanha`) ### Ciclo 1 — Núcleos (entregue) -MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de composição/infra/fixtures): auth isolada `campaignUser` (`geral` / `coordenador` / `lideranca`), núcleos com slug canônico, designação de coordenador, lideranças, estimativas sugerir/confirmar com versão UUID, atualizações semanais, convites WhatsApp (`wa.me`, hash, uso único), dashboard e hardening. Território atual ainda é `region`/`city`/`neighborhood` unitários; multi-município permanece no item A1. Detalhes: [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). +MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de composição/infra/fixtures): auth isolada `campaignUser` (`geral` / `coordenador` / `lideranca`), núcleos com slug canônico, designação de coordenador, lideranças, estimativas sugerir/confirmar com versão UUID, atualizações semanais, convites WhatsApp (`wa.me`, hash, uso único), dashboard e hardening. Detalhes: [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). + +### Ciclo 2 — entregues em 2026-07-18 (mesclados e deployados) + +- **A1 Território multi-município/bairro** — `regions`/`cities`/`neighborhoods` viram arrays `hasMany` com backfill (`20260718_190559_territorio_multi_municipio_bairro`); bairros exigem município único; regiões derivadas no servidor. [Plano](plans/territorio-multi-municipio-bairro.md). +- **A3 Baseline TSE 2022 — Fase 1** — collections `electionTally`/`electionCandidateVote`/`electionCandidate` (grupo admin `Dados Eleitorais`), migration `20260718_195854_add_election_results`, import local `pnpm db:seed:tse` idempotente por escopo (year, office, turn). Sem UI ainda — a UI é A4. [Plano](plans/baseline-eleitoral-tse.md). +- **B1 Overview da lista de núcleos** — painel agregado (estimativa de votos, cobertura, últimas atualizações) entre filtros e lista, reagindo aos mesmos filtros da URL, com escopo por papel. [Plano](plans/overview-lista-nucleos.md). +- **C1 Compartilhar página** — diálogo no detalhe do núcleo com destinatários WhatsApp (coordenação geral, coordenadores, lideranças engajadas) + copiar link; envia só o link, não concede acesso. [Plano](plans/compartilhar-pagina.md). +- **D1 PWA `/campanha`** — manifest + service worker escopados em `/campanha`, shell offline, toast de instalação (Android/iOS) e limpeza de cache no logout; fundação do push (D2). [Plano](plans/pwa-campanha.md). ### Referências de design (UX Pilot, 2026-07-18) @@ -57,7 +65,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | B1 Overview da lista de núcleos | `Lista-Nucleos-Overview` | [overview-lista-nucleos.md](plans/overview-lista-nucleos.md) | | C1 Compartilhar página | `Compartilhar-Nucleo` | [compartilhar-pagina.md](plans/compartilhar-pagina.md) | | A1 Território multi-município/bairro | `Formulario-Territorio` (bloco Território) | [territorio-multi-municipio-bairro.md](plans/territorio-multi-municipio-bairro.md) | -| A2 Zonas TSE por município | `Formulario-Territorio` (seção Zonas TSE) | [zonas-por-municipio.md](plans/zonas-por-municipio.md) | +| A2 Zonas TSE + sugestões cruzadas | `Formulario-Territorio` (seção Zonas TSE + chips `{rótulo} +`) | [zonas-por-municipio.md](plans/zonas-por-municipio.md) | | A3/A4 Baseline TSE 2022 + Gap vs 2022 | `Baseline-Eleitoral-2022` (+ bloco "Baseline 2022" em `Lista-Nucleos-Overview`) | [baseline-eleitoral-tse.md](plans/baseline-eleitoral-tse.md) | | A5 Insights (5 planos) | `Baseline-Eleitoral-2022` (card "Insights do território", uma linha por insight) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | | C2 Cadastro nominal de apoiadores | `Apoiadores-Lista`, `Apoiador-Ficha`, `Importar-CSV-Apoiadores` | [cadastro-nominal-apoiadores.md](plans/cadastro-nominal-apoiadores.md) | @@ -80,8 +88,8 @@ flowchart TD end subgraph TrilhaA["Trilha A — dados eleitorais e território"] - A1["A1 Território multi-município/bairro
(migration)"] - A2["A2 Zonas TSE por município
(auto-preenchimento)"] + A1["A1 Território multi-município/bairro
(migration) ✓"] + A2["A2 Zonas TSE + sugestões cruzadas
(chips opt-in território↔ZE)"] A3["A3 Baseline TSE 2022 — Fase 1
(import, collections) ✓"] A4["A4 Baseline no produto + Gap vs 2022
(detalhe, overview)"] A5["A5 Insights: conversão, classificação,
alavancagem, mobilização, competitiva"] @@ -89,14 +97,14 @@ flowchart TD end subgraph TrilhaB["Trilha B — superfícies de coordenação"] - B1["B1 Overview da lista de núcleos"] + B1["B1 Overview da lista de núcleos ✓"] B2["B2 Mapa Fase 1 (geometrias)"] B3["B3 Mapa Fase 2 (Leaflet nas superfícies)"] B4["B4 Camada de zonas TSE no mapa"] end subgraph TrilhaC["Trilha C — operação de campo"] - C1["C1 Compartilhar página"] + C1["C1 Compartilhar página ✓"] C2["C2 Cadastro nominal de apoiadores"] C3["C3 Eventos / agenda de mobilização"] C4["C4 Demandas"] @@ -104,7 +112,7 @@ flowchart TD end subgraph TrilhaD["Trilha D — plataforma e engajamento"] - D1["D1 PWA /campanha"] + D1["D1 PWA /campanha ✓"] D2["D2 Notificações (push + sino)"] end @@ -130,20 +138,20 @@ flowchart TD JUR -.chave de push.-> D2 ``` -Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, B1, B2, C1, C2 (engenharia — produção espera o jurídico), D1, além dos fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **A3 entregue** (2026-07-18) — collections + `pnpm db:seed:tse`. +Itens sem seta de entrada (**paralelizáveis a qualquer momento**): B2, C2 (engenharia — produção espera o jurídico), além dos fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **Entregues em 2026-07-18** (✓ no grafo): A1, A3, B1, C1 e D1 — ver "Ciclo 2" acima. Destravados por eles: A2 (nasce contra `cities[]`; UX revisada 2026-07-18 para chips `{rótulo} +` opt-in), A4 (A3 + B1 prontos), C3/C4 (A1 pronto) e D2 push (D1 pronto; falta a chave jurídica). ### Sequência de execução por janela do calendário **Janela 1 — agora → 05/08 (convenções): colocar a vertical em produção e consertar a fundação de dados.** -| Ordem | Item | Plano | Depende de | Paralelizável com | -| ----- | ----------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ----------------- | -| 1 | Onda 0 (jurídico em lote + deploy + lançamento) | — | externo | tudo | -| 2 | A1 Território multi-município/bairro **(entregue 2026-07-18)** | [detalhes](plans/territorio-multi-municipio-bairro.md) | — | B1, C1, A3 | -| 3 | A2 Zonas TSE por município (auto-preenchimento) | [detalhes](plans/zonas-por-municipio.md) | A1 (nasce contra `cities[]`) | B1, C1, A3 | -| 4 | B1 Overview da lista de núcleos | [detalhes](plans/overview-lista-nucleos.md) | — | A1, A2, C1 | -| 5 | C1 Compartilhar página (quick win de campo) | [detalhes](plans/compartilhar-pagina.md) | — | tudo | -| 6 | A3 Baseline TSE 2022 — Fase 1 (import) **feito** | [detalhes](plans/baseline-eleitoral-tse.md) | — (dado público; sem bloqueador LGPD) | tudo | +| Ordem | Item | Plano | Depende de | Paralelizável com | +| ----- | ---------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------- | ----------------- | +| 1 | Onda 0 (jurídico em lote + deploy + lançamento) | — | externo | tudo | +| 2 | A1 Território multi-município/bairro **(entregue 2026-07-18)** | [detalhes](plans/territorio-multi-municipio-bairro.md) | — | B1, C1, A3 | +| 3 | A2 Zonas TSE + sugestões cruzadas (chips opt-in território↔ZE) | [detalhes](plans/zonas-por-municipio.md) | A1 (nasce contra `cities[]`) | B1 ✓, C1 ✓, A3 ✓ | +| 4 | B1 Overview da lista de núcleos **(entregue 2026-07-18)** | [detalhes](plans/overview-lista-nucleos.md) | — | A1, A2, C1 | +| 5 | C1 Compartilhar página **(entregue 2026-07-18)** | [detalhes](plans/compartilhar-pagina.md) | — | tudo | +| 6 | A3 Baseline TSE 2022 — Fase 1 (import) **(entregue 2026-07-18)** | [detalhes](plans/baseline-eleitoral-tse.md) | — (dado público; sem bloqueador LGPD) | tudo | **Janela 2 — 05/08 → 16/08 (pré-propaganda): base nominal + inteligência + agenda prontas para o arranque.** @@ -155,13 +163,13 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, B1, B2, **Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.** -| Ordem | Item | Plano | Depende de | Paralelizável com | -| ----- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------- | -| 10 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | B2/B3, C4, D1 | -| 11 | B2 + B3 Mapa da Bahia (geometrias + Leaflet) | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1+B2 (suave: A4 para coroplético de baseline/classe) | A5, C4, D1 | -| 12 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, B3, D1 | -| 13 | D1 PWA `/campanha` | [detalhes](plans/pwa-campanha.md) | — | tudo | -| 14 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, B3, C4 | +| Ordem | Item | Plano | Depende de | Paralelizável com | +| ----- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------- | +| 10 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | B2/B3, C4, D1 | +| 11 | B2 + B3 Mapa da Bahia (geometrias + Leaflet) | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1+B2 (suave: A4 para coroplético de baseline/classe) | A5, C4, D1 | +| 12 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, B3, D1 | +| 13 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo | +| 14 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, B3, C4 | **Janela 4 — set → 04/10 (reta final): dobradinha, dia D e estabilização.** @@ -179,12 +187,13 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, B1, B2, - **Reset de senha self-service + foto de perfil** (UX adiada do ciclo 1). _(plano MVP)_ - **Higiene de código:** varredura PascalCase dos componentes legados. _(notebook Núcleos)_ -**Cortes seguros se o prazo apertar** (nesta ordem): B4 camada de zonas, B3 mapa Leaflet, C4 demandas, D2 push (mantendo o sino), D1 PWA, fill-ins. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap — são respectivamente o risco legal, a base de dados, a operação da propaganda e o instrumento de alocação de esforço. +**Cortes seguros se o prazo apertar** (nesta ordem): B4 camada de zonas, B3 mapa Leaflet, C4 demandas, D2 push (mantendo o sino), fill-ins (D1 PWA já entregue, saiu da lista de cortes). **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap — são respectivamente o risco legal, a base de dados, a operação da propaganda e o instrumento de alocação de esforço. ### Itens consolidados/removidos nesta revisão (2026-07-17) - **"Insight: Gap vs 2022" como item separado** — removido: é a Fase 4 do plano de [baseline TSE 2022](plans/baseline-eleitoral-tse.md) (item A4), não um item próprio. - **"Import do cadastro oficial de zonas TSE e/ou polígonos GeoJSON"** — absorvido: o cadastro tabular é o plano [zonas-por-municipio](plans/zonas-por-municipio.md) (A2); os polígonos são a camada de zonas do [plano do mapa](plans/mapa-bahia-geometrias.md) (B4). +- **"Sugestões cruzadas no formulário de território"** (chips `{Município} +` / `{TI} +` em ZEs; irmãos do TI e cidades da ZE em Municípios) — absorvido em A2 na revisão 2026-07-18; substitui o auto-preenchimento forçado + read-only da versão 2026-07-17 do mesmo plano. - **"Notificações WhatsApp Business API"** — movido para fora de escopo: a Meta veda o WhatsApp Business API para campanhas políticas no Brasil e a Res. TSE 23.610 (art. 33) veda disparo em massa (pesquisa em [cadastro-nominal-apoiadores.md](plans/cadastro-nominal-apoiadores.md)). Push + sino ([notifications.md](plans/notifications.md)) cobrem a necessidade. - **"Previsão estatística de votos"** — mantido, mas explicitamente **fora do horizonte deste ciclo eleitoral**: sem dado acumulado suficiente antes de 04/10; o baseline TSE + estimativa manual + insights são o "mínimo honesto" (design-ux §4.5). Reavaliar pós-eleição. - **Dependência baseline → zonas-por-municipio** — rebaixada de dura para suave: `citiesForTerritory` já existe e as rows de `electionTally` resolvem cidade↔zona (ver revisão nos planos). diff --git a/src/components/campaign/ShareNucleusDialog.tsx b/src/components/campaign/ShareNucleusDialog.tsx index 915a551c..a8bda7e3 100644 --- a/src/components/campaign/ShareNucleusDialog.tsx +++ b/src/components/campaign/ShareNucleusDialog.tsx @@ -1,18 +1,13 @@ 'use client' -import { useState } from 'react' import { CheckIcon, CopyIcon, LinkIcon, MessageCircleIcon } from 'lucide-react' +import { useState } from 'react' import type { NucleusShareRecipientsResult } from '@/app/(campaign)/campanha/(app)/nucleos/[slug]/shareRecipientsActions' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' import { Avatar, AvatarFallback } from '@/components/ui/Avatar' import { Button } from '@/components/ui/button' -import { - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' +import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Spinner } from '@/components/ui/Spinner' import type { NucleusShareRecipient } from '@/utilities/nucleusShareRecipients' import { buildWhatsAppUrl } from '@/utilities/phone' @@ -141,7 +136,7 @@ export const ShareNucleusDialog = ({ } return ( - + Compartilhar núcleo @@ -180,7 +175,7 @@ export const ShareNucleusDialog = ({ ) : null} {data && !loading ? ( -
+
-