Align docs with cycle 2 delivery and add roadmap-item skill.
Mark A1/A3/B1/C1/D1 as shipped, revise the A2 zone-suggestion plan, and tighten the share dialog overflow.
This commit is contained in:
parent
b035cc4a2f
commit
202f1579ec
15 changed files with 429 additions and 142 deletions
|
|
@ -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-<buildId>` 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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
110
.cursor/skills/roadmap-item/SKILL.md
Normal file
110
.cursor/skills/roadmap-item/SKILL.md
Normal file
|
|
@ -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/<slug>.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/<slug>.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/<slug>.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/<slug>.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.
|
||||
84
.cursor/skills/roadmap-item/plan-template.md
Normal file
84
.cursor/skills/roadmap-item/plan-template.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Template de plano (`docs/plans/<slug>.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
|
||||
# <Título do item em pt-BR>
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: <YYYY-MM-DD>
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (<seção e/ou ID, ex.: "Trilha C, item C6">)
|
||||
Responsável: —
|
||||
|
||||
## Referência visual (UX Pilot)
|
||||
|
||||
<!-- Só se existir design em docs/design-refs/latest/. -->
|
||||
|
||||
Design: [`<Nome>.png`](../design-refs/latest/<Nome>.png) · [`<Nome>.html`](../design-refs/latest/<Nome>.html)
|
||||
|
||||

|
||||
|
||||
Como usar:
|
||||
|
||||
- **Adotar a estrutura:** <o que do design corresponde aos objetivos deste plano>.
|
||||
- **Fora deste plano:** <partes do design que pertencem a outros planos, com links>.
|
||||
- **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
|
||||
|
||||
<Estado atual do sistema (com caminhos de arquivo reais), o problema/oportunidade, e a decisão
|
||||
de produto que originou o item, com data. Responde "por que isso existe".>
|
||||
|
||||
## Objetivos
|
||||
|
||||
- <Resultados verificáveis, um por bullet. Incluir os "guardrails" que valem para o item
|
||||
inteiro: access control, sem migration / com migration, sem Consent / qual Consent, etc.>
|
||||
|
||||
## Decisões travadas
|
||||
|
||||
- **<Decisão em negrito>.** <Racional e fonte com data (decisão de produto YYYY-MM-DD,
|
||||
AGENTS.md, roadmap...). Decisão sem "por quê" não é decisão travada.>
|
||||
- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (<listar os principais
|
||||
nomes propostos>), strings visíveis em pt-BR.
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
- **<Pergunta>?** <Contexto curto.> **Recomendação:** <posição concreta>. <"Definir com
|
||||
produto" quando aplicável — mas nunca pergunta sem recomendação.>
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
\```mermaid
|
||||
flowchart LR
|
||||
<fluxo de dados/componentes da solução>
|
||||
\```
|
||||
|
||||
Componentes:
|
||||
|
||||
- **`<NomeDoComponente/função>`** (em `src/<caminho real>`): <responsabilidade, assinatura,
|
||||
o que reusa. Queries Payload com `user` levam `overrideAccess: false`; escrita
|
||||
multi-collection é transacional; hooks propagam `req`.>
|
||||
- <um bullet por componente/arquivo novo ou alterado>
|
||||
- **<Migration>**: <se houver: nome sugerido `pnpm migrate:create <nome>`, o que adiciona,
|
||||
se tem backfill. Se não houver: "Sem migration, sem collection, sem server action.">
|
||||
|
||||
## Dependências
|
||||
|
||||
- <Itens do roadmap dos quais depende (duras e suaves, identificadas) — ou "Nenhuma de
|
||||
outro plano." + o que reusa do código existente, com caminhos.>
|
||||
|
||||
## Não escopo
|
||||
|
||||
- <Cada exclusão explícita, citando o plano/item do roadmap para onde ela vai.>
|
||||
|
||||
## Referências
|
||||
|
||||
- `docs/roadmap.md` (<seção/linhas>)
|
||||
- <arquivos-fonte reais que o implementador vai abrir, um por bullet, com o porquê>
|
||||
- AGENTS.md — <quais convenções se aplicam: Campaign auth, naming, overrideAccess, etc.>
|
||||
````
|
||||
|
||||
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.
|
||||
18
AGENTS.md
18
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-<buildId>` 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/*)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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-<buildId>` (`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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||

|
||||
|
||||
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)<br/>estado: region/city/neighborhood/tseZones"]
|
||||
Geo["Geografia selecionada"]
|
||||
Auto1["Modo 1: cidade, sem bairro<br/>tseZonesForCity → read-only"]
|
||||
Auto2["Modo 2: cidade + bairro<br/>restrito a tseZonesForCity (editável)"]
|
||||
Auto3["Modo 3: só território<br/>restrito a tseZonesForTerritory (editável)"]
|
||||
Auto4["Modo 4: sem território/cidade<br/>manual livre 1-999"]
|
||||
Region["territoryForCity(city)<br/>→ região auto (se cidade)"]
|
||||
Hidden["hidden inputs:<br/>region, city, neighborhood, tseZones"]
|
||||
Server["validateNucleusTerritoryAndZones<br/>+ validateGeographyAndZones<br/>revalida os 4 modos (create)"]
|
||||
Coord["NucleusTerritoryAndZonesFields<br/>estado: regions/cities/<br/>neighborhoods/tseZones"]
|
||||
Data["bahiaTseZones + bahiaTerritories"]
|
||||
SugZE["Chips → Zonas<br/>cidade+ / TI+"]
|
||||
SugCity["Chips → Municípios<br/>irmão TI+ / cidade da ZE+"]
|
||||
Regions["territoriesForCities<br/>regions derivados"]
|
||||
Hidden["hidden inputs"]
|
||||
Server["validateNucleusTerritoryAndZones<br/>geografia + parse ZE<br/>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)
|
||||
|
|
|
|||
|
|
@ -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<br/>(migration)"]
|
||||
A2["A2 Zonas TSE por município<br/>(auto-preenchimento)"]
|
||||
A1["A1 Território multi-município/bairro<br/>(migration) ✓"]
|
||||
A2["A2 Zonas TSE + sugestões cruzadas<br/>(chips opt-in território↔ZE)"]
|
||||
A3["A3 Baseline TSE 2022 — Fase 1<br/>(import, collections) ✓"]
|
||||
A4["A4 Baseline no produto + Gap vs 2022<br/>(detalhe, overview)"]
|
||||
A5["A5 Insights: conversão, classificação,<br/>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).
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-md">
|
||||
<DialogContent className="max-h-[90vh] overflow-x-hidden overflow-y-auto sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Compartilhar núcleo</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
|
@ -180,7 +175,7 @@ export const ShareNucleusDialog = ({
|
|||
) : null}
|
||||
|
||||
{data && !loading ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex min-w-0 flex-col gap-5">
|
||||
<RecipientSection
|
||||
title="Com coordenação geral"
|
||||
roleLabel="Coord. Geral"
|
||||
|
|
@ -209,7 +204,12 @@ export const ShareNucleusDialog = ({
|
|||
<LinkIcon className="size-4 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">{data.nucleusUrl}</span>
|
||||
</div>
|
||||
<Button type="button" variant="secondary" className="min-h-11 shrink-0" onClick={copyLink}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="min-h-11 shrink-0"
|
||||
onClick={copyLink}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon data-icon="inline-start" aria-hidden="true" />
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in a new issue