diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md
index cf3af639..76bb9dae 100644
--- a/docs/CHANGELOG-AGENTS.md
+++ b/docs/CHANGELOG-AGENTS.md
@@ -4,6 +4,8 @@
**Recently resolved (2026-08-10):** **OPS35+** (Issue #616) — restaurado o pin de browser que o OPS35 documentou como perdido: a **visibilidade CSS real** das colunas de rede (Assessor / Liderança / Dobradinha) e dos sortables de `/campanha/territorios` a 2200px. Spec novo mínimo `tests/e2e/campaignTerritoriesColumns.e2e.spec.ts` (projeto `campaign`, corpo exato do teste 3 do spec deletado — `getByRole('columnheader')` visível por rung container-query `@min-[Nrem]/territory-list`, Cobertura `count 0`); divisão de trabalho com o HTTP preservada (presença + classes de rung continuam no `campaignTerritoriesHttp`). Manifesto `e2e-affected`: entrada territórios agora mapeia os 2 specs **e** o arquivo dono das rungs `TerritoryListColumns.tsx` (uma mudança de rung acorda o pin; o seletor faz união, specs de municípios continuam acordando). Prova de não-vazio por mutação (rung `advisor` → `@min-[200rem]` fez o spec falhar em "Assessor"; revertido). Sem migration/access/Consent/UI/CSS. Intenção [`restaurar-pin-visibilidade-colunas-territorios.md`](plans/restaurar-pin-visibilidade-colunas-territorios.md); impl [`restaurar-pin-visibilidade-colunas-territorios-impl.md`](plans/restaurar-pin-visibilidade-colunas-territorios-impl.md).
+**Recently resolved (2026-08-10):** **B197** (Issue #627) — coluna de e-mail oculta por padrão nas cinco listas de pessoas do `/campanha` (lideranças, dobradinhas, pessoas, apoiadores, assessores), religável pelo seletor de colunas B17 (cookie `campaign_columns`, default vale só para quem nunca tocou no seletor — escolha salva vence). Defaults novos em `DEFAULT_HIDDEN_COLUMN_IDS` (`email` nas 5) e `assessores` entrou em `CAMPAIGN_LIST_IDS` (primeira lista sem `CampaignTable` no mecanismo). **Pessoas (C100 reaberto):** o e-mail saiu da célula "Contato" (fica só telefone, `—` sem contato) e virou coluna própria oculta — cards mobile intocados (anti-goal). **Apoiadores:** coluna "E-mail" nova no `SupporterList` (dado já existia via `contact`; `SupporterListItemViewModel` ganhou `email`), nascendo oculta. **Assessores:** o encaixe — picker B17 na lista (trailing no omnibox, `advisorPickerColumns` espelhando os `
`) e `AdvisorsTable` (client) passou a receber `columnVisibility` com ` | `/` | ` condicionais; **exceção única: a linha-draft de criação mantém o input de e-mail sempre** (campo obrigatório no create). Sem migration/access/Consent/cache. Testes: unit do mecanismo (parser passa a guardar `assessores`; defaults + stored-wins) e e2e novo no `campaignColumnPicker` (default oculto, religar, persistência no reload, draft com input). Intenção [`ocultar-email-por-padrao-listas.md`](plans/ocultar-email-por-padrao-listas.md); impl [`ocultar-email-por-padrao-listas-impl.md`](plans/ocultar-email-por-padrao-listas-impl.md).
+
**Recently resolved (2026-08-10):** **B195** (Issue #591) — wizard "Registrar pedido" completo (A5, adiado no B85): o passo final saiu do stub e virou formulário funcional no shell do wizard — município no header do app (chrome `municipalityLabel`), **sem título grande de passo** (`stepTitle={null}` + `contentFocus="none"`), Tipo → Atividade (opcional) → **campo único** "O que você precisa?"; salvar ("Abrir demanda") → toast + retorno à origem (`returnPath`). **Mudança de modelo da demanda:** o cadastro (wizard e `/demandas/nova`, sem divergência) tem UM campo de texto (`description` required, min 2, max 4000 — constante `CAMPAIGN_DEMAND_BODY_MAX_LENGTH`); o **título virou derivado por IA** (`src/utilities/ai/campaignDemandTitle.ts` — `generateText` DeepSeek `deepseek-v4-flash` como o Sollinha, timeout 4s, sem key → `null`, prompt com rótulo do tipo) com **fallback = texto truncado** (`src/lib/demandTitle.ts`, puro: colapsa whitespace, ≤160) no create e **keep-previous na edição** (novo `updateCampaignDemandRecord` + editor inline no detalhe — recalc só quando a descrição muda; slug preservado pela invariante canônica); `campaignDemandCreateSchema` perdeu `title` (só os drafts de demanda da atividade mantêm título digitado — vertical fora de escopo, schema desacoplado). **Unicidade de slug defensiva só no create das forms** (`ensureUniqueDemandSlug`, opt-in `context.campaignDemandUniqueSlug` fail-closed em transação ativa — precedente `skipContactPhoneInvariant`): probe único da janela `-2…-20` + advisory lock `campaign-demand-slug:` contra race; activity drafts/admin mantêm a constraint estrita (contrato de rollback C90 pinado em teste). Page: branch `register-demand` no router + **staff gate ANTES do load do escopo** (`gate: 'staff'` → líder vai a `/campanha/contatos`, B43 — o 403 do scope load pré-existente virava erro não determinístico; stub `WizardMunicipalitySelectedStub` removido, placeholder "em breve" apagado). **Bug latente corrigido no hook compartilhado** `useCampaignFormSuccessToast` (dependência `[state.message, state.status]` → `[state]`: segundo save com a mesma mensagem — caso do editor — não disparava toast/refresh). Copy compartilhada em `schemas/campaignDemand.ts`; parser único `parseCampaignDemandCreateFormData` (`utilities/demand/`). **Sem migration** (título continua required, preenchido pelo record). Testes: unit do fallback/limites exatos; int (create deriva, colisão `-2`/`-3`, min 2, update keep-previous/skip, leader edit bloqueado, pin "sem `DEEPSEEK_API_KEY`" no `beforeAll`); e2e novo `campaignRegisterDemand` (fluxo completo com título na lista, back-nav à busca, lockdown de líder). Intenção [`registrar-pedido-wizard.md`](plans/registrar-pedido-wizard.md); impl [`registrar-pedido-wizard-impl.md`](plans/registrar-pedido-wizard-impl.md).
**Recently resolved (2026-08-10):** **OPS33** (Issue #595) — `worktree next` **claima deterministicamente** antes de criar o worktree: mesma fila/ordem e lock otimista do `pnpm agent:claim`, com o claim extraído para `scripts/lib/agent-github.mjs` (`claimIssue` — lock + flip + comentário; `claimBriefLines` — brief compartilhado; `claimQueueEntry` — shape para o reopen; `claimTargetVerdict` — decisão pura testada) e o `agent-claim.mjs` virou CLI fino com a **mesma saída**; claim falhou (fila vazia/race/`--issue` inválido) → motivo e saída **sem worktree órfão** (branch validada antes do claim; hint de falha pós-claim). `--issue ` claima a Issue direcionada (`ready`) ou **reabre** uma já claimada (`in-progress` — sem re-claim, idempotente; recupera também falha pós-claim); a saída imprime o brief e avisa "já claimada — NÃO rodar `pnpm agent:claim`". O **launch** do terminal agora envia `/work-issue --issue ` (prompt citado na diretiva) e a função `worktree()` de `.agents/shell/worktree.sh` passou a **tokenizar a diretiva via xargs** (quote-aware, nunca eval) — o valor do `--prompt` carrega espaço. `--stay` suprime cd+launch mas o claim acontece; o comando `/worktree` roda o mesmo script (claim incluso, sem launch). Guard `-B` contra reset de commits de sessão no reopen. Pool intacto (`claimIssueForPool` coordenado). Docs: AGENTS.md ("Per-worktree environments"), header/uso do script, `.opencode/commands/worktree.md`, skill `worktree-next-issue` (sai "read-only"), AGENT-OPS. Sem migration/dados/access/UI. Intenção [`worktree-next-claima-issue-deterministicamente.md`](plans/worktree-next-claima-issue-deterministicamente.md); impl [`worktree-next-claima-issue-deterministicamente-impl.md`](plans/worktree-next-claima-issue-deterministicamente-impl.md).
diff --git a/docs/plans/ocultar-email-por-padrao-listas-impl.md b/docs/plans/ocultar-email-por-padrao-listas-impl.md
new file mode 100644
index 00000000..b37ebaa1
--- /dev/null
+++ b/docs/plans/ocultar-email-por-padrao-listas-impl.md
@@ -0,0 +1,79 @@
+# Impl: Ocultar a coluna de e-mail por padrão nas listas de pessoas
+
+Status: aprovado
+Atualizado em: 2026-08-10
+Issue: #627
+Intenção: docs/plans/ocultar-email-por-padrao-listas.md
+Appetite restante: herdado (~0,5–1 dia eng)
+
+## Leitura da intenção
+
+- **Outcome:** nas cinco listas de pessoas (lideranças, dobradinhas, pessoas, apoiadores, assessores) a coluna de e-mail nasce oculta para quem nunca tocou no seletor de colunas, e qualquer um pode religá-la pela mecânica B17 já existente — escolha lembrada por dispositivo (cookie `campaign_columns`). Na lista de pessoas, o e-mail sai da coluna "Contato" (que fica só com telefone) e vira coluna própria oculta. Na lista de assessores (hoje sem seletor), a mecânica B17 chega junto: ocultar por padrão + religar.
+- **O que NÃO negociar:** default oculto nas cinco listas; religar manualmente sempre possível (assessores incluído); quem já tem escolha salva no cookie continua com a escolha inteira respeitada (contrato atual do mecanismo — o default vale só para quem nunca mexeu); nada de reordenar colunas, redesenhar tabelas, mexer em fichas/cards mobile ou em outras colunas.
+- **O que reavaliar:** a intenção assume "só muda o default" nas quatro listas com seletor. Confirmado na leitura do código: `email` já é coluna própria em lideranças (`liderancas/page.tsx:160`) e dobradinhas (`dobradinhas/page.tsx:163`); em pessoas está embutida na célula "Contato" (`pessoas/page.tsx:135-149`) e em apoiadores **não existe coluna** (`SupporterList.tsx`) — precisa nascer (oculta). Assessores tem tabela própria client-side sem seletor (`AdvisorsTable.tsx`) — o encaixe é o ponto de maior risco.
+
+## Abordagem recomendada
+
+```mermaid
+flowchart LR
+ A[campaignColumnVisibility.ts + assessores em CAMPAIGN_LIST_IDS] --> B[defaults por lista email oculto nas 5]
+ B --> C[liderancas/dobradinhas só default]
+ B --> D[pessoas: split Contato + coluna email própria]
+ B --> E[apoiadores: coluna email nova oculta por padrão]
+ B --> F[assessores: picker B17 + th/td condicionais]
+ C & D & E --> G[(cookie campaign_columns)]
+ F --> G
+```
+
+**Opções consideradas:** A (defaults no mecanismo B17 + encaixe assessores) | B (merge do default com cookie existente) | C (converter AdvisorsTable para `CampaignTable`)
+**Recomendação:** A — o mecanismo já resolve "cookie ausente → default" (`resolveCampaignColumnVisibility`); as quatro listas `CampaignTable` viram uma linha no `DEFAULT_HIDDEN_COLUMN_IDS`; assessores ganha a MESMA mecânica (picker + cookie) sem reescrever a tabela.
+**Rejeitadas:**
+
+- **B (merge default+cookie):** quebra o contrato B17 documentado ("hiding is stored, never the visible set") e a decisão de produto assumida ("quem já tem escolha salva continua" — `resolvedCampaignColumnVisibility` já a implementa). Usuário que mexeu no seletor de `pessoas` antes vê o e-mail visível; o default vale para quem nunca tocou. É o aceite.
+- **C (portar AdvisorsTable para CampaignTable):** a tabela tem modos client-side que o shell não modela (linha-draft de criação com validação, edição inline debounced por célula). Redesenho de superfície — corte explícito da intenção ("sem refactor da superfície").
+- **D (pessoas: esconder só a linha de e-mail dentro da célula Contato):** fora da mecânica de colunas — quem religa não ganha nada e o picker fica mentindo. Intenção recomenda A (coluna própria) e o código dá suporte trivial.
+
+### Componentes / mudanças
+
+- **`DEFAULT_HIDDEN_COLUMN_IDS`** (`src/lib/campaignColumnVisibility.ts:70`): `liderancas: ['email']`, `dobradinhas: ['email']`, `pessoas: ['email']`, `apoiadores: ['email']`, `assessores: ['email']` — um comentário apontando B197.
+- **`CAMPAIGN_LIST_IDS`** (`campaignColumnVisibility.ts:20`): `+ 'assessores'` (primeira lista sem seletor a entrar — o id vira chave do cookie e label do picker).
+- **`liderancas/page.tsx` / `dobradinhas/page.tsx`:** zero mudança — o default resolve. (Verificar no e2e.)
+- **`pessoas/page.tsx`:** célula `contact` perde o `` de e-mail (fica só telefone, `—` quando vazio); nova coluna `{ id: 'email', label: 'E-mail', cell: row.email ?? '—' }` logo após `contact` (posição de leitura natural; oculta no primeiro paint). Card mobile intocado (anti-goal).
+- **`SupporterList.tsx`:** nova coluna `{ id: 'email', label: 'E-mail', cell: (s) => s.email ?? '—' }` após `city` — `SupporterListItemViewModel.email` já existe (`supporterViewModels.ts:29`); `supporterPickerColumns` deriva e ganha a coluna automaticamente. Card mobile intocado.
+- **`assessores/page.tsx`:** `readCampaignColumnVisibility('assessores')`; `trailing={}` no `AdvisorFilters`; `columnVisibility` passado ao `AdvisorsTable`.
+- **`AdvisorFilters.tsx`:** prop `trailing?: ReactNode` repassada ao `CampaignListOmnibox` (slot já existe — padrão de `PeopleFilters`/`StateDeputyFilters`).
+- **`AdvisorsTable.tsx`** (client): prop `columnVisibility: CampaignColumnVisibility`; `emailHidden = hiddenColumnIds.includes('email')`; `` "E-mail" e o `` da linha só renderizam quando visível. **Exceção única:** a linha-draft de criação mantém o input de e-mail SEMPRE (e-mail é obrigatório para criar — `saveDraft` valida; coluna oculta esconde leitura, não o formulário). Em modo edição, a coluna oculta esconde também o input de e-mail — quem quer editar liga a coluna (um clique no picker; comportamento consistente com o pedido).
+- **Picker de assessores:** array estático no page (`advisorPickerColumns`): Nome (mandatory), E-mail, Celular, Municípios, Ações — rótulos idênticos aos ``.
+- **Migration:** sem migration (apresentação pura; nada de schema/access/Consent; páginas já dinâmicas por auth — sem cache a invalidar).
+- **Access / Consent:** nenhum.
+- **UI:** Impeccable B — reusa integralmente os shells existentes (picker, cookie, trailing); nenhuma superfície nova.
+
+## Fases verificáveis
+
+1. **Mecanismo (lib + unit):** `CAMPAIGN_LIST_IDS` + defaults; atualizar `campaignColumnVisibility.unit.spec.ts` (o teste da linha 35 que espera `assessores` **dropped** pelo parser agora passa a guardá-la; novos asserts de default por lista e do contrato "cookie existente vence o default"). Gate: `pnpm test` (unit do arquivo).
+2. **Quatro listas CampaignTable:** pessoas (split + coluna), apoiadores (coluna nova), liderancas/dobradinhas (nada a tocar — conferência visual/e2e). Gate: `pnpm exec tsc --noEmit` + unit.
+3. **Assessores (encaixe):** page + AdvisorFilters + AdvisorsTable (th/td condicionais + exceção do draft). Gate: `tsc` + e2e focado.
+4. **E2e:** novo `test.describe('E-mail oculto por padrão (B197)')` em `campaignColumnPicker.e2e.spec.ts` — em `/campanha/liderancas` o header "E-mail" nasce ausente, o picker mostra `1 oculta`, religar pinta a coluna e o reload persiste; em `/campanha/assessores` a coluna nasce oculta, o picker existe, religa e persiste, e a linha-draft continua com o input de e-mail. Gate: `pnpm test:e2e` (spec).
+5. **Gates finais:** `pnpm gate:fast` (lint zero, format, tsc, knip, cycles), `pnpm test` (unit+int), `pnpm test:e2e` (arquivo), `pnpm build` local; CHANGELOG-AGENTS (uma entrada curta) + planos commitados.
+
+## Rabbit holes / Não escopo (engenharia)
+
+- Merge default↔cookie por coluna (B rejeitada — contrato B17 intacto).
+- Portar `AdvisorsTable` para `CampaignTable` (C rejeitada — superfície).
+- "Já que estamos aqui": esconder telefone/municípios, reordenar, redesenhar cards mobile — fora de escopo (intenção).
+- Sincronizar com C112 (telefones, mesmas páginas): serialização externa — nada a fazer neste código.
+- Não mexer na largura `w-[24%]` do th de e-mail de assessores (table-fixed redistribui quando a coluna some).
+
+## Riscos e mitigação
+
+- **AdvisorsTable (client) dessincronizado do cookie:** o picker commita com `router.refresh()`; o server re-renderiza com o cookie novo e passa `hiddenColumnIds` de volta — mesma fonte de verdade das outras listas. Mitigação extra: prop derivada no server, nunca estado local no client.
+- **Linha-draft com 5 células vs header com 4 quando oculto:** desalinhamento visual só na linha de formulário; aceito (campo obrigatório). Comentário no código explicando a exceção.
+- **Staff com cookie antigo vê e-mail nas 4 listas:** decisão de produto assumida ("escolha existente é respeitada"); para assessores ninguém tem cookie ainda → 100% nascendo oculto.
+- **Teste unit existente que pina o parser descartando `assessores`:** atualização explícita (fase 1) — é o contrato MUDANDO de propósito.
+- **E2e pesado (duas rotas novas no spec do picker):** spec focado, `test.slow()` no assessores (rota de compilação dev), padrão REFRESH do arquivo.
+
+## Aceite de engenharia
+
+- [ ] Aceite de produto da intenção ainda coberto (5 listas, religável, cookie respeitado, sem outras mudanças de coluna)
+- [ ] Invariantes AGENTS/engineering-standards (nada de schema/access/Consent/migration; páginas dinâmicas sem cache)
+- [ ] Testes previstos: unit do mecanismo (defaults + parser assessores) e e2e do encaixe assessores + default nas listas
diff --git a/src/app/(campaign)/campanha/(app)/assessores/page.tsx b/src/app/(campaign)/campanha/(app)/assessores/page.tsx
index 87dc3c96..6cfca92c 100644
--- a/src/app/(campaign)/campanha/(app)/assessores/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/assessores/page.tsx
@@ -4,12 +4,17 @@ import { getPayload } from 'payload'
import { AdvisorFilters } from '@/components/campaign/advisor/AdvisorFilters'
import { AdvisorsTable } from '@/components/campaign/advisor/AdvisorsTable'
+import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing'
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
import {
CampaignListPendingBoundary,
CampaignListResults,
} from '@/components/campaign/shared/CampaignListPending'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
+import {
+ toCampaignColumnPickerColumns,
+ type CampaignColumnPickerColumn,
+} from '@/lib/campaignColumnVisibility'
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
import { hasAdvisorListActiveFilters } from '@/utilities/advisor/advisorListFilters'
@@ -18,6 +23,7 @@ import {
loadAdvisorListPageData,
parseAdvisorListParams,
} from '@/utilities/advisorData'
+import { readCampaignColumnVisibility } from '@/utilities/campaignColumnVisibilityCookie'
import { firstValue } from '@/utilities/campaignListUrl'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex'
@@ -34,6 +40,17 @@ type AdvisorsPageProps = {
searchParams: Promise>
}
+// B197 — labels mirror the `| ` copy of `AdvisorsTable`; the table's own
+// columns are hand-rolled (not `CampaignTableColumn`), so the picker menu is
+// spelled out here instead of derived.
+const advisorPickerColumns: readonly CampaignColumnPickerColumn[] = [
+ { id: 'name', label: 'Nome', mandatory: true },
+ { id: 'email', label: 'E-mail' },
+ { id: 'phone', label: 'Celular' },
+ { id: 'municipalities', label: 'Municípios' },
+ { id: 'actions', label: 'Ações' },
+]
+
export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps) {
const rawSearchParams = await searchParams
const [, payload] = await Promise.all([
@@ -43,7 +60,8 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps)
const state = parseAdvisorListParams(rawSearchParams)
const autoCreateDraft = firstValue(rawSearchParams[ADVISOR_QUICK_CREATE_PARAM]) === '1'
- const [{ rows, totalDocs, totalPages }, municipalityIndex] = await Promise.all([
+ const [columnVisibility, { rows, totalDocs, totalPages }, municipalityIndex] = await Promise.all([
+ readCampaignColumnVisibility('assessores'),
loadAdvisorListPageData(payload, state),
loadMunicipalityPortfolioIndex(),
])
@@ -58,13 +76,23 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps)
return (
-
+
+ }
+ />
(
-
- {row.phone ? (
- {formatBrazilianPhoneInput(row.phone)}
- ) : null}
-
- {row.email ?? '—'}
-
-
- ),
+ cell: (row) =>
+ row.phone ? (
+ {formatBrazilianPhoneInput(row.phone)}
+ ) : (
+ —
+ ),
+ },
+ {
+ // B197 — the email left the "Contato" cell (which now carries only the
+ // phone) and became its own column, hidden by default. The mobile cards
+ // still show it as the no-phone fallback, untouched.
+ id: 'email',
+ label: 'E-mail',
+ cellClassName: 'max-w-56 whitespace-normal',
+ cell: (row) => (row.email ? row.email : —),
},
{
id: 'assessora',
diff --git a/src/components/campaign/advisor/AdvisorFilters.tsx b/src/components/campaign/advisor/AdvisorFilters.tsx
index d6fc0f16..aa3b9e8c 100644
--- a/src/components/campaign/advisor/AdvisorFilters.tsx
+++ b/src/components/campaign/advisor/AdvisorFilters.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useMemo, useState } from 'react'
+import { useMemo, useState, type ReactNode } from 'react'
import {
CampaignListOmnibox,
@@ -22,9 +22,11 @@ import {
export const AdvisorFilters = ({
state,
municipalityFilterOptions,
+ trailing,
}: {
state: AdvisorListState
municipalityFilterOptions: AdvisorFilterOption[]
+ trailing?: ReactNode
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@@ -94,6 +96,7 @@ export const AdvisorFilters = ({
onClearAll={() => {
runAction(clearAdvisorOmnibox(state))
}}
+ trailing={trailing}
/>
)
diff --git a/src/components/campaign/advisor/AdvisorsTable.tsx b/src/components/campaign/advisor/AdvisorsTable.tsx
index 88bee994..32032355 100644
--- a/src/components/campaign/advisor/AdvisorsTable.tsx
+++ b/src/components/campaign/advisor/AdvisorsTable.tsx
@@ -34,6 +34,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/Table'
+import type { CampaignColumnVisibility } from '@/lib/campaignColumnVisibility'
import { campaignInlineInputClassName } from '@/lib/campaignInlineInput'
import type { MunicipalityPortfolioIndexEntry } from '@/lib/municipalityPortfolio'
import {
@@ -57,6 +58,8 @@ type AdvisorsTableProps = {
rows: AdvisorRowViewModel[]
municipalityIndex: MunicipalityPortfolioIndexEntry[]
hasQuery: boolean
+ /** B197 — hidden column ids from the `campaign_columns` cookie (server truth). */
+ columnVisibility: CampaignColumnVisibility
updateProfileAction: (
state: CampaignFormActionState,
formData: FormData,
@@ -92,6 +95,7 @@ export const AdvisorsTable = ({
rows,
municipalityIndex,
hasQuery,
+ columnVisibility,
updateProfileAction,
municipalitiesAction,
createAction,
@@ -103,6 +107,12 @@ export const AdvisorsTable = ({
const [draft, setDraft] = useState(null)
const [isCreating, startCreate] = useTransition()
+ // B197 — email starts hidden on this list (cookie default); the picker
+ // writes the cookie and the server re-renders with a fresh value. The
+ // create-draft row below is the ONE exception: email is required to create
+ // an advisor, so its input renders even while the column is hidden.
+ const emailHidden = columnVisibility.hiddenColumnIds.includes('email')
+
const refresh = () => router.refresh()
// One array per row per render of `rows`, not per render of this table: the
@@ -211,7 +221,7 @@ export const AdvisorsTable = ({
Nome
- E-mail
+ {!emailHidden ? E-mail : null}
Celular
Municípios
@@ -340,22 +350,24 @@ export const AdvisorsTable = ({
)}
-
- {editing ? (
-
- ) : (
-
- )}
-
+ {!emailHidden ? (
+
+ {editing ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
{editing ? (
> =
label: 'Cidade',
cell: (supporter) => supporter.city ?? '—',
},
+ {
+ // B197 — born hidden by default; the picker can turn it on. The mobile
+ // card keeps no email (unchanged surface).
+ id: 'email',
+ label: 'E-mail',
+ cellClassName: 'max-w-56 whitespace-normal',
+ cell: (supporter) => (supporter.email ? supporter.email : '—'),
+ },
{
id: 'municipality',
label: 'Município',
diff --git a/src/lib/campaignColumnVisibility.ts b/src/lib/campaignColumnVisibility.ts
index 2403f09d..0dfefea5 100644
--- a/src/lib/campaignColumnVisibility.ts
+++ b/src/lib/campaignColumnVisibility.ts
@@ -26,6 +26,9 @@ const CAMPAIGN_LIST_IDS = [
'apoiadores',
'territorios',
'pessoas',
+ // B197 — the advisors surface is a hand-rolled client table (no
+ // `CampaignTable`), but it joined the picker with the same cookie key.
+ 'assessores',
] as const
export type CampaignListId = (typeof CAMPAIGN_LIST_IDS)[number]
@@ -72,6 +75,14 @@ const DEFAULT_HIDDEN_COLUMN_IDS: Partial
diff --git a/src/utilities/supporter/supporterViewModels.ts b/src/utilities/supporter/supporterViewModels.ts
index ba079531..f69230a2 100644
--- a/src/utilities/supporter/supporterViewModels.ts
+++ b/src/utilities/supporter/supporterViewModels.ts
@@ -9,6 +9,7 @@ export type SupporterListItemViewModel = {
id: number
name: string
phone: string
+ email: string | null
city: string | null
municipalityName: string | null
municipalitySlug: string | null
@@ -57,6 +58,7 @@ export const toSupporterListItemViewModel = (
id: supporter.id,
name: contact?.name ?? 'Contato sem nome',
phone: contact?.phone ?? '',
+ email: contact?.email ?? null,
city: contact?.city ?? null,
municipalityName: municipality?.name ?? null,
municipalitySlug: municipality?.slug ?? null,
diff --git a/tests/e2e/campaignColumnPicker.e2e.spec.ts b/tests/e2e/campaignColumnPicker.e2e.spec.ts
index f7987401..16d1fcb8 100644
--- a/tests/e2e/campaignColumnPicker.e2e.spec.ts
+++ b/tests/e2e/campaignColumnPicker.e2e.spec.ts
@@ -111,3 +111,73 @@ test.describe('Seletor de colunas', () => {
await expect(mandatory).toBeDisabled()
})
})
+
+test.describe('E-mail oculto por padrão nas listas de pessoas (B197)', () => {
+ test('lideranças nasce sem a coluna de e-mail; o picker religa e a escolha persiste', async ({
+ campaign,
+ page,
+ }) => {
+ test.slow()
+ await page.setViewportSize({ width: 1800, height: 900 })
+
+ const { fixtures } = campaign
+ const coordinator = await fixtures.createCampaignUser('coordinator', {
+ name: fixtures.value('Coordenadora B197'),
+ })
+ await campaign.login(page, coordinator.email!, coordinator.password)
+ await page.goto('/campanha/liderancas')
+
+ // A fresh device starts with the default: email hidden, picker saying so.
+ const emailHeader = page.getByRole('columnheader', { name: 'E-mail', exact: true })
+ await expect(emailHeader).toHaveCount(0)
+ const picker = page.getByRole('button', { name: /Mostrar ou ocultar colunas/ })
+ await expect(picker).toBeVisible()
+ await expect(page.getByRole('button', { name: /1 oculta/ })).toBeVisible()
+
+ // Re-enabling paints the column and the choice survives a reload.
+ await picker.click()
+ await page.getByRole('checkbox', { name: 'E-mail', exact: true }).click()
+ await page.keyboard.press('Escape')
+ await expect(emailHeader).toBeVisible(REFRESH)
+ await page.reload()
+ await expect(emailHeader).toBeVisible(REFRESH)
+ })
+
+ test('assessores ganha o picker com o e-mail oculto; o draft de criação mantém o input', async ({
+ campaign,
+ page,
+ }) => {
+ test.slow()
+ await page.setViewportSize({ width: 1800, height: 900 })
+
+ const { fixtures } = campaign
+ const coordinator = await fixtures.createCampaignUser('coordinator', {
+ name: fixtures.value('Coordenadora B197 Assessores'),
+ })
+ const advisorName = fixtures.value('Assessor B197')
+ await fixtures.createCampaignUser('advisor', { name: advisorName })
+ await campaign.login(page, coordinator.email!, coordinator.password)
+ await page.goto('/campanha/assessores')
+
+ // The row renders without the email column; the picker is there and says 1 hidden.
+ await expect(page.getByRole('link', { name: advisorName, exact: true })).toBeVisible()
+ const emailHeader = page.getByRole('columnheader', { name: 'E-mail', exact: true })
+ await expect(emailHeader).toHaveCount(0)
+ const picker = page.getByRole('button', { name: /Mostrar ou ocultar colunas/ })
+ await expect(picker).toBeVisible()
+ await expect(page.getByRole('button', { name: /1 oculta/ })).toBeVisible()
+
+ // The create-draft row keeps its email input while the column is hidden
+ // (email is required to create an advisor).
+ await page.getByRole('button', { name: 'Novo assessor', exact: true }).click()
+ await expect(page.getByLabel('E-mail do novo assessor')).toBeVisible()
+
+ // Re-enabling paints the column and the choice survives a reload.
+ await picker.click()
+ await page.getByRole('checkbox', { name: 'E-mail', exact: true }).click()
+ await page.keyboard.press('Escape')
+ await expect(emailHeader).toBeVisible(REFRESH)
+ await page.reload()
+ await expect(emailHeader).toBeVisible(REFRESH)
+ })
+})
diff --git a/tests/unit/campaignColumnVisibility.unit.spec.ts b/tests/unit/campaignColumnVisibility.unit.spec.ts
index e9c33f61..14d23066 100644
--- a/tests/unit/campaignColumnVisibility.unit.spec.ts
+++ b/tests/unit/campaignColumnVisibility.unit.spec.ts
@@ -32,7 +32,7 @@ describe('parseCampaignHiddenColumns', () => {
})
it('drops unknown lists and column ids that are not identifiers', () => {
- expect(parseCampaignHiddenColumns('assessores:email|municipios:kind')).toEqual({
+ expect(parseCampaignHiddenColumns('desconhecida:email|municipios:kind')).toEqual({
municipios: ['kind'],
})
expect(parseCampaignHiddenColumns('municipios:kind~ | | |