diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 870ba4f9..8f35893a 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -10,6 +10,7 @@ alwaysApply: false - **B71 entregue em código (2026-07-30):** polimento visual dos resultados de busca no Início (UX-1) — `src/lib/homeSearchUi.ts` (`HOME_SEARCH_HIT_ROW_WRAPPER_CLASS` = bleed + hover + `focus-within`; heading/list); `HomeSearchHitRow` faixa full-bleed `-mx-4 px-4 md:-mx-6 md:px-6` (mantém `onMouseDown` preventDefault de B66); todos os `HomeSearch*Group` com `ul list-none` e título `text-xs` sentence-case (sem `uppercase`/`tracking-wide`). Unit `homeSearchUi.unit.spec.tsx`. Sem migration/action/JSON. Plano: [`polimento-resultados-busca-inicio.md`](../../../docs/plans/polimento-resultados-busca-inicio.md). - **B66 entregue em código (2026-07-30):** modo focado da busca no **focus** (emenda B47) — `homeSearchUiFocused` (`inputFocused || query.isActive`) já no contrato/hook; overlap B68 (sugestões no focus vazio). **Nesta entrega:** `CampaignHomeLayout` retrai summary+spacer (`data-slot="home-chrome"`) e actions com grid `0fr/1fr` + opacity 220ms (`motion-reduce:transition-none`) em vez de `hidden`; `md:flex-none` no chrome; `HomeSearchHitRow` `onMouseDown` preventDefault (blur→click); e2e focus sem digitar + regressão com digitação. Sem migration. Plano: [`modo-focado-busca-no-focus.md`](../../../docs/plans/modo-focado-busca-no-focus.md). - **B61 entregue em código (2026-07-29):** etapa de ajuste de votos no wizard A1 — `WizardExpectedVotesStep` substitui o stub B60 em `atualizar-votos` com `?municipio=`; três passos explícitos média→pessimista→otimista via `?cenario=`; atalhos `2×`/±50/±100; validação client com jump+warning (`getWizardVoteViolation`); **um POST batch** ao concluir otimista (`POST /campanha/municipios/expected-votes`). Helpers em `wizardVoteEstimate.ts` + `wizardActionHref` estendido. Sem migration/Consent. Unit `wizardVoteEstimate`; e2e avanço de cenário + violação. Plano: [`ajuste-votos-wizard.md`](../../../docs/plans/ajuste-votos-wizard.md). Desbloqueia wiring A1 / **B64** (prefill votos). +- **B69 entregue em código (2026-07-30):** modelo de liderança simplificado — `leadership.exclusive` boolean NOT NULL DEFAULT true ("Apoio exclusivo"; migration `20260730_043306_simplify_leadership_fields`, gerada em duas passadas não-interativas e fundida à mão porque o prompt de rename do gerador trava sem TTY; `NOT NULL` emendado) + DROP `sector`/`sectorNotes`/`consentNote` (collection, zod, forms, lista com filtros/sort B29, invite autofill/redemption, dossiê). **Decisão de produto 2026-07-30 que reverteu o rascunho: `organization` FICA** — mapeia ramificações políticas (várias lideranças na mesma org, ex. SindMed); vertical `/campanha/organizacoes`, join, badges, contadores e `activity.organizations` intactos (a remoção da vertical inteira foi avaliada e descartada na conversa). Lista ganha coluna "Apoio exclusivo" com badge outline "Não exclusivo" só quando false; forms com checkbox default-on + input hidden "false" na criação (último valor vence; ausência total da chave cai no default do zod — pin de int cobre os três caminhos). Coluna Organizações continua texto simples (melhoria = fill-in futuro). `?sector=` legado canonicaliza fora (pin unit). LGPD do convite (`consent`/`consentContentHash`/`consentedAt`) intacto. Gate: tsc/lint/format/cycles verde; knip idêntico ao clean tree (P3 + drift B66/B71 pré-existentes); unit 890+16 falhas pré-existentes idênticas ao clean tree; int 518/518; build; Aikido 0 findings. Plano: [`simplificar-modelo-lideranca.md`](../../../docs/plans/simplificar-modelo-lideranca.md). Desbloqueia **B70** (wizard atualizar liderança — grid + form curto com exclusivo). - **B62 entregue em código (2026-07-29):** modelo de sinal simplificado — `municipalityUpdate` kind `sinal` agora é só `signalType` + `body`; migration `20260730_010601_simplify_municipality_signal_fields` dropa `signal_source`/`triangulated` (o gerador reemitiu `ADD VALUE 'adiada'` por drift de snapshot — removido à mão, e o snapshot novo sara a cadeia); labels/descriptions dos 5 tipos reescritas para a mesa (`Rede esfriou`, `Adversário apareceu`, `Alguém pediu algo` — sem "broker" no modelo de sinal; `activity.origin` `pedido_broker` e a copy do suggestionCatalog ficaram fora por decisão da sessão). Forms: `MunicipalitySignalFields` (lista B26) e o bloco inline de `MunicipalityUpdateForm` perderam Fonte/Triangulado; feed/dossiê (`MunicipalityUpdateFeed` + `municipalityUpdatePageData`) só exibem o badge do tipo. **E11: presença de sinal de adversário lê como confirmada — triage 1** (decisão do usuário: "eles não vão adicionar sinais improváveis"), `adversarySignal: { present: true } | null`, fator único "Sinal de adversário registrado", glossário E18 alinhado. Fixa B63. Bônus: teste e2e `campaignMunicipalities` "advisor scopes…" estava obsoleto desde B43 (home da liderança) — atualizado para `/campanha/contatos` + redirect pós-B43, e o pin de bypass do `municipalityElectoralBaseline` baixado 9→8 (drift pré-existente). Gate: tsc/lint/format/cycles/knip(P3 pré-existente)/876 unit/489 int/e2e alvo verde/build (`/campanha` 233 kB); Aikido 0 findings. Plano: [`simplificar-modelo-sinal.md`](../../../docs/plans/simplificar-modelo-sinal.md). Desbloqueia **B63** (wizard registrar sinal). - **E10+ entregue em código (2026-07-29):** Escala e DRY pós-E10 — F1: `loadMunicipalityScopeFromDocs` no `municipalityPageData.ts` evita a segunda query de `municipality` quando `isPagedByPayload === false`; F2: `computeAllMunicipalityPotentials` memoizado por processo (retorna cache `Map`); F3: `TooltipProvider` movido para o layout de `(app)`, restaurando o agrupamento de delay do Radix para os tooltips das listas; F4: DRY nos filtros multi do mobile em `MunicipalityFilters` via `.map` sobre `municipalityFilterDefinitions` (que ganhou `emptyLabel`). Sem migration/UI nova. Plano: [`escala-dry-pos-e10.md`](../../../docs/plans/escala-dry-pos-e10.md). - **B60 entregue em código (2026-07-29):** etapa de busca de município nos wizards UX-1 — `WizardMunicipalitySearchStep` (debounce B47, `POST /campanha/home-search` modo `wizard-municipality`, só municípios scoped); select = `wizardActionHref` com `?municipio=`; loader compartilhado `searchStaffMunicipalityHits` (extraído de B48); `HomeSearchMunicipalityVoteTrailing` + `HomeSearchHitRow` link; stub B61 `WizardMunicipalitySelectedStub` com caption sticky; `WizardMunicipalityPlaceholderStep` removido. Sem migration/Consent. Unit `campaignActionRoutes`; int `searchStaffMunicipalityHits`; e2e wizard → `?municipio=cairu`. Plano: [`busca-municipio-wizard.md`](../../../docs/plans/busca-municipio-wizard.md). Desbloqueia **B61** / **B63** / **B64** / **B70**. diff --git a/AGENTS.md b/AGENTS.md index 8d5b6f8e..b503d17f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,8 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap. **Recently resolved (2026-07-25): Engineering Consolidation Pass 2** (tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2; architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); agent map: `.cursor/rules/codebase-map.mdc`). Highlights: **W1 sistema de listas** — colunas como dado em `CampaignTable` (seams B17/B18), URL state compartilhado (`campaignListUrl` + módulos por domínio; o god module `municipalityUi.ts` virou `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`), shells (`CampaignListFooter`, `CampaignListEmptyState`), 8 superfícies migradas (municípios, lideranças, organizações, dobradinhas, demandas, apoiadores, assessores, atividades-shells), B16+ absorvido (`useOptimistic`, hrefs por serializador canônico, facet por slugs, facets no mesmo `Promise.all`); **W2** — fronteiras: `slug`/`phone`/`wordStartFilter`/`voteEstimate` movidos para `lib/`, 21 loaders `server-only`, `campaignRoles.ts` client-safe (sidebar sem o barrel de access), `votePledgeViews` (contract) vs `votePledgeData` (loaders), ciclo supporter quebrado, `components/campaign` em 15 subpastas de domínio; decisão D1: sem `src/domains/`, sem ports-and-adapters; **W4a** — `electionInsights.ts` (941 linhas, 2 exports vivos) deletado; sobreviventes em `electionFormat.ts`/`voteTrend.ts`; E10 nasce limpo; **W4b** — knip `exports`/`types`/`enumMembers` em **ERROR** no CI (0 findings; kit ui sem peças mortas); **W4c** — tabs das páginas de detalhe extraídas (`MunicipalityDetailTabs`, `ActivityOverviewTab`) + factory `detailTabUi`; **W4d** — `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções documentadas); **W4e** — tipos de fonte única; **W4f** — copy "Praça"→"Município" varrida (apoiadores desambiguado: cidade do contato = "Cidade"); **D3** — consent do WhatsApp por chave estável (`whatsapp-inscricao`, fail-closed). Suites: 46 unit / 48 int / 7 e2e. **Recently resolved (2026-07-30):** **B71** Polimento visual dos resultados de busca no Início — `src/lib/homeSearchUi.ts` centraliza bleed (`-mx-4 px-4 md:-mx-6 md:px-6`), heading discreto (`text-xs` sentence-case) e `ul list-none`; `HomeSearchHitRow` pinta hover/focus na faixa full-bleed; todos os `HomeSearch*Group` alinhados. Sem migration/action/contrato JSON. Plano: [`docs/plans/polimento-resultados-busca-inicio.md`](docs/plans/polimento-resultados-busca-inicio.md). + +**Recently resolved (2026-07-30):** **B69** Simplificar modelo de liderança — `leadership.exclusive` boolean NOT NULL DEFAULT true ("Apoio exclusivo") via migration `20260730_043306_simplify_leadership_fields` + DROP `sector`/`sectorNotes`/`consentNote` (collection, zod, forms, filtros/sort B29 da lista, invite autofill/redemption, dossiê; `?sector=` legado canonicaliza fora). **Decisão de produto 2026-07-30 reverteu o rascunho: `organization` FICA** — mapeia ramificações políticas (SindMed com várias lideranças); vertical `/campanha/organizacoes` e todos os vínculos intactos. Lista ganha coluna "Apoio exclusivo" (badge outline "Não exclusivo" só quando false); forms com checkbox default-on + hidden "false" na criação (último valor vence; ausência cai no default do zod). Nota operacional: o prompt interativo de rename do `migrate:create` trava sem TTY — gerar drops e adds em passadas separadas (`CI=1`) e fundir à mão foi o caminho; o `NOT NULL` foi emendado sobre o SQL gerado. Gate: tsc/lint/format/cycles/build; int 518/518; unit com as mesmas 16 falhas pré-existentes do clean tree (drift B66/B71 na base); knip idêntico à base (P3 + drift); Aikido 0 findings. Plano: [`docs/plans/simplificar-modelo-lideranca.md`](docs/plans/simplificar-modelo-lideranca.md). Desbloqueia **B70**. **Recently resolved (2026-07-29):** **B62** Simplificar modelo de sinal — `municipalityUpdate` kind `sinal` is now `signalType` + `body` only: migration `20260730_010601_simplify_municipality_signal_fields` drops `signal_source` and `triangulated` (the generator re-emitted `ADD VALUE 'adiada'` from snapshot drift — removed by hand; this migration's committed `.json` heals the chain), and the five signal-type labels/descriptions were rewritten for the field team (`Rede esfriou`, `Adversário apareceu`, `Alguém pediu algo` — no "broker" in the signal model; `activity.origin`'s `Pedido de broker` and suggestionCatalog copy were explicitly left out by session decision). `MunicipalitySignalFields` (B26 list control) and the inline block in `MunicipalityUpdateForm` lost Fonte/Triangulado; `municipalityUpdatePageData` + `MunicipalityUpdateFeed` (detail + dossier) render only the type badge. **E11 changed semantics: any adversary signal in the window reads as confirmed — P1 triage 1** (session decision, replacing the triangulated/single-source split), `MunicipalityTriggerInput.adversarySignal: { present: true } | null`, single factor copy, E18 glossary aligned. Bonus gate fixes: the `campaignMunicipalities` "advisor scopes…" e2e was stale since B43 (leader home → `/campanha/contatos`) and was updated, and the `municipalityElectoralBaseline` bypass pin dropped 9→8 (pre-existing main drift). Gate: 876 unit / 489 int / targeted e2e green / build; Aikido 0 findings. Plano: [`docs/plans/simplificar-modelo-sinal.md`](docs/plans/simplificar-modelo-sinal.md). **Recently resolved (2026-07-29):** **B59** Chassis do wizard de campanha — rotas staff em `/campanha/acoes/` (`campaignActionRoutes.ts`, `campaignActionEntryHref`), layout `acoes/layout.tsx` (gate staff, coluna `max-w-lg`), `CampaignWizardShell` (voltar mobile, caption de município sticky, `trailingAction`, foco no `h1` ao trocar passo), cinco páginas-ponte até B60 e strip do Início com `href` nos fluxos wizard. Sem migration. Plano: [`docs/plans/chassis-wizard-campanha.md`](docs/plans/chassis-wizard-campanha.md). diff --git a/docs/plans/simplificar-modelo-lideranca.md b/docs/plans/simplificar-modelo-lideranca.md index 420c2779..a7ec8e8b 100644 --- a/docs/plans/simplificar-modelo-lideranca.md +++ b/docs/plans/simplificar-modelo-lideranca.md @@ -1,12 +1,14 @@ # Simplificar modelo de liderança (exclusivo + limpeza) -Status: rascunho -Atualizado em: 2026-07-29 +Status: entregue 2026-07-30 (org permanece — ver revisão) +Atualizado em: 2026-07-30 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B69 — UX-1 / A4) Impeccable: B — schema + forms/lista/convite existentes; UI de grid do wizard = **B70** -Appetite: ~1–1,25 dia eng; uma migration (drop + `exclusive`); zod/collection/UI lista/forms/invite; access `OwnLeadership` +Appetite: ~1–1,25 dia eng; uma migration (drop + `exclusive`); zod/collection/UI lista/forms/invite Responsável: — +**Revisão 2026-07-30 (decisão de produto, pré-implementação):** `organization` **permanece** — o conceito mapeia como as relações políticas se ramificam na região (várias lideranças na mesma org, ex. SindMed). O escopo final dropou apenas `sector`/`sectorNotes`/`consentNote`; a vertical `/campanha/organizacoes`, o join `leadership.organizations`, badges/colunas/contadores e `activity.organizations` ficam intactos. Confirmado na mesma sessão: `exclusive` aparece na lista (badge "Não exclusivo") + toggle nos forms; coluna Organizações continua texto simples (melhoria vira fill-in). + ## Design (Impeccable) Âncoras: `PRODUCT.md` / `DESIGN.md` · tema `campaign`. Superfícies tocadas = ficha/lista/convite já no Field Desk — craft compacto, não shape de rota nova. @@ -29,38 +31,38 @@ Dados: N/A como métrica nova — boolean categórico + remoção de campos. Dec `leadership` (`src/collections/Leadership.ts`) carrega hoje: `organizations`, `sector`/`sectorNotes`, `consentNote` (“Registro de consentimento **externo**”), além de `supportStatus`, `notes`, `stateDeputies`, municípios e o trio de Consent do **convite** (`consent` / `consentContentHash` / `consentedAt`). -Pedido de produto (2026-07-29), no pacote A4 / Cairu ([fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md)): +Pedido de produto (2026-07-29), no pacote A4 / Cairu ([fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md)), **ajustado em 2026-07-30**: 1. Conceito de parceria **exclusiva** (default = exclusivo): liderança que era a articuladora principal e passa a dividir votos com outro candidato deixa de ser exclusiva — e pode voltar. Não é “principal vs secundária” na v1 (ranking implícito); é **exclusividade do apoio**. -2. Remover da mesa os conceitos pouco usados: **organização**, **setor** (e notas de setor), **registro de consentimento externo** (`consentNote`). +2. Remover da mesa os conceitos pouco usados: **setor** (e notas de setor), **registro de consentimento externo** (`consentNote`). **Organização fica** (revisão 2026-07-30): ramificação das relações políticas — várias lideranças na mesma org (SindMed). 3. “A própria liderança é a dobradinha” — `stateDeputy` hoje é entidade por **nome** (sem `Contact`); `leadership` tem `Contact` único. Não unificar collections neste item. -Lista `/campanha/liderancas` já filtra/ordena por `sector` (B29 ✓); coluna Organizações é texto morto. Invite autofill ainda pede `sector`/`sectorNotes`. +Lista `/campanha/liderancas` já filtra/ordena por `sector` (B29 ✓). Invite autofill ainda pede `sector`/`sectorNotes`. ## Objetivos -- Migration `pnpm migrate:create simplify_leadership_fields` (nome sugerido): - - **DROP** `organizations` (tabela de join), `sector`, `sector_notes`, `consent_note`. - - **ADD** `exclusive` boolean **NOT NULL DEFAULT true** (backfill implícito = todos nascem exclusivos — honesto com o default de produto). -- Collection + zod (`src/lib/schemas/leadership.ts`): espelhar; `exclusive` staff-managed como `supportStatus`/`notes`. -- Remover de create/internal forms, list columns/filters/sort de setor, badges de org no `MunicipalityLeadershipsPanel`, `organizationNames` / `organizationIDs` do view model. -- Invite: dropar `sector`/`sectorNotes` do schema + `CampaignInviteForm` + redemption profile write (não tocar `consent*` do LGPD do convite). -- Access: `getOwnEngagedLeadership` deixa de selecionar/expor `organizationIDs` (`municipalities.ts`); qualquer leitor de org via liderança engajada ajusta ou morre. -- `organizationData` counts “lideranças da org” → **zero path** ou remover a métrica se só existia via esse join (verificar call sites; não inventar proxy). -- Labels: `exclusive` → “Apoio exclusivo”; false → leitura “Não exclusivo” / badge discreto na lista (opcional neste item; B70 mostra no tile/form). +- Migration `20260730_043306_simplify_leadership_fields`: + - **DROP** `sector`, `sector_notes`, `consent_note` (+ `enum_leadership_sector`). + - **ADD** `exclusive` boolean **NOT NULL DEFAULT true** (backfill implícito = todos nascem exclusivos — honesto com o default de produto). Gerada em duas passadas não-interativas (drop, depois add) e fundida à mão — o prompt interativo de rename trava sem TTY; o `NOT NULL` foi emendado sobre o SQL gerado. +- Collection + zod (`src/lib/schemas/leadership.ts`): espelhado; `exclusive` staff-managed como `supportStatus`/`notes`. +- Removido de create/internal forms, list columns/filters/sort de setor, badges de setor no dossiê; `sector`/`sectorNotes` fora dos view models. +- Invite: `sector`/`sectorNotes` fora do schema + `CampaignInviteForm` + redemption profile write (o trio `consent*` do LGPD do convite intacto). +- Lista: coluna setor → coluna **"Apoio exclusivo"** com badge outline "Não exclusivo" só quando `false` (Restrained; badge não compete com os vermelhos da linha). +- **Checkbox padrão-marcado + input hidden "false"** no form de criação (último valor vence), para que desmarcar grave `false` e ausência total da chave caia no default do zod — pin de int cobre os três caminhos. - Sem Consent novo. Sem UI de grid do wizard (**B70**). -- Unit/int: create default `exclusive: true`; update para `false`; filtros de setor removidos; invite sem setor. +- Unit/int: create default `exclusive: true`; update para `false`; `?sector=` legado canonicaliza fora; invite sem setor. ## Decisões travadas - **Campo `exclusive: boolean`, default `true`, no documento `leadership`.** **Rejeitado:** `principal`/`prioritaria` (sugerem ranking 1º/2º, não o relato “dividiu apoio”); exclusividade por par liderança×município nesta fatia (caro — join/metadata; o relato do CG é sobre a parceria com a campanha, não nuance A vs B). Gatilho abaixo se a mesma pessoa precisar ser exclusiva em um município e não em outro. -- **Dropar `organizations`, `sector`, `sectorNotes`, `consentNote`.** **Rejeitado:** deixar opcionais “por se acaso” (pedido: simplificar; campos mortos travam atualização); dropar o trio `consent*` do **convite** (é LGPD `lideranca-autopreenchimento`, Onda 0 — distinto do `consentNote` externo). +- **Dropar `sector`, `sectorNotes`, `consentNote`.** **Rejeitado:** deixar opcionais “por se acaso” (campos mortos travam atualização); dropar o trio `consent*` do **convite** (é LGPD `lideranca-autopreenchimento`, Onda 0 — distinto do `consentNote` externo). +- **`organization` PERMANECE (revisão 2026-07-30, produto).** O rascunho dropava o join; revertido — a org mapeia ramificações políticas (SindMed com várias lideranças). **Rejeitado na revisão:** remover a vertical `/campanha/organizacoes` inteira (avaliada e descartada pelo produto na conversa); dropar `activity.organizations` junto. - **“Liderança é a dobradinha” = reusar `stateDeputy` + `leadership.stateDeputies`.** Criar/vincular a ficha de dobradinha com o nome da pessoa; chips B31 ✓ já expressam a rede. **Rejeitado:** collection unificada; `Contact` obrigatório em `stateDeputy` agora; flag `actsAsStateDeputy` sem entity (segunda verdade). Atalho “criar dobradinha a partir desta liderança” → Adiado. - **i18n:** identifier `exclusive`; copy “Apoio exclusivo” / “Não exclusivo”. ## Questões em aberto -- **Badge “Não exclusivo” na lista `/liderancas` neste item ou só no B70?** **Opções:** A lista + forms | B só forms/wizard. **Recomendação:** A — coluna ou chip discreto na lista (1 célula), senão o campo some da varredura. _(assumido)_ +- ~~**Badge “Não exclusivo” na lista `/liderancas` neste item ou só no B70?**~~ **Fechado 2026-07-30 (produto):** na lista — coluna própria com badge outline só quando `false`. - **Exclusividade por município?** **Opções:** A documento (v1) | B join. **Recomendação:** A; revisitação no Adiado. _(assumido — validar com produto se Cairu tiver o mesmo nome em dois municípios com papéis diferentes)_ ## Abordagem proposta @@ -69,19 +71,16 @@ Lista `/campanha/liderancas` já filtra/ordena por `sector` (B29 ✓); coluna Or flowchart LR Mig["migration DROP + exclusive"] --> Zod["zod + Leadership.ts"] Zod --> UI["forms + lista + invite"] - Zod --> Access["OwnLeadership sem orgs"] - Zod --> Orgs["organizationData counts"] ``` -Componentes: +Componentes (como entregue): -- Migration + `Leadership.ts` + `src/lib/schemas/leadership.ts` (+ invite schema se setor sair). -- `LeadershipForm` / `LeadershipInternalForm` — remover org/setor/consentNote; toggle `exclusive` (default on). -- `liderancas/page.tsx` + `leadershipListUrl` / filters — dropar `sector`; opcional coluna/filtro `exclusive`. -- `leadershipData.ts` — view models sem org/setor; incluir `exclusive`. -- `CampaignInviteForm` + redemption — sem setor. -- `municipalities.ts` `getOwnEngagedLeadership` — só municípios. -- **Migration:** sim (drop + add boolean default true). +- Migration + `Leadership.ts` + `src/lib/schemas/leadership.ts` + invite schema. +- `LeadershipForm` / `LeadershipInternalForm` — remover setor/consentNote; checkbox `exclusive` (default on; hidden "false" na criação). +- `liderancas/page.tsx` + `leadershipListUrl` / filters — dropar `sector`; coluna "Apoio exclusivo". +- `leadershipData.ts` — view models sem setor/`sectorNotes`/`consentNote`; incluir `exclusive` (orgs mantidas). +- `CampaignInviteForm` + redemption + page data — sem setor. +- **Migration:** sim (`20260730_043306_simplify_leadership_fields`, drop + add boolean default true NOT NULL). ## Dependências @@ -92,12 +91,11 @@ Componentes: - Grid de tiles + form do wizard A4 → **B70**. - Wiring “quer também liderança?” no A1 → fatia A1. - Unificar `stateDeputy` com `Contact` / atalho criar-dobradinha → Adiado. -- Collection `organization` em si (vertical `/campanha/organizacoes` permanece; só some o vínculo na liderança). +- Collection `organization` e a vertical `/campanha/organizacoes` — **intactas por decisão 2026-07-30** (inclui `leadership.organizations`, badges no painel/dossiê e contadores do `organizationData`). ## Rabbit holes - **Exclusividade por município + histórico de mudanças.** **Mitigação:** boolean no doc; nota em `notes` se precisar matizar. -- **Reescrever access de leader via organizações.** **Mitigação:** remover o ramo; leader continua lockdown em contatos — confirmar que `organizationIDs` não alimenta rota staff crítica antes de dropar. - **Backfill `exclusive: false` a partir de texto em `notes`.** **Mitigação:** não — default true; mesa corrige no wizard. ## Adiado com gatilho diff --git a/docs/roadmap.md b/docs/roadmap.md index 87749090..b52ebe09 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-30 — ~~**B76**~~ ✓ coerência wizard ajuste votos; **B74** gap strip v2; **B75** header mobile do wizard; **B73** remover bottom nav; ~~**B66**~~ ✓ busca no focus; ~~**B71**~~ ✓ polish busca; ~~**B61**~~ ✓; ~~**B51**~~ ✓; ~~**B52**~~ ✓; ~~**B72**~~ ✓; **B69**–**B70** A4 liderança; ~~**B62**~~ ✓ simplificar sinal (E11 triage 1 por presença); ~~**B56**~~ ✓; ~~**B67**~~ ✓; ~~**B68**~~ ✓; ~~**B59**~~ ✓; ~~**B65**~~ ✓; ~~**B60**~~ ✓; ~~**B48**~~ ✓; ~~**B49**~~ ✓; ~~**B50**~~ ✓; B53–B55; wizards ~~B60~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓/B63–B64. +Atualizado em: 2026-07-30 — ~~**B76**~~ ✓ coerência wizard ajuste votos; **B74** gap strip v2; **B75** header mobile do wizard; **B73** remover bottom nav; ~~**B66**~~ ✓ busca no focus; ~~**B71**~~ ✓ polish busca; ~~**B61**~~ ✓; ~~**B51**~~ ✓; ~~**B52**~~ ✓; ~~**B72**~~ ✓; ~~**B69**~~ ✓ liderança exclusiva (org fica); **B70** A4 liderança; ~~**B62**~~ ✓ simplificar sinal (E11 triage 1 por presença); ~~**B56**~~ ✓; ~~**B67**~~ ✓; ~~**B68**~~ ✓; ~~**B59**~~ ✓; ~~**B65**~~ ✓; ~~**B60**~~ ✓; ~~**B48**~~ ✓; ~~**B49**~~ ✓; ~~**B50**~~ ✓; B53–B55; wizards ~~B60~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓/B63–B64. Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). ## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026) @@ -83,7 +83,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re ### Demais itens abertos -- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis Início:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → ~~**B45**~~ ✓ → ~~**B46**~~ ✓ → ~~**B47**~~ ✓ → ~~**B58**~~ ✓ → ~~**B67**~~ ✓ (pan strip) → ~~**B65**~~ ✓ (âncora inferior mobile) → ~~**B66**~~ ✓ (focus limpa tela + animação) → ~~**B48**~~ ✓ → ~~**B49**~~ ✓ → ~~**B68**~~ ✓ → ~~**B71**~~ ✓ (polish resultados busca) · ~~**B72**~~ ✓ (gap strip) → **B74** (gap strip v2) → **B73** (remover bottom nav mobile) → ~~**B50**~~ ✓ → ~~**B51**~~ ✓ → ~~**B52**~~ ✓ → **B53–B55** · ~~**B56**~~ ✓ · **B57** futuro. **Wizards:** ~~**B59**~~ ✓ → **B75** (header mobile wizard) → ~~**B60**~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓ /**B64** · ~~**B62**~~ ✓ → **B63** · **B69** → **B70** (A4 liderança). +- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis Início:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → ~~**B45**~~ ✓ → ~~**B46**~~ ✓ → ~~**B47**~~ ✓ → ~~**B58**~~ ✓ → ~~**B67**~~ ✓ (pan strip) → ~~**B65**~~ ✓ (âncora inferior mobile) → ~~**B66**~~ ✓ (focus limpa tela + animação) → ~~**B48**~~ ✓ → ~~**B49**~~ ✓ → ~~**B68**~~ ✓ → ~~**B71**~~ ✓ (polish resultados busca) · ~~**B72**~~ ✓ (gap strip) → **B74** (gap strip v2) → **B73** (remover bottom nav mobile) → ~~**B50**~~ ✓ → ~~**B51**~~ ✓ → ~~**B52**~~ ✓ → **B53–B55** · ~~**B56**~~ ✓ · **B57** futuro. **Wizards:** ~~**B59**~~ ✓ → **B75** (header mobile wizard) → ~~**B60**~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓ /**B64** · ~~**B62**~~ ✓ → **B63** · ~~**B69**~~ ✓ → **B70** (A4 liderança). - ~~**B43** Início em branco + Quadro~~ — **entregue 2026-07-29** (`/campanha` blank; staff dashboard em `/campanha/quadro`; liderança em `/campanha/contatos`; nav Início+Quadro / Início+Meus contatos; `requireCampaignPageActor` manda leader bloqueado → `/campanha/contatos`; sem migration) · [plano](plans/inicio-em-branco-quadro.md) - ~~**B44** Botão de ação do Início + lista horizontal~~ — **entregue 2026-07-29** (`CampaignHomeActionButton` + `CampaignHomeActionStrip`; Tooltip fine / long-press Drawer coarse; montado no Início por B45 ✓) · ~1d · Janela 1 · Impeccable C · [plano](plans/botao-acao-inicio-strip.md) - ~~**B45** Catálogo de ações por persona~~ — **entregue 2026-07-29** (`src/lib/campaignHomeActions.ts` + `CampaignHomeActions`; staff 6 ações com `uncovered-municipalities` → `?coverage=sem_assessor&sort=votos`; leader 2 com `my-contacts` → `/campanha/contatos`; demais inertes; ilha client por limite RSC↔ícones Lucide) · [plano](plans/catalogo-acoes-inicio-por-persona.md) @@ -96,7 +96,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - ~~**B62** Simplificar modelo de sinal~~ — **entregue 2026-07-29** (sinal = tipo + texto; migration drop `signal_source`/`triangulated`; labels mesa sem "broker" — `Rede esfriou`/`Adversário apareceu`/`Alguém pediu algo`; E11 sem dimensão triangulado: presença = confirmado, **triage 1**; forms/lista/detalhe/dossiê alinhados; e2e stale-B43 corrigido de bônus) · [plano](plans/simplificar-modelo-sinal.md) - **B63** Wizard registrar sinal (grid de tipos + Drawer info + texto + “Pular” se origem ≠ register-signal) · Depende de B59 + B62 · soft B60 · ~1–1,25d · Janela 1 · Impeccable C · [plano](plans/wizard-registro-sinal.md) - **B64** Wizard mudar tendência (grid colorido ≠ atual + Drawer info + justificativa com prefill/Limpar/Salvar + “Pular” sempre) · Depende de B59 · soft B60 · soft B24 ✓ · ~1–1,25d · Janela 1 · Impeccable C · [plano](plans/wizard-mudar-tendencia.md) -- **B69** Simplificar modelo de liderança (`exclusive` default true; dropar org/setor/`consentNote`; dobradinha-self = `stateDeputy`+chips) · Depende de — · ~1–1,25d · Janela 1 · Paralelizável com B59/B62 · Impeccable B · [plano](plans/simplificar-modelo-lideranca.md) +- ~~**B69** Simplificar modelo de liderança (`exclusive` default true; dropar setor/`consentNote`; **org fica** — ramificação política, decisão 2026-07-30)~~ ✓ (entregue 2026-07-30) · Depende de — · Janela 1 · [plano](plans/simplificar-modelo-lideranca.md) - **B70** Wizard atualizar liderança (grid 2/3/4 + form curto + skip + Continuar dirty + focus) · Depende de B59 + B69 · soft B60 · ~1,25–1,5d · Janela 1 · Impeccable C · [plano](plans/wizard-atualizar-lideranca.md) - ~~**B46** Posição dos botões de ação (thumb zone)~~ — **entregue 2026-07-29** (`CampaignHomeLayout`: coluna `min-h-full` + spacer `flex-1` só em mobile; `data-slot="home-actions"` / `home-search`; ordem flex `order-*` — mobile ações→busca, `md+` busca→ações); `/campanha` usa `CampaignPageShell` `min-h-full` + layout; decisão sticky vs coluna = coluna (opção B). Unit `campaignHomeLayout.unit.spec.ts`; e2e B45 intacto. Sem migration. Âncora inferior no mobile corrigida por **B65** ✓ (2026-07-29). · [plano](plans/posicao-botoes-acao-inicio-thumb-zone.md) - ~~**B47** Busca global — input (debounce, sem botão; modo focado: sobe e esconde o resto)~~ — **entregue 2026-07-29** (`CampaignHomeStaffChrome` + `CampaignHomeSearch` + `useHomeSearchQuery`; debounce 250 ms; query ativa ≥2 chars; `HomeSearchContext` para B48+; staff-only via `isStaffCampaignRole`; Escape limpa; região `aria-live` sem hits ainda; sem migration) · [plano](plans/busca-global-inicio-input.md) · **emenda B66:** limpeza no **focus** (não só na digitação) + animação @@ -251,7 +251,7 @@ flowchart TD B62n["B62 ✓ Simplificar
modelo de sinal"] B63n["B63 Wizard sinal
tipo + texto"] B64n["B64 Wizard tendência
escolha + justificativa"] - B69n["B69 Simplificar modelo
liderança + exclusive"] + B69n["B69 ✓ Liderança exclusiva
sem setor/consentNote"] B70n["B70 Wizard atualizar
liderança grid+form"] B15n["B15 ✓ Ordenar lista
por coluna"] B16n["B16 ✓ Filtros no header
da lista"] @@ -476,11 +476,11 @@ flowchart TD B8F2 -.rank por ZE = débito aberto.-> B14n ``` -Paralelizáveis agora: **B73** (remover bottom nav — paralelo a B74); **B74** (gap strip v2 — após ~~B72~~ ✓; paralelo a B73); **B75** (header mobile wizard — após ~~B59~~ ✓; soft nos wizards restantes B63/B64/B70); **B69** (simplificar liderança — paralelo a B59/B62; desbloqueia B70); **B49–B53** após B47 ✓ / B48 ✓ (depois B54/B55). Após **B59**: ~~**B60**~~ ✓ ∥ ~~**B61**~~ ✓ ∥ **B64** (B75 soft). **B63** espera B59+B62 ✓. **B70** espera B59+B69 (+ soft B75). Prefills ricos de B64 com B61/B63 são soft. **B57** futuro (depende B56 ✓). Fill-ins / débitos `*+` e itens já ✓ permanecem fora do caminho crítico UX-1. **D3** só após smoke + folga se UX-1 não absorver o Zap. +Paralelizáveis agora: **B73** (remover bottom nav — paralelo a B74); **B74** (gap strip v2 — após ~~B72~~ ✓; paralelo a B73); **B75** (header mobile wizard — após ~~B59~~ ✓; soft nos wizards restantes B63/B64/B70); **B49–B53** após B47 ✓ / B48 ✓ (depois B54/B55). Após **B59**: ~~**B60**~~ ✓ ∥ ~~**B61**~~ ✓ ∥ **B64** (B75 soft). **B63** espera B59+B62 ✓. **B70** espera ~~B59+B69~~ ✓ (+ soft B75). Prefills ricos de B64 com B61/B63 são soft. **B57** futuro (depende B56 ✓). Fill-ins / débitos `*+` e itens já ✓ permanecem fora do caminho crítico UX-1. **D3** só após smoke + folga se UX-1 não absorver o Zap. ### Sequência por janela (só pendentes) -**Janela 1 — agora → 05/08 (convenções):** **prioridade pós-29/07 — ~~B43–B48~~ ✓ → ~~B67~~ ✓ (pan strip) **→ ~~B65~~ ✓** (âncora inferior) **→ ~~B66~~ ✓** (focus limpa busca) **→ ~~B68~~ ✓\*\* **→ ~~B71~~ ✓** (polish resultados) **· ~~B72~~ ✓** / **B74** (gap strip) **→ B73** (remover bottom nav) **→** **wizards ~~B59~~ ✓ → B75** (header mobile) → ~~B60~~ ✓/~~B61~~ ✓/~~B76~~ ✓ ∥B64 · ~~B62~~ ✓ → B63 · B69 → B70 (soft 03/08) · **B49–B55** · [fluxos](plans/fluxos-acao-primeiro-inicio.md); smoke + onboarding Onda 0; **E4R ✓** aplicar seed em produção após smoke; demais entregues/pendentes da janela inalterados no detalhe abaixo — foco operacional = polish Início B74 + chrome B73/B75 + busca B49+ + wizards B63–B64 + A4 B69/B70. +**Janela 1 — agora → 05/08 (convenções):** **prioridade pós-29/07 — ~~B43–B48~~ ✓ → ~~B67~~ ✓ (pan strip) **→ ~~B65~~ ✓** (âncora inferior) **→ ~~B66~~ ✓** (focus limpa busca) **→ ~~B68~~ ✓\*\* **→ ~~B71~~ ✓** (polish resultados) **· ~~B72~~ ✓** / **B74** (gap strip) **→ B73** (remover bottom nav) **→** **wizards ~~B59~~ ✓ → B75** (header mobile) → ~~B60~~ ✓/~~B61~~ ✓/~~B76~~ ✓ ∥B64 · ~~B62~~ ✓ → B63 · ~~B69~~ ✓ → B70 (soft 03/08) · **B49–B55** · [fluxos](plans/fluxos-acao-primeiro-inicio.md); smoke + onboarding Onda 0; **E4R ✓** aplicar seed em produção após smoke; demais entregues/pendentes da janela inalterados no detalhe abaixo — foco operacional = polish Início B74 + chrome B73/B75 + busca B49+ + wizards B63–B64 + A4 ~~B69~~ ✓/B70. **Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; ~~**E9** fila de alocação~~ (entregue 2026-07-24, antecipada da janela 2); ~~**E16** dossiê do município~~ (entregue 2026-07-25, antecipado da janela 2); D2 se sobrar folga. @@ -490,7 +490,7 @@ Paralelizáveis agora: **B73** (remover bottom nav — paralelo a B74); **B74** ### Cortes seguros / não cortáveis -**Não cortáveis:** Onda 0 (jurídico/Consent); ~~**B43–B48** chassis UX-1 + busca Municípios~~ ✓; ~~**B67** pan na strip de ações~~ ✓ (entregue 2026-07-29 — B58 escondia a barra; drag fine restaurado); ~~**B65** âncora inferior mobile~~ ✓ (entregue 2026-07-29); ~~**B66** focus limpa busca~~ ✓ (entregue 2026-07-30); **B59–B61** etapas mínimas do wizard de votos (chassis + município sticky + busca município + ajuste — cortar = ações do Início continuam inertes no ritual A1 **e** o CG perde o contexto de município no meio do passo); ~~**B76** coerência do ajuste de votos~~ ✓ (entregue 2026-07-30); **B75** header mobile do wizard (cortar = Voltar+título no conteúdo competem com Mandate Red+sidebar no meio do ritual — emenda explícita a B59); ~~**B62** simplificar sinal~~ ✓ (entregue 2026-07-29); **B69** simplificar liderança (schema `exclusive` + limpeza — cortar = A4/B70 sem modelo e ficha continua inchada); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); ~~**E8** conta da cadeira~~ (entregue 2026-07-24); ~~**C12** registro-fundação~~ (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**E9** fila de alocação~~ (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); ~~**B19** gerenciar assessores~~ (entregue 2026-07-24 — CG/candidato não entram em `/admin`); **B39** lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção). +**Não cortáveis:** Onda 0 (jurídico/Consent); ~~**B43–B48** chassis UX-1 + busca Municípios~~ ✓; ~~**B67** pan na strip de ações~~ ✓ (entregue 2026-07-29 — B58 escondia a barra; drag fine restaurado); ~~**B65** âncora inferior mobile~~ ✓ (entregue 2026-07-29); ~~**B66** focus limpa busca~~ ✓ (entregue 2026-07-30); **B59–B61** etapas mínimas do wizard de votos (chassis + município sticky + busca município + ajuste — cortar = ações do Início continuam inertes no ritual A1 **e** o CG perde o contexto de município no meio do passo); ~~**B76** coerência do ajuste de votos~~ ✓ (entregue 2026-07-30); **B75** header mobile do wizard (cortar = Voltar+título no conteúdo competem com Mandate Red+sidebar no meio do ritual — emenda explícita a B59); ~~**B62** simplificar sinal~~ ✓ (entregue 2026-07-29); ~~**B69** simplificar liderança~~ ✓ (entregue 2026-07-30 — `exclusive` default true; setor/`consentNote` dropados; **organização fica**: ramificação política, ex. SindMed); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); ~~**E8** conta da cadeira~~ (entregue 2026-07-24); ~~**C12** registro-fundação~~ (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**E9** fila de alocação~~ (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); ~~**B19** gerenciar assessores~~ (entregue 2026-07-24 — CG/candidato não entram em `/admin`); **B39** lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção). **Cortes seguros** (se o prazo apertar, nesta ordem): ~~**B71**~~ ✓; **B74** gap strip v2 (B72 já apertou uma vez — cortar = strip um pouco esparça); **B68** sugestões na busca aberta (digitação B48 continua; cortar = empty mudo no focus até folga); **B64** wizard tendência standalone (A1 ainda pode embutir tendência via B24 ✓ / passo curto; cortar = sem grid colorido + prefill até folga); **B63** wizard sinal standalone (A1 ainda pode embutir select pós-B62; cortar = sem grid/Drawer de tipos até folga); **B70** wizard liderança standalone (A1 ainda pode embutir passo curto pós-B69; cortar = sem grid 2/3/4 até folga — pacote Cairu sofre); **B57** delta 7d (Início continua com total+cobertura do B56; Δ era futuro de propósito); **B56** resumo no Início (Quadro já tem os mesmos agregados — cortar = Início sem briefing numérico); **B55** ações WA/share na busca (linha continua clicável); **B54** grid/cap (empilhar sempre); **B51**/**B53**/**B52**/**B50**/**B49** grupos secundários da busca (nessa ordem — Municípios B48 fica); **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter a fila E9 ✓ sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); ~~**E14** níveis~~ (entregue 2026-07-27 — `priority` continua, intocado); ~~**B8 F2** polígonos~~ (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); **D2** push (adiar); **A6**; ~~**B14** município mais próximo~~ (entregue 2026-07-26); ~~**B38** sidebar recolhido em tablet~~ (entregue 2026-07-26); ~~**B40** login biométrico~~ (entregue 2026-07-28 — senha + B39 ✓ continuam, e cortar teria custado só o atalho de digital/Face ID; era o mais caro e o primeiro a cair desta leva B39–B42); **B42** polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); ~~**B41** scroll horizontal + sticky~~ (entregue 2026-07-27) (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai **depois** de B42 porque o bug de layout em mesa é mais caro que o polish mobile); ~~**B18** filtros salvos~~ ✓ entregue (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B21** página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — **B25** cai junto por definição (sem destino, sem link); ~~**B17** seletor de colunas~~ ✓ entregue (tabela completa continua; scroll horizontal); ~~**B22** explicação no header~~ (entregue 2026-07-26); ~~**B23** tooltip nas células~~ (entregue 2026-07-26); ~~**B24** auto-save + justificativa na Tendência~~ (entregue 2026-07-26); **B27** combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); **B28** e-mail + celular na lista de lideranças (a ficha `/liderancas/[id]` continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); ~~**B30** convite de completar cadastro por WhatsApp na lista~~ (entregue 2026-07-26); **B31** coluna de dobradinhas nas lideranças (o `RelationMultiSelect` da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); ~~**B32** editar Status de apoio na célula~~ (entregue 2026-07-26); **B34** chips editáveis de municípios na lista de lideranças (o texto simples `row.municipalityNames.join(', ')` continua legível e a ficha `/liderancas/[id]` continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); **B37** chips editáveis de municípios na lista de dobradinhas (o número `municipalityCount` continua legível e a ficha de município continua vinculando dobradinha pelo `MunicipalityStrategyForm` — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); ~~**B36** coluna de lideranças na lista de dobradinhas~~ (entregue 2026-07-26; a contagem `leadershipCount` era o fallback e a ficha `/liderancas/[id]` continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); ~~**B33** sort + filtro no header das dobradinhas~~ (entregue 2026-07-26); **B29** sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai **antes** do B26, que é o que sustenta o gate de adoção); **B26** registrar sinal na célula (o formulário completo em `/[slug]?tab=updates` continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai **depois** de B22/B23/B24); ~~**C13** renomear Plano de Ação → Atividade~~ (entregue 2026-07-25); **B20** prioritários no Início (lista `?priority=alta` continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in **renomear coluna Cobertura** (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in **remover coluna Tipo** (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); ~~fill-in **remover lista de zonas do Início**~~ ✓ (entregue 2026-07-28 — o mapa B8 F2 ✓ continua abrindo cada ZE); ~~**B16** filtros no header~~ (entregue 2026-07-25); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); ~~**E18** documentação de conceitos~~ (entregue 2026-07-24); ~~**E16** dossiê~~ (entregue 2026-07-25); demais débitos/fill-ins. diff --git a/src/app/(campaign)/campanha/(app)/liderancas/[id]/formActions.ts b/src/app/(campaign)/campanha/(app)/liderancas/[id]/formActions.ts index 660debd1..ac0f99a1 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/[id]/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/liderancas/[id]/formActions.ts @@ -12,7 +12,6 @@ import { } from '@/lib/formData' import { LEADERSHIP_MUNICIPALITY_SCOPE_MESSAGE, - leadershipSectors, leadershipSupportStatuses, } from '@/lib/schemas/leadership' import { @@ -31,7 +30,6 @@ export const updateLeadershipInternalFormAction = async ( ): Promise => runCampaignFormAction({ execute: async () => { - const sector = optionalFormText(formData, 'sector') const supportStatus = optionalFormText(formData, 'supportStatus') await updateLeadershipInternal({ @@ -39,16 +37,13 @@ export const updateLeadershipInternalFormAction = async ( municipalities: repeatedRelationshipFormValues(formData, 'municipalities'), organizations: repeatedRelationshipFormValues(formData, 'organizations'), stateDeputies: repeatedRelationshipFormValues(formData, 'stateDeputies'), - sector: leadershipSectors.includes(sector as (typeof leadershipSectors)[number]) - ? (sector as (typeof leadershipSectors)[number]) - : null, + exclusive: formData.get('exclusive') === 'true', supportStatus: leadershipSupportStatuses.includes( supportStatus as (typeof leadershipSupportStatuses)[number], ) ? (supportStatus as (typeof leadershipSupportStatuses)[number]) : undefined, notes: nullableFormText(formData, 'notes'), - consentNote: nullableFormText(formData, 'consentNote'), }) revalidatePath('/campanha/liderancas/[id]', 'page') return { message: 'Ficha da liderança atualizada.' } diff --git a/src/app/(campaign)/campanha/(app)/liderancas/nova/formActions.ts b/src/app/(campaign)/campanha/(app)/liderancas/nova/formActions.ts index 6a51cd09..aaff42dd 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/nova/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/liderancas/nova/formActions.ts @@ -6,7 +6,6 @@ import { optionalFormText, repeatedRelationshipFormValues, requiredFormText } fr import { LEADERSHIP_DUPLICATE_MESSAGE, LEADERSHIP_MUNICIPALITY_SCOPE_MESSAGE, - leadershipSectors, leadershipSupportStatuses, } from '@/lib/schemas/leadership' import { @@ -28,8 +27,11 @@ export const createLeadershipFormAction = async ( ): Promise => runCampaignRedirectFormAction({ execute: () => { - const sector = optionalFormText(formData, 'sector') const supportStatus = optionalFormText(formData, 'supportStatus') + // Checkbox ambiguity: unchecked submits nothing, so the form pairs the + // checkbox with a hidden "false"; last value wins. A genuinely absent + // key (tests, non-form callers) falls back to the schema default. + const exclusiveValues = formData.getAll('exclusive') return createLeadership({ name: requiredFormText(formData, 'name'), @@ -38,16 +40,13 @@ export const createLeadershipFormAction = async ( municipalities: repeatedRelationshipFormValues(formData, 'municipalities'), organizations: repeatedRelationshipFormValues(formData, 'organizations'), stateDeputies: repeatedRelationshipFormValues(formData, 'stateDeputies'), - sector: leadershipSectors.includes(sector as (typeof leadershipSectors)[number]) - ? (sector as (typeof leadershipSectors)[number]) - : undefined, + exclusive: exclusiveValues.length === 0 ? undefined : exclusiveValues.at(-1) === 'true', supportStatus: leadershipSupportStatuses.includes( supportStatus as (typeof leadershipSupportStatuses)[number], ) ? (supportStatus as (typeof leadershipSupportStatuses)[number]) : 'a_abordar', notes: optionalFormText(formData, 'notes'), - consentNote: optionalFormText(formData, 'consentNote'), }) }, redirectTo: (leadership) => `/campanha/liderancas/${leadership.id}`, diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx index f7a5bcdb..aa12db71 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx @@ -46,7 +46,7 @@ import { type MunicipalityPortfolioIndexEntry, } from '@/lib/municipalityPortfolio' import { formatBrazilianPhoneInput, whatsAppHrefForPhone } from '@/lib/phone' -import { MAX_LEADERSHIP_MUNICIPALITIES, isLeadershipSector } from '@/lib/schemas/leadership' +import { MAX_LEADERSHIP_MUNICIPALITIES } from '@/lib/schemas/leadership' import { cn } from '@/lib/utils' import { getAdvisorMunicipalityIds } from '@/utilities/campaignAccess' import { readCampaignColumnVisibility } from '@/utilities/campaignColumnVisibilityCookie' @@ -71,7 +71,6 @@ import { resolveLeadershipListUrl, type LeadershipListState, } from '@/utilities/leadership/leadershipListUrl' -import { leadershipSectorLabels } from '@/utilities/leadership/leadershipUi' import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex' import { @@ -175,13 +174,9 @@ const leadershipColumns = ({ ), }, { - id: 'sector', - label: leadershipListSortLabels.sector, - head: , - cell: (row) => { - if (!row.sector || !isLeadershipSector(row.sector)) return row.sector || '—' - return leadershipSectorLabels[row.sector] - }, + id: 'exclusive', + label: 'Apoio exclusivo', + cell: (row) => (row.exclusive ? null : Não exclusivo), }, { id: 'municipalities', diff --git a/src/app/(campaign)/campanha/actions/leadership.ts b/src/app/(campaign)/campanha/actions/leadership.ts index eb48a8b8..e8d1a2d3 100644 --- a/src/app/(campaign)/campanha/actions/leadership.ts +++ b/src/app/(campaign)/campanha/actions/leadership.ts @@ -138,11 +138,9 @@ const createValidatedLeadershipRecord = async ( municipalities: data.municipalities, organizations: data.organizations ?? [], stateDeputies: data.stateDeputies ?? [], - sector: data.sector, - sectorNotes: data.sectorNotes, + exclusive: data.exclusive, supportStatus: data.supportStatus, notes: data.notes, - consentNote: data.consentNote, createdBy: currentActor.id, }, depth: 0, diff --git a/src/app/(campaign)/campanha/convite/[token]/formActions.ts b/src/app/(campaign)/campanha/convite/[token]/formActions.ts index 13bd6e43..f415c348 100644 --- a/src/app/(campaign)/campanha/convite/[token]/formActions.ts +++ b/src/app/(campaign)/campanha/convite/[token]/formActions.ts @@ -34,8 +34,6 @@ const profileFromForm = (formData: FormData) => ({ phone: requiredFormText(formData, 'phone'), email: nullableFormText(formData, 'email'), gender: nullableFormText(formData, 'gender'), - sector: nullableFormText(formData, 'sector'), - sectorNotes: nullableFormText(formData, 'sectorNotes'), }) const inviteFormError = (error: unknown): CampaignInviteFormState => diff --git a/src/collections/Leadership.ts b/src/collections/Leadership.ts index 45aefa8c..8d65e723 100644 --- a/src/collections/Leadership.ts +++ b/src/collections/Leadership.ts @@ -50,7 +50,7 @@ export const Leadership: CollectionConfig = { admin: { group: 'Campanha', useAsTitle: 'contact', - defaultColumns: ['contact', 'municipalities', 'sector', 'supportStatus', 'updatedAt'], + defaultColumns: ['contact', 'municipalities', 'exclusive', 'supportStatus', 'updatedAt'], }, access: { create: canCreateLeadership, @@ -102,28 +102,16 @@ export const Leadership: CollectionConfig = { index: true, }, { - name: 'sector', - type: 'select', - label: 'Setor', + name: 'exclusive', + type: 'checkbox', + label: 'Apoio exclusivo', + defaultValue: true, index: true, - options: [ - { label: 'Religioso', value: 'religioso' }, - { label: 'Sindical', value: 'sindical' }, - { label: 'Comunitário', value: 'comunitario' }, - { label: 'Rural', value: 'rural' }, - { label: 'Empresarial', value: 'empresarial' }, - { label: 'Juventude', value: 'juventude' }, - { label: 'Saúde', value: 'saude' }, - { label: 'Educação', value: 'educacao' }, - { label: 'Cultura', value: 'cultura' }, - { label: 'Outro', value: 'outro' }, - ], - }, - { - name: 'sectorNotes', - type: 'textarea', - label: 'Observações do setor', - maxLength: 1000, + access: { + create: canManageCampaignStaffField, + read: canReadCampaignStaffField, + update: canManageCampaignStaffField, + }, }, { name: 'supportStatus', @@ -200,17 +188,6 @@ export const Leadership: CollectionConfig = { update: canManageCampaignStaffField, }, }, - { - name: 'consentNote', - type: 'textarea', - label: 'Registro de consentimento externo', - maxLength: 2000, - access: { - create: canManageCampaignStaffField, - read: canReadCampaignStaffField, - update: canManageCampaignStaffField, - }, - }, systemStampedActorField({ readAccess: canReadCampaignStaffField, setAccess: canSetAdministrativeLeadershipField, diff --git a/src/components/campaign/invite/CampaignInviteForm.tsx b/src/components/campaign/invite/CampaignInviteForm.tsx index 34136b16..b9f781a3 100644 --- a/src/components/campaign/invite/CampaignInviteForm.tsx +++ b/src/components/campaign/invite/CampaignInviteForm.tsx @@ -23,25 +23,19 @@ import { 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 { errorProps, fieldError } from '@/utilities/campaignFormFields' -import { leadershipGenderLabels, leadershipSectorLabels } from '@/utilities/leadership/leadershipUi' +import { leadershipGenderLabels } from '@/utilities/leadership/leadershipUi' type CampaignInviteProfile = { name: string phone: string email: string | null gender: keyof typeof leadershipGenderLabels | null - sector: keyof typeof leadershipSectorLabels | null | undefined - sectorNotes: string | null } const genderOptions = Object.entries(leadershipGenderLabels) as Array< [NonNullable, string] > -const sectorOptions = Object.entries(leadershipSectorLabels) as Array< - [NonNullable, string] -> const CampaignInviteFeedback = ({ state }: { state: CampaignInviteFormState }) => { if (state.status === 'success') return null @@ -102,8 +96,6 @@ const ProfileFields = ({ const phone = inviteField('phone') const email = inviteField('email') const gender = inviteField('gender') - const sector = inviteField('sector') - const sectorNotes = inviteField('sectorNotes') return ( @@ -172,44 +164,7 @@ const ProfileFields = ({ {gender.error ? {gender.error} : null} - - - Setor - - Não informar - {sectorOptions.map(([value, label]) => ( - - {label} - - ))} - - {sector.error ? {sector.error} : null} - - - - - Como você atua nesse setor? (opcional) - -