From 3af97e960f1ad410089dc467e1359400190f3c46 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 11 Aug 2026 10:22:20 -0300 Subject: [PATCH] =?UTF-8?q?plans(C116,C117,C118):=20lista=20de=20pessoas?= =?UTF-8?q?=20=E2=80=94=20edi=C3=A7=C3=A3o=20onde=20voc=C3=AA=20v=C3=AA,?= =?UTF-8?q?=20ordena=C3=A7=C3=A3o/filtros=20de=20aus=C3=AAncia=20e=20detal?= =?UTF-8?q?he=20por=20capacidades?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/pessoas-detalhe-por-capacidades.md | 73 ++++++++++++++ docs/plans/pessoas-edicao-inplace-lista.md | 97 +++++++++++++++++++ .../pessoas-ordenacao-filtros-ausencia.md | 80 +++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 docs/plans/pessoas-detalhe-por-capacidades.md create mode 100644 docs/plans/pessoas-edicao-inplace-lista.md create mode 100644 docs/plans/pessoas-ordenacao-filtros-ausencia.md diff --git a/docs/plans/pessoas-detalhe-por-capacidades.md b/docs/plans/pessoas-detalhe-por-capacidades.md new file mode 100644 index 00000000..f867d334 --- /dev/null +++ b/docs/plans/pessoas-detalhe-por-capacidades.md @@ -0,0 +1,73 @@ +# Pessoas — página de detalhe da pessoa (customizada por capacidades) + +Status: rascunho +Atualizado em: 2026-08-11 +Issue: #657 +Priority: P2 +Model: composer-2.5 +Impeccable: C — fluxo novo: rota de detalhe + composição de seções por capacidade +Canvas UI: /home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c118-ui-draft.canvas.tsx +Appetite: ~2 dias eng; um outcome verificável — toda pessoa da lista tem um detalhe que mostra só o que ela é +Responsável: — + +## Intenção + +A lista de pessoas ganhou profundidade de uso (edição in-place na C116, ordenação na C117) e o Nome vira link — mas hoje não existe uma página de detalhe de pessoa: só `/campanha/liderancas/[id]`, que só cobre quem é liderança. Uma dobradinha, um assessor ou um apoiador não têm detalhe nenhum. A mesa precisa de uma ficha única da pessoa que se monta sozinha conforme as capacidades dela: a mesma rota serve para qualquer pessoa e mostra **só as seções que ela tem** (liderança, dobradinha, assessora, assessorado, apoiador). + +## Persona e fluxo + +- **Persona / contexto:** coordenador e assessores (staff) abrindo a ficha de uma pessoa a partir da lista; leader não acessa. +- **Job principal:** ver a ficha inteira da pessoa num lugar só — dados, vínculos territoriais e capacidades — sem juntar pedaços de várias telas. +- **Fluxo desejado:** + - Na lista de pessoas, clicar no Nome abre `/campanha/pessoas/` (ou rota equivalente). + - A página monta as seções pela capacidade: ficha (nome, partido, contato, base), **Liderança** (municípios, status de apoio, acesso ao app), **Dobradinha** (partido, municípios), **Assessora** (carteira), **Assessorado** (quem assessora), **Apoiador** (se houver registro), **Ações** (WhatsApp, convidar, apagar). + - Seções ausentes não aparecem — uma pessoa só-dobradinha não vê blocos vazios de liderança. +- **Anti-goals de produto:** não vira tela de admin do Payload (sem edição de schema); não duplica o detalhe de liderança existente para sempre — o detalhe de liderança pode ser absorvido ou linkado; não vira dashboard com KPIs da pessoa. + +## Objetivo e aceite + +- Rota nova de detalhe de pessoa acessível a staff; aberta pelo Nome na lista de pessoas (C116 troca o link para cá quando esta existir). +- Seções por capacidade: cada capacidade presente da pessoa renderiza seu bloco; a página de uma pessoa sem liderança não mostra o bloco de liderança. +- Escopo preservado: assessor só vê/abre pessoas da carteira; leader não acessa a rota; sem migration; `Contact` continua a fonte da ficha. +- O detalhe de liderança existente (`/campanha/liderancas/[id]`) não regride: este item decide convivência (absorver no novo detalhe ou linkar). + +## Dados (intenção) + +- **Vou apresentar dados?** Não — a página mostra vínculos e ficha (mesmos dados da lista, em profundidade), não métricas. Sem KPI de pessoa (votos/estimativas só nas superfícies que já os exibem, com a assimetria atual intacta — liderança vê `declaredVotes`, nunca estimativas). + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** rota nova em `src/app/(campaign)/campanha/(app)/pessoas/[id]/`; loader por `contactID` reusando o merge de `src/utilities/people/peopleData.ts` (ou um irmão `loadPersonDetail`); seções como componentes em `src/components/campaign/people/`; dados por capacidade vêm dos utilitários de domínio já existentes (leadership/stateDeputy/advisor/supporter). +- **Precedente a olhar:** `/campanha/liderancas/[id]` (detalhe existente), `peopleData.ts` (merge por Contact), C100 (lista), `municipalityPortfolio`/catálogo para nomes de municípios. +- **Risco de acoplamento:** o escopo do assessor no detalhe deve espelhar o da lista (merge por capacidade, nunca alargado); leader lockdown; assimetria votos declarados × estimados preservada se qualquer bloco mostrar votos. + +## Dependências + +- **C99/C100** (prontas). **Suaves:** C116 (é quem passa a linkar o Nome para cá quando existir; sem dependência de ordem — o link da C116 nasce apontando para a liderança e troca quando C118 entrar); C117 (mesma lista, sem dependência). + +## Fora de escopo + +- Edição na página de detalhe (a edição é in-place na lista, C116; editar aqui pode ser item futuro). +- Migração do detalhe de liderança para esta rota (decisão de convivência no item, sem derrubar a rota atual). +- Página de apoiador dedicada; histórico/atividades da pessoa; timeline. + +## Rabbit holes de produto + +- **"Detalhe vira mini-admin do Payload."** Formulários de edição de todos os campos, histórico de versões, relação de tudo com tudo. **Corte:** página de leitura + navegação; escrita fica na lista (C116) e nas superfícies atuais. +- **"Seções vazias com placeholder 'Nada aqui ainda'."** Pessoa sem liderança não precisa do bloco. **Corte:** seção só quando a capacidade existe (o pedido explícito). +- **"Absorver o detalhe de liderança de uma vez."** Migrar todo o fluxo (incluindo ações/votos) para a rota nova num item só. **Corte:** v1 é leitura + convivência; a migração do detalhe de liderança é avaliada com uso. +- **KPIs da pessoa.** "Engajamento", "score", contagens decorativas no topo. **Corte:** sem métrica nova; a ficha mostra vínculos. + +## Questões em aberto (produto) + +- **Convivência com o detalhe de liderança:** manter `/campanha/liderancas/[id]` intacto e o novo detalhe linkar para ele na seção de liderança, ou migrar a rota de liderança para o detalhe novo? **Recomendação:** v1 mantém as duas — a seção Liderança do detalhe novo mostra o resumo e linka para a página rica existente (que tem votos/ações). Migração total fica avaliada com uso. _(assumido — validar no gate)_ +- **Ações no detalhe:** repetir WhatsApp/convidar/apagar da lista? **Recomendação:** sim, as mesmas ações (padrão da lista) — é o destino natural delas quando a mesa está na ficha. _(assumido — validar no gate)_ +- **Apoiador:** a seção de apoiador mostra o registro (fonte, município, intenção de voto)? **Recomendação:** sim, resumo do registro de apoiador quando houver — mesmo consent guardado das superfícies atuais. _(assumido — validar no gate)_ + +## Referências + +- `src/app/(campaign)/campanha/(app)/liderancas/[id]/` — detalhe existente (molde de seções/leitura) +- `src/utilities/people/peopleData.ts` — merge por `Contact` e view model (capacidades da pessoa) +- `src/components/campaign/people/` — componentes de lista (base para seções do detalhe) +- `src/app/(campaign)/campanha/(app)/pessoas/page.tsx` — colunas da lista que viram seções do detalhe +- Canvas UI (gate): `/home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c118-ui-draft.canvas.tsx` diff --git a/docs/plans/pessoas-edicao-inplace-lista.md b/docs/plans/pessoas-edicao-inplace-lista.md new file mode 100644 index 00000000..0f0bf50d --- /dev/null +++ b/docs/plans/pessoas-edicao-inplace-lista.md @@ -0,0 +1,97 @@ +# Pessoas — edição onde você vê (edit-where-you-see) na lista desktop + +Status: rascunho +Atualizado em: 2026-08-11 +Issue: #655 +Priority: P2 +Model: composer-2.5 +Impeccable: B — encaixe em tela existente (`/campanha/pessoas`, tabela desktop); sem rota nova +Canvas UI: /home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c116-ui-draft.canvas.tsx +Appetite: ~1,5–2 dias eng; um outcome verificável — a linha da lista é editável sem sair da tabela +Responsável: — + +## Intenção + +A mesa corrige dados de pessoas o dia inteiro (telefone errado, município que saiu da rede, assessor que mudou) e hoje precisa abrir a ficha, o detalhe ou o admin para cada ajuste — mesmo quando a correção é uma célula da lista. O pedido: na tabela desktop de `/campanha/pessoas`, toda célula (exceto a coluna Ações) vira um editor in-place — clicou, editou, salvou. Quem lidera/é aliada em várias cidades ainda precisa enxergar o recorte por território sem perder a capacidade de ajustar cidade a cidade. + +## Persona e fluxo + +- **Persona / contexto:** coordenador e assessores na mesa (staff), trabalhando a lista de pessoas com a base nominal real. +- **Job principal:** corrigir o dado de uma pessoa — um campo de texto ou um vínculo territorial — sem sair da linha em que está. +- **Paradigma da superfície (travado com o humano 2026-08-11):** a célula **é** um input permanente. Sem estado de "edição" com label, borda ou fundo diferente: a célula parece texto e se comporta como input. **Chips são internos ao input** (tags dentro do box, transparente como o resto). +- **Fluxo desejado:** + - Células de texto (Contato, E-mail, Base): input sempre, sem destaque; salva no blur ou Enter (Esc descarta). Vazio mostra placeholder discreto ("—" atual vira placeholder apagado). + - Nome: input sempre cujo valor é estilizado como **link** para a página da pessoa; navegação convive com a edição sem trocar de estado (mecanismo em "Questões em aberto"). + - Células de municípios (Assessora, Lidera, Aliada em) e Assessorado: input com **chips internos** — municípios agrupados em chip de território quando a pessoa tem **todos** os municípios do território ("Sertão do São Francisco (5)"), as 19 zonas de Salvador num chip "Salvador (19)"; excedente "+X" expande. + - Digitar + autocomplete: selecionar uma sugestão **ou** teclar espaço/Enter com o texto batendo (match) no catálogo vira chip imediatamente. Sem match, não vira chip. + - Clicar num chip de território expande as cidades dele dentro do input; ao sair da célula com o mouse, se a pessoa ainda tiver todas as cidades do território, colapsa de novo. + - Hover num chip mostra o **X** no canto; clicar no X (não no chip) apaga a cidade daquela linha. + - Hover nos botões de Ações mostra a **label** da ação em tooltip. +- **Anti-goals de produto:** não vira planilha/data-grid (sem edição em lote, sem seleção múltipla); não é segundo cadastro de pessoa (a ficha continua `Contact`); mobile não muda; "Assessorado" não ganha autocomplete de criar pessoa (só vínculo); nada é criado fora do catálogo (texto sem match não vira chip). + +## Objetivo e aceite + +- Toda célula da tabela desktop é um **input permanente sem destaque** (sem label de edição, sem borda, sem fundo diferente) — o conteúdo parece texto e edita no lugar; salva no blur/Enter (Esc descarta), escrevendo no campo dono da ficha `Contact` (nome, telefone, e-mail, cidade) — sem migration. +- O Nome mantém o valor como **link** para a página de detalhe da pessoa (quando existir), com a edição convivendo no mesmo input. +- As colunas de municípios (Assessora, Lidera, Aliada em) e Assessorado usam **chips internos ao input** com colapso por território: grupo completo → chip "Território (N)"; 19 zonas de Salvador completas → chip "Salvador (19)"; excedente → "+X" que expande. +- Digitar e selecionar no autocomplete — ou teclar espaço/Enter com o texto batendo no catálogo — transforma o valor em chip; texto sem match não vira chip. +- Remover uma cidade pelo X do chip persiste no servidor; expansão/recolapso por hover não altera dados. +- Hover nas ações mostra a label (WhatsApp, Convidar, Apagar) em tooltip. +- Guardrails: foco por teclado mantém indicador discreto (acessibilidade) mesmo sem caixa de edição; escopo preservado (assessor só edita a carteira dele; leader fora da rota); nenhuma coluna nova; mobile intacto. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — as colunas expressam vínculos (território/município), não métricas. O chip de território mostra contagem de cidades do grupo, não é dado analítico. +- **Decisões desbloqueadas:** nenhuma leitura decisória nova; a superfície muda de "ler e navegar" para "ler e corrigir". + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/app/(campaign)/campanha/(app)/pessoas/page.tsx` (colunas + `PeopleMunicipalityCell`); `src/components/campaign/people/` (filters/chrome); componentes compartilhados já existentes — `CampaignInlineEditableCell` (B163: célula de texto com clique→input→blur/Enter, já suporta `href` no Nome com edição por clique fora do valor), `RelationChipCell` (B157/B159/B169: chips de relação com X no hover, input de autocomplete inline; hoje colapsa por linhas "Ver mais…" — o colapso **por território** com expandir/recolapsar por hover é o trabalho novo), `useCampaignCellAutosave`; escrita via as rotas/actions de célula já usadas pelas listas vizinhas. +- **Precedente a olhar:** [editar-campos-lista-dobradinhas.md](editar-campos-lista-dobradinhas.md) (B163 — mesmo padrão de célula de texto), [generalizar-colunas-relacao-municipios.md](generalizar-colunas-relacao-municipios.md) e [chips-municipios-lista-dobradinhas.md](chips-municipios-lista-dobradinhas.md) (B159/B157 — chips de relação), [estabilizar-lista-dobradinhas-followup.md](estabilizar-lista-dobradinhas-followup.md) (B170 — contrato de colapso de chips), `src/lib/municipalityCatalog.ts` (TI por município, `region`; zonas de Salvador como grupo próprio). +- **Risco de acoplamento:** leader lockdown e escopo do assessor (rota staff; a escrita deve respeitar o access da coleção dona — ex. carteira de outro assessor); **B155+ (#368, in-progress)** migra o padrão de escrita de célula de form action para rota — se entrar antes, seguir o padrão novo; **C112 (#626, in-progress)** muda o shape de telefone (múltiplos) — o editor de Contato deve conviver com a transição. + +## Dependências + +- **C99/C100** (prontas — ficha `Contact` e lista unificada). +- **Suaves:** C112 (telefones múltiplos — editor de Contato acompanha o shape), B155+ (padrão de escrita de célula), C117 (mesma tabela; sem dependência de ordem entre elas), **C118** (detalhe de pessoa — quando entrar, o link do Nome aponta para ela). + +## Fora de escopo + +- Mobile (cards): os fluxos próprios já existem; este item é a tabela desktop. +- Edição em lote / spreadsheet mode / reorder de colunas. +- Página de detalhe de pessoa (customizada por capacidades) → **C118** ([pessoas-detalhe-por-capacidades.md](pessoas-detalhe-por-capacidades.md)); este item usa o detalhe existente (liderança) para o link do Nome. +- Ordenação e filtros de ausência na lista → **C117** ([pessoas-ordenacao-filtros-ausencia.md](pessoas-ordenacao-filtros-ausencia.md)). +- Criar pessoa nova na lista (cadastro segue nas rotas atuais). + +## Rabbit holes de produto + +- **"Já que editamos, vamos editar tudo em lote."** Seleção múltipla, "aplicar a todos", CSV de volta — explode para spreadsheet mode. **Corte neste item:** uma célula por vez, autosave por célula (padrão B163), nada de lote. +- **"Chips internos ao input = contenteditable de verdade."** `contenteditable` com formatação livre, colar HTML, estado de cursor imprevisível — vira um editor de texto. **Corte:** a implementação deve manter o comportamento de input (valor simples, chips como dados, teclado previsível), mesmo que o mecanismo seja um wrapper com chips + input; o executor decide a mecânica. +- **"Expansão de território com animações e estado global."** Guardar qual célula está expandida, coordenar entre linhas, animar transições — vira motor de UI. **Corte:** expansão é local à célula, dirigida por hover/foco, sem estado persistido. +- **Colapso por território querendo virar tree-view.** Chip de território com sub-chips aninhados persistentes (acordeão) é outra interação. **Corte:** grupo colapsa/expande in loco; cidades do grupo aparecem como os chips normais (removíveis), nada de hierarquia persistente. +- **Editar "Assessorado" exigindo criar assessor.** O autocomplete da coluna inversa só vincula pessoas existentes (staff). **Corte:** sem "Criar…" aqui; criação de conta/staff fica nas rotas atuais. + +## Questões em aberto (produto) + +Resolvidas no gate 2026-08-11: + +- **Link do Nome:** `/campanha/liderancas/[id]` quando a pessoa é liderança (sem link senão); a **página de detalhe de pessoa** (customizada por capacidades) entra como Issue própria **C118** ([pessoas-detalhe-por-capacidades.md](pessoas-detalhe-por-capacidades.md)) — quando existir, o link do Nome passa a apontar para ela. +- **Assessorado editável:** sim — autocomplete de assessores existentes (escreve nos vínculos de assessor da liderança/dobradinha; sem "Criar…" aqui). +- **Paradigma:** células são **inputs permanentes sem destaque**; chips internos ao input; nome mantém o valor como link. +- **Navegação do Nome (mecanismo travado com o humano):** o valor é **exibido como link**; cada tecla digitada no input atualiza o texto do link de acordo com o valor (draft), **mantendo o valor real do input em vazio** — o draft vive no display; clique no link navega; salva no blur/Enter. A mecânica exata de sobreposição/display é do plano de implementação, com o requisito de link clicável + digitação no mesmo lugar. +- **Sem match no catálogo:** ao teclar Enter/espaço sem o texto bater em município/assessor, o texto **permanece** no input (não vira chip); nada é criado fora do catálogo. + +Ainda em aberto (uma rodada no gate): + +- **Quem pode editar a carteira de um assessor ("Assessora")?** **Recomendação:** manter a regra de acesso da coleção dona (coordenação/candidato; assessor só a própria) — sem alargar permissão na lista. _(assumido — validar no gate)_ +- **Ordenação do excedente e do autocomplete:** manter a ordem do catálogo (alfabética pt-BR) e sugerir apenas municípios ainda não vinculados. **Recomendação:** sim — mesma regra dos chips das outras listas. _(assumido — validar no craft)_ + +## Referências + +- `src/app/(campaign)/campanha/(app)/pessoas/page.tsx` — tabela, colunas, `PeopleMunicipalityCell` (chips atuais "+2") +- `src/components/campaign/shared/CampaignInlineEditableCell.tsx` — célula de texto B163 (clique→input→blur/Enter, `href` no Nome) +- `src/components/campaign/shared/RelationChipCell.tsx` — chips de relação B157/B169 (X no hover, input inline, colapso por linhas) +- `src/components/campaign/shared/CampaignCellEditOverlay.tsx` / `useCampaignCellAutosave.ts` — overlay e autosave de célula +- `src/utilities/people/peopleData.ts` — view model (`assessoraMunicipalityIDs`, `leadershipMunicipalityIDs`, `deputyMunicipalityIDs`, …) +- `src/lib/municipalityCatalog.ts` — TI por município (`region`), zonas de Salvador (grupo próprio de 19) +- Canvas UI (gate): `/home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c116-ui-draft.canvas.tsx` diff --git a/docs/plans/pessoas-ordenacao-filtros-ausencia.md b/docs/plans/pessoas-ordenacao-filtros-ausencia.md new file mode 100644 index 00000000..f4cbd09f --- /dev/null +++ b/docs/plans/pessoas-ordenacao-filtros-ausencia.md @@ -0,0 +1,80 @@ +# Pessoas — ordenação (omnibox + header) e filtros de ausência + +Status: rascunho +Atualizado em: 2026-08-11 +Issue: #656 +Priority: P2 +Model: composer-2.5 +Impeccable: B — encaixe em tela existente (`/campanha/pessoas`); sem rota nova +Canvas UI: /home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c117-ui-draft.canvas.tsx +Appetite: ~1–1,5 dia eng; um outcome verificável — recortar e ordenar a lista pelo critério do dia +Responsável: — + +## Intenção + +A lista de pessoas hoje só ordena por nome (fixo) e só filtra por capacidade/município/status. A mesa precisa de dois movimentos: **ordenar por qualquer coluna** (quem tem mais rede? quem está sem base?) e **filtrar ausência** ("Sem assessor", "Sem base", "Sem contato" — para atacar exatamente quem não tem o dado). Ordenação entra em dois lugares com o mesmo estado: o header das colunas (desktop) e o omnibox (onde já existe o padrão de ordenação nas outras listas). + +## Persona e fluxo + +- **Persona / contexto:** coordenador e assessores (staff) na lista de pessoas; leader não acessa. +- **Job principal:** recortar a lista pelo critério do dia e ordená-la pela coluna que importa, sem sair da tela nem decorar quem é quem. +- **Fluxo desejado:** + - Clicar no header de uma coluna ordena por ela e mostra a **seta de direção** ao lado do header; clicar de novo inverte; clicar em outra coluna move o indicador. Ordenação padrão: alfabética pelo nome. + - Na omnibox, opção **Ordenação** com cada coluna × crescente/decrescente (padrão já usado na lista de Territórios) — **em todos os breakpoints**, inclusive no mobile (a omnibox é edge-to-edge e o mobile tem as mesmas capacidades de ordenação). + - Na omnibox, filtros de ausência: **Sem assessor** (quem não tem assessorado), **Sem base** (sem cidade no cadastro), **Sem contato** (sem telefone registrado). +- **Anti-goals de produto:** não vira spreadsheet mode; nada de ordenar por colunas ocultas (toda chave de ordenação pousa numa coluna visível); filtro de ausência não expõe o valor do dado (só a presença/ausência — telefone nunca é listado em facet). + +## Objetivo e aceite + +- Header de cada coluna sortável ordena a lista global (conjunto filtrado, não só a página), com `aria-sort` e seta de direção; 2º clique inverte; a coluna ativa é única (mudou de coluna, o indicador move). +- Omnibox ganha o recorte "Ordenação" (padrão de Territórios) com as mesmas chaves/direções do header. +- Omnibox ganha os filtros de ausência "Sem assessor", "Sem base", "Sem contato" — combináveis entre si e com os filtros atuais, estado na URL como os demais. +- Ordenação default inalterada (nome A–Z) — a lista abre como hoje. +- Guardrails: sem migration, sem collection nova; escopo do assessor preservado (ordena/filtra sobre o que ele enxerga); leader segue fora da rota. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — nenhuma métrica nova: a entrega é recorte e ordenação sobre a tabela existente (mesma natureza do B29 em lideranças). +- **Decisões desbloqueadas:** staff: "quem da minha carteira está sem telefone para disparar a ação?"; "quais pessoas lideram mais municípios?"; "quem está sem assessor na região X?". Ausência é recorte operacional, não dado exibido. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/utilities/people/peopleListUrl.ts` (estado `sort`/`dir` + filtros de ausência no contrato de URL, espelhando `territoryListUrl.ts`); `src/utilities/people/peopleListFilters.ts` e `peopleOmnibox.ts` (chips/sugestões de ordenação e ausência, molde `territoryOmnibox.ts`); `src/utilities/people/peopleData.ts` (aplica sort sobre o conjunto filtrado — a lista já é merge em memória); `src/components/campaign/people/PeopleFilters.tsx` (omnibox); página `pessoas/page.tsx` (headers sortáveis via `CampaignSortableHead`/`CampaignTransitionAnchor`, padrão B15). +- **Precedente a olhar:** [ordenacao-colunas-lista-municipios.md](ordenacao-colunas-lista-municipios.md) (B15 — header sort com `?sort=`/`?dir=` na URL), [ordenacao-filtros-lista-liderancas.md](ordenacao-filtros-lista-liderancas.md) (B29 — wrappers de domínio sobre chrome compartilhado), `src/utilities/territory/territoryOmnibox.ts` (chip "Ordenação" no omnibox com chaves asc/desc). +- **Risco de acoplamento:** a lista é merge de três fontes (leadership/dobradinha/staff) com filtros in-memory — o sort deve operar sobre o conjunto filtrado **antes** da paginação (precedente B15 branch in-memory), respeitando o escopo do assessor no merge; chaves de ordenação só sobre colunas visíveis; nulls ("Sem…") sempre no fim (precedente B15). + +## Dependências + +- **C99/C100** (prontas). **Suaves:** C116 (mesma tabela; sem dependência de ordem), B155+ (padrão de escrita de célula — só afeta se o sort interagir com edição). + +## Fora de escopo + +- Edição in-place / chips de território → **C116** ([pessoas-edicao-inplace-lista.md](pessoas-edicao-inplace-lista.md)). +- Ordenar por colunas ocultas; filtros salvos novos (o padrão B18 já cobre a lista se a mesa pedir). +- "Sem e-mail" como filtro (o pedido cobre assessor/base/contato; e-mail oculto por padrão desde B197). +- Export CSV / view alternativas. + +## Rabbit holes de produto + +- **"Ordenar por coluna oculta ou por campo sem coluna."** Chave de ordenação que não é coluna visível é estado que mente. **Corte:** toda chave do omnibox = uma coluna da tabela; nada extra. +- **"Filtro de ausência vira campo de busca."** Digitar "sem telefone" na busca e esperar que interprete. **Corte:** facet explícito "Sem…" na omnibox, combinável, mesmo contrato dos demais. +- **Sort client-side da página de 25.** Ordena a página, não a lista — ranking falso. **Corte:** sort sobre o conjunto filtrado completo (a lista já carrega o filtrado para merge; o custo é o mesmo do B15 in-memory). +- **Nova métrica de "cobertura" para justificar o sort.** Contagem de vazios agregada no topo da página vira dashboard de vaidade. **Corte:** ausência é filtro e ordenação, não KPI. + +## Questões em aberto (produto) + +Resolvidas no gate 2026-08-11: + +- Colunas de municípios ordenam **por contagem** (mais municípios primeiro em `desc`); vazios ("Sem…") sempre no fim. +- "Sem contato" = **sem telefone** registrado (coluna Contato = telefone desde B197). +- Coluna Ações não é sortável (inerte, padrão B15). +- Mobile: o recorte "Ordenação" na omnibox vale em todos os breakpoints — o mobile tem as mesmas capacidades de ordenação e filtro. + +## Referências + +- `src/app/(campaign)/campanha/(app)/pessoas/page.tsx` — tabela + `PeopleFilters` (omnibox) +- `src/utilities/people/peopleListUrl.ts` / `peopleListFilters.ts` / `peopleOmnibox.ts` — contrato de URL e omnibox atuais ("no sort in v1") +- `src/utilities/territory/territoryOmnibox.ts` + `territoryListUrl.ts` — molde do recorte "Ordenação" no omnibox +- `src/components/campaign/shared/CampaignTable.tsx`, `CampaignSortableHead` (B15/B29) — headers sortáveis com `aria-sort` + seta +- `src/utilities/people/peopleData.ts` — merge in-memory; ponto onde o sort global se aplica +- Canvas UI (gate): `/home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c117-ui-draft.canvas.tsx`