diff --git a/docs/plans/escala-dry-pos-e1.md b/docs/plans/escala-dry-pos-e1.md new file mode 100644 index 00000000..bdbaa2cb --- /dev/null +++ b/docs/plans/escala-dry-pos-e1.md @@ -0,0 +1,114 @@ +# Escala e DRY pós-E1+E3 (metas, prioridade, estratégia manual) + +Status: registrado no roadmap (fases pendentes) +Atualizado em: 2026-07-19 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha E, item E6) +Responsável: — + +## Contexto + +O E1+E3 ([mapa-projecao-municipios.md](mapa-projecao-municipios.md)) entrega `voteGoals` (`good`/`regular`/`minimum`), `priority` (`alta|normal`), `dobradinhaNotes` e `nextSteps` em `electoralNucleus` (migration `20260719_054522_add_nucleus_goals_strategy`), com UI no detalhe (`NucleusVoteGoals`), formulário, inteligência, filtro `?priority=alta`, somas no overview B1 e KPI "Meta regular (soma)" no dashboard geral. Domínio centralizado em `src/utilities/voteGoals.ts` (`getVoteGoalsOrderViolation`, `aggregateVoteGoals`, `sumVoteGoals`, `voteGoalProgressPercent`); select compartilhado `nucleusVoteGoalsSelect` em `nucleusViewModels.ts`. + +Duas passagens `/simplify` no mesmo branch já limparam o que cabia em cleanup: select fragment único, remoção de `voteGoals` do `nucleusListSelect` (lista só usa `priority`), remoção de `priority` do select do dashboard geral, `aggregateVoteGoals` em um passe no overview VM, labels E3 em `nucleusUi.ts` (client-safe), `step={1}` nos inputs de meta, testes esparsos de ordem. + +Os revisores (performance / reuse / quality) marcaram como **importantes e maiores que simplify** os follow-ups abaixo. Sem registro, E2/E4 e B3 (coroplético com metas) herdam o mesmo custo de I/O na lista de núcleos. + +1. **Query duplicada na página `/campanha/nucleos`.** `page.tsx` chama em paralelo `loadNucleusListPageData` (paginado, `depth: 1`) e `loadNucleusListOverviewData` (`pagination: false`, conjunto filtrado inteiro). Overview precisa do agregado; a lista precisa da página — mas hoje são **dois round-trips** sobre linhas sobrepostas. Em filtros amplos, o fetch unpaginado do overview domina. +2. **Geografia resolvida duas vezes para baseline no overview.** `nucleusListOverviewPageData.ts` mapeia com `toNucleusElectionGeographyInput`; `loadNucleusBaseline2022Overview` chama `resolveNucleusElectionGeography` de novo por núcleo (mais pesado: `tseZonesForCity`, interseções). +3. **Agregados de metas só em memória.** `buildNucleusListOverviewViewModel` e `buildGeneralDashboardViewModel` carregam todos os núcleos do escopo e somam com `aggregateVoteGoals` / `sumVoteGoals`. O precedente C6 (`supporterListOverviewAggregate.ts`, `COUNT(*) FILTER`) ainda não foi aplicado a metas/prioridade. +4. **DRY de formatação e KPIs.** `formatElectionNumber` já existe em `electionInsights.ts` e é usado em `NucleusVoteGoals`, mas `NucleusCard`, `NucleusList`, `NucleusListOverview` e `CampaignDashboard` ainda instanciam `Intl.NumberFormat('pt-BR')` local. `percentage(part, total)` está duplicado em `nucleusListOverviewViewModels.ts` e `campaignDashboardViewModels.ts`. Rótulos de cenário (Bom/Regular/Mínimo) estão hardcoded em quatro superfícies. +5. **`DashboardNucleusRecord` enganoso para não-`geral`.** `toDashboardNucleusRecord` sempre preenche `voteGoals`/`priority`, mas selects de coordenador/liderança omitem esses campos → nulls/`'normal'` sintéticos. + +**Explicitamente fora (revisores pediram skip no simplify):** `NucleusPriorityBadge` como componente (só 3 call sites), merge Zod parcial de `voteGoals` no patch (Payload `beforeValidate` já faz merge), mover `voteGoals.ts` para `src/lib/` por layering, tipos `VoteGoalsFields` vs `VoteGoalsViewModel` (cerimônia aceitável), bug `hasError` em `LeadershipPrimaryContactAction.tsx` (pré-existente em `main`, C9). + +## Objetivos + +- Abrir `/campanha/nucleos` com filtro amplo não escala com **duas** queries full-scan equivalentes; agregados de metas/prioridade preferem **uma** fonte de verdade (loader compartilhado ou SQL aggregate). +- Card "Baseline 2022" no overview não re-resolve geografia que o mapper de núcleos já calculou. +- Formatação de votos/metas e percentuais de KPI reutilizam helpers únicos (`formatElectionNumber`, `campaignPercentage`, `voteGoalScenarioLabels`). +- Guardrails: Fases 1–2 podem ser sem migration (loader/SQL read-only); sem Consent (metas não são PII). Access continua `overrideAccess: false` + selects/VMs por papel. Valores enum `alta|normal` permanecem dados em português (AGENTS.md). + +## Decisões travadas + +- **Um item E6, quatro fases ordenadas.** Mesmo racional de A7/C8/C10: um ID no roadmap, PRs por fase. Ordem: loader/aggregate (custo real) → cache de geografia baseline → DRY UI/helpers → tipos do dashboard por papel (qualidade). +- **Dependência dura de E1+E3 mergeados em `main`.** Só faz sentido com campos e UI já no produto; não reabre escopo de E2/E4. +- **Fase 1 não remove o painel overview nem o KPI do dashboard.** Otimiza como os dados chegam; paridade com a planilha permanece. +- **Cortável se o número de núcleos ativos permanecer pequeno** (dezenas). Vira não-cortável quando B3 coroplético ou E4 import multiplicarem o conjunto filtrado. +- **i18n e naming** (AGENTS.md): identificadores em inglês (`loadNucleusListPageBundle`, `nucleusListOverviewAggregate`, `voteGoalScenarioLabels`, `formatNullableElectionNumber`); strings visíveis e valores enum `alta|normal` em pt-BR. + +## Questões em aberto + +- **Fase 1: unificar loaders vs SQL aggregate puro?** **Recomendação:** espelhar C6 — `nucleusListOverviewAggregate.ts` com `SUM`/`COUNT(*) FILTER` espelhando `buildNucleusListWhere` + access; lista paginada mantém query própria; overview consome só aggregate + preview de updates (não re-lê todos os núcleos para somar metas). Unificar lista+overview num único `find` unpaginado só se a paginação puder ser in-memory sem regressão de `depth`/coordinadores — validar com teste int. +- **Fase 2: passar geografia resolvida ou memoizar no contexto da request?** **Recomendação:** estender `NucleusListOverviewNucleusRecord` com o objeto já resolvido (ou mapa `id → NucleusElectionGeography`) e fazer `loadNucleusBaseline2022Overview` aceitar entrada pré-resolvida; fallback para resolve atual em outros call sites. +- **Fase 4: tipos separados por role no dashboard?** **Recomendação:** `DashboardGeralNucleusRecord` com `voteGoals` + `DashboardScopedNucleusRecord` sem — `buildScopedDashboardViewModel` deixa de receber campos fantasma. + +## Abordagem proposta + +```mermaid +flowchart TD + E1["E1+E3 Metas/prioridade/E3 ✓"] --> F1 + F1["Fase 1 — Aggregate SQL ou loader único
(metas + prioritários + KPIs)"] + F1 --> F2["Fase 2 — Geografia baseline sem re-resolve"] + F1 --> F3["Fase 3 — formatElectionNumber + percentage + scenario labels"] + F1 --> F4["Fase 4 — Dashboard records por role"] + F1 -.menos I/O.-> E4["E4 Import planilha"] + F1 -.menos I/O.-> B3["B3 Coroplético com metas"] + F2 -.mesmo path.-> A7["A7 Baseline loader"] +``` + +### Fase 1 — Aggregate / loader da lista de núcleos + +- Novo `src/utilities/nucleusListOverviewAggregate.ts` (ou extensão de `nucleusListOverviewPageData.ts`): uma query drizzle/SQL com `SUM(vote_goals_regular)` etc. e `COUNT(*) FILTER (WHERE priority = 'alta')`, espelhando `buildNucleusListWhere` + constraint de access do usuário (mesmo padrão de `supporterListOverviewAggregate.ts`). +- Dashboard geral: reutilizar o mesmo aggregate para `regularVoteGoalTotal` em vez de `sumVoteGoals` sobre todos os docs — ou query dedicada `status = ativo` se o escopo for global. +- Opcional na mesma fase: fundir `loadNucleusListOverviewData` com parte do list loader se o teste de perf mostrar ganho claro; caso contrário, manter duas queries mas overview **não** materializa array completo só para somar metas. +- Testes: unit do SQL/where mirror; int com filtros `priority=alta` + múltiplos núcleos assertando totais. + +### Fase 2 — Cache de geografia no overview → baseline + +- Em `nucleusListOverviewPageData.ts`, persistir geografia resolvida no record passado a `loadNucleusBaseline2022Overview`. +- Ajustar `nucleusElectoralBaseline.ts` para aceitar geografia pré-computada (overload ou tipo `PreresolvedNucleusGeography`). +- Critério: uma passagem `resolveNucleusElectionGeography` por núcleo no path do overview. + +### Fase 3 — DRY de formatação e rótulos + +- `src/lib/electionInsights.ts`: `formatNullableElectionNumber(value, empty = '—')`. +- Substituir `Intl.NumberFormat` local em `NucleusCard`, `NucleusList`, `NucleusListOverview`, `CampaignDashboard`. +- `src/utilities/voteGoals.ts`: `voteGoalScenarioLabels` (`good`/`regular`/`minimum` → Bom/Regular/Mínimo); usar em `NucleusForm`, `NucleusVoteGoals`, `NucleusListOverview`. +- Extrair `campaignPercentage` (ex. `src/utilities/campaignMath.ts`) usado por overview e dashboard. +- Opcional: `NucleusPriorityBadge` se os três call sites incomodarem no próximo touch. + +### Fase 4 — Dashboard nucleus records por papel + +- Separar tipos/loader: geral carrega `voteGoals`; coordenador/liderança não. +- `toDashboardNucleusRecord` vira duas funções ou genérico com select tipado. + +**Migration:** nenhuma nas quatro fases (só leitura/agregação e tipos). + +## Dependências + +- **Dura:** E1 Metas + prioridade e E3 campos manuais — merge em `main` com migration `20260719_054522`. +- **Suave:** E4 import planilha e B3 coroplético — amplificam custo se Fase 1 atrasar; A7 compartilha loader de baseline com Fase 2. +- Reusa: `voteGoals.ts`, `nucleusVoteGoalsSelect`, `buildNucleusListWhere`, `supporterListOverviewAggregate.ts` (padrão), `formatElectionNumber`, `loadNucleusBaseline2022Overview`. + +## Não escopo + +- E2 série 2014/2018 → permanece no [mapa-projecao-municipios.md](mapa-projecao-municipios.md). +- E4 seed da planilha → idem. +- Dobradinha estruturada A6 / import nominal de pessoas da planilha. +- Renomear enum `alta` → `high` (migration + backfill). +- Zod merge de `voteGoals` parcial no patch. +- `LeadershipPrimaryContactAction` `hasError` (C9). + +## Referências + +- `docs/roadmap.md` (Trilha E, E6; E1–E5) +- `docs/plans/mapa-projecao-municipios.md` — plano pai E1–E5 +- `docs/plans/escala-dry-pos-a4.md` / `escala-dry-pos-c6.md` — precedente pós-`/simplify` +- `src/app/(campaign)/campanha/(app)/nucleos/page.tsx` — parallel loaders +- `src/utilities/nucleusPageData.ts` / `nucleusListOverviewPageData.ts` +- `src/utilities/nucleusListOverviewViewModels.ts` / `campaignDashboardViewModels.ts` +- `src/utilities/voteGoals.ts` / `nucleusViewModels.ts` +- `src/utilities/nucleusElectoralBaseline.ts` — `resolveNucleusElectionGeography`, `loadNucleusBaseline2022Overview` +- `src/utilities/supporterListOverviewAggregate.ts` — padrão SQL aggregate +- `src/lib/electionInsights.ts` — `formatElectionNumber` +- AGENTS.md — naming, `overrideAccess: false`, enum values em português como dado diff --git a/docs/plans/mapa-projecao-municipios.md b/docs/plans/mapa-projecao-municipios.md index f5062d6f..41dc4b75 100644 --- a/docs/plans/mapa-projecao-municipios.md +++ b/docs/plans/mapa-projecao-municipios.md @@ -1,7 +1,7 @@ # Mapa de projeção de votos por município (equiparar e superar as planilhas 2026) Status: rascunho -Atualizado em: 2026-07-19 +Atualizado em: 2026-07-19 (revisão access E1/E3 — metas/prioridade/E3 sem `canSetDerivedNucleusField`; escrita via `canUpdateElectoralNucleus` + selects/VMs) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha E — itens E1–E5, "Ciclo 3") Responsável: — @@ -56,7 +56,7 @@ flowchart LR ### E1 — Metas em cenários + prioridade no Núcleo (migration) -- **`electoralNucleus`** (`src/collections/ElectoralNucleus.ts`): grupo `voteGoals` (`good`/`regular`/`minimum`, numbers opcionais, validação `good ≥ regular ≥ minimum`) e `priority` (select `alta|normal`, default `normal`, indexado). Escrita staff — `geral` ou coordenador designado, mesma regra da estimativa confirmada (`canSetDerivedNucleusField`/padrões em `src/utilities/campaignAccess.ts`); leitura para os três papéis. +- **`electoralNucleus`** (`src/collections/ElectoralNucleus.ts`): grupo `voteGoals` (`good`/`regular`/`minimum`, numbers opcionais, validação `good ≥ regular ≥ minimum`) e `priority` (select `alta|normal`, default `normal`, indexado). Escrita staff via `canUpdateElectoralNucleus` (`geral` ou coordenador designado no núcleo) — **sem** `canSetDerivedNucleusField` (esse helper é só Payload-admin, usado nas estimativas derivadas). Leitura para os três papéis; E3 (`dobradinhaNotes`/`nextSteps`) staff-only via select/VM como `strengths`/`risks`. - **UI:** card "Metas 2026" na aba Visão geral (3 cenários + `Progress` da estimativa confirmada vs. meta regular, ao lado de `NucleusElectoralBaseline`); filtro `priority` na lista (padrão dos filtros existentes em `src/utilities/nucleusUi.ts` / `buildNucleusListWhere`); bloco no overview B1 (`src/components/campaign/NucleusListOverview.tsx`): soma das metas por cenário no conjunto filtrado + contagem de prioritários; dashboard: soma estadual das metas (equivalente à "META: 71.000"). - **Migration:** `pnpm migrate:create add_nucleus_goals_strategy` (uma migration só para E1+E3), `pnpm generate:types` (skill `payload-migrations`). diff --git a/docs/roadmap.md b/docs/roadmap.md index f26ad031..1287e526 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 Escala e DRY pós-C3 implementado (Fases 1–5; feed O(n) condicional fora de escopo); C8 Escala e DRY pós-C6 implementado e mesclado em `main` (Fases 1–4 + duas passagens `/simplify`); C9 Escala e DRY pós-C8 implementado e mesclado em `main` (Fases 1–4 + simplify); C10 registrado — escala/DRY pós-C9 do `/simplify`; **C11 registrado** — escala/DRY pós-C7 do `/simplify` — [plano](plans/escala-dry-pos-c7.md); B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md); **Onda 0 MVP** — Consent/privacidade provisórios auto-provisionados (migrations) + `/privacidade`; hold de PII real até jurídico — [plano](plans/onda-0.md); **O0+ registrado** — escala/DRY pós-Onda 0 do `/simplify` — [plano](plans/escala-dry-pos-onda0.md); **Visitados recentemente MVP** implementado (fill-in, client-side `localStorage`) — [plano](plans/visitados-recentemente.md); **VR+ registrado** — escala/DRY pós-visitados recentemente do `/simplify` — [plano](plans/escala-dry-pos-visitados-recentemente.md)) +Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 Escala e DRY pós-C3 implementado (Fases 1–5; feed O(n) condicional fora de escopo); C8 Escala e DRY pós-C6 implementado e mesclado em `main` (Fases 1–4 + duas passagens `/simplify`); C9 Escala e DRY pós-C8 implementado e mesclado em `main` (Fases 1–4 + simplify); C10 registrado — escala/DRY pós-C9 do `/simplify`; **C11 registrado** — escala/DRY pós-C7 do `/simplify` — [plano](plans/escala-dry-pos-c7.md); B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md); **E6 registrado** — escala/DRY pós-E1+E3 do `/simplify` (aggregate lista/overview, geografia baseline, DRY formatação) — [plano](plans/escala-dry-pos-e1.md); **Onda 0 MVP** — Consent/privacidade provisórios auto-provisionados (migrations) + `/privacidade`; hold de PII real até jurídico — [plano](plans/onda-0.md); **O0+ registrado** — escala/DRY pós-Onda 0 do `/simplify` — [plano](plans/escala-dry-pos-onda0.md); **Visitados recentemente MVP** implementado (fill-in, client-side `localStorage`) — [plano](plans/visitados-recentemente.md); **VR+ registrado** — escala/DRY pós-visitados recentemente do `/simplify` — [plano](plans/escala-dry-pos-visitados-recentemente.md)) 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. @@ -87,7 +87,8 @@ MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de Objetivo: equiparar e superar as planilhas "Mapa de projeção de votos Solla 2026" (`docs/sheets/`) dentro do produto — metas em cenários, prioridade, série histórica com tendência, dobradinhas/encaminhamentos manuais e import único da planilha. Decisões de produto (2026-07-19): **sem entidade nova por município** (`electoralNucleus` absorve a camada estratégica) e **série 2014/2018 vem do TSE oficial** (estende o seed A3), não dos números da planilha. [Plano único](plans/mapa-projecao-municipios.md) para os cinco itens: -- **E1 Metas em cenários + prioridade** — grupo `voteGoals` (`good`/`regular`/`minimum`) + `priority` (`alta|normal`) em `electoralNucleus` (migration compartilhada com E3); card "Metas 2026" no detalhe, filtro na lista, somas no overview B1 e no dashboard (meta estadual derivada). +- **E1 Metas em cenários + prioridade** — grupo `voteGoals` (`good`/`regular`/`minimum`) + `priority` (`alta|normal`) em `electoralNucleus` (migration compartilhada com E3); card "Metas 2026" no detalhe, filtro na lista, somas no overview B1 e no dashboard (meta estadual derivada). Passagem `/simplify` no branch; débitos maiores → E6. +- **E6 Escala e DRY pós-E1+E3 registrado (2026-07-19)** — débitos do `/simplify` que não entraram no cleanup: query duplicada lista+overview, re-resolve de geografia no baseline do overview, aggregate SQL de metas/prioridade (padrão C6), DRY `formatElectionNumber`/`campaignPercentage`/`voteGoalScenarioLabels`, tipos do dashboard por papel. [Plano](plans/escala-dry-pos-e1.md). - **E2 Série histórica TSE 2014/2018 + tendência** — seed `db:seed:tse` parametrizado por ano (só dep. federal, turno 1, BA); `computeVoteTrend` → `queda|mantem|aumento|semBaseline` derivado em leitura (sem migration); série + badge no baseline card e distribuição no overview. - **E3 Dobradinhas + encaminhamentos manuais** — `dobradinhaNotes` e `nextSteps` staff-only na inteligência do núcleo (excluídos dos view models de `lideranca`); versão manual imediata que A6/C4 complementam depois. - **E4 Import único da planilha** — `pnpm db:seed:mapa` idempotente por slug, município canônico via `canonicalizeMunicipalityName`; lideranças/assessores da planilha entram como **texto staff-only, nunca `Contact`/`leadership`** (sem telefone/consent). @@ -117,6 +118,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | O0+ Escala e DRY pós-Onda 0 | — (sem design novo; reusa `/privacidade` + padrão revalidate `posts`) | [escala-dry-pos-onda0.md](plans/escala-dry-pos-onda0.md) | | VR+ Escala e DRY pós-visitados | — (sem design novo; reusa dashboard + padrões de lista de núcleos) | [escala-dry-pos-visitados-recentemente.md](plans/escala-dry-pos-visitados-recentemente.md) | | B5 Escala e DRY pós-B2 | — (sem design novo; reusa geometrias B2 + padrões de scripts CLI) | [escala-dry-pos-b2.md](plans/escala-dry-pos-b2.md) | +| E6 Escala e DRY pós-E1+E3 | — (sem design novo; reusa Metas 2026 + overview B1) | [escala-dry-pos-e1.md](plans/escala-dry-pos-e1.md) | | D2 Notificações (sino) | `Notificacoes-PWA` (central de notificações) | [notifications.md](plans/notifications.md) | | D1 PWA `/campanha` | `Notificacoes-PWA` (banner "Instale o app da campanha") | [pwa-campanha.md](plans/pwa-campanha.md) | | C5 Operação dia D / GOTV | `Dia-D-GOTV` | sem plano ainda — o design é a primeira materialização do item (check-off de apoiadores por seção/zona, progresso "62% da base", filtro "Ainda não confirmados", estado "Sincronizado" offline-tolerante) | @@ -178,6 +180,7 @@ flowchart TD E3["E3 Dobradinhas + encaminhamentos
manuais (staff-only)"] E4["E4 Import único da planilha
(pnpm db:seed:mapa)"] E5["E5 Salvador por bairro
(futuro; validar valor)"] + E6["E6 Escala e DRY pós-E1+E3
(aggregate / geo / DRY UI)"] end A1 --> A2 @@ -215,6 +218,8 @@ flowchart TD JUR -.chave de push.-> D2 DEPLOY -.engenharia entregue.-> O0DRY A4 --> E2 + E1 --> E6 + E6 -.perf lista/overview.-> E4 E1 --> E4 E3 --> E4 E2 -.tendência no import.-> E4 @@ -224,7 +229,7 @@ flowchart TD A7 -.mesmo loader do baseline.-> E2 ``` -Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0+** (após engenharia Onda 0 ✓), **VR+** (após Visitados recentemente ✓) e fill-ins (listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6, C7, C8, C9 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C10 (após C9 ✓; cortável se a base nominal permanecer pequena), C11 (após C7 ✓; cortável se a agenda permanecer pequena; Fase 1 feed condicional) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import). +Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0+** (após engenharia Onda 0 ✓), **VR+** (após Visitados recentemente ✓) e fill-ins (listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6, C7, C8, C9 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), **E6 (após merge E1+E3 em `main`)**, B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C10 (após C9 ✓; cortável se a base nominal permanecer pequena), C11 (após C7 ✓; cortável se a agenda permanecer pequena; Fase 1 feed condicional) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import; suave: E6 F1 reduz custo da lista antes do import em massa). ### Sequência de execução por janela do calendário @@ -252,14 +257,15 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0 | 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2, C10 | | 14 | C10 Escala e DRY pós-C9 (dedup access lista, scope único de núcleos do coordenador, `errorProps` nos forms) _(cortável se a base nominal permanecer pequena; Fase 3 DRY pode entrar isolada)_ | [detalhes](plans/escala-dry-pos-c9.md) | C9 (merge) | A4, C3, C7, E1+E3, E2, C11 | | 15 | C11 Escala e DRY pós-C7 (feed O(n) condicional, loaders do detalhe, selects por aba, hot paths mutação/form, utilities de busca) _(cortável se a agenda permanecer pequena; Fase 1 migration só com volume medido)_ | [detalhes](plans/escala-dry-pos-c7.md) | C7 (merge) | A4, C3, C10, E1+E3, E2 | -| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais (migration compartilhada em `electoralNucleus`) | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela | -| 17 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, C7, C8, C9, C10, C11 | +| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais (migration compartilhada em `electoralNucleus`; `/simplify` → E6) | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela | +| 17 | E6 Escala e DRY pós-E1+E3 (aggregate SQL metas/prioridade, geografia baseline, DRY formatação) _(cortável se poucos núcleos ativos)_ | [detalhes](plans/escala-dry-pos-e1.md) | E1+E3 merge em `main` | E2, C10, A7 | +| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, C7, C8, C9, C10, C11 | **Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.** | Ordem | Item | Plano | Depende de | Paralelizável com | | ----- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------- | -| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 + E3 (suave: E2 — tendência visível na revisão) | A5, A7, B3, B5, C4, D2 | +| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 + E3 (suave: E2 — tendência visível na revisão; E6 F1 — perf da lista) | A5, A7, B3, B5, C4, D2 | | 19 | 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 | A7, B3, B5, C4, E4, D1 | | 20 | A7 Escala e DRY pós-A4 (agregar federal no detalhe, `cityCode` TSE, Alert `confirmed` + Progress) | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, B3, B5, C4, E4, D1 | | 21 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, B5, C4, E4, D1 | @@ -286,7 +292,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0 - **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): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), C9 (escala/DRY pós-C8 — só se a base nominal permanecer pequena; Fases 3–4 DRY de forms podem entrar isoladas), C10 (escala/DRY pós-C9 — só se a base nominal permanecer pequena; Fase 3 `errorProps` pode entrar isolada; Fase 1 access dedup é a mais valiosa se coordenadores usarem `/apoiadores` com frequência), C11 (escala/DRY pós-C7 — só se a agenda permanecer pequena; Fase 1 migration `actionPlanUpdate` só com volume medido; Fases 2–3 loaders/selects são as mais valiosas se `/campanha/planos` tiver uso real), **O0+ Fase 4–5** (Lexical/layout shell e Footer condicional — Fases 1–3 preferíveis antes de tráfego real em `/privacidade`), **VR+ Fases 3–5** (row shell, layout wrapper, micro-perf — Fase 1 refresh bfcache é a mais valiosa se usuários reclamarem de painel stale), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 (entregue), C8 fases 1–2 (perf import/lista), C9 Fase 1 (filtro unificado — evita drift KPI/lista), C10 Fase 1 (access dedup na lista), C11 Fases 2–3 (loaders/selects por aba) se a agenda for usada antes de 16/08, **O0+ Fase 1** (revalidate globals pós-migration) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa. +**Cortes seguros se o prazo apertar** (nesta ordem): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), **E6 F2–F4** (geografia baseline / DRY UI / tipos dashboard — Fase 1 aggregate só se poucos núcleos; preferir E6 F1 antes de E4 ou B3 se a lista ficar lenta), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), C9 (escala/DRY pós-C8 — só se a base nominal permanecer pequena; Fases 3–4 DRY de forms podem entrar isoladas), C10 (escala/DRY pós-C9 — só se a base nominal permanecer pequena; Fase 3 `errorProps` pode entrar isolada; Fase 1 access dedup é a mais valiosa se coordenadores usarem `/apoiadores` com frequência), C11 (escala/DRY pós-C7 — só se a agenda permanecer pequena; Fase 1 migration `actionPlanUpdate` só com volume medido; Fases 2–3 loaders/selects são as mais valiosas se `/campanha/planos` tiver uso real), **O0+ Fase 4–5** (Lexical/layout shell e Footer condicional — Fases 1–3 preferíveis antes de tráfego real em `/privacidade`), **VR+ Fases 3–5** (row shell, layout wrapper, micro-perf — Fase 1 refresh bfcache é a mais valiosa se usuários reclamarem de painel stale), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 (entregue), C8 fases 1–2 (perf import/lista), C9 Fase 1 (filtro unificado — evita drift KPI/lista), C10 Fase 1 (access dedup na lista), C11 Fases 2–3 (loaders/selects por aba) se a agenda for usada antes de 16/08, **O0+ Fase 1** (revalidate globals pós-migration), **E6 Fase 1** (aggregate metas/prioridade — evita dupla query na lista antes de E4/B3) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa. ### Itens consolidados/removidos nesta revisão (2026-07-17) diff --git a/src/app/(campaign)/campanha/actions/nucleus.ts b/src/app/(campaign)/campanha/actions/nucleus.ts index 177327da..bf81322b 100644 --- a/src/app/(campaign)/campanha/actions/nucleus.ts +++ b/src/app/(campaign)/campanha/actions/nucleus.ts @@ -44,7 +44,7 @@ export const updateElectoralNucleus = async ( return payload.update({ collection: 'electoralNucleus', id, - data, + data: data as never, depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/collections/ElectoralNucleus.ts b/src/collections/ElectoralNucleus.ts index ee8d1f84..c1da2ea7 100644 --- a/src/collections/ElectoralNucleus.ts +++ b/src/collections/ElectoralNucleus.ts @@ -23,9 +23,62 @@ import { eligibleNucleusCoordinatorWhere } from '@/utilities/nucleusCoordinatorO import { acquirePrimaryContactInvariantLocks } from '@/utilities/primaryContactInvariantLock' import { relationshipId } from '@/utilities/relationship' import { slugify } from '@/utilities/slug' +import { + getVoteGoalsOrderViolation, + VOTE_GOALS_ORDER_ERROR_MESSAGE, + type VoteGoalsFields, +} from '@/utilities/voteGoals' const trimmedText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') +const voteGoalNumber = (value: unknown): number | null | undefined => { + if (value === null || value === undefined || value === '') return null + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + throw new APIError('Cada meta de votos deve ser um número inteiro não negativo.', 400) + } + return Math.trunc(value) +} + +const validateVoteGoals: CollectionBeforeValidateHook = ({ data, originalDoc, operation }) => { + if (!data) return data + + const mergedGoals = + data.voteGoals !== undefined + ? { ...(originalDoc?.voteGoals ?? {}), ...(data.voteGoals as object) } + : operation === 'update' + ? originalDoc?.voteGoals + : undefined + + if (!mergedGoals || typeof mergedGoals !== 'object') return data + + const good = voteGoalNumber((mergedGoals as VoteGoalsFields).good) + const regular = voteGoalNumber((mergedGoals as VoteGoalsFields).regular) + const minimum = voteGoalNumber((mergedGoals as VoteGoalsFields).minimum) + + if (getVoteGoalsOrderViolation({ good, regular, minimum })) { + throw new APIError(VOTE_GOALS_ORDER_ERROR_MESSAGE, 400) + } + + if (data.voteGoals && typeof data.voteGoals === 'object') { + data.voteGoals = { + good: + (data.voteGoals as { good?: unknown }).good === undefined + ? (originalDoc?.voteGoals?.good ?? null) + : good, + regular: + (data.voteGoals as { regular?: unknown }).regular === undefined + ? (originalDoc?.voteGoals?.regular ?? null) + : regular, + minimum: + (data.voteGoals as { minimum?: unknown }).minimum === undefined + ? (originalDoc?.voteGoals?.minimum ?? null) + : minimum, + } + } + + return data +} + const validateNucleusTerritoryAndZones: CollectionBeforeValidateHook = (args) => { const validateTerritory = createCampaignTerritoryValidationHook({ entityLabel: 'núcleo' }) const data = validateTerritory(args) @@ -113,6 +166,7 @@ export const ElectoralNucleus: CollectionConfig = { setCanonicalNucleusSlug, validateNucleusTerritoryAndZones, validateNucleusCoordinators, + validateVoteGoals, ], beforeChange: [ ({ data, operation, req }) => { @@ -415,6 +469,61 @@ export const ElectoralNucleus: CollectionConfig = { }, ], }, + { + name: 'dobradinhaNotes', + type: 'textarea', + label: 'Dobradinhas (notas)', + maxLength: 4000, + admin: { + description: + 'Quem dobra no território hoje e o estado da negociação. Distinto do bloco estruturado “Dobrada”.', + }, + }, + { + name: 'nextSteps', + type: 'textarea', + label: 'Encaminhamentos', + maxLength: 4000, + admin: { + description: 'Próximos passos operacionais para o núcleo.', + }, + }, + { + name: 'voteGoals', + type: 'group', + label: 'Metas de votos 2026', + fields: [ + { + name: 'good', + type: 'number', + label: 'Bom', + min: 0, + }, + { + name: 'regular', + type: 'number', + label: 'Regular', + min: 0, + }, + { + name: 'minimum', + type: 'number', + label: 'Mínimo', + min: 0, + }, + ], + }, + { + name: 'priority', + type: 'select', + label: 'Prioridade', + defaultValue: 'normal', + index: true, + options: [ + { label: 'Alta', value: 'alta' }, + { label: 'Normal', value: 'normal' }, + ], + }, { name: 'confirmedVoteEstimate', type: 'number', diff --git a/src/components/campaign/CampaignDashboard.tsx b/src/components/campaign/CampaignDashboard.tsx index 4a36b3f4..89979745 100644 --- a/src/components/campaign/CampaignDashboard.tsx +++ b/src/components/campaign/CampaignDashboard.tsx @@ -185,13 +185,17 @@ const GeneralDashboard = ({ view, now }: { view: GeneralDashboardViewModel; now:

Indicadores da campanha

-
+
+ ( <> {nucleus.kind === 'staff' ? ( @@ -78,6 +79,8 @@ const OverviewContent = ({ risks: nucleus.risks, voterProfiles: nucleus.voterProfiles, ticketAlliance: nucleus.ticketAlliance, + dobradinhaNotes: nucleus.dobradinhaNotes, + nextSteps: nucleus.nextSteps, }} primaryContact={primaryContact} searchPrimaryContacts={searchPrimaryContactOptionsFormAction.bind(null, context.slug)} @@ -129,9 +132,7 @@ const OverviewContent = ({
{nucleus.ticketAlliance.partnerName} {nucleus.ticketAlliance.office ? ( - - · {nucleus.ticketAlliance.office} - + · {nucleus.ticketAlliance.office} ) : null}
+ + {nucleus.dobradinhaNotes || nucleus.nextSteps ? ( +
+ {nucleus.dobradinhaNotes ? ( + + + {nucleusDobradinhaNotesLabel} + + +

{nucleus.dobradinhaNotes}

+
+
+ ) : null} + {nucleus.nextSteps ? ( + + + {nucleusNextStepsLabel} + + +

{nucleus.nextSteps}

+
+
+ ) : null} +
+ ) : null} ) : ( @@ -197,14 +223,17 @@ const OverviewContent = ({ Informações básicas do território em que você atua -

- {formatNucleusTerritoryLabel(nucleus)} -

+

{formatNucleusTerritoryLabel(nucleus)}

)}
+
@@ -262,9 +291,7 @@ const TerritoryContent = ({ nucleus }: { nucleus: NucleusTabsViewModel }) => ( Bahia -

- {formatNucleusTerritoryLabel(nucleus)} -

+

{formatNucleusTerritoryLabel(nucleus)}

{nucleus.kind === 'staff' && nucleus.territoryNotes ? (

{nucleus.territoryNotes}

) : null} @@ -350,13 +377,7 @@ export const NucleusActiveTab = async ({ user: CampaignUser view: NucleusDetailViewModel }) => { - const data = await loadNucleusActiveTabPageData( - payload, - user, - context, - activeTab, - searchParams, - ) + const data = await loadNucleusActiveTabPageData(payload, user, context, activeTab, searchParams) if (data.tab === 'overview') { return ( @@ -366,9 +387,11 @@ export const NucleusActiveTab = async ({ confirmedVoteEstimate={view.confirmedVoteEstimate} context={context} nucleus={view.tabs} + priority={view.priority} primaryContact={data.primaryContactPageData.current} searchParams={searchParams} updatePreview={data.updatePreview} + voteGoals={view.voteGoals} /> ) } diff --git a/src/components/campaign/NucleusCard.tsx b/src/components/campaign/NucleusCard.tsx index 98116f89..3a463d09 100644 --- a/src/components/campaign/NucleusCard.tsx +++ b/src/components/campaign/NucleusCard.tsx @@ -11,6 +11,7 @@ import { } from '@/components/ui/card' import { TseZoneBadge } from '@/components/campaign/TseZoneBadge' import { cn } from '@/lib/utils' +import { nucleusPriorityLabels } from '@/utilities/nucleusUi' const voteFormatter = new Intl.NumberFormat('pt-BR') @@ -21,6 +22,7 @@ export type NucleusCardProps = { tseZones?: number[] confirmedVoteEstimate?: number | null hasPendingEstimate?: boolean + isHighPriority?: boolean lastUpdateLabel?: string isUpdateOverdue?: boolean leadershipCounts?: { @@ -39,6 +41,7 @@ export const NucleusCard = ({ tseZones = [], confirmedVoteEstimate, hasPendingEstimate = false, + isHighPriority = false, lastUpdateLabel, isUpdateOverdue = false, leadershipCounts, @@ -47,7 +50,10 @@ export const NucleusCard = ({ }: NucleusCardProps) => ( - {name} +
+ {name} + {isHighPriority ? {nucleusPriorityLabels.alta} : null} +
{[organization, territory].filter(Boolean).join(' · ')} {tseZones.length > 0 ? (
diff --git a/src/components/campaign/NucleusFilters.tsx b/src/components/campaign/NucleusFilters.tsx index b29c508a..ba3c7be0 100644 --- a/src/components/campaign/NucleusFilters.tsx +++ b/src/components/campaign/NucleusFilters.tsx @@ -16,6 +16,7 @@ import { buildNucleusListSearchParams, nucleusListCoverageLabels, nucleusListEstimateLabels, + nucleusPriorityFilterLabel, type NucleusListState, } from '@/utilities/nucleusUi' import { @@ -30,6 +31,7 @@ type FilterValues = { tseZone: string coverage: string estimate: string + priority: string } const valuesFromState = (state: NucleusListState): FilterValues => ({ @@ -39,9 +41,10 @@ const valuesFromState = (state: NucleusListState): FilterValues => ({ tseZone: state.tseZone ? String(state.tseZone) : '', coverage: state.coverage ?? '', estimate: state.estimate ?? '', + priority: state.priority ?? '', }) -const filterNames = ['region', 'city', 'tseZone', 'coverage', 'estimate'] as const +const filterNames = ['region', 'city', 'tseZone', 'coverage', 'estimate', 'priority'] as const type FilterFieldsProps = { values: FilterValues @@ -61,7 +64,7 @@ const FilterFields = ({
event.preventDefault()} - className="grid gap-4 lg:grid-cols-5" + className="grid gap-4 lg:grid-cols-3 xl:grid-cols-6" > Território de identidade @@ -136,6 +139,19 @@ const FilterFields = ({ + + Prioridade + updateFilter('priority', event.target.value)} + className="w-full **:data-[slot=native-select]:min-h-11 **:data-[slot=native-select]:rounded-[6px]" + > + Todas + {nucleusPriorityFilterLabel} + +
) @@ -160,6 +176,7 @@ export const NucleusFilters = ({ state }: { state: NucleusListState }) => { tseZone: nextValues.tseZone ? Number(nextValues.tseZone) : undefined, coverage: nextValues.coverage as NucleusListState['coverage'], estimate: nextValues.estimate as NucleusListState['estimate'], + priority: nextValues.priority as NucleusListState['priority'], }) const query = params.toString() startTransition(() => { diff --git a/src/components/campaign/NucleusForm.tsx b/src/components/campaign/NucleusForm.tsx index 85377501..5a259f9a 100644 --- a/src/components/campaign/NucleusForm.tsx +++ b/src/components/campaign/NucleusForm.tsx @@ -1,7 +1,7 @@ 'use client' -import { useActionState } from 'react' import Link from 'next/link' +import { useActionState } from 'react' import { NucleusTerritoryAndZonesFields } from '@/components/campaign/NucleusTerritoryAndZonesFields' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' @@ -21,9 +21,13 @@ import { Input } from '@/components/ui/input' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' import { Spinner } from '@/components/ui/Spinner' import { Textarea } from '@/components/ui/textarea' -import type { NucleusCoordinatorOption } from '@/utilities/nucleusCoordinatorOptions' import { fieldError } from '@/utilities/campaignFormFields' -import { organizationKindLabels, sectorKindLabels } from '@/utilities/nucleusUi' +import type { NucleusCoordinatorOption } from '@/utilities/nucleusCoordinatorOptions' +import { + nucleusPriorityLabels, + organizationKindLabels, + sectorKindLabels, +} from '@/utilities/nucleusUi' import type { NucleusFormViewModel } from '@/utilities/nucleusViewModels' export type NucleusFormState = { @@ -210,6 +214,83 @@ export const NucleusFormFields = ({ + + + Metas 2026 + + + +
+ + Bom + + + + Regular + + + + Mínimo + + +
+ {['voteGoals.good', 'voteGoals.regular', 'voteGoals.minimum'].map((field) => + errorFor(field) ? ( + + {errorFor(field)} + + ) : null, + )} + + Quando informadas, as metas devem seguir a ordem Bom ≥ Regular ≥ Mínimo. + + + {Object.entries(nucleusPriorityLabels).map(([value, label]) => ( + + {label} + + ))} + +
+
+
+ {!nucleus ? ( @@ -229,10 +310,7 @@ export const NucleusFormFields = ({ {coordinators.map((coordinator) => ( - + {coordinator.name} {coordinator.isCurrent ? ' (você)' : ''} diff --git a/src/components/campaign/NucleusIntelligenceDialog.tsx b/src/components/campaign/NucleusIntelligenceDialog.tsx index 58ddf28b..f7793b3b 100644 --- a/src/components/campaign/NucleusIntelligenceDialog.tsx +++ b/src/components/campaign/NucleusIntelligenceDialog.tsx @@ -1,18 +1,18 @@ 'use client' -import { useActionState, useEffect, useState } from 'react' import { PlusIcon, Trash2Icon } from 'lucide-react' import { useRouter } from 'next/navigation' +import { useActionState, useEffect, useState } from 'react' import { toast } from 'sonner' import { updateNucleusIntelligenceFormAction, type NucleusIntelligenceFormState, } from '@/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIntelligenceFormActions' +import { PrimaryContactCombobox } from '@/components/campaign/PrimaryContactCombobox' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/Checkbox' -import { PrimaryContactCombobox } from '@/components/campaign/PrimaryContactCombobox' import { DialogContent, DialogDescription, @@ -32,12 +32,18 @@ import { import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { Textarea } from '@/components/ui/textarea' +import { fieldError } from '@/utilities/campaignFormFields' +import { + nucleusDobradinhaNotesDescription, + nucleusDobradinhaNotesLabel, + nucleusNextStepsDescription, + nucleusNextStepsLabel, +} from '@/utilities/nucleusUi' +import type { StaffNucleusTabsViewModel } from '@/utilities/nucleusViewModels' import type { NucleusPrimaryContactPageData, PrimaryContactOption, } from '@/utilities/primaryContactPageData' -import type { StaffNucleusTabsViewModel } from '@/utilities/nucleusViewModels' -import { fieldError } from '@/utilities/campaignFormFields' type EditableProfile = StaffNucleusTabsViewModel['voterProfiles'][number] @@ -46,7 +52,7 @@ type NucleusIntelligenceDialogProps = { nucleusId: number intelligence: Pick< StaffNucleusTabsViewModel, - 'strengths' | 'risks' | 'voterProfiles' | 'ticketAlliance' + 'strengths' | 'risks' | 'voterProfiles' | 'ticketAlliance' | 'dobradinhaNotes' | 'nextSteps' > primaryContact: PrimaryContactOption | null searchPrimaryContacts: (query: string) => Promise @@ -357,6 +363,40 @@ export const NucleusIntelligenceDialog = ({ +
+ {nucleusDobradinhaNotesLabel} + {nucleusDobradinhaNotesDescription} + + + {nucleusDobradinhaNotesLabel} + +