diff --git a/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md new file mode 100644 index 00000000..a9ecdf6e --- /dev/null +++ b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md @@ -0,0 +1,91 @@ +--- +target: /campanha/login +total_score: 26 +p0_count: 0 +p1_count: 2 +timestamp: 2026-07-19T21-47-04Z +slug: src-app-campaign-campanha-login +--- +# Critique: /campanha/login + +Method: dual-agent (A: 8a8b1ae0-9a8a-45f7-907f-54afcdf62db8 · B: 3899acad-4f36-4817-91d6-4135d72d2e96) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 3 | Pending state clear; no other status chrome | +| 2 | Match System / Real World | 3 | PT-BR + e-mail/celular fit; megaphone mark generic | +| 3 | User Control and Freedom | 2 | Only “Esqueceu a senha?”; no exit to public site | +| 4 | Consistency and Standards | 2 | h1 left vs centered muted copy; leading clash | +| 5 | Error Prevention | 2 | required + inputMode help; dual identifier still easy to mistype | +| 6 | Recognition Rather Than Recall | 3 | Labels/placeholder clear | +| 7 | Flexibility and Efficiency | 3 | E-mail or phone + adaptive inputMode | +| 8 | Aesthetic and Minimalist Design | 2 | Header rhythm/alignment + always-on help prose | +| 9 | Error Recovery | 3 | FieldError + leadership recovery hint | +| 10 | Help and Documentation | 3 | Help exists; always-visible first-access competes | +| **Total** | | **26/40** | **Acceptable** | + +## Anti-Patterns Verdict + +**LLM:** Low–moderate slop. Not purple-SaaS; stock shadcn auth shell. Dual muted blurbs feel like copy padding. Typesetting bugs read unfinished. + +**Detector CLI:** exit 0, 0 source findings on login + CampaignAuthPageShell. + +**Runtime (Playwright inject):** `flat-type-hierarchy` (body; sizes 14/16/20px); `nested-cards` on CardHeader — likely false positive (shadcn Card > CardHeader). + +## Overall Impression + +Competent Field Desk palette discipline, phone-first form craft — undermined by a campaign-theme `h1 { text-align: left }` leak and article-style `p { leading-7 }` on auth copy. Biggest opportunity: fix header typesetting; do **not** add split-screen public posts. + +## What's Working + +1. Phone-first: min-h-11, enterKeyHint, identifierInputMode +2. Red as signal ≤10% +3. Auth-error recovery hint for liderança phone-only accounts + +## Priority Issues + +### P1 — Title misaligned with subtitle +Campaign `[data-theme='campaign'] h1 { text-align: left }` overrides CardHeader `text-center`. Title glyph start ~28px left of centered subtitle. +**Fix:** Scope left-align to app chrome, or `text-center` on auth heading. +**Command:** `/impeccable typeset` or `/impeccable layout` + +### P1 — Subtitle vertical rhythm inverted +CardHeader `gap-1` (4px) between title and description; orphan `

` inherits base `leading-7` (28px) while CardDescription stays ~20px. Title glued to howto; second muted block breathes like article body. +**Fix:** One muted block with explicit leading-snug; bump title→desc gap; never bare `

` under campaign theme without leading override. +**Command:** `/impeccable layout` + `/impeccable typeset` + +### P2 — Split-screen + public publications: recommend No +Login is ops gate (noindex); posts are public register. Phone users get scroll soup. Motivation ≠ news feed on unlock. +**Soft maybe:** one quiet purpose line under mark — not a publication panel. +**Command:** `/impeccable shape` only if product insists on panel (internal voice, not posts) + +### P2 — Generic Megaphone mark +Not Solla / Field Desk identity. +**Command:** `/impeccable polish` (reuse campaign-logo) + +### P3 — Always-on first-access paragraph +Progressive disclosure for returning users. +**Command:** `/impeccable distill` or `/impeccable clarify` + +## Persona Red Flags + +**Casey:** Misalignment reads broken; two gray blurbs delay thumb-to-input; split-screen would be fatal. +**Jordan:** Copy helps; visual hierarchy undercuts trust; no public-site escape. +**Lia:** Input path strong; header bugs tax one-handed use; keep error recovery; reject post panel. +**Sam:** Muted contrast scrapes AA (~4.83:1); labels/ARIA look solid. + +## Minor Observations + +- CardHeader grid-rows assumes 2 rows; third orphan `

` is API misuse +- Desktop narrow column is correct for this task +- Placeholder dual-format is heavy + +## Questions to Consider + +1. If the desk is the product, why does the door still look like every shadcn login? +2. Is “inspiring” one quiet line under the mark — or only after auth? +3. Would merging the two muted sentences fix more trust than any split-screen? +4. Who unlocks at 7am in a núcleo — why would they want a publication? +5. Why does auth inherit dashboard `h1` left-align rules? diff --git a/docs/plans/escala-dry-pos-reset-senha-perfil.md b/docs/plans/escala-dry-pos-reset-senha-perfil.md index 439d77be..c796c690 100644 --- a/docs/plans/escala-dry-pos-reset-senha-perfil.md +++ b/docs/plans/escala-dry-pos-reset-senha-perfil.md @@ -1,7 +1,7 @@ # Escala e DRY pós-reset de senha + foto de perfil Status: registrado no roadmap (fases pendentes) -Atualizado em: 2026-07-19 (Fase 3 parcial entregue em `main` @ `246ca85`; absorção `capture-review-debts`) +Atualizado em: 2026-07-19 (Fase 3 parcial: polish `/campanha/login` + simplify; absorção `capture-review-debts` 2026-07-19) Item do roadmap: [docs/roadmap.md](../roadmap.md) (RS+, fill-in de engenharia pós-MVP) Responsável: — @@ -11,13 +11,15 @@ O fill-in **Reset de senha self-service + foto de perfil** ([reset-senha-foto-pe **Já resolvido no simplify (não reabrir):** `CampaignFormActionState` compartilhado; constantes de copy (`CAMPAIGN_PASSWORD_RESET_*`, `CAMPAIGN_LEADERSHIP_FORGOT_PASSWORD_MESSAGE`, `CAMPAIGN_SESSION_EXPIRED_MESSAGE`); `campaignUserShellView` / `CampaignUserShellView`; `canUpdateCampaignUserAvatar` delegando para `canUpdateCampaignUser`; `fieldError` nos forms; `FormActionStatus` no perfil; remoção de wrappers `passwordFormError`/`profileFormError`; exports internos de `resetCampaignPassword`/`changeCampaignPassword`; teste int renomeado para login com senha errada. -**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, copy de primeiro acesso, `w-full` no submit — fecha P1–P3 do critique Impeccable `/campanha/login` da sessão. +**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, `w-full` no submit. + +**Já resolvido no polish + `/simplify` de `/campanha/login` (2026-07-19 — não reabrir):** critique Impeccable P1–P3 fechados (alinhamento título/subtítulo `!text-center`, ritmo `gap-2` + `leading-snug`, `CampaignLogo` no shell, `

` para primeiro acesso); `CampaignAuthCardHeader`, `CampaignAuthBackToLoginLink`; tokens `campaignAuthDescriptionClassName`, `campaignAuthMutedTextClassName`, `campaignAuthTextLinkClassName`; `CAMPAIGN_LOGIN_SUBTITLE` (conta individual); `fieldError` sem dupla chamada em forgot/reset. Snapshot: [`.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md`](../../.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md). ## Objetivos - Auth hot path sem join de `media` em rotas que não exibem avatar (núcleos, apoiadores, planos, etc.). - Upload de avatar sem segurar transação Postgres aberta durante I/O de blob/sharp. -- Shell compartilhado das páginas públicas de auth (`login`, `esqueci-senha`, `redefinir-senha`) — **parcial ✓** (`CampaignAuthPageShell` + headings/copy; ver Fase 3). +- Shell compartilhado das páginas públicas de auth (`login`, `esqueci-senha`, `redefinir-senha`) — **parcial ✓** (shell + card header + logo + critique login fechados; pendem password fields + login state — Fase 3). - Um único módulo de validação/campos de senha reutilizado por convite, reset e perfil. - Login alinhado ao shape `CampaignFormActionState` + `mapCampaignFormActionError` das demais actions de campanha. - Guardrails: sem novo `Consent`; sem SMS/WhatsApp para token; comportamento de produto 1C inalterado salvo bugs encontrados. @@ -44,7 +46,7 @@ flowchart TD RS["Reset senha + perfil MVP ✓"] --> F1 F1["Fase 1 — Auth read perf
depth 0 + getCampaignUserWithAvatar
sessão leve em rotas públicas"] F1 --> F2["Fase 2 — Avatar write perf
blob fora da TX + revalidate estreito"] - F2 --> F3["Fase 3 — DRY auth UI/senha
shell ✓ parcial · password fields + login state"] + F2 --> F3["Fase 3 — DRY auth UI/senha
shell/card ✓ parcial · password fields + login state"] F3 --> F4["Fase 4 — DRY copy/role + actions
campaignRoleOptions + getCampaignActionContext"] F4 --> F5["Fase 5 — Opcional
avatar intent único + int change password"] ``` @@ -64,7 +66,7 @@ flowchart TD ### Fase 3 — DRY UI e senha -**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell` + headings/copy unificados nas três rotas públicas e no card de token inválido; polish de login do critique Impeccable (ver Contexto). +**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell`, `CampaignAuthCardHeader`, `CampaignAuthBackToLoginLink`, tokens de copy/classes em `campaignAuthCopy.ts`, `CampaignLogo` no shell, polish Impeccable `/campanha/login` (ver Contexto). **Pendente nesta fase:** @@ -74,6 +76,7 @@ flowchart TD ### Fase 4 — DRY copy, role e action context +- Consolidar strings de liderança/convite (`CAMPAIGN_LEADERSHIP_*`, `CAMPAIGN_FIRST_ACCESS_HINT`, `CAMPAIGN_LOGIN_SUBTITLE` + mensagem server em `campaignPasswordReset.ts`) numa fonte única com variantes por contexto. - **`campaignRoleLabels`** ↔ opções do campo `role` em `CampaignUser.ts` (fonte única `campaignRoleOptions`). - Propagar para **`NucleusUpdateFeed.tsx`** e outros `roleLabels` locais quando esses arquivos forem tocados. - **`password.ts` / `profile.ts`:** adotar `getCampaignActionContext()` (`src/utilities/campaignActionContext.ts`) como `supporter.ts` / `actionPlan.ts`. @@ -98,13 +101,21 @@ flowchart TD - RBAC em `users` (admin Payload) → [roadmap](../roadmap.md) Admin Payload. - Unificar **todo** o ciclo 1 de auth (convite + login + admin) num único mega-refactor — RS+ cobre só o fill-in entregue e vizinhos diretos. +## Explicitamente fora (critique/simplify desta sessão — não reabrir) + +- Split-screen login com publicações ou painel motivacional (decisão produto 2026-07-19; critique recomendou não). +- Link de saída para o site público na tela de login (heurística 3, score 2). +- Placeholder longo dual-format no identificador; submit helper global (`Spinner` + `aria-live`); constantes de rota auth; `flat-type-hierarchy` do detector runtime no body. +- Comentário em `styles.css` para auth — workaround com tokens/componentes já suficiente. + ## Referências - [reset-senha-foto-perfil.md](reset-senha-foto-perfil.md) — MVP entregue - [escala-dry-pos-visitados-recentemente.md](escala-dry-pos-visitados-recentemente.md) — precedente RS+/VR+ - `src/utilities/campaignAuth.ts`, `src/app/(campaign)/campanha/actions/password.ts`, `profile.ts` - `src/components/campaign/CampaignProfileSettings.tsx`, `CampaignSidebar.tsx`, `CampaignUserAvatar.tsx` -- `src/components/campaign/CampaignAuthPageShell.tsx` — shell entregue; ver Fase 3 pendente +- `src/components/campaign/CampaignAuthPageShell.tsx`, `CampaignAuthCardHeader.tsx`, `CampaignAuthBackToLoginLink.tsx` +- `src/lib/campaignAuthCopy.ts` - `src/app/(campaign)/campanha/login/`, `esqueci-senha/`, `redefinir-senha/` - `src/lib/schemas/campaignPassword.ts`, `src/lib/schemas/invite.ts` - `src/collections/CampaignUser.ts` diff --git a/docs/roadmap.md b/docs/roadmap.md index 74ce8402..24356a31 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -110,7 +110,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | A3/A4 Baseline TSE 2022 + Gap vs 2022 | `Baseline-Eleitoral-2022` (+ bloco "Baseline 2022" em `Lista-Nucleos-Overview`) | [baseline-eleitoral-tse.md](plans/baseline-eleitoral-tse.md) | | A5 Insights (5 planos) | `Baseline-Eleitoral-2022` (card "Insights do território", uma linha por insight) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) (Fase 1 conversão da base + Fase 2 virada: vencedor dir. **ou** share dir. alto) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | | A7 Escala e DRY pós-A4 | — (sem design novo; reusa baseline A4 + Alert/Progress) | [escala-dry-pos-a4.md](plans/escala-dry-pos-a4.md) | -| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) | +| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) | | A8 Perfis do eleitorado (IBGE + manuais) | — (sem design nesta leva; reusa aba Eleitorado + `NucleusIntelligenceDialog`) | [perfil-eleitorado-ibge.md](plans/perfil-eleitorado-ibge.md) | | C2 Cadastro nominal de apoiadores | `Apoiadores-Lista`, `Apoiador-Ficha`, `Importar-CSV-Apoiadores` | [cadastro-nominal-apoiadores.md](plans/cadastro-nominal-apoiadores.md) | | C6 Escala e DRY pós-C2 | — (sem design novo; reusa os de C2 + padrões de núcleos) | [escala-dry-pos-c2.md](plans/escala-dry-pos-c2.md) | @@ -122,12 +122,12 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | C11 Escala e DRY pós-C7 | — (sem design novo; reusa os de C3 + padrões de núcleos/C6/C9) | [escala-dry-pos-c7.md](plans/escala-dry-pos-c7.md) | | 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) | -| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) | -| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) | -| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md) | +| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) | +| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) | +| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.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) | -| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) | +| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) | | 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) | @@ -266,36 +266,36 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap **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 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) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 | -| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 | -| 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, C8 | -| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 | -| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(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-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 | -| 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 **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/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 ✓ | E2, C10, A7, E4 | -| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 | -| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 | +| 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) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 | +| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 | +| 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, C8 | +| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 | +| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(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-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 | +| 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 **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/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 ✓ | E2, C10, A7, E4 | +| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 | +| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 | **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; E6 F1 — perf da lista) | A5, A7, A8, 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; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [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, A8, B3, B5, C4, E4, D1 | -| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 | -| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 | -| 22 | 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, A8, B5, B6, C4, E4, D1 | -| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 | -| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 | -| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 | -| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo | -| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 | +| 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; E6 F1 — perf da lista) | A5, A7, A8, 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; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [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, A8, B3, B5, C4, E4, D1 | +| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 | +| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 | +| 22 | 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, A8, B5, B6, C4, E4, D1 | +| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 | +| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 | +| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 | +| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo | +| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 | **Janela 4 — set → 04/10 (reta final): dobradinha, dia D e estabilização.** @@ -312,7 +312,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap - **Visitados recentemente** ✓ (client-side, `localStorage`; MVP entregue 2026-07-19). → [detalhes](plans/visitados-recentemente.md) - **VR+ Escala e DRY pós-visitados recentemente** (refresh bfcache/storage, DRY tempo relativo, `CampaignLinkListRow`, shell do dashboard) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-visitados-recentemente.md) - **Reset de senha self-service + foto de perfil** ✓ (Resend + `/campanha/perfil`; MVP entregue 2026-07-19). → [detalhes](plans/reset-senha-foto-perfil.md) -- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **`CampaignAuthPageShell` ✓ parcial**, schemas de senha unificados, login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md) +- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **auth UI parcial ✓** — shell/card/logo + critique login fechados; pendem `CampaignPasswordFields` + login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md) - **Field Desk polish** (Impeccable quieter→polish; critique 28→32) — shells/strips/freshness/filas-first/Mais filtros/empties; em branch local 2026-07-19. → `PRODUCT.md` / `DESIGN.md` - **FD+ Escala e DRY pós-Field Desk** (`CampaignPageShell` size, heading class, VoteGoals→MetricStrip, datas canônicas, NucleusFilters bag) — engenharia pós-polish; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-field-desk.md) - **FD2 UX Field Desk pós-critique** (bulk na fila Sem coordenador, glossário inline, outline h2 Filas, empty coordenador+CTA, Sidebar motion cortável) — produto/UX pós-critique; _(validar prioridades com produto)_. → [detalhes](plans/field-desk-ux-pos-critique.md) diff --git a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx index f33b31e0..e9197fa6 100644 --- a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx +++ b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx @@ -1,17 +1,18 @@ 'use client' -import Link from 'next/link' import { useActionState } from 'react' import { requestCampaignPasswordResetFormAction } from '@/app/(campaign)/campanha/actions/password' +import { CampaignAuthBackToLoginLink } from '@/components/campaign/CampaignAuthBackToLoginLink' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT, - campaignAuthHeadingClassName, + campaignAuthMutedTextClassName, } from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -21,20 +22,21 @@ export const ForgotPasswordForm = () => { requestCampaignPasswordResetFormAction, {} satisfies CampaignFormActionState, ) + const emailError = fieldError(state.fieldErrors, 'email') return ( - -

Esqueceu a senha?

- - Informe o e-mail cadastrado na sua conta. {CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT} - -
+ Informe o e-mail cadastrado na sua conta. {CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT} + } + /> {state.status === 'success' && state.message ? ( -

+

{state.message} -

+
) : (
@@ -51,21 +53,12 @@ export const ForgotPasswordForm = () => { /> {state.message ? {state.message} : null} - {fieldError(state.fieldErrors, 'email') ? ( - {fieldError(state.fieldErrors, 'email')} - ) : null} + {emailError ? {emailError} : null} -

- - Voltar ao login - -

+
)} diff --git a/src/app/(campaign)/campanha/login/LoginForm.tsx b/src/app/(campaign)/campanha/login/LoginForm.tsx index 8b1a689e..f6ebfe6c 100644 --- a/src/app/(campaign)/campanha/login/LoginForm.tsx +++ b/src/app/(campaign)/campanha/login/LoginForm.tsx @@ -4,15 +4,20 @@ import Link from 'next/link' import { useActionState, useState } from 'react' import { loginCampaignFormAction, type LoginResult } from '@/app/(campaign)/campanha/actions/auth' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { + CAMPAIGN_FIRST_ACCESS_HINT, CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT, - campaignAuthHeadingClassName, + CAMPAIGN_LOGIN_SUBTITLE, + campaignAuthMutedTextClassName, + campaignAuthTextLinkClassName, } from '@/lib/campaignAuthCopy' +import { cn } from '@/lib/utils' const LOGIN_ERROR_ID = 'login-credentials-error' @@ -35,13 +40,7 @@ export const LoginForm = () => { return ( - -

Entrar na campanha

- Use o e-mail da equipe ou o celular da liderança. -

- Primeiro acesso? Peça um convite no WhatsApp ao coordenador do seu núcleo. -

-
+
@@ -78,16 +77,13 @@ export const LoginForm = () => { />
- + Esqueceu a senha? {hasAuthError ? ( -

+

{CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT} -

+
) : null}
@@ -97,6 +93,19 @@ export const LoginForm = () => { {pending ? 'Entrando...' : 'Entrar'} +
+ + Primeiro acesso? + +
+ {CAMPAIGN_FIRST_ACCESS_HINT} +
+
diff --git a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx index a75213fc..98983a77 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx @@ -1,15 +1,15 @@ 'use client' -import Link from 'next/link' import { useActionState } from 'react' import { resetCampaignPasswordFormAction } from '@/app/(campaign)/campanha/actions/password' +import { CampaignAuthBackToLoginLink } from '@/components/campaign/CampaignAuthBackToLoginLink' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' -import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -18,13 +18,15 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { resetCampaignPasswordFormAction.bind(null, token), {} satisfies CampaignFormActionState, ) + const passwordError = fieldError(state.fieldErrors, 'password') + const passwordConfirmationError = fieldError(state.fieldErrors, 'passwordConfirmation') return ( - -

Redefinir senha

- Escolha uma nova senha com pelo menos 8 caracteres. -
+
@@ -53,24 +55,15 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { /> {state.message ? {state.message} : null} - {fieldError(state.fieldErrors, 'password') ? ( - {fieldError(state.fieldErrors, 'password')} - ) : null} - {fieldError(state.fieldErrors, 'passwordConfirmation') ? ( - {fieldError(state.fieldErrors, 'passwordConfirmation')} + {passwordError ? {passwordError} : null} + {passwordConfirmationError ? ( + {passwordConfirmationError} ) : null} -

- - Voltar ao login - -

+
diff --git a/src/app/(campaign)/campanha/redefinir-senha/page.tsx b/src/app/(campaign)/campanha/redefinir-senha/page.tsx index 6efb1004..d2a90d71 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/page.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/page.tsx @@ -3,10 +3,10 @@ import Link from 'next/link' import { redirect } from 'next/navigation' import { ResetPasswordForm } from '@/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { CampaignAuthPageShell } from '@/components/campaign/CampaignAuthPageShell' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' -import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' +import { Card, CardContent } from '@/components/ui/card' import { CAMPAIGN_PASSWORD_RESET_TOKEN_MIN_LENGTH } from '@/lib/schemas/campaignPassword' import { getCampaignUser } from '@/utilities/campaignAuth' import { CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE } from '@/utilities/campaignPasswordReset' @@ -37,10 +37,10 @@ export default async function CampaignResetPasswordPage({ return ( - -

Link inválido

- {CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE} -
+