Fix Vercel build failing on sync exports from supporter server actions.

Next.js requires every export from a 'use server' module to be async; move the import type guard into a shared util and mark createSupporterRecord async. Also mark C2 as merged in the roadmap/docs.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-18 21:17:58 -03:00
parent c12fc0787c
commit b2856abceb
8 changed files with 110 additions and 67 deletions

View file

@ -36,7 +36,7 @@ alwaysApply: false
- **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.
- **Em 2026-07-18, C2 Cadastro nominal de apoiadores — engenharia pronta (pendente merge/deploy + lote jurídico).** Collection `supporter` (join `Contact`↔campanha, núcleo opcional), migration `20260718_222656_add_supporter` com `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent por chaves `apoiador-cadastro` / `apoiador-intencao-voto` via `campaignConsent.ts` genérico, actions (create / intenção / import CSV só `geral` / remover dados), UI `/campanha/apoiadores` (lista+KPIs, ficha, wizard import, kit mínimo `wa.me`). Telefone obrigatório; `lideranca` sem acesso à área. Produção com dados reais espera Consent keys + aprovação jurídica (Onda 0).
- **Em 2026-07-18, C2 Cadastro nominal de apoiadores — engenharia pronta e mesclada em `main` (pendente deploy + lote jurídico).** Collection `supporter` (join `Contact`↔campanha, núcleo opcional), migration `20260718_222656_add_supporter` com `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent por chaves `apoiador-cadastro` / `apoiador-intencao-voto` via `campaignConsent.ts` genérico, actions (create / intenção / import CSV só `geral` / remover dados), UI `/campanha/apoiadores` (lista+KPIs, ficha, wizard import, kit mínimo `wa.me`). Telefone obrigatório; `lideranca` sem acesso à área. Produção com dados reais espera deploy (build Vercel aplica a migration) + Consent keys + aprovação jurídica (Onda 0).
- **Em 2026-07-18, C6 Escala e DRY pós-C2 registrado no roadmap** (após `/simplify` do C2): shells compartilhados com listas de núcleos, import bulk drizzle + skip phone-invariant, preview token, KPI agregado SQL, StrictCombobox/AlertDialog. Plano: [`docs/plans/escala-dry-pos-c2.md`](../../../docs/plans/escala-dry-pos-c2.md). Depende do merge de C2; fases 3–5 cortáveis se a base permanecer pequena.
## Decisões

View file

@ -38,9 +38,9 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
1. **Payload admin RBAC is still absent.** The campaign auth collection now has `role`, but the `users` admin collection still has no roles; every admin user has full Payload admin access. Add admin roles before opening `/admin` to a broader team.
2. **Legacy public consent flows still hardcode document IDs** (e.g. `consent: 2` in `submitWhatsapp.ts`). The campaign leadership flow does not: it requires `Consent.key = 'lideranca-autopreenchimento'` and fails closed when missing. Migrating the older public flows remains separate work.
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).
4. **Campaign LGPD consent texts are 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. The C2 supporter registry (merged to `main`, production-blocked) additionally requires `Consent.key = 'apoiador-cadastro'` and `Consent.key = 'apoiador-intencao-voto'` (vote intention is sensitive data) — same fail-closed behavior. All four keys are bundled in the single legal batch of Onda 0 (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`. 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. Still on 2026-07-18, **A2** shipped: static Bahia municipality↔TSE zone map (`src/lib/bahiaTseZones.ts` from TSE 2024 `detalhe_votacao_munzona` BA) plus opt-in territory/zone suggestion chips in the nucleus form (`territorySuggestions`, `NucleusTerritoryAndZonesFields`) — no migration, no forced equality on save. Also the same day: C3 Planos de Ação (`actionPlan` + `/campanha/planos`, migration `20260718_222832_add_action_plan`, blocos "Próximos eventos"). 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. Still on 2026-07-18, **A2** shipped: static Bahia municipality↔TSE zone map (`src/lib/bahiaTseZones.ts` from TSE 2024 `detalhe_votacao_munzona` BA) plus opt-in territory/zone suggestion chips in the nucleus form (`territorySuggestions`, `NucleusTerritoryAndZonesFields`) — no migration, no forced equality on save. Also the same day: **C2** Cadastro nominal de apoiadores (`supporter` join `Contact`↔campanha with optional `nucleus`, migration `20260718_222656_add_supporter` with `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent keys `apoiador-cadastro` / `apoiador-intencao-voto` resolved via a generalized `campaignConsent.ts` that fails closed, `/campanha/apoiadores` vertical with list+KPIs/detail/CSV-import wizard; phone required in v1, `lideranca` has no access to the area; production use with real data still waits on Consent keys + legal sign-off). Also the same day: C3 Planos de Ação (`actionPlan` + `/campanha/planos`, migration `20260718_222832_add_action_plan`, blocos "Próximos eventos"). 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)
@ -104,6 +104,25 @@ The first `/campanha` operational cycle is centered on `electoralNucleus`; a Nú
- **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.
## Campaign supporters (C2)
Nominal supporter registry backing the campaign's base of declared supporters (roadmap item C2; engineering ready and merged to `main` on 2026-07-18, production-blocked on the legal batch). One collection in admin group `Campanha`:
- **`supporter`** (`src/collections/Supporter.ts`) — join `Contact`↔campaign with **optional `nucleus`** (transversal base; aggregates by territory when linked). Fields: `contact` (required, indexed), `nucleus` (optional, indexed), `voteIntention` (select `certo | tende_a_certo | indeciso | outro`, indexed — **sensitive data**, field access restricted to `geral`/`coordenador`), paired consent fields for registration and vote intention (`consent`/`consentContentHash`/`consentedAt` and `voteIntentionConsent`/`voteIntentionConsentContentHash`/`voteIntentionConsentedAt`), `source` (`import_csv | manual | convite | evento`; v1 writes only the first two), `consentNote`, `notes` (staff-only), `createdBy` (readOnly). Uniqueness: `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)` (Postgres ≥15) via migration `20260718_222656_add_supporter`. A `beforeChange` hook sets `createdBy` and rejects a `contact` that is already a `leadership` of the same `nucleus`.
- **Consent by stable key.** `campaignConsent.ts` was generalized to `getConsentByKey` / `requireConsentByKey` (fail-closed). The two keys are `apoiador-cadastro` (declared support) and `apoiador-intencao-voto` (vote intention — highlighted, separate consent). The app fails closed if either key is missing — same pattern as `lideranca-autopreenchimento`.
- **Access.** `geral` sees/manages all; `coordenador` sees supporters of assigned nuclei (`nucleus in getAccessibleNucleusIds`); supporters without a nucleus are `geral`-only; `lideranca` has no access to the area. Delete is admin-only. Access helpers live in `campaignAccess.ts` (`canReadSupporter` / `canManageSupporter` / `canCreateSupporter`).
- **Server actions** (`src/app/(campaign)/campanha/actions/supporter.ts`): `createSupporter`, `setSupporterVoteIntention`, `previewSupporterImport` / `confirmSupporterImport` (CSV, `geral`-only, transactional with `req: { transactionID }`, dedup by phone via `contactPhoneInvariant`), and `removeSupporterData` (deletes the `supporter`; anonymizes/clears `Contact` PII only if no other join — `leadership`/`signature`/`subscription`/other `supporter` — references it).
- **UI** `/campanha/apoiadores` — list with KPIs (total / "Certo + Tende" / "Indecisos"), filters (vote intention, territory), detail with LGPD consent block + vote-intention segmented control (disabled until consent) + minimal share kit (`wa.me` + copy text), and a 3-step CSV import wizard (Upload → Preview → Confirm). Phone is required in v1.
- **Mandatory production blocker:** do not import real supporter data or capture vote intention until electoral counsel documents the LGPD art. 11 basis (vote intention is sensitive data), approves the versioned consent texts, and an admin creates `Consent.key = 'apoiador-cadastro'` and `Consent.key = 'apoiador-intencao-voto'`. The app intentionally fails closed when these keys are absent.
## Campaign action plans (C3)
Mobilization events / agenda backing the campaign's field operations (roadmap item C3; implemented and merged to `main` on 2026-07-18). One collection in admin group `Campanha`:
- **`actionPlan`** (`src/collections/ActionPlan.ts`) — a calendarizable action/event (caminhada, comício, etc.) with its own territory (no link to `electoralNucleus`), task checklist, and update feed. Fields: `title`, canonical immutable `slug` (from `title`), `kind` / `status` enums in pt-BR (data values), `startAt` (optional only while `rascunho`; required to move to `planejado`/`confirmado`/`realizado`/`cancelado` — card shows "Data a definir" when absent), territory arrays `regions`/`cities`/`neighborhoods` (Bahia validation shared via `campaignTerritoryValidation.ts`; list/overview filters use `{ equals }` on arrays, A1 semantics), people (`coordinators` → `campaignUser`, `responsible` → `Contact`, `leadership` → `leadership` optional), `tasks` and `updates` as arrays in the MVP (append-only for updates; `doneAt`/`author`/`createdAt` derived server-side). No `Consent` (internal staff data). Migration `20260718_222832_add_action_plan`.
- **Access.** `geral` everything; `coordenador` where listed in `coordinators` (may create, auto-included); `lideranca` where `leadership` ∈ engaged — write limited to toggling `tasks.done` and appending `updates`. Domain writes go through `withPayloadTransaction` (`src/utilities/payloadTransaction.ts`).
- **UI** `/campanha/planos` — list with tabs Próximos (default) / Todos / Realizados / Rascunhos, filters (kind, territory), cards with kind/status badge + date/time + municipality + TI chip + "Resp:" + task progress; detail with tabs Visão geral / Tarefas / Atualizações (pattern `NucleusTabNav`); new/edit forms. "Próximos eventos" blocks surface on the nuclei list overview and the dashboard.
## 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`.
@ -144,7 +163,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, ElectionTally, ElectionCandidateVote, ElectionCandidate, Media, Petition, Signature, Consent, Contact, Subscription, Post, Tag
├── collections/ # Users, CampaignUser, CampaignInvite, ElectoralNucleus, Leadership, NucleusUpdate, Supporter, ActionPlan, 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, bahiaTseZones.ts, territorySuggestions.ts, formData.ts, zod schemas (lib/schemas/*)

View file

@ -1,6 +1,6 @@
# Cadastro nominal de apoiadores em massa
Status: engenharia pronta (pendente merge/deploy + lote jurídico)
Status: engenharia pronta e mesclada em `main` (pendente deploy + lote jurídico)
Atualizado em: 2026-07-18
Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos")
Responsável: —

View file

@ -1,6 +1,6 @@
# Eventos / presença e agenda de mobilização
Status: implementado
Status: implementado e mesclado em `main`
Atualizado em: 2026-07-18
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Janela 2, ordem 9 — C3)
Responsável: —

View file

@ -1,6 +1,6 @@
# Roadmap — Teqo
Atualizado em: 2026-07-18 (MVP + Ciclo 2 deployados; A2 entregue; C2 engenharia pronta; C3 Planos de Ação implementado; C6/C7 registrados — escala/DRY pós-C2 e pós-C3)
Atualizado em: 2026-07-18 (MVP + Ciclo 2 deployados; A2 entregue; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6/C7 registrados — escala/DRY pós-C2 e pós-C3)
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 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:
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), A2 (zonas TSE + sugestões cruzadas — sem migration), A3 (baseline TSE 2022 — modelo + import), B1 (overview da lista), C1 (compartilhar página), C3 (planos de ação / agenda) e D1 (PWA). C2 (cadastro nominal de apoiadores) tem a engenharia pronta e mesclada em `main`, mas segue bloqueada em produção pelo lote jurídico (Consent keys). As migrations (`20260718_010733_consolidate_campaign_schema`, `20260718_190559_territorio_multi_municipio_bairro`, `20260718_195854_add_election_results`, `20260718_222656_add_supporter`, `20260718_222832_add_action_plan`) 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).
@ -60,6 +60,11 @@ MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de
- **A2 Zonas TSE + sugestões cruzadas** — cadastro estático `bahiaTseZones` (TSE 2024 `detalhe_votacao_munzona` BA, 417 municípios), motor puro `territorySuggestions` (inclui `outsideZones`), coordenador `NucleusTerritoryAndZonesFields` com chips `{rótulo} +` opt-in (município/TI → ZEs; irmãos do TI e cidades da ZE → Municípios), `TseZoneInput` controlado. Sem migration, sem igualdade forçada no save. [Plano](plans/zonas-por-municipio.md).
### Ciclo 2+ — C2 e C3 mesclados em `main` (2026-07-18)
- **C2 Cadastro nominal de apoiadores** — collection `supporter` (join `Contact`↔campanha, núcleo opcional), migration `20260718_222656_add_supporter` com `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent por chaves estáveis `apoiador-cadastro` / `apoiador-intencao-voto` via `campaignConsent.ts` genérico (`getConsentByKey` / `requireConsentByKey`, falha fechada), actions (create / intenção de voto / import CSV só `geral` / `removeSupporterData`), UI `/campanha/apoiadores` (lista+KPIs, ficha, wizard de import, kit mínimo `wa.me`). Telefone obrigatório no v1; `lideranca` sem acesso à área. Engenharia pronta e mesclada — produção com dados reais espera deploy (build Vercel aplica a migration) + Consent keys + aprovação jurídica (Onda 0). [Plano](plans/cadastro-nominal-apoiadores.md).
- **C3 Eventos / agenda de mobilização** — collection `actionPlan` + vertical `/campanha/planos` (lista com tabs Próximos/Todos/Realizados/Rascunhos, detalhe com tabs Visão geral/Tarefas/Atualizações, forms novo/editar), blocos "Próximos eventos" no overview de núcleos e no dashboard; `startAt` opcional só em rascunho (obrigatório ao sair de `rascunho`); access por `coordinators`/`leadership` (escopo `lideranca` só toggle `tasks.done` + append `updates`); transações via `withPayloadTransaction`; migration `20260718_222832_add_action_plan`. Sem `Consent` (dado interno de staff). [Plano](plans/eventos-agenda-mobilizacao.md).
### Referências de design (UX Pilot, 2026-07-18)
Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/design-refs/latest/`](design-refs/latest/) (pares `.png` + `.html` com o mesmo nome). **A UX/estrutura é a referência a seguir; a paleta não é** — os arquivos usam a paleta antiga (vermelho escuro `#8E0E23`, navy `#1B2B4B`, dourado `#C8874B`); toda implementação usa os tokens claros do tema `data-theme='campaign'` (`src/app/(frontend)/styles.css`: fundo branco, primário `#C51414`, superfícies neutras). Cada plano detalha o uso na sua seção "Referência visual (UX Pilot)".
@ -111,7 +116,7 @@ flowchart TD
subgraph TrilhaC["Trilha C — operação de campo"]
C1["C1 Compartilhar página ✓"]
C2["C2 Cadastro nominal de apoiadores"]
C2["C2 Cadastro nominal de apoiadores ✓"]
C6["C6 Escala e DRY pós-C2<br/>(import/listas/forms)"]
C3["C3 Eventos / agenda de mobilização ✓"]
C7["C7 Escala e DRY pós-C3<br/>(território/contato/leituras)"]
@ -150,7 +155,7 @@ flowchart TD
JUR -.chave de push.-> D2
```
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, A2, A3, B1, C1, C3 e D1 — ver "Ciclo 2" acima e C3 abaixo. Destravados por eles: A4 (A2/A3 + B1 prontos), C4 (A1 + C3 prontos), C6 (após merge de C2), C7 (após C3; suave: C6 F2) e D2 push (D1 pronto; falta a chave jurídica).
Itens sem seta de entrada (**paralelizáveis a qualquer momento**): B2, além dos fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18** (✓ no grafo): A1, A2, A3, B1, C1, C2, C3 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: A4 (A2/A3 + B1 prontos), C4 (A1 + C3 prontos), C6 (merge de C2 ✓), C7 (após C3; suave: C6 F2) e D2 push (D1 pronto; falta a chave jurídica).
### Sequência de execução por janela do calendário
@ -167,13 +172,13 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): B2, C2 (enge
**Janela 2 — 05/08 → 16/08 (pré-propaganda): base nominal + inteligência + agenda prontas para o arranque.**
| Ordem | Item | Plano | Depende de | Paralelizável com |
| ----- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------- | ----------------- |
| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta 2026-07-18 — merge/deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 |
| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7 |
| 9 | A4 Baseline no produto + insight Gap vs 2022 | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7 |
| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7 |
| 11 | C7 Escala e DRY pós-C3 (território/contato compostos, leituras por aba, RMW/índice) | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6 |
| Ordem | Item | Plano | Depende de | Paralelizável com |
| ----- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------- | ----------------- |
| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 |
| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7 |
| 9 | A4 Baseline no produto + insight Gap vs 2022 | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7 |
| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7 |
| 11 | C7 Escala e DRY pós-C3 (território/contato compostos, leituras por aba, RMW/índice) | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6 |
**Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.**

View file

@ -23,54 +23,22 @@ import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
import { withPayloadTransaction } from '@/utilities/payloadTransaction'
import { normalizeBrazilianPhone } from '@/utilities/phone'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
import {
isSupporterImportOkRow,
type SupporterImportPreviewResult,
type SupporterImportPreviewRow,
type SupporterImportPreviewRowBase,
} from '@/utilities/supporterImport'
export type {
SupporterImportOkRow,
SupporterImportPreviewResult,
SupporterImportPreviewRow,
SupporterImportRowStatus,
} from '@/utilities/supporterImport'
const MAX_IMPORT_ROWS = 5000
export type SupporterImportRowStatus =
| 'ok'
| 'duplicado_pelo_telefone'
| 'telefone_invalido'
| 'municipio_nao_reconhecido'
| 'nome_invalido'
| 'intencao_invalida'
type SupporterImportPreviewRowBase = {
line: number
nome: string
telefone: string
municipio: string
intencao: string
}
export type SupporterImportOkRow = SupporterImportPreviewRowBase & {
status: 'ok'
normalizedPhone: string
canonicalCity?: string
voteIntention?: SupporterVoteIntention
}
export type SupporterImportPreviewRow =
| SupporterImportOkRow
| (SupporterImportPreviewRowBase & {
status: Exclude<SupporterImportRowStatus, 'ok'>
normalizedPhone?: string
canonicalCity?: string
voteIntention?: SupporterVoteIntention
})
export const isSupporterImportOkRow = (
row: SupporterImportPreviewRow,
): row is SupporterImportOkRow => row.status === 'ok'
export type SupporterImportPreviewResult = {
rows: SupporterImportPreviewRow[]
counts: {
ok: number
duplicate: number
error: number
}
}
const getFreshStaffActor = async (
payload: Payload,
actor: CampaignUser,
@ -267,8 +235,11 @@ const createValidatedSupporter = async (payload: Payload, actor: CampaignUser, i
}
}
export const createSupporterRecord = (payload: Payload, actor: CampaignUser, input: unknown) =>
createValidatedSupporter(payload, actor, input)
export const createSupporterRecord = async (
payload: Payload,
actor: CampaignUser,
input: unknown,
) => createValidatedSupporter(payload, actor, input)
export const createSupporter = async (input: unknown) => {
const { payload, actor } = await getCampaignActionContext()

View file

@ -8,10 +8,7 @@ import { toast } from 'sonner'
import {
confirmSupporterImport,
isSupporterImportOkRow,
previewSupporterImport,
type SupporterImportPreviewResult,
type SupporterImportPreviewRow,
} from '@/app/(campaign)/campanha/actions/supporter'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
import { Badge } from '@/components/ui/Badge'
@ -31,6 +28,11 @@ import {
import { Textarea } from '@/components/ui/textarea'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
import { cn } from '@/lib/utils'
import {
isSupporterImportOkRow,
type SupporterImportPreviewResult,
type SupporterImportPreviewRow,
} from '@/utilities/supporterImport'
import { supporterVoteIntentionLabels } from '@/utilities/supporterUi'
const importStatusLabels: Record<SupporterImportPreviewRow['status'], string> = {

View file

@ -0,0 +1,46 @@
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
export type SupporterImportRowStatus =
| 'ok'
| 'duplicado_pelo_telefone'
| 'telefone_invalido'
| 'municipio_nao_reconhecido'
| 'nome_invalido'
| 'intencao_invalida'
export type SupporterImportPreviewRowBase = {
line: number
nome: string
telefone: string
municipio: string
intencao: string
}
export type SupporterImportOkRow = SupporterImportPreviewRowBase & {
status: 'ok'
normalizedPhone: string
canonicalCity?: string
voteIntention?: SupporterVoteIntention
}
export type SupporterImportPreviewRow =
| SupporterImportOkRow
| (SupporterImportPreviewRowBase & {
status: Exclude<SupporterImportRowStatus, 'ok'>
normalizedPhone?: string
canonicalCity?: string
voteIntention?: SupporterVoteIntention
})
export type SupporterImportPreviewResult = {
rows: SupporterImportPreviewRow[]
counts: {
ok: number
duplicate: number
error: number
}
}
export const isSupporterImportOkRow = (
row: SupporterImportPreviewRow,
): row is SupporterImportOkRow => row.status === 'ok'