diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 089e769c..f33ecadb 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -8,6 +8,7 @@ alwaysApply: false ## Status - **B70 entregue em código (2026-07-30):** wizard atualizar liderança (A4 standalone) — `/campanha/acoes/atualizar-lideranca?municipio=` com grid 2/3/4 (`WizardLeadershipStep`), form curto de 6 campos (`WizardLeadershipForm`), skip via `trailingAction` quando `entryAction !== 'update-leadership'`, Continuar após Salvar dirty → Início + toast; `loadWizardLeadershipTiles`; writes `updateLeadershipWizard`/`createLeadershipWizard` (Contact+Leadership em transação); `WIZARD_ENTRY_ACTION_QUERY_KEY` em `campaignActionRoutes.ts`; `truncateNameAtWordBoundary`. Sem migration. Plano: [`wizard-atualizar-lideranca.md`](../../../docs/plans/wizard-atualizar-lideranca.md). +- **B63 entregue em código (2026-07-30):** wizard registrar sinal (UX-1) — duas etapas no chassis B59: `WizardSignalTypeStep` (grid 2×3 de tiles transparentes + borda, ícone Lucide, Drawer bottom com `infoContent` provisório, `contentAlign="end"` no mobile) e `WizardSignalBodyStep` (textarea + Salvar explícito via `createMunicipalityListSignalFormAction` → `createMunicipalityUpdate` `kind: 'sinal'`). Contrato URL: `?municipio=` (B60) → `?signalType=` (tipo) → body; `?entry=` opcional — skip "Pular registro de sinal" visível só quando embutido (`entry` presente e ≠ `register-signal`), href skip → `/campanha` na v1. Helpers: `wizardSignalHref`, `resolveWizardSignalTypeParam`, `parseWizardEntryActionParam` em `campaignActionRoutes.ts`; `municipalitySignalTypeMeta.ts`; `wizardSignalUi.ts`. B63+ alinha `flowTitle`/`skip` ao chrome B75. Sem migration/Consent. Unit `campaignActionRoutes` + `wizardSignalUi`; e2e standalone + info não avança + skip embutido. Plano: [`wizard-registro-sinal.md`](../../../docs/plans/wizard-registro-sinal.md). Desbloqueia prefill de **B64** (soft). - **B75 entregue em código (2026-07-30):** header mobile do wizard — em `/campanha/acoes/*` no mobile (`< md`), o Mandate Red deixa de ser sidebar+marca e vira chrome de fluxo: título do catálogo B45 (`wizardFlowTitleForSlug`), subtítulo do município após B60, 1ª etapa só X (`aria-label="Sair da ação"`), 2ª+ Voltar+X; `skip` substitui X quando encadeado (B63+). `CampaignWizardChromeProvider` + `CampaignMobileTopBar` no layout `(app)`; `CampaignWizardShell` publica chrome via `useLayoutEffect` (handoff entre passos sem flash app-mode); desktop mantém caption muted no `
` (`md:block`). Helpers: `campaignWizardActionIdForSlug`, `wizardFlowTitleForActionId`, `toCampaignWizardChromeState`. `data-wizard-route` em `acoes/layout.tsx`. Unit `campaignMobileTopBar`, `campaignWizardShell`, `campaignWizardCopy`; e2e `campaignWizardChrome.e2e.spec.ts` (viewport 390). Débito adiado: skip no desktop; split read/write context do provider (gatilho: re-render medido no inset). Plano: [`header-mobile-wizard-campanha.md`](../../../docs/plans/header-mobile-wizard-campanha.md). Desbloqueia polish mobile de B63/B64/B70/B77/B78 sobre chrome único. - **B79–B90 registrados (2026-07-30):** pós-**B73** (remover bottom nav), chrome mobile fora do Início = bottom drawer **não-modal** (ações contextuais + busca global) — chassis **B79** + catálogos por vertical **B80–B90** (Municípios, Territórios, Lideranças, Dobradinhas, Atividades, Demandas, Apoiadores, Assessores, Organizações, Contatos leader, Conceitos+Perfil). **Quadro fora** do inventário. Granularidade = uma vertical (lista+detalhe). Lote inteiro **não cortável**. Planos em `docs/plans/chassis-bottom-drawer-acoes-rapidas.md` e `acoes-rapidas-*.md`. - **B77 entregue em código (2026-07-30):** wizard ajuste de votos numa tela — `WizardExpectedVotesStep` substitui o ritual `?cenario=` de B61/B76 por editor único (strip + 3 inputs compact + atalhos no cenário focado); coerência `pessimistic ≤ central ≤ optimistic` só no confirm (`getWizardVoteViolation` + `getWizardVoteViolationHighlights`); `?cenario=` legado → redirect canônico; órfãos `wizardVoteStepTitle`/`mergeWizardVoteEstimate`/etc. removidos. Unit `wizardExpectedVotesStep`/`wizardVoteEstimate`; e2e B61 atualizado. Sem migration. Plano: [`simplificar-ajuste-votos-wizard.md`](../../../docs/plans/simplificar-ajuste-votos-wizard.md). Desbloqueia wiring A1 com menos atrito antes de **B63**/**B64**. diff --git a/docs/plans/wizard-registro-sinal.md b/docs/plans/wizard-registro-sinal.md index e654148e..51a86479 100644 --- a/docs/plans/wizard-registro-sinal.md +++ b/docs/plans/wizard-registro-sinal.md @@ -1,12 +1,30 @@ # Wizard registrar sinal — escolher tipo + detalhar texto -Status: rascunho -Atualizado em: 2026-07-29 +Status: entregue (2026-07-30) +Atualizado em: 2026-07-30 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B63 — UX-1 wizards) Impeccable: C — UI nova (grid de tipos + drawer de info + passo de texto) Appetite: ~1–1,25 dia eng; 2 etapas no B59; write via action `createMunicipalityUpdate` existente Responsável: — +## Contrato de URL (as-built) + +```text +/campanha/acoes/registrar-sinal + → B60 WizardMunicipalitySearchStep + +/campanha/acoes/registrar-sinal?municipio= + → WizardSignalTypeStep (grid) + +/campanha/acoes/registrar-sinal?municipio=&signalType= + → WizardSignalBodyStep (textarea + Salvar) + +Query opcional (propagar entre passos): + ?entryAction=register-signal|update-votes|… +``` + +Helpers em `src/lib/campaignActionRoutes.ts`: `WIZARD_SIGNAL_TYPE_QUERY_KEY`, `WIZARD_ENTRY_ACTION_QUERY_KEY`, `parseWizardEntryActionParam`, `resolveWizardSignalTypeParam`, `wizardSignalHref`. Skip: `shouldShowWizardSignalSkip` em `wizardSignalUi.ts` — oculto quando `entryAction` ausente ou `register-signal`; visível caso contrário (href → `/campanha` na v1). + ## Design (Impeccable) Âncoras: `PRODUCT.md` / `DESIGN.md` · `Drawer` shadcn · tema `campaign` · labels de **B62**. diff --git a/docs/roadmap.md b/docs/roadmap.md index 0f71fe80..0f1692d8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-30 — **B79–B90** bottom drawer de ações rápidas + busca (chassis + catálogos por vertical; sem Quadro; lote não cortável); **B78** pending nav wizard; ~~**B77**~~ ✓ simplificar ajuste votos; ~~**B70**~~ ✓ wizard A4 liderança; **B74** gap strip v2; ~~**B75**~~ ✓ header mobile wizard; **B73** remover bottom nav; ~~B76/B66/B71/B69/B62/B56/B67/B68/B59/B65/B60/B48–B52/B72~~ ✓; B53–B55; wizards B63/B64/B78. +Atualizado em: 2026-07-30 — **B79–B90** bottom drawer de ações rápidas + busca (chassis + catálogos por vertical; sem Quadro; lote não cortável); **B78** pending nav wizard; ~~**B77**~~ ✓ simplificar ajuste votos; ~~**B70**~~ ✓ wizard A4 liderança; **B74** gap strip v2; ~~**B75**~~ ✓ header mobile wizard; **B73** remover bottom nav; ~~**B63**~~ ✓ wizard registrar sinal; ~~B76/B66/B71/B69/B62/B56/B67/B68/B59/B65/B60/B48–B52/B72~~ ✓; B53–B55; wizards ~~B63~~ ✓/B64/B78. 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) → **B79** (bottom drawer ações+busca fora do Início) → **B80–B90** (catálogos por vertical; Quadro fora) → ~~**B50**~~ ✓ → ~~**B51**~~ ✓ → ~~**B52**~~ ✓ → **B53–B55** · ~~**B56**~~ ✓ · **B57** futuro. **Wizards:** ~~**B59**~~ ✓ → ~~**B75**~~ ✓ (header mobile wizard) → **B78** (pending navegação shell) → ~~**B60**~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓/~~**B77**~~ ✓ → **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) → **B79** (bottom drawer ações+busca fora do Início) → **B80–B90** (catálogos por vertical; Quadro fora) → ~~**B50**~~ ✓ → ~~**B51**~~ ✓ → ~~**B52**~~ ✓ → **B53–B55** · ~~**B56**~~ ✓ · **B57** futuro. **Wizards:** ~~**B59**~~ ✓ → ~~**B75**~~ ✓ (header mobile wizard) → **B78** (pending navegação shell) → ~~**B60**~~ ✓/~~**B61**~~ ✓/~~**B76**~~ ✓/~~**B77**~~ ✓ → **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) @@ -95,7 +95,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - **B76** ~~Corrigir coerência do wizard de ajuste de votos~~ — **entregue 2026-07-30** (`useEffect` preserva `violationEditedScenario` no jump ao cenário violado; limpa ao editar draft/atalho; unit `wizardExpectedVotesStep.unit.spec.ts`) · Depende de B61 ✓ · [plano](plans/corrigir-coerencia-wizard-ajuste-votos.md) · UX de três passos → **B77** - **B77** ~~Simplificar wizard ajuste de votos~~ — **entregue 2026-07-30** (`WizardExpectedVotesStep` reescrito: três cenários numa tela com `VoteEstimateScenarioInputs` compact + strip + atalhos no foco; validação só no confirm com `Alert` + highlight; POST batch inalterado; `?cenario=` legado redireciona para URL canônica; órfãos do ritual B61 removidos de `wizardVoteEstimate`/`campaignActionRoutes`) · Depende de B61 ✓ / B76 ✓ · soft ~~B75~~ ✓ · ~0,75–1d · Janela 1 · Impeccable B · [plano](plans/simplificar-ajuste-votos-wizard.md) - ~~**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) +- ~~**B63** Wizard registrar sinal (grid de tipos + Drawer info + texto + “Pular” se origem ≠ register-signal)~~ — **entregue 2026-07-30** (`WizardSignalTypeStep` + `WizardSignalBodyStep`; contrato `?signalType=` + `?entryAction=`; `municipalitySignalTypeMeta` + `wizardSignalUi`; write via `createMunicipalityListSignalFormAction`; standalone → Início + toast; sem migration) · 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 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)~~ — **entregue 2026-07-30** (`WizardLeadershipStep` + `WizardLeadershipForm`; `loadWizardLeadershipTiles`; `updateLeadershipWizard`/`createLeadershipWizard` com Contact+Leadership; `entryAction` em `campaignActionRoutes`; e2e smoke) · Depende de B59 + B69 ✓ · soft B60 ✓ · ~1,25–1,5d · Janela 1 · Impeccable C · [plano](plans/wizard-atualizar-lideranca.md) diff --git a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx index 4e323164..5394cb8f 100644 --- a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx @@ -1,6 +1,8 @@ import { notFound, redirect } from 'next/navigation' import { WizardLeadershipStep } from '@/components/campaign/leadership/WizardLeadershipStep' +import { WizardSignalBodyStep } from '@/components/campaign/municipality/WizardSignalBodyStep' +import { WizardSignalTypeStep } from '@/components/campaign/municipality/WizardSignalTypeStep' import { WizardExpectedVotesStep } from '@/components/campaign/shared/WizardExpectedVotesStep' import { WizardMunicipalitySearchStep } from '@/components/campaign/shared/WizardMunicipalitySearchStep' import { WizardMunicipalitySelectedStub } from '@/components/campaign/shared/WizardMunicipalitySelectedStub' @@ -10,10 +12,13 @@ import { isCampaignWizardActionSlug, parseWizardEntryActionParam, parseWizardMunicipioParam, + resolveWizardSignalTypeParam, WIZARD_ENTRY_ACTION_QUERY_KEY, WIZARD_MUNICIPIO_QUERY_KEY, WIZARD_SCENARIO_QUERY_KEY, + WIZARD_SIGNAL_TYPE_QUERY_KEY, wizardActionHref, + wizardSignalHref, } from '@/lib/campaignActionRoutes' import { CAMPAIGN_HOME } from '@/lib/campaignPaths' import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate' @@ -94,5 +99,40 @@ export default async function CampaignActionWizardPage({ ) } + if (slug === CAMPAIGN_WIZARD_ACTION_SLUGS['register-signal']) { + const entryAction = parseWizardEntryActionParam( + resolvedSearchParams[WIZARD_ENTRY_ACTION_QUERY_KEY], + ) + const { signalType, invalid } = resolveWizardSignalTypeParam( + resolvedSearchParams[WIZARD_SIGNAL_TYPE_QUERY_KEY], + ) + + if (invalid) { + redirect(wizardSignalHref(slug, municipalitySlug, undefined, entryAction)) + } + + if (signalType) { + return ( + + ) + } + + return ( + + ) + } + return } diff --git a/src/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions.ts b/src/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions.ts index 093a7c2a..383f1bfa 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions.ts +++ b/src/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions.ts @@ -19,6 +19,7 @@ import { parsePoliticalTrendStatusFormValue, } from '@/lib/schemas/municipality' import { parseMunicipalitySignalType } from '@/lib/schemas/municipalityUpdate' +import { WIZARD_SIGNAL_SAVED_MESSAGE } from '@/lib/wizardSignalUi' import { runCampaignFormAction, type CampaignFormActionState, @@ -100,7 +101,7 @@ export const createMunicipalityListSignalFormAction = async ( signalType: parseMunicipalitySignalType(optionalFormText(formData, 'signalType')), }) revalidateMunicipalityListPaths({ slug: optionalMunicipalitySlugFromForm(formData) }) - return { message: 'Sinal registrado.' } + return { message: WIZARD_SIGNAL_SAVED_MESSAGE } }, genericMessage: 'Não foi possível registrar o sinal. Verifique seu acesso e tente novamente.', }) diff --git a/src/components/campaign/municipality/WizardSignalBodyStep.tsx b/src/components/campaign/municipality/WizardSignalBodyStep.tsx new file mode 100644 index 00000000..ae34d8c9 --- /dev/null +++ b/src/components/campaign/municipality/WizardSignalBodyStep.tsx @@ -0,0 +1,116 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useActionState } from 'react' + +import { createMunicipalityListSignalFormAction } from '@/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions' +import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage' +import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell' +import { useCampaignFormSuccessToast } from '@/components/campaign/shared/useCampaignFormSuccessToast' +import { Button } from '@/components/ui/button' +import { Spinner } from '@/components/ui/Spinner' +import { Textarea } from '@/components/ui/textarea' +import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes' +import { wizardSignalHref } from '@/lib/campaignActionRoutes' +import { CAMPAIGN_HOME } from '@/lib/campaignPaths' +import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate' +import { municipalitySignalTypeLabels } from '@/lib/schemas/municipalityUpdate' +import { + shouldShowWizardSignalSkip, + WIZARD_SIGNAL_BODY_STEP_TITLE_PREFIX, + WIZARD_SIGNAL_SAVE_LABEL, + WIZARD_SIGNAL_SKIP_LABEL, + wizardSignalSkipHref, +} from '@/lib/wizardSignalUi' + +type WizardSignalBodyStepProps = { + actionSlug: string + municipalityId: number + municipalityName: string + municipalitySlug: string + signalType: MunicipalitySignalType + entryAction?: CampaignWizardActionId +} + +export const WizardSignalBodyStep = ({ + actionSlug, + municipalityId, + municipalityName, + municipalitySlug, + signalType, + entryAction, +}: WizardSignalBodyStepProps) => { + const router = useRouter() + const [state, submitAction, isPending] = useActionState( + createMunicipalityListSignalFormAction, + {}, + ) + const showSkip = shouldShowWizardSignalSkip(entryAction) + const stepTitle = `${WIZARD_SIGNAL_BODY_STEP_TITLE_PREFIX}: ${municipalitySignalTypeLabels[signalType]}` + + useCampaignFormSuccessToast(state, () => { + router.push(CAMPAIGN_HOME) + }) + + return ( + + {WIZARD_SIGNAL_SKIP_LABEL} + + ) : undefined + } + > +
+ + + + +
+ +