diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 0fffbf05..cc6695ec 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -6,7 +6,11 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **Recently resolved (2026-08-11):** **C115** (Issue #636) — **Google Calendar → Teqo**: quem tem permissão no calendário remarca/renomeia/cancela o compromisso direto no Google e a mudança volta para a atividade (bidirecional sobre o espelho C114). **Motor único bidirecional** no `runSyncPass` do C114 (estende o dono): para cada evento nosso — conteúdo difere? → **regra do relógio**: Google mais novo aplica **título + horário** (all-day incluído; prefixo `[Município] ` só é destripado quando bate com o município — desfazemos só a nossa prefixação; cap 160) com **registro auditável** no `updates` feed (`Google Calendar: …`, `author: null`, feed mostra "Google Calendar" como autor pelo prefixo) — senão o Teqo re-asserta. **Baseline do relógio = `activity.lastMirroredChangeAt`** (novo campo system-write, carimbado no beforeChange só quando um campo espelhado muda): task toggle/update/resultado bumpam `updatedAt` sem tocar o espelho — comparar contra `updatedAt` silenciaria uma edição pendente do Google. **Escrita reversa sem usuário** (`req.user` stripped): staff que dispara a passada não ganha autoria do registro nem passa pelo gate de re-agendamento do deputado. **Cancelamento:** `listEvents` com `showDeleted=true` — evento `status: 'cancelled'` (lixeira) com atividade `confirmado` → cancela **se o cancelamento for mais novo que o último carimbo** (senão é reopen do staff: o trashed é limpo e o evento recriado — nunca re-cancela); `realizado` é histórico protegido. **Remoção permanente** distinguida de "nunca criado" por snapshot `lastSeenEventIds` (JSONB `{calendarId, ids}`, gravado só em passada bem-sucedida e **excluindo ids que o próprio motor deletou** — senão o reopen virava re-cancelamento fantasma); troca de `calendarId` → snapshot stale → recria sem cancelar. **Webhook público** `POST /campanha/agenda/google-webhook/[secret]` (fora de `(app)`, precedente `ical/[secret]`, `maxDuration=60`, `force-dynamic`, allowlist do `campaignJsonMutationRoute` com razão): pre-filter barato (secret ≥32 + headers) ANTES do bootstrap do Payload + validação fail-closed em lib pura testada (`isGooglePushNotificationValid` — secret da URL + `X-Goog-Channel-ID` + `X-Goog-Resource-ID` + `X-Goog-Channel-Token`, constant-time, 404; `not_exists` → estado de erro; `sync` → ignora); ping sem payload → passada completa; 200 mesmo se a passada falhar (sem retry storm). **Canal lazy sem cron (Hobby):** `ensureGoogleCalendarPushChannel` em toda passada — watch com UUID novo, TTL 30d, **segredo rotaciona a cada criação** (URL vazada auto-cura na renovação), **novo canal persistido ANTES do stop do antigo** (falha do stop não orfana o novo); expira em <48h → watch com id novo + `channels.stop` do antigo; troca de calendário → re-watch; falha → `pushChannelError` (state), nunca pausa (o reverso roda em todo trigger local; canal só dá imediatismo — limitação de 30d de silêncio documentada). Campos novos na `googleCalendarSync` (system-write; **identity fields com `read: false`** — o conjunto de credenciais do webhook nunca sai por path de usuário; create admin-only + leitura prefere linha configurada); migration **hand-written** (mesmo motivo do C114). **Título editável pelo Google** exigiu `mutationKind: 'googleCalendarSync'` dedicado (título muda, slug canônico congelado) + **skip do hook de sync na própria escrita reversa** (sem passada aninhada). UI (Impeccable B): seção "Edições pelo Google" no dialog (copy + expiração do canal + erro). Testes: unit (reverse mapping 18, validação do webhook 7, dialog/feed render 6, allowlist), int (13 novos no motor com stub: Google novo aplica+converge, task toggle não silencia edição pendente, Teqo novo re-asserta, cancelled cancela+limpa, reopen pós-cancelamento não re-cancela, remoção permanente via snapshot, create falhou não cancela, troca de calendário não cancela+re-watch, all-day ida-e-volta, realizado protegido, description-only sem registro, ciclo do canal cria/renova/rotaciona/stop, falha de canal não pausa — asserções herméticas a atividades ambiente de arquivos irmãos no DB compartilhado), e2e (webhook 404 fail-closed + caminhos 200 validados: sync/not_exists/change). Suíte completa verde (unit 2064, int 749, e2e 5/5). Intenção [`c115-google-calendar-edicao-bidirecional.md`](plans/c115-google-calendar-edicao-bidirecional.md); impl [`c115-google-calendar-edicao-bidirecional-impl.md`](plans/c115-google-calendar-edicao-bidirecional-impl.md). -**Recently resolved (2026-08-11):** **C115** (Issue #636) — **Google Calendar → Teqo**: quem tem permissão no calendário remarca/renomeia/cancela o compromisso direto no Google e a mudança volta para a atividade (bidirecional sobre o espelho C114). **Motor único bidirecional** no `runSyncPass` do C114 (estende o dono): para cada evento nosso — conteúdo difere? → **regra do relógio**: Google mais novo aplica **título + horário** (all-day incluído; prefixo `[Município] ` só é destripado quando bate com o município — desfazemos só a nossa prefixação; cap 160) com **registro auditável** no `updates` feed (`Google Calendar: …`, `author: null`, feed mostra "Google Calendar" como autor pelo prefixo) — senão o Teqo re-asserta. **Baseline do relógio = `activity.lastMirroredChangeAt`** (novo campo system-write, carimbado no beforeChange só quando um campo espelhado muda): task toggle/update/resultado bumpam `updatedAt` sem tocar o espelho — comparar contra `updatedAt` silenciaria uma edição pendente do Google. **Escrita reversa sem usuário** (`req.user` stripped): staff que dispara a passada não ganha autoria do registro nem passa pelo gate de re-agendamento do deputado. **Cancelamento:** `listEvents` com `showDeleted=true` — evento `status: 'cancelled'` (lixeira) com atividade `confirmado` → cancela **se o cancelamento for mais novo que o último carimbo** (senão é reopen do staff: o trashed é limpo e o evento recriado — nunca re-cancela); `realizado` é histórico protegido. **Remoção permanente** distinguida de "nunca criado" por snapshot `lastSeenEventIds` (JSONB `{calendarId, ids}`, gravado só em passada bem-sucedida e **excluindo ids que o próprio motor deletou** — senão o reopen virava re-cancelamento fantasma); troca de `calendarId` → snapshot stale → recria sem cancelar. **Webhook público** `POST /campanha/agenda/google-webhook/[secret]` (fora de `(app)`, precedente `ical/[secret]`, `maxDuration=60`, `force-dynamic`, allowlist do `campaignJsonMutationRoute` com razão): pre-filter barato (secret ≥32 + headers) ANTES do bootstrap do Payload + validação fail-closed em lib pura testada (`isGooglePushNotificationValid` — secret da URL + `X-Goog-Channel-ID` + `X-Goog-Resource-ID` + `X-Goog-Channel-Token`, constant-time, 404; `not_exists` → estado de erro; `sync` → ignora); ping sem payload → passada completa; 200 mesmo se a passada falhar (sem retry storm). **Canal lazy sem cron (Hobby):** `ensureGoogleCalendarPushChannel` em toda passada — watch com UUID novo, TTL 30d, **segredo rotaciona a cada criação** (URL vazada auto-cura na renovação), **novo canal persistido ANTES do stop do antigo** (falha do stop não orfana o novo); expira em <48h → watch com id novo + `channels.stop` do antigo; troca de calendário → re-watch; falha → `pushChannelError` (state), nunca pausa (o reverso roda em todo trigger local; canal só dá imediatismo — limitação de 30d de silêncio documentada). Campos novos na `googleCalendarSync` (system-write; **identity fields com `read: false`** — o conjunto de credenciais do webhook nunca sai por path de usuário; create admin-only + leitura prefere linha configurada); migration **hand-written** (mesmo motivo do C114). **Título editável pelo Google** exigiu `mutationKind: 'googleCalendarSync'` dedicado (título muda, slug canônico congelado) + **skip do hook de sync na própria escrita reversa** (sem passada aninhada). UI (Impeccable B): seção "Edições pelo Google" no dialog (copy + expiração do canal + erro). Testes: unit (reverse mapping 18, validação do webhook 7, dialog/feed render 6, allowlist), int (13 novos no motor com stub: Google novo aplica+converge, task toggle não silencia edição pendente, Teqo novo re-asserta, cancelled cancela+limpa, reopen pós-cancelamento não re-cancela, remoção permanente via snapshot, create falhou não cancela, troca de calendário não cancela+re-watch, all-day ida-e-volta, realizado protegido, description-only sem registro, ciclo do canal cria/renova/rotaciona/stop, falha de canal não pausa — asserções herméticas a atividades ambiente de arquivos irmãos no DB compartilhado), e2e (webhook 404 fail-closed + caminhos 200 validados: sync/not_exists/change). Suíte completa verde (unit 2064, int 749, e2e 5/5). Intenção [`c115-google-calendar-edicao-bidirecional.md`](plans/c115-google-calendar-edicao-bidirecional.md); impl [`c115-google-calendar-edicao-bidirecional-impl.md`](plans/c115-google-calendar-edicao-bidirecional-impl.md). +**Recently resolved (2026-08-11):** **C129** (Issue #696) — dobradinha ganha **nome de legenda** (campo + display discreto sob o nome): campo `ballotName` (texto opcional, `maxLength: 30` — limite real do nome de urna do TSE; **sem index** — nunca é chave de busca/ordenação/facet, o nome real continua sendo a chave) em `stateDeputy`, ao lado de `party`. **Migration** `20260811_234822_add_state_deputy_ballot_name` (gerada; o diff do gerador re-emitia um no-op de `ADD VALUE 'lembranca'` por snapshot stale — limpa à mão para só o `ADD COLUMN`; nullable, sem backfill). **Display "discreto sob o nome"** (mesma família, `text-xs text-muted-foreground`, sem label — a posição é o rótulo) em 3 pontos: 2ª linha da célula de Nome de `/campanha/pessoas` via seam pura `peopleNameSubline` em `peopleData` (C130 estende com o fallback `?? city` — regra combinada final legenda-sobrepõe-base registrada nos dois planos; nesta branch, sem legenda nada muda na linha, aceite da intenção), sublinha fora da região click-to-edit da célula de Nome na lista de Dobradinhas (irmã da célula no flex-col — clicar nela nunca abre o editor do nome), e sublinha no Nome da seção Contato da ficha. **Edição** (onde já se edita dado de dobradinha, B163): coluna "Nome de legenda" na lista de Dobradinhas (mesma máquina de célula do partido, `editTrigger="cell"`, **default-hidden** em `DEFAULT_HIDDEN_COLUMN_IDS.dobradinhas` — precedente E-mail B197; a sublinha é o display always-on e a coluna o opt-in de edição) + campo no `StateDeputyForm` (criar e editar; schemas create/update/inline `trimmedOptionalText(30)`/`trimmedNullableText(30)` — vazio limpa o campo). Escrita: `updateStateDeputyBallotNameRecord` espelhando o partido (`runStaffEntityMutation`, staff-only, sem transação/locks/Consent novos — escrita single-collection); `CampaignInlineEditableField` ganhou `'ballotName'` (aditivo). Sem migration destrutiva/access/Consent. Testes: unit (merge carrega `ballotName`; `peopleNameSubline` legenda-only/null; default de colunas dobradinhas `['email','ballotName']`), int novo `campaignStateDeputyBallotName` (staff seta/limpa, advisor ok, leader negado, criação com legenda); verificado ao vivo em dev (pessoas/dobradinhas/ficha, edição inline persistindo na sublinha). Gates: unit 2062, int 750, build OK; knip = P3 ledgered pré-existente (idêntico no baseline). Intenção [`pessoas-nome-de-legenda-dobradinha.md`](plans/pessoas-nome-de-legenda-dobradinha.md); impl [`pessoas-nome-de-legenda-dobradinha-impl.md`](plans/pessoas-nome-de-legenda-dobradinha-impl.md). (feat(C129): nome de legenda da dobradinha — campo ballotName + display discreto sob o nome (Closes #696)) + +**Recently resolved (2026-08-11):** **C126** (Issue #686) — os int specs do espelho Google (C114) deixam de flakear sob a suíte paralela: o motor ganhou **escopo injetável** `activityWhere?: Where` em `CampaignCalendarSyncOptions` (`src/utilities/googleCalendarSync.ts`) — appendado ao `and` do push set (`loadSyncActivities`) **e** do delete-guard set (`loadAliveActivityIds`); produção (hooks + action) omite o param → espelho cheio byte-identical. A race era mais ampla que o par da Issue: **5** specs criam atividades in-window no banco compartilhado (`campaignActivity`, `calendarFeed`, `homeSearchActivities`, `municipalityDossierData` + `campaignSuggestions` com `realizado` no lookback — o 5º que a Issue não nomeou), e `outcome.created` global contava todas; serialização não era construtível (vitest não serializa par-a-par; `fileParallelism:false` mataria o paralelismo da suíte) e "nunca relaxar a asserção" foi mantido à letra. O spec roda cada pass via helper `runSync(client, reason)` com `activityWhere: { title: { like: 'C114%' } }` (mesmo filtro do afterEach) e ganhou **dois pins**: (1) atividade estrangeira in-window **não** é espelhada; (2) o delete-guard honra o escopo (evento de atividade alheia viva é removido — espelho escopado é autoritativo no seu escopo). **Conflito com o C122 (#705, mergeado antes):** o C122 endereçou a C126 pela abordagem oposta (asserções globais **removidas** em favor de scoping por event-id, com delegação explícita do desfecho: "quem a detém decide") — a resolução do rebase mantém o contrato da Issue (contagens globais restauradas e determinísticas via escopo) e absorve a infra do C122 (credential compartilhada `googleCalendarTestKey`, cleanup de config por owned id, datas C127 derivadas do agora). **Contraprova:** fix 3/3 rodadas × 5 arquivos verdes (62/62); motor antigo re-flakeia 3–5 testes/rodada nas mesmas condições, e o pin 1 falha 100% — regressão viva. Gates: unit 2050, int 747, lint/typecheck/format/knip/cycles/build verdes. Sem migration/access/Consent/UI. Débito C127/#688 (datas hardcoded) já resolvido pelo C122 (#705). Impl [`c126-espelho-google-int-flake-isolamento-impl.md`](plans/c126-espelho-google-int-flake-isolamento-impl.md) (Issue = spec, sem intenção própria). + +**Recently resolved (2026-08-11):** **OPS41** (Issue #689) — o `AGENTS.md` de main carregava **marcadores de conflito commitados** no bullet "Per-worktree environments" (` <<<<<<< HEAD` indentado + lado HEAD pré-OPS33 com artefato `- # ` + fechamento corrompido `> > > > > > > 9e18bec8`; o `=======` foi removido no próprio commit). Origem: o commit OPS33 f0f5b73f entrou com o arquivo **já conflitado** (resolve do conflito OPS33×OPS28/OPS31 commitado com marcadores); OPS37 herdou o estado. Resolução **perda zero validada por diff**: manter o lado superset (OPS28 + OPS31 + OPS33 — claim determinístico, `--issue N`, prompt citado) e descartar o lado HEAD; −3 linhas no diff; scan whitespace-tolerant de todos os arquivos trackeados confirmou `AGENTS.md` como único afetado. **Guardrail anti-reincidência** (2ª vez que marcador chega a main): novo describe `no conflict markers in committed files` em `tests/unit/codebaseConventions.unit.spec.ts` — varre `git ls-files`, falha em `<<<<<<<`/`>>>>>>>` (tolerante a indentação) e na forma corrompida commitada `^\s*(> ){7}`; roda no CI via `pnpm test:unit`. Sem migration/access/Consent/UI/schema. Intenção: body da Issue (sem plano linkado); impl [`agents-md-sem-marcadores-conflito-impl.md`](plans/agents-md-sem-marcadores-conflito-impl.md). **Recently resolved (2026-08-11):** **Plan-issue — rascunho UI/UX do gate vira HTML+Tailwind commitado no repo (ad-hoc, decisão do usuário).** O artefato deixa de ser **Cursor canvas** (`~/.cursor/projects//canvases/plan--ui-draft.canvas.tsx`, fora do repo, só renderizava no IDE de quem planejou e apodrecia) e passa a ser **HTML + Tailwind v4** (`docs/plans/-ui-draft.html` — `@tailwindcss/browser@4` via jsdelivr, mesma major do app 4.1.x; rede só na hora de renderizar) **commitado com o plano** e **renderizado em PNG** por `pnpm ui-draft:render` (`scripts/render-ui-draft.mjs`, Chromium do Playwright via `@playwright/test`, fallback `load` se o CDN não settle — nunca toca banco/app): uma PNG por cena `
` (ex. `mobile`, `desktop-vazio`) → `docs/plans/-ui-draft-.png`, ou full-page sem `data-shot`; PNGs **embutidos no plano** (visíveis no chat do gate E no PR — o aceite vira registro permanente; o HTML é a fonte iterável). **Contrato de fidelidade explícito em `ui-draft-html.md`** (substitui `ui-draft-canvas.md`, deletado; refs atualizadas em SKILL.md/frontmatter, intention-template.md — campo renomeado `Canvas UI:` → `Rascunho UI:` + secção de embed, skills-map.md, shaping.md): o rascunho DEVE ter layout real, espaçamentos/tamanhos na escala do app (`p-4`, `gap-3`, `h-11`, `text-sm`), breakpoints reais (390/1280) e estados críticos como cenas estáticas; é PROIBIDO ser funcional (zero JS de comportamento — cada estado é cena separada), usar código de app (`src/`), brand/polish (gradientes/emojis/sombras/logo/paleta do produto — cinza neutro + um acento), mock de dados realistas ou decisão de engenharia. Gate: iterar HTML → re-render → embutir até confirmação explícita. Verificado: render real do skeleton (classes v4 aplicadas — oklch zinc, rounded-lg 8px, text-lg 18px). Sem migration/access/Consent/UI de produto/schema. Sem issue — ad-hoc no worktree `work/5`. **Recently resolved (2026-08-11):** **OPS40** (Issue #682) — `pnpm dev` honra o `PORT` do `.env.local`: `next dev` resolve a porta **antes** de `@next/env` carregar env-files (commander `.env('PORT')` no `bin/next` — causa confirmada no fonte 15.4.11), então o `PORT=3100+slot` do provisionador de worktree era ignorado e todo dev server subia em 3000 (colisão entre worktrees paralelos; o e2e escapava porque o webServer injeta `PORT` no env real). **`pnpm dev`/`devsafe` agora passam por `scripts/dev.mjs`** (wrapper): importa o `guard-dev-db.mjs` (preflight fail-closed inline, mesma saída), `loadCliEnv()` (`.env.local` → `.env`, `override:false`) e entrega a porta ao `next dev` como **flag CLI `-p`** — a única alavanca que o repo controla. Precedência: env real do processo (webServer do Playwright manda) → `.env.local` (worktree) → `.env` → sem `-p` (default 3000 + `allowRetry` do Next intactos no main repo). Derivação pura unit-testada em `scripts/lib/cli.mjs` (`resolveDevPort` — null quando ausente, throw fail-closed em PORT inválida; `nextDevArgs`); spawn `node next/dist/bin/next` com `NODE_OPTIONS=--no-deprecation` (paridade cross-env), stdio inherit, forward SIGINT/SIGTERM/SIGHUP, propagação de exit code; extras (`--turbopack`) repassados após o `-p` derivado. Sem migration/access/Consent/UI. Verificação: boot manual no worktree → "Local: http://localhost:3140" (antes 3000), unit 8/8 novos, e2e dev-mode `campaignAuth` 3/3 (2 runs de prewarm estouraram o orçamento sob load ~50+ da máquina — flake de contenção fichado B17/B27/B32/B42, terceira run verde com cache quente). Nota fora de escopo: `AGENTS.md` em `main` carrega marcadores de conflito commitados (OPS33/OPS37) — área de main-mess da OPS38; o novo bullet do Database section foi adicionado **fora** da região conflitada. Intenção [`worktree-dev-port-nao-honrado.md`](plans/worktree-dev-port-nao-honrado.md); impl [`worktree-dev-port-nao-honrado-impl.md`](plans/worktree-dev-port-nao-honrado-impl.md). diff --git a/docs/plans/pessoas-nome-de-legenda-dobradinha-impl.md b/docs/plans/pessoas-nome-de-legenda-dobradinha-impl.md new file mode 100644 index 00000000..9c55d9b0 --- /dev/null +++ b/docs/plans/pessoas-nome-de-legenda-dobradinha-impl.md @@ -0,0 +1,108 @@ +# Impl: Pessoas — nome de legenda da dobradinha (campo + display discreto sob o nome) + +Status: aprovado +Atualizado em: 2026-08-11 +Issue: #696 +Intenção: docs/plans/pessoas-nome-de-legenda-dobradinha.md +Appetite restante: ~0,5–1 dia eng; um campo + display em 3 superfícies (pessoas, dobradinhas lista/ficha) + +## Leitura da intenção + +- **Outcome:** a dobradinha ganha um campo opcional **nome de legenda**; na tabela de `/campanha/pessoas` ele aparece discreto sob o nome real; é editável onde hoje se edita dado de dobradinha (lista de Dobradinhas, precedente B163) e display-only na tabela de pessoas. +- **O que NÃO negociar:** o nome real continua sendo a chave (sem busca/ordenação/facets por legenda); sem legenda a linha não muda (sem linha vazia, sem traço); **não** inline na célula de Nome de `/campanha/pessoas` (gate 2026-08-11: dois inputs na mesma célula não funcionam); regra da 2ª linha compartilhada com C130 — precedência combinada legenda sobrepõe base. +- **O que reavaliar:** a hipótese de "exibir na lista/ficha de Dobradinhas se barato" — decidido no gate: aparece lá, mesmo padrão discreto sob o nome. Onde editar na ficha (seção Contato vs form "Editar dobradinha") não estava decidido — resolvido aqui (D4). + +## Abordagem recomendada + +```mermaid +flowchart LR + M[Migration: ballotName text nullable, sem backfill] --> C[StateDeputy: campo ballotName] + C --> S[Schemas: create/update/inline ballotName] + S --> A[Action updateStateDeputyBallotName + formAction] + A --> L[Dobradinhas: coluna Nome de legenda default-hidden + sublinha no Nome] + C --> P[peopleData: ballotName no merge + peopleNameSubline] + P --> U[Pessoas: 2ª linha do Nome = peopleNameSubline] + C --> F[Ficha: sublinha no Nome + campo no StateDeputyForm] + A --> R[Revalida: dobradinhas lista/ficha + liderancas + municipios] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — campo novo `ballotName` na collection `stateDeputy` (texto, opcional), plumbado no merge de pessoas e nas duas superfícies, reusando a máquina de célula B163 (mesmo padrão do partido) para edição na lista de Dobradinhas; display "discreto sob o nome" em pessoas (2ª linha), lista e ficha de Dobradinhas. Custo concentrado em: migration + campo, schema/action espelhando o partido, e a regra da 2ª linha num único lugar nomeado (`peopleNameSubline`) para C130. + +### Decisões de engenharia + +- **D1 — Campo `ballotName`** (`StateDeputy`): text opcional, `maxLength: 30` (limite real do nome de urna do TSE — é o dado que a mesa registra), **sem index** (nunca é consultado/filtrado; partido é indexado porque tem facet). Label pt-BR "Nome de legenda". Sem access próprio: herda `canManageStateDeputy` como `party`. + - Opções: A `ballotName` | B `urnaName` | C `legendName`. + - Recomendação: A — termo inglês padrão para "nome de urna"; `legendName` colide semanticamente com "legenda" = sigla partidária no TSE (confusão real). + - Rejeitadas: B porque a mesa chama de "nome de legenda" e o domínio interno não usa "urna"; C porque "legenda" em eleitoral brasileiro é o partido (ambiguidade). +- **D2 — Regra da 2ª linha do Nome em `/campanha/pessoas`: legenda-only nesta branch, numa costura nomeada.** `peopleNameSubline(row)` (puro, em `peopleData.ts`) retorna `row.ballotName`; comentário declara que C130 estende com `?? row.city` (regra final combinada: legenda sobrepõe base — registrada nos dois planos). Render: `{subline ? {subline} : null}` — mesma marcação que a branch C130 já usa para a base, então o merge final é trivial. + - Opções: A legenda-only com função nomeada | B `legenda ?? base` já agora | C markup inline. + - Recomendação: A — cumpre o aceite "sem nome de legenda, nada muda na linha" nesta branch (com `?? base` a cidade apareceria duas vezes: sublinha + coluna Base, que só sai em C130) e dá a C130 um seam de uma expressão só. + - Rejeitadas: B porque viola o aceite da intenção e duplica a base no interim; C porque inline espalharia a regra em 2 lugares sem ponto único para C130. +- **D3 — Lista de Dobradinhas: sublinha sob o Nome (display decidido no gate) + coluna "Nome de legenda" default-hidden.** A coluna usa `CampaignInlineEditableCell` (B163, `editTrigger="cell"`, `saveOnChange={false}`) como Partido; entra em `DEFAULT_HIDDEN_COLUMN_IDS.dobradinhas` (precedente da coluna E-mail, B197). A sublinha fica **fora** da região click-to-edit da célula do Nome (a célula é um div autossuficiente; a sublinha é irmã dela num wrapper flex-col), então clicar na sublinha não dispara edição do nome. + - Opções: A coluna própria default-hidden + sublinha | B sublinha como editor (click na sublinha → input) | C só sublinha, edição apenas na ficha | D coluna default-visible. + - Recomendação: A — display "discreto sob o nome" (decidido) e edição no lugar onde a lista já edita dado de dobradinha (B163), sem duplicar o valor duas vezes na largura padrão; quem edita acha a coluna no picker. + - Rejeitadas: B porque reintroduz "dois inputs na mesma célula" (o princípio do gate); C porque "edita onde hoje se edita dado de dobradinha" aponta para a lista (B163); D porque duplica o valor em toda linha com legenda — sublinha + coluna. +- **D4 — Ficha: sublinha no Nome da seção Contato (display) + campo no `StateDeputyForm` (edição).** O `StateDeputyForm` é o form dono dos campos de `stateDeputy` (partido/observações) e serve criar + editar — `ballotName` entra lá (create e update schemas). A seção Contato (células B163) fica de fora: ela edita campos de `Contact`, e `ballotName` é campo da dobradinha. + - Opções: A form dono | B seção Contato | C ambos. + - Recomendação: A — uma superfície de edição só para o campo, no lugar que já edita os demais campos da entidade. + - Rejeitadas: B porque a seção Contato é a fronteira dos campos de Contact (B163 separou as duas); C porque duas superfícies de edição para um campo opcional infla o appetite sem ganho. +- **D5 — Escrita: espelhar a máquina do partido, sem transação/locks/Consent novos.** `stateDeputyBallotNameUpdateSchema` (= `stateDeputyPartyUpdateSchema`, com `trimmedNullableText(30)` — vazio limpa o campo), `updateStateDeputyBallotNameRecord` via `runStaffEntityMutation` (escrita single-collection, staff-only), form action própria no `dobradinhas/formActions.ts`, `revalidateStateDeputyPaths` no wrapper público. `CampaignInlineEditableField` ganha `'ballotName'` (aditivo no union compartilhado). + - Rejeitadas: rota/action nova de célula genérica (não há 3º consumidor); lock advisory (sem unicidade/concorrência de escrita multi-collection). + +### Componentes / mudanças + +- **`src/collections/StateDeputy.ts`**: campo `ballotName` (text, `maxLength: 30`, label "Nome de legenda", sem index) ao lado de `party`. +- **Migration:** `pnpm migrate:create add_state_deputy_ballot_name` — coluna nullable, sem backfill, sem índice; down drop. Revisar SQL e snapshot antes de rodar. +- **`src/lib/schemas/stateDeputy.ts`**: `ballotName: trimmedOptionalText(30)` no `stateDeputyCreateSchema` e no `stateDeputyUpdateSchema`; novo `stateDeputyBallotNameUpdateSchema` + tipos. +- **`src/app/(campaign)/campanha/actions/stateDeputy.ts`**: `updateStateDeputyBallotNameRecord` (clone do party), `updateStateDeputyBallotName` público com `revalidateStateDeputyPaths`; `StateDeputyCreationData` + `createStateDeputyWithContact` propagam `ballotName`; update form flui pelo schema. +- **`src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts`**: `updateStateDeputyBallotNameFormAction` (clone do party). +- **`src/utilities/people/peopleData.ts`**: `ballotName` em `PeopleDeputySource`, `toPeopleDeputySource`, `MergedPerson`, `personFor`, `mergePeopleSources`, `toPeopleRowViewModel`, `PeopleRowViewModel`; `select: { …, ballotName: true }` no `loadPeopleListPageData`; novo `peopleNameSubline` (puro, seam C130). +- **`src/utilities/stateDeputyData.ts`**: `ballotName` em `StateDeputyRowViewModel` + loader select/map; `StateDeputyDetailViewModel` + `loadStateDeputyDetail`. +- **`src/components/campaign/shared/CampaignInlineEditableCell.tsx`**: `'ballotName'` no union `CampaignInlineEditableField` (display/type pass-through — sem formatação). +- **`src/app/(campaign)/campanha/(app)/pessoas/page.tsx`**: célula de Nome vira flex-col; 2ª linha = `peopleNameSubline(row)` (mesma classe do C130). +- **`src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx`**: coluna `ballotName` ("Nome de legenda", cell B163, `editTrigger="cell"`) + wrapper flex-col no Nome com sublinha. +- **`src/lib/campaignColumnVisibility.ts`**: `DEFAULT_HIDDEN_COLUMN_IDS.dobradinhas` += `'ballotName'`. +- **`src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx` + `StateDeputyContactSection.tsx`**: sublinha sob o Nome da seção Contato. +- **`src/components/campaign/stateDeputy/StateDeputyForm.tsx`**: campo "Nome de legenda" (Input, maxLength 30) no form criar/editar. +- **Access / Consent:** nenhum — dado interno de staff; herda `canManageStateDeputy`; sem `Consent` (sem PII nova de opt-in). +- **UI:** Impeccable B — encaixe nas células existentes; sem rota nova; mesma família tipográfica, `text-xs text-muted-foreground`. + +### Dados → forma + +- **Vou apresentar dados?** Não — texto de identidade (nome de urna), sem métrica (pergunta 3 de data-presentation não se aplica). Forma: texto discreto sob o nome, sem label (a posição é o rótulo). + +## Fases verificáveis + +1. **Tracer / schema+server** (quota principal): migration + campo + schemas + action + form action; `pnpm generate:types`; criar/editar dobradinha com legenda via int spec. +2. **Dados**: plumb `ballotName` em `peopleData`/`stateDeputyData` + `peopleNameSubline`; unit tests (merge carrega o valor; subline legenda-only). +3. **UI**: sublinha em pessoas; coluna + sublinha em dobradinhas (+ default-hidden); ficha (sublinha + form). +4. **Gates**: `pnpm gate:fast`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test`, `pnpm test:e2e` (se viável) e `pnpm build` local; int specs novos/focados. + +## Rabbit holes / Não escopo (engenharia) + +- Busca/ordenação/facets por legenda (aceite: nome real é a chave) — nem `q` nem sort tocam `ballotName`. +- Nome de legenda para liderança/staff (pedido atual é só dobradinha). +- Redesign das superfícies de dobradinha (só sublinha + coluna). +- Index em `ballotName`, admin column picker customizado, campo na seção Contato da ficha (D4). +- Edição inline na célula de Nome de `/campanha/pessoas` (gate). +- Mobile cards de pessoas (intenção: tabela desktop). + +## Riscos e mitigação + +- **C130 em paralelo toca a mesma célula de Nome e `peopleData`** (branch C130 em progresso, com mudanças uncommitted): a sublinha nasce na função nomeada `peopleNameSubline` com a MESMA marcação do diff C130 (`truncate text-xs text-muted-foreground`), então o rebase pós-merge resolve substituindo o bloco da base pela sublinha combinada `legenda ?? base` (regra documentada nos dois planos de intenção). Se C130 mergir primeiro, o rebase converte o bloco `{row.city…}` na sublinha combinada. +- **Union `CampaignInlineEditableField` compartilhado**: aditivo; `tsc --noEmit` pega qualquer consumidor quebrar. +- **Migration em prod via build**: nullable, sem backfill, sem índice — reversível e não-destrutiva. +- **Cookie de colunas**: id novo é inerte em cookies antigos → visível para quem nunca mexeu no picker (contrato documentado em `toggleHiddenColumn`); default-hidden só vale para picker intacto — comportamento esperado (precedente E-mail). +- **Clicar na sublinha da lista de Dobradinhas disparar edição do Nome**: a sublinha é irmã da célula no wrapper flex-col, fora da região do cell editor — sem `stopPropagation` necessário. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto (campo opcional; display discreto; editável na lista de Dobradinhas; display-only em pessoas; sem linha vazia) +- [ ] Identificadores em inglês (`ballotName`), labels pt-BR ("Nome de legenda") +- [ ] Escrita staff-only via `runStaffEntityMutation` (precedente partido), sem alargar access +- [ ] Seam `peopleNameSubline` documentado para C130 (regra combinada legenda sobrepõe base) +- [ ] Sem migration destrutiva; `pnpm generate:types` após migration +- [ ] Testes de domínio: unit (merge + subline) e int (update ballotName por coordenador/assessor; líder negado; limpeza do campo) + +**Self-score decision-quality: 5/5** — decisões caras (D1–D5) têm alternativas rejeitadas; appetite bounded (~0,5–1 dia); rabbit holes nomeados; depth check reusa B163, `runStaffEntityMutation`, `DEFAULT_HIDDEN_COLUMN_IDS` e a máquina de partido; outcome da intenção intacto (display-only em pessoas, edição na lista, legenda não vira chave de busca). diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts index 89fed8dc..524b8bfd 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts @@ -29,6 +29,7 @@ export const updateStateDeputyFormAction = async ( await updateStateDeputy({ id: requiredRelationshipFormValue(formData, 'stateDeputyId'), party: nullableFormText(formData, 'party'), + ballotName: nullableFormText(formData, 'ballotName'), notes: nullableFormText(formData, 'notes'), }) revalidatePath('/campanha/dobradinhas/[id]', 'page') diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx index b2087d34..119394fe 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx @@ -68,6 +68,7 @@ export default async function StateDeputyDetailPage({ params }: StateDeputyDetai @@ -182,6 +183,7 @@ export default async function StateDeputyDetailPage({ params }: StateDeputyDetai initial={{ id: stateDeputy.id, party: stateDeputy.party, + ballotName: stateDeputy.ballotName, notes: stateDeputy.notes, }} /> diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts index ac66627d..aeafd5ac 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts @@ -4,6 +4,7 @@ import { setLeadershipStateDeputyMembership } from '@/app/(campaign)/campanha/ac import { setStateDeputyAdvisorMembership, setStateDeputyMunicipalitiesBatch, + updateStateDeputyBallotName, updateStateDeputyContact, updateStateDeputyParty, } from '@/app/(campaign)/campanha/actions/stateDeputy' @@ -42,6 +43,9 @@ const stateDeputyContactSafeMessages = [ const stateDeputyPartySafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const +/** The party and ballotName inline cells share the same refusal vocabulary. */ +const stateDeputyInlineFieldSafeMessages = stateDeputyPartySafeMessages + /** Per-field Contact edit for B163 (lista + ficha). */ export const updateStateDeputyContactFormAction = async ( _state: CampaignFormActionState, @@ -103,6 +107,24 @@ export const updateStateDeputyPartyFormAction = async ( genericMessage: 'Não foi possível salvar o partido. Verifique os dados e tente novamente.', }) +/** Inline "Nome de legenda" edit for C129 (lista de dobradinhas, B163 machinery). */ +export const updateStateDeputyBallotNameFormAction = async ( + _state: CampaignFormActionState, + formData: FormData, +): Promise => + runCampaignFormAction({ + execute: async () => { + await updateStateDeputyBallotName({ + id: requiredRelationshipFormValue(formData, 'stateDeputyId'), + ballotName: nullableFormText(formData, 'ballotName'), + }) + return { message: 'Salvo.' } + }, + safeMessages: stateDeputyInlineFieldSafeMessages, + genericMessage: + 'Não foi possível salvar o nome de legenda. Verifique os dados e tente novamente.', + }) + /** One chip toggle in the "Lideranças" column of `/campanha/dobradinhas` (B36). */ export const setLeadershipStateDeputyMembershipFormAction = async ( _state: CampaignFormActionState, diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts index 06d143cd..afd1e667 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts @@ -20,6 +20,7 @@ export const createStateDeputyFormAction = async ( createStateDeputy({ name: requiredFormText(formData, 'name'), party: optionalFormText(formData, 'party'), + ballotName: optionalFormText(formData, 'ballotName'), notes: optionalFormText(formData, 'notes'), }), redirectTo: (stateDeputy) => `/campanha/dobradinhas/${stateDeputy.id}`, diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index 10bf7bb3..4e48d4bb 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -13,6 +13,7 @@ import { CampaignTransitionAnchor, } from '@/components/campaign/shared/CampaignListPending' import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { CampaignTable, CampaignTableHead, @@ -70,6 +71,7 @@ import { setLeadershipStateDeputyMembershipFormAction, setStateDeputyAdvisorMembershipFormAction, setStateDeputyMunicipalitiesFormAction, + updateStateDeputyBallotNameFormAction, updateStateDeputyContactFormAction, updateStateDeputyPartyFormAction, } from './formActions' @@ -121,17 +123,27 @@ const stateDeputyColumns = ( mandatory: true, head: , cell: (row) => ( - +
+ + {/* C129 — the ballot name sits OUTSIDE the name cell's click-to-edit + region (sibling, not child), so clicking it never opens the name + editor. A legenda identical to the real name is skipped (same + policy as `peopleNameSubline`). */} + +
), }, { @@ -159,6 +171,26 @@ const stateDeputyColumns = ( /> ), }, + { + // C129 — the "Nome de legenda" column, hidden by default (email precedent): + // the subline under the name is the always-on display; the column is where + // the mesa edits it (B163 machinery, same as Partido). + id: 'ballotName', + label: 'Nome de legenda', + cellClassName: 'min-w-32', + cell: (row) => ( + + ), + }, { id: 'email', label: 'E-mail', diff --git a/src/app/(campaign)/campanha/(app)/pessoas/page.tsx b/src/app/(campaign)/campanha/(app)/pessoas/page.tsx index d6a0ef12..acf718fa 100644 --- a/src/app/(campaign)/campanha/(app)/pessoas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/pessoas/page.tsx @@ -21,6 +21,7 @@ import { CampaignTransitionAnchor, } from '@/components/campaign/shared/CampaignListPending' import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { CampaignTable, CampaignTableHead, @@ -54,7 +55,11 @@ import { readCampaignColumnVisibility } from '@/utilities/campaignColumnVisibili import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { loadEligibleAdvisorOptions } from '@/utilities/campaignRelationOptions' import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex' -import { loadPeopleListPageData, type PeopleRowViewModel } from '@/utilities/people/peopleData' +import { + loadPeopleListPageData, + peopleNameSubline, + type PeopleRowViewModel, +} from '@/utilities/people/peopleData' import { buildPeopleFilterHref, clearPeopleListFilters, @@ -170,21 +175,29 @@ const peopleColumns = ({ label: 'Nome', mandatory: true, head: , - cell: (row) => ( -
- - {row.party ? ({row.party}) : null} -
- ), + cell: (row) => { + const subline = peopleNameSubline(row) + return ( +
+
+ + {row.party ? ( + ({row.party}) + ) : null} +
+ +
+ ) + }, }, { id: 'contact', diff --git a/src/app/(campaign)/campanha/actions/stateDeputy.ts b/src/app/(campaign)/campanha/actions/stateDeputy.ts index 7db83759..72245a65 100644 --- a/src/app/(campaign)/campanha/actions/stateDeputy.ts +++ b/src/app/(campaign)/campanha/actions/stateDeputy.ts @@ -14,12 +14,14 @@ import { STATE_DEPUTY_STAFF_MESSAGE, municipalityStateDeputyCreateSchema, stateDeputyAdvisorMembershipSchema, + stateDeputyBallotNameUpdateSchema, stateDeputyCreateSchema, stateDeputyMunicipalitiesBatchSchema, stateDeputyPartyUpdateSchema, stateDeputyUpdateSchema, type MunicipalityStateDeputyCreateInput, type StateDeputyAdvisorMembershipInput, + type StateDeputyBallotNameUpdateInput, type StateDeputyCreateInput, type StateDeputyMunicipalitiesBatchInput, type StateDeputyPartyUpdateInput, @@ -52,6 +54,7 @@ const stateDeputyPolicy: StaffEntityPolicy = { type StateDeputyCreationData = { name: string party?: string | null + ballotName?: string | null notes?: string | null } @@ -85,6 +88,7 @@ const createStateDeputyWithContact = async ( data: hookFilledCreateData<'stateDeputy'>({ contact: contact.id, ...(data.party === undefined ? {} : { party: data.party }), + ...(data.ballotName === undefined ? {} : { ballotName: data.ballotName }), ...(data.notes === undefined ? {} : { notes: data.notes }), }), depth: 0, @@ -152,6 +156,25 @@ export const updateStateDeputyPartyRecord = async ( ) } +/** C129 — inline "Nome de legenda" edit, same single-field shape as the party twin. */ +export const updateStateDeputyBallotNameRecord = async ( + payload: Payload, + actor: CampaignUser, + input: StateDeputyBallotNameUpdateInput, +) => { + const { id, ballotName } = stateDeputyBallotNameUpdateSchema.parse(input) + return runStaffEntityMutation(payload, actor, stateDeputyPolicy, (currentActor) => + payload.update({ + collection: 'stateDeputy', + id, + data: { ballotName }, + depth: 0, + user: currentActor, + overrideAccess: false, + }), + ) +} + export const updateStateDeputyContactRecord = async ( payload: Payload, actor: CampaignUser, @@ -239,6 +262,13 @@ export const updateStateDeputyParty = async (input: StateDeputyPartyUpdateInput) return updated } +export const updateStateDeputyBallotName = async (input: StateDeputyBallotNameUpdateInput) => { + const { payload, actor } = await getCampaignActionContext() + const updated = await updateStateDeputyBallotNameRecord(payload, actor, input) + revalidateStateDeputyPaths(updated.id) + return updated +} + export const updateStateDeputyContact = async (input: ContactFieldUpdateInput) => { const { payload, actor } = await getCampaignActionContext() const updated = await updateStateDeputyContactRecord(payload, actor, input) diff --git a/src/collections/StateDeputy.ts b/src/collections/StateDeputy.ts index c4809f1d..e942c2cf 100644 --- a/src/collections/StateDeputy.ts +++ b/src/collections/StateDeputy.ts @@ -134,6 +134,19 @@ export const StateDeputy: CollectionConfig = { maxLength: 32, index: true, }, + { + // C129 — the ballot name ("nome de legenda") the campaign will use on the + // urna. Optional identity text, never a search/sort/facet key: the real + // name stays the person's key everywhere. No index — nothing queries it. + name: 'ballotName', + type: 'text', + label: 'Nome de legenda', + maxLength: 30, + admin: { + description: + 'Como o nome da dobradinha deve aparecer na urna (ex.: apelido ou abreviação).', + }, + }, { name: 'notes', type: 'textarea', diff --git a/src/components/campaign/shared/CampaignInlineEditableCell.tsx b/src/components/campaign/shared/CampaignInlineEditableCell.tsx index e6bc9888..980cb5e6 100644 --- a/src/components/campaign/shared/CampaignInlineEditableCell.tsx +++ b/src/components/campaign/shared/CampaignInlineEditableCell.tsx @@ -20,7 +20,13 @@ import type { CampaignFormActionState } from '@/utilities/campaignFormActionErro const SAVE_DEBOUNCE_MS = 500 -export type CampaignInlineEditableField = 'name' | 'email' | 'phone' | 'party' | 'city' +export type CampaignInlineEditableField = + | 'name' + | 'email' + | 'phone' + | 'party' + | 'ballotName' + | 'city' type CampaignInlineEditableCellProps = { recordId: number diff --git a/src/components/campaign/shared/CampaignNameSubline.tsx b/src/components/campaign/shared/CampaignNameSubline.tsx new file mode 100644 index 00000000..85d692e0 --- /dev/null +++ b/src/components/campaign/shared/CampaignNameSubline.tsx @@ -0,0 +1,19 @@ +/** + * The "discreto sob o nome" secondary line (C129): the same muted small text + * under a name in every surface — people table, dobradinhas list and ficha. + * The position is the visual label; an optional `srLabel` names the line for + * screen readers (the muted/small styling does not exist in a linear read). + */ +export const CampaignNameSubline = ({ + value, + srLabel, +}: { + value: string | null | undefined + srLabel?: string +}) => + value ? ( + + {srLabel ? {srLabel} : null} + {value} + + ) : null diff --git a/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx index f7671792..8cc17c87 100644 --- a/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx +++ b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx @@ -2,12 +2,15 @@ import { updateStateDeputyContactFormAction } from '@/app/(campaign)/campanha/(app)/dobradinhas/formActions' import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { PhonesFieldEditor } from '@/components/campaign/shared/PhonesFieldEditor' type StateDeputyContactSectionProps = { stateDeputyId: number name: string email: string | null + /** C129 — the "nome de legenda" (ballot name), discreet under the name. */ + ballotName: string | null /** Every number of the ficha, order = priority (C112) — primary first. */ phones: string[] } @@ -15,6 +18,7 @@ type StateDeputyContactSectionProps = { export const StateDeputyContactSection = ({ stateDeputyId, name, + ballotName, email, phones, }: StateDeputyContactSectionProps) => ( @@ -26,15 +30,20 @@ export const StateDeputyContactSection = ({
Nome
- +
+ + {ballotName !== name ? ( + + ) : null} +
diff --git a/src/components/campaign/stateDeputy/StateDeputyForm.tsx b/src/components/campaign/stateDeputy/StateDeputyForm.tsx index 934030d4..b5457e5d 100644 --- a/src/components/campaign/stateDeputy/StateDeputyForm.tsx +++ b/src/components/campaign/stateDeputy/StateDeputyForm.tsx @@ -19,6 +19,7 @@ type StateDeputyFormProps = { initial?: { id: number party: string | null + ballotName: string | null notes: string | null } initialName?: string @@ -59,6 +60,17 @@ export const StateDeputyForm = ({ formAction, initial, initialName }: StateDeput placeholder="Ex.: PT" /> + + Nome de legenda + + Observações