Merge pull request #729 from fsolla/plans/plan-issue-43

plan(C139): Contatos - pagina da entidade Contact + cancelamento dos planos abertos de Pessoas
This commit is contained in:
Francisco Solla 2026-08-13 15:56:35 +00:00 committed by GitHub
commit 977dab044e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 721 additions and 145 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,611 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>UI draft — Contatos: página da entidade Contact</title>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<main class="mx-auto max-w-5xl p-8">
<p class="text-sm text-zinc-400">
Mesa (coordenação/assessores) na lista de Contatos — atualizar ficha e contatar pessoas.
Cenas: desktop 1280px (lista + linha nova) e mobile 390px (cards + sheet de edição). Acento
único = CTA primário (vermelho do app). Chips dentro do input; células = inputs.
</p>
<!-- ============ CENA: DESKTOP ============ -->
<h2 class="mt-8 text-sm font-semibold uppercase tracking-wide text-zinc-500">
Desktop — /campanha/contatos (1280px)
</h2>
<section
data-shot="desktop"
class="mt-4 overflow-hidden rounded-xl border border-zinc-800 bg-zinc-100 text-zinc-900"
>
<!-- app header -->
<div class="flex h-14 items-center justify-between border-b border-zinc-300 bg-white px-4">
<div class="flex items-baseline gap-2">
<p class="text-sm font-semibold">Contatos</p>
<p class="text-xs text-zinc-500">
Fichas da campanha — atualize dados e fale com as pessoas
</p>
</div>
<span
class="flex size-9 items-center justify-center rounded-full bg-zinc-200 text-xs text-zinc-600"
>SR</span
>
</div>
<!-- omnibox: chips DENTRO do input -->
<div class="border-b border-zinc-300 bg-white px-4 py-2">
<div class="flex items-center gap-1.5">
<div
class="flex min-h-9 flex-1 items-center gap-1 rounded-lg border border-zinc-300 px-2"
>
<span class="text-xs text-zinc-400">⌕</span>
<span
class="flex items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-100"
>
Gênero: Feminino <span class="text-zinc-400">✕</span>
</span>
<span
class="flex items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-100"
>
Estado: BA <span class="text-zinc-400">✕</span>
</span>
<span
class="flex items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-100"
>
Sem e-mail <span class="text-zinc-400">✕</span>
</span>
<input
class="min-h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-zinc-500"
placeholder="Buscar contato — nome, e-mail ou telefone…"
/>
<span
class="flex size-8 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-xs text-zinc-500"
>✕</span
>
</div>
<button
class="flex h-9 shrink-0 items-center gap-1 rounded-lg bg-red-600 px-3 text-xs font-medium text-white"
>
+ Criar contato
</button>
<button
class="flex h-9 shrink-0 items-center gap-1 rounded-lg bg-zinc-900 px-3 text-xs font-medium text-zinc-100"
>
Colunas
</button>
</div>
<!-- sugestões (dropdown aberto) -->
<div class="mt-2 rounded-lg border border-zinc-300 bg-white p-1 shadow-sm">
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
⌕ Buscar: “maria”
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">Lideranças: Sim</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
Dobradinhas: Sim
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">Assessores: Sim</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">Equipe: Sim</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
Gênero: Masculino
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
Gênero: Não binário
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
Gênero: Não informado
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">
Cidade: Salvador
</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">Sem telefone</p>
<p class="flex items-center gap-1 px-2 py-1.5 text-xs text-zinc-600">Ordenar: Cidade</p>
</div>
</div>
<!-- tabela: Nome, Gênero, Telefone, Email, Cidade, Estado, Ações -->
<div class="overflow-x-auto bg-white">
<table class="w-full text-left text-sm">
<thead>
<tr class="border-b border-zinc-300 text-xs uppercase tracking-wide text-zinc-500">
<th class="px-4 py-2 font-medium">Nome</th>
<th class="px-4 py-2 font-medium">Gênero</th>
<th class="px-4 py-2 font-medium">Telefone</th>
<th class="px-4 py-2 font-medium">E-mail</th>
<th class="px-4 py-2 font-medium">Cidade</th>
<th class="px-4 py-2 font-medium">Estado</th>
<th class="px-4 py-2 text-right font-medium">Ações</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200">
<!-- linha 1: telefone único -->
<tr class="bg-white">
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Maria dos Santos"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Feminino"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="(71) 99999-0001"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="maria@email.com"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Salvador"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="BA"
/>
</td>
<td class="px-4 py-2">
<div class="flex justify-end gap-1">
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✆</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✉</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>🗑</span
>
</div>
</td>
</tr>
<!-- linha 2: dois telefones empilhados, sem e-mail -->
<tr class="bg-white">
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="João Pereira"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Masculino"
/>
</td>
<td class="px-4 py-2">
<div class="flex flex-col gap-1">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="(75) 98888-0002"
/>
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="(75) 97777-0009"
/>
</div>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-dashed border-zinc-300 bg-white px-2 py-1.5 text-sm text-zinc-400 outline-none"
value="Sem e-mail"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Feira de Santana"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="BA"
/>
</td>
<td class="px-4 py-2">
<div class="flex justify-end gap-1">
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✆</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs text-zinc-300"
>✉</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>🗑</span
>
</div>
</td>
</tr>
<!-- linha 3: linkada a liderança + dobradinha, sem telefone -->
<tr class="bg-white">
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Ana Lima"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Não binário"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-dashed border-zinc-300 bg-white px-2 py-1.5 text-sm text-zinc-400 outline-none"
value="Sem telefone"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="ana@email.com"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="Salvador"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm outline-none"
value="BA"
/>
</td>
<td class="px-4 py-2">
<div class="flex justify-end gap-1">
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs text-zinc-300"
>✆</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✉</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>🗑</span
>
</div>
</td>
</tr>
</tbody>
</table>
<div
class="flex items-center justify-between border-t border-zinc-300 px-4 py-2 text-xs text-zinc-500"
>
<span>Mostrando 3 de 435 contatos</span>
<span>‹ 1 · 2 · 3 ›</span>
</div>
</div>
</section>
<!-- ============ CENA: DESKTOP — linha nova ============ -->
<h2 class="mt-8 text-sm font-semibold uppercase tracking-wide text-zinc-500">
Desktop — linha nova (após “+ Criar contato”)
</h2>
<section
data-shot="desktop-novo"
class="mt-4 overflow-hidden rounded-xl border border-zinc-800 bg-zinc-100 text-zinc-900"
>
<div class="flex h-14 items-center justify-between border-b border-zinc-300 bg-white px-4">
<p class="text-sm font-semibold">Contatos</p>
<span
class="flex size-9 items-center justify-center rounded-full bg-zinc-200 text-xs text-zinc-600"
>SR</span
>
</div>
<div class="border-b border-zinc-300 bg-white px-4 py-2">
<div class="flex min-h-9 items-center gap-1 rounded-lg border border-zinc-300 px-2">
<span class="text-xs text-zinc-400">⌕</span>
<input
class="min-h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-zinc-500"
placeholder="Buscar contato…"
/>
</div>
</div>
<div class="overflow-x-auto bg-white">
<table class="w-full text-left text-sm">
<thead>
<tr class="border-b border-zinc-300 text-xs uppercase tracking-wide text-zinc-500">
<th class="px-4 py-2 font-medium">Nome</th>
<th class="px-4 py-2 font-medium">Gênero</th>
<th class="px-4 py-2 font-medium">Telefone</th>
<th class="px-4 py-2 font-medium">E-mail</th>
<th class="px-4 py-2 font-medium">Cidade</th>
<th class="px-4 py-2 font-medium">Estado</th>
<th class="px-4 py-2 text-right font-medium">Ações</th>
</tr>
</thead>
<tbody>
<!-- linha nova em rascunho -->
<tr class="border-b-2 border-red-600 bg-red-50">
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="Nome"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="Gênero"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="(71) 9…"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="email@…"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="Cidade"
/>
</td>
<td class="px-4 py-2">
<input
class="w-full rounded-md border border-red-300 bg-white px-2 py-1.5 text-sm outline-none"
placeholder="UF"
/>
</td>
<td class="px-4 py-2">
<div class="flex justify-end gap-1">
<button
class="flex h-8 items-center rounded-lg bg-zinc-900 px-3 text-xs font-medium text-zinc-100"
>
Salvar
</button>
<button
class="flex h-8 items-center rounded-lg border border-zinc-300 px-3 text-xs font-medium text-zinc-600"
>
Descartar
</button>
</div>
</td>
</tr>
<tr class="bg-white">
<td class="px-4 py-2.5 font-medium">Maria dos Santos</td>
<td class="px-4 py-2.5 text-zinc-700">Feminino</td>
<td class="px-4 py-2.5 text-zinc-700">(71) 99999-0001</td>
<td class="px-4 py-2.5 text-zinc-700">maria@email.com</td>
<td class="px-4 py-2.5 text-zinc-700">Salvador</td>
<td class="px-4 py-2.5 text-zinc-700">BA</td>
<td class="px-4 py-2.5">
<div class="flex justify-end gap-1">
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✆</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✉</span
>
<span
class="flex size-8 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>🗑</span
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- ============ CENA: MOBILE ============ -->
<h2 class="mt-8 text-sm font-semibold uppercase tracking-wide text-zinc-500">
Mobile — cards (390px, omnibox edge-to-edge sem borda)
</h2>
<section
data-shot="mobile"
class="mt-4 w-[390px] overflow-hidden rounded-xl border border-zinc-800 bg-zinc-100 text-zinc-900"
>
<!-- app header -->
<div class="flex h-14 items-center justify-between border-b border-zinc-300 bg-white px-4">
<p class="text-sm font-semibold">Contatos</p>
<span
class="flex size-9 items-center justify-center rounded-full bg-zinc-200 text-xs text-zinc-600"
>SR</span
>
</div>
<!-- omnibox sem moldura, edge-to-edge -->
<div class="border-b border-zinc-300 bg-white px-3 py-1.5">
<div class="flex min-h-9 items-center gap-1">
<span class="text-xs text-zinc-400">⌕</span>
<span
class="flex items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-100"
>
Gênero: Feminino <span class="text-zinc-400">✕</span>
</span>
<input
class="min-h-9 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-zinc-500"
placeholder="Buscar contato…"
/>
</div>
</div>
<!-- cards -->
<ul class="flex flex-col divide-y divide-zinc-300 bg-zinc-100">
<li class="flex flex-col gap-2 bg-white px-4 py-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="font-medium">Maria dos Santos</p>
<p class="truncate text-xs text-zinc-500">(71) 99999-0001</p>
</div>
<div class="flex shrink-0 items-center gap-1">
<span class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✆</span
>
<span class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✉</span
>
</div>
</div>
<div class="flex flex-wrap gap-1.5 text-xs">
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700">Feminino</span>
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700">Salvador · BA</span>
</div>
</li>
<li class="flex flex-col gap-2 bg-white px-4 py-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="font-medium">João Pereira</p>
<p class="truncate text-xs text-zinc-500">(75) 98888-0002</p>
</div>
<div class="flex shrink-0 items-center gap-1">
<span class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✆</span
>
<span
class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs text-zinc-300"
>✉</span
>
</div>
</div>
<div class="flex flex-wrap gap-1.5 text-xs">
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700">Masculino</span>
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700"
>Feira de Santana · BA</span
>
</div>
</li>
<li class="flex flex-col gap-2 bg-white px-4 py-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="font-medium">Ana Lima</p>
<p class="truncate text-xs text-zinc-500">ana@email.com</p>
</div>
<div class="flex shrink-0 items-center gap-1">
<span
class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs text-zinc-300"
>✆</span
>
<span class="flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-xs"
>✉</span
>
</div>
</div>
<div class="flex flex-wrap gap-1.5 text-xs">
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700">Não binário</span>
<span class="rounded-full bg-zinc-200 px-2 py-0.5 text-zinc-700">Salvador · BA</span>
</div>
</li>
</ul>
<!-- FAB de criação (substitui o de ações rápidas; FAB de IA permanece) -->
<div class="relative h-20 bg-zinc-100">
<div
class="absolute bottom-4 right-4 flex size-14 items-center justify-center rounded-full bg-red-600 text-2xl text-white"
>
+
</div>
<div
class="absolute bottom-4 right-20 flex size-14 items-center justify-center rounded-full bg-zinc-900 text-xs text-zinc-100"
>
IA
</div>
</div>
</section>
<!-- ============ CENA: MOBILE — sheet de edição ============ -->
<h2 class="mt-8 text-sm font-semibold uppercase tracking-wide text-zinc-500">
Mobile — bottom sheet de edição (toque no card)
</h2>
<section
data-shot="mobile-edicao"
class="mt-4 w-[390px] overflow-hidden rounded-xl border border-zinc-800 bg-zinc-100 text-zinc-900"
>
<!-- lista desfocada atrás (representação) -->
<div class="h-24 bg-zinc-100 opacity-40">
<div class="flex h-14 items-center border-b border-zinc-300 bg-white px-4">
<p class="text-sm font-semibold">Contatos</p>
</div>
<div class="flex flex-col gap-1.5 px-4 pt-2">
<div class="h-5 w-2/3 rounded bg-zinc-300"></div>
<div class="h-4 w-1/2 rounded bg-zinc-300"></div>
</div>
</div>
<!-- sheet -->
<div class="rounded-t-2xl border-t border-zinc-300 bg-white px-4 pb-4 pt-3">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-zinc-300"></div>
<p class="mb-3 text-sm font-semibold">Editar contato</p>
<div class="flex flex-col gap-2.5">
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="Maria dos Santos"
/>
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="Feminino"
/>
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="(71) 99999-0001"
/>
<p class="text-xs font-medium text-zinc-500">+ Adicionar telefone</p>
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="maria@email.com"
/>
<div class="grid grid-cols-2 gap-2">
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="Salvador"
/>
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="BA"
/>
</div>
<input
class="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm outline-none"
value="40000-000"
placeholder="CEP"
/>
</div>
<div class="mt-4 flex gap-2">
<button
class="flex h-10 flex-1 items-center justify-center rounded-lg border border-zinc-300 text-sm font-medium text-zinc-600"
>
Cancelar
</button>
<button
class="flex h-10 flex-1 items-center justify-center rounded-lg bg-red-600 text-sm font-medium text-white"
>
Salvar
</button>
</div>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,110 @@
# Contatos — página da entidade Contact (fichas da campanha)
Status: rascunho
Atualizado em: 2026-08-13
Issue: #728
Priority: P1
Model: composer-2.5
Impeccable: C — fluxo novo: rota nova + tabela desktop + cards mobile + omnibox
Rascunho UI: docs/plans/contatos-pagina-da-entidade-contact-ui-draft.html
Appetite: ~2–3 dias eng; uma rota nova com chassis existentes
Responsável: —
## Intenção
A página de Pessoas (`/campanha/pessoas`) nasceu de uma necessidade real — editar o contato de todo mundo rápido num lugar só — mas o que ela virou (lista all-featuring, com colunas de capacidade por território) não tem caso de uso: a mesa não navega por capacidades nela, e cada lista especializada já faz isso melhor. O que a mesa realmente precisa é **uma lista de fichas de contato**: ver os dados de contato de qualquer pessoa da campanha num lugar só, atualizar ficha (nome, e-mail, telefones, gênero, estado, cidade, CEP) e **contatar pessoas específicas** (WhatsApp/e-mail) sem saltar entre listas.
O padrão que deu certo — **toda entidade de pessoa tem uma ficha `Contact`** — é exatamente a fundação do caso de uso: a ficha é a fonte única de dados de contato. Esta entrega constrói a página da ficha. A página de Pessoas **fica no ar como referência** enquanto esta nasce (e cai depois, em decisão futura — ver Fora de escopo).
## Persona e fluxo
- **Persona / contexto:** coordenação e assessores na mesa, com um celular na mão ("preciso falar com fulana") ou editando a ficha de quem entrou na campanha.
- **Job principal:** achar a pessoa por qualquer dado de contato (nome, e-mail, telefone, cidade…), ver a ficha completa e atualizar o que estiver errado, e partir dela para o contato (WhatsApp / e-mail).
- **Fluxo desejado:** abre `/campanha/contatos` → vê a lista de fichas (tabela no desktop, cards no mobile) → filtra na omnibox por cada propriedade da ficha (gênero, estado, cidade, ausências como "sem telefone") e/ou busca geral → ordena → edita a ficha onde vê (nome, e-mail, telefones, gênero, estado, cidade, CEP) → contata pela ação da linha (WhatsApp / e-mail / copiar telefone).
- **Anti-goals de produto:** **não** vira a página de Pessoas de novo — sem colunas de capacidade (assessora/lidera/dobra/apoiador), sem territórios, sem papel; **não** é segundo cadastro de pessoa (a ficha continua sendo `Contact`); **não** substitui as listas especializadas (`/liderancas`, `/dobradinhas`, `/apoiadores`, `/assessores` seguem intocadas); **não** é edição em massa / spreadsheet.
## Decisões de produto (travadas com o humano em 2026-08-13)
- **A página é da ficha `Contact`, pura.** Colunas desktop, nesta ordem: **Nome, Gênero, Telefone, E-mail, Cidade, Estado** (CEP fica dado da ficha, editável no mobile sheet, sem coluna). Sem coluna de vínculos — foi o que matou a página de Pessoas.
- **Gênero (vocabulário de produto):** Masculino, Feminino, **Não binário**, Não informado — facet e edição usam exatamente esses 4 valores.
- **Busca geral cobre telefone.** O `q` da omnibox casa nome, e-mail e **qualquer** telefone da ficha (multi-telefone, C112) — absorve o gatilho de C121 ("tenho um número na mão, acho a pessoa").
- **Filtros por propriedade da ficha:** gênero, estado, cidade, ausências ("sem telefone", "sem e-mail"). **Filtros de vínculo** (só facet, nunca coluna): **Lideranças, Dobradinhas, Assessores, Equipe** (nome pt-BR que cobre assessores/coordenadores/candidato — "Equipe" assumido; facet booleano "Sim" para cada vínculo).
- **Ordenação:** pelas colunas relevantes (nome default, cidade, estado, e-mail) — padrão C117 (header sortable + omnibox). Sem ordenação por telefone.
- **Edição onde se vê (desktop):** **as células de dados são inputs** — salva no blur ou Enter; multi-telefones empilhados um sobre o outro na célula. Mobile: toque no card abre **bottom sheet com todos os campos** editáveis (nome, gênero, telefones + adicionar, e-mail, cidade, estado, CEP). Mesmo padrão de escopo do `updatePersonContact` (assessor só edita ficha da carteira dele).
- **Criação de contato:** desktop botão **"+ Criar contato" ao lado de "Colunas"** → linha vazia no topo com ações **Descartar/Salvar**; mobile **FAB de criação** (substitui o FAB de ações rápidas nesta rota; FAB de IA permanece) → bottom sheet de criação.
- **Ações por linha:** **Mensagem no WhatsApp** (`wa.me`, só com telefone — `whatsAppHrefForPhone`), **Enviar e-mail** (`mailto`, só com e-mail), **Apagar contato** — com **alerta de confirmação que lista o contato e todas as entidades linkadas a ele** (liderança, dobradinha, assessor, apoiador, conta de acesso…) que serão removidas.
- **Escopo de acesso = `canReadContacts` existente:** assessor vê só fichas da carteira (via lideranças/dobradinhas/apoiadores do escopo); coordenador/candidato veem tudo. **Leader não acessa** (lockdown — a ferramenta "Meus contatos" do leader fica intocada).
- **Nenhuma migration.** `Contact` já existe; nada no schema muda.
- **Mobile: cards mostram só o telefone principal; "Cidade · Estado"** (nessa ordem); **sem rótulo "Sem e-mail"** — o que não existe simplesmente não aparece. **Omnibox sem bordas, edge-to-edge.**
## Objetivo e aceite
- `/campanha/contatos` lista as fichas que o ator pode ler, com **tabela no desktop e cards no mobile** (mesmo chassis das listas atuais).
- Omnibox com facet por propriedade (gênero com os 4 valores, estado, cidade, ausências) + vínculos (Lideranças/Dobradinhas/Assessores/Equipe) + busca geral por nome/e-mail/qualquer telefone + ordenação (nome/cidade/estado/e-mail). **Chips do filtro dentro do input.**
- Edição where-you-see: desktop células são inputs (salva no blur/Enter), telefones múltiplos empilhados; mobile sheet com todos os campos, CEP incluso. Escopo do ator respeitado.
- Criação: desktop "+ Criar contato" ao lado de "Colunas" (linha vazia + Descartar/Salvar); mobile FAB (no lugar do de ações rápidas, IA permanece) → bottom sheet.
- Ações por linha: WhatsApp (só com telefone), e-mail (só com e-mail), Apagar contato com alerta listando as entidades linkadas.
- Mobile: card mostra só telefone principal; "Cidade · Estado"; sem "Sem e-mail"; omnibox edge-to-edge sem bordas.
- Leader não acessa a rota; "Meus contatos" do leader intacta.
- Nenhuma rota pública muda; nenhuma migration; `/campanha/pessoas` continua no ar como referência.
## Dados (intenção)
- **Vou apresentar dados?** Não — as colunas expressam a ficha, não métricas. Nenhum agregado entra na superfície.
## Direção no codebase (hipótese)
- **Áreas prováveis:** rota nova `src/app/(campaign)/campanha/(app)/contatos/` (ver Questões — decidido: A, mover o leader tool); shells do Pass 2 W1 (URL state, omnibox, tabela, cards, paginação); utilitários de domínio já existentes (`canReadContacts` em `src/utilities/access/contacts.ts`, `contactIdentity.ts`, `contactPhoneLocks.ts`, `src/lib/phone.ts` com `whatsAppHrefForPhone`/`formatBrazilianPhoneInput`).
- **Precedente a olhar:** C116/C130 (células editáveis in-place + célula de contato), C117 (ordenação header + omnibox), C125 (facet de ausência), C112 (multi-telefone), C100 (lista de pessoas como referência estrutural), B18 (filtros salvos — padrão disponível, não prometido no v1), `updatePersonContact` (`src/app/(campaign)/campanha/actions/person.ts` — padrão de escopo de edição de ficha), **apagar com manifest de vínculos** (precedente do `personDelete`/delete de pessoa de C100 — reusar o contrato "o que a remoção leva junto" para o alerta).
- **Risco de acoplamento:** a edição da ficha deve preservar o escopo (assessor só a carteira) e os invariantes do `Contact` (normalização de telefone, nome de dobradinha vinculada); a rota `contatos` hoje pertence ao leader tool — **mover para `/campanha/meus-contatos`** exige atualizar `LEADER_CONTACTS_HOME` (`src/lib/campaignPaths.ts`) e o catálogo `campaignPageChrome`; leader lockdown é o limite da rota; facets de vínculo precisam dos joins existentes (leadership/stateDeputy/campaignUser por ficha) — não podem vazar escopo de assessor.
## Dependências
- Nenhuma dura. Soft: C111/C112 (telefone não único / múltiplos telefones — entregues) e C120 (race de telefone em `findOrCreateContactByPhone`, aberta — não bloqueia).
## Fora de escopo
- **Derrubar a página de Pessoas** — fica no ar como referência durante esta entrega; a remoção é decisão futura do produto (a rota de Pessoas e seus planos abertos morrem juntos — planos cancelados neste lote).
- Substituir as listas especializadas (lideranças/dobradinhas/apoiadores/assessores).
- Colunas de capacidade/vínculos na lista de contatos (anti-goal explícito — vínculos entram **só** como facet).
- Filtros salvos (padrão B18 existe e pode ser reusado depois se a mesa pedir).
- Dedupe/merge de fichas duplicadas (pré-existente, fora do v1).
## Rabbit holes de produto
- **"Só completar" com colunas de capacidade.** Se alguém adicionar "Lidera/Assessora/Dobra" como coluna, vira a página de Pessoas de novo. **Corte:** ficha pura; vínculos só como facet da omnibox.
- **"Só completar" com edição em massa.** Editar várias fichas de uma vez = spreadsheet mode sem pedido. **Corte:** edição in-place de uma ficha por vez.
- **"Só completar" com a busca virando filtro por município.** Município não é propriedade da ficha `Contact` (é do vínculo). **Corte:** facet de cidade/estado (texto da ficha), não de município de capacidade.
- **"Só completar" com copiar telefone nas ações.** A mesa não pediu; WhatsApp + e-mail + apagar cobrem o job. **Corte:** sem ação extra no v1.
## Questões em aberto (produto)
_Resolvidas no gate 2026-08-13:_ rota = **A** (leader tool move para `/campanha/meus-contatos`; nova página toma `/campanha/contatos`); CEP editável = **A** (sim, no sheet mobile); ordenação por telefone = **não**; kill list = **confirmado**.
- **Nome do facet de staff?** **Opções:** A) "Equipe" | B) "Staff" | C) "Pessoal da campanha". **Recomendação:** A — pt-BR curto que cobre assessores, coordenadores e candidato. _(assumido — validar)_
## Referências
- GitHub Issue #728
- Rascunho UI (gate): docs/plans/contatos-pagina-da-entidade-contact-ui-draft.html + PNGs embutidos abaixo
- Planos cancelados neste lote (decisão "Pessoas vira referência"): C130+ (#717), C132 (#699), C135 (#714), C136 (#722), C137 (#724), C138 (#725), C121 (#664)
- Abrir primeiro: `src/collections/Contact.ts`, `src/utilities/access/contacts.ts`, `src/app/(campaign)/campanha/(app)/pessoas/page.tsx`, `src/utilities/people/peopleData.ts`, `src/components/campaign/shared/CampaignListOmnibox.tsx`
- `AGENTS.md` — `Contact` é a ficha de pessoa (convenção travada); leader lockdown; escopo de assessor por carteira
## Rascunho UI (gate)
Desktop — tabela (Nome, Gênero, Telefone, E-mail, Cidade, Estado), células = inputs, chips dentro do input, "+ Criar contato" ao lado de "Colunas":
![Rascunho UI — desktop](contatos-pagina-da-entidade-contact-ui-draft-desktop.png)
Desktop — linha nova após "+ Criar contato" (Descartar/Salvar):
![Rascunho UI — desktop, linha nova](contatos-pagina-da-entidade-contact-ui-draft-desktop-novo.png)
Mobile — cards edge-to-edge, omnibox sem borda, só telefone principal, "Cidade · Estado", FAB de criação (IA permanece):
![Rascunho UI — mobile](contatos-pagina-da-entidade-contact-ui-draft-mobile.png)
Mobile — bottom sheet de edição (toque no card):
![Rascunho UI — mobile, sheet de edição](contatos-pagina-da-entidade-contact-ui-draft-mobile-edicao.png)

View file

@ -1,76 +0,0 @@
# Escala e DRY pós-C130 (registro de facets da omnibox)
Status: rascunho
Atualizado em: 2026-08-11
Item do roadmap: fill-in **C130+** pós-[#697](https://github.com/fsolla/teqo/issues/697)
Impeccable: A — sem superfície UI nova (DRY/estrutura)
Appetite: ~0,5–1 dia eng
Responsável: —
## Contexto
O C130 adicionou o **5º facet** da lista de pessoas (`party`), cada um com o mesmo
bloco em `peopleOmnibox.ts` (seeds com `createOmniboxSuggestionSeed`, chips,
branches de prefixo em `applyPeopleOmniboxSuggestion`/`removePeopleOmniboxChip`)
mais o toggle correspondente em `peopleListFilters.ts` e o param no
`peopleListUrl.ts`. O padrão já era repetido para capacity/municipality/status/
ausência — e a mesma forma existe nas outras listas (lideranças, municípios,
apoiadores). A cada facet novo, o custo de adição é ~5 edições em 3 módulos, com
risco de esquecer um branch (facet que aparece nas sugestões mas não filtra).
## Objetivos
- **Registro declarativo de facet** (um lugar por facet: prefixo do id, grupo,
label, keywords, toggle/parse): `peopleOmnibox.ts` e `peopleListFilters.ts`
derivam seeds/chips/branches do registro; a adição de um facet novo vira uma
entrada, não cinco edições.
- Sem mudança de URL/contrato (params, chaves e labels pt-BR iguais).
- Escopo mínimo: **pessoas** (os 5 facets). Generalizar para as outras listas
só quando a segunda lista adotar (2º consumidor) — mesmo corte do C130.
- **Hazard documentado no registro:** valores de facet de texto livre (partido)
não podem conter `:` (separador do id de sugestão) — validação no registro.
## Já resolvido no simplify/critique (não reabrir)
- Parse do `party` simplificado para `allParamValues(...).filter(≤32)` (o
helper já trima/deduplica) — redundância removida.
- Comentários de sort key (`peopleListUrl`/`peopleData`) reconciliados com a
exceção C130 (base/party invisíveis vs `email` oculta-por-toggle).
- Comentários "Aliada em" → "Dobra em" (3 pontos).
- Orfandade `field: 'city'` removida (type/schema/record/formAction).
- Tooltip span-wrappers documentados por site (disabled vs ref-forwarding).
## Explicitamente fora
- **Flake `googleCalendarSync.int.spec.ts`** (falha intermitente, reproduz no
main limpo) — classe de teste instável pré-existente, fora deste lote.
- **Flakes e2e sob carga da máquina** (setup prewarm socket hang-up; picker
"2022"/B34 em runs combinados com load ~60+; passam isolados e no main) —
classe já documentada no changelog B197+; reavaliar com harness dev
estabilizado.
- **Tooltip `openOnTouch` nas ações da tabela de pessoas** — a tabela é
desktop-only (`md:block`); touch não a alcança. Gatilho: se a tabela ganhar
cobertura coarse, seguir o precedente B23 (`openOnTouch={false}` em
triggers que abrem Popover/Dialog).
- **Label mobile "Base:" vs 2ª linha desktop sem label** — decisão de gate
(mobile muda só o nome da coluna; C130 anti-goal). Gatilho: redesign dos
cards mobile.
- **Janela do save já-despachado no Escape** (draft descartado localmente mas
o save em voo persiste) — mitigada com `requestId` bump; janela
completada-save irrecuperável client-side, documentada no código. Gatilho:
se o autosave da célula virar transacional.
## Fases
1. **Registro de facets de pessoas** — declarar os 5 facets existentes; derivar
seeds/chips/apply/remove/toggles do registro; unit tests do
`peopleOmnibox`/`peopleListFilters` inalterados em comportamento.
2. **Gates** — `pnpm gate:fast`, e2e `campaignPeople`.
## Guardrails
- Identificadores em inglês; labels pt-BR intactos (grupos "Capacidade",
"Município", "Apoio", "Ausência", "Partido", "Ordenação").
- Sem migration/access/Consent/URL nova.
- Precedentes: `escala-dry-pos-c6.md` (F3 — DRY de filtros/forms),
`escala-dry-pos-c8.md` (facet de município de apoiadores).

View file

@ -1,69 +0,0 @@
# Pessoas: criação rápida de pessoa na lista (linha nova no topo com botão Salvar)
Status: rascunho
Atualizado em: 2026-08-11
Issue: #699
Priority: P1
Model: composer-2.5
Impeccable: B — encaixe na tabela/omnibox de `/campanha/pessoas` (botão ao lado do seletor de colunas + linha-draft)
Canvas UI: /home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-pessoas-ajustes-ui-draft.canvas.tsx (seção "Criação rápida de pessoa")
Appetite: ~0,5–1 dia eng; uma linha-draft + ação de criação
Responsável: —
## Intenção
Cadastrar uma pessoa nova exige hoje sair da lista (fluxo de criação das listas especializadas). A mesa quer criar direto em `/campanha/pessoas`: um botão "Nova pessoa" ao lado do seletor de colunas abre uma **linha em branco no topo da lista**, onde nome/telefone/capacidades são preenchidos inline e um botão de **Salvar** (na coluna de ações) confirma a criação.
## Persona e fluxo
- **Persona / contexto:** coordenador/candidato (e assessor na carteira dele) cadastrando contato novo durante atendimento.
- **Job principal:** criar a ficha da pessoa e já amarrar a primeira capacidade (cidade assessorada/liderada/onde dobra) sem trocar de tela.
- **Fluxo desejado:** clico em "Nova pessoa" → uma linha vazia aparece no topo da lista (nome e telefone em branco, capacidades vazias) → digito nome e telefone → adiciono a primeira cidade de uma capacidade (com C128, a pessoa nasce naquela tabela) → clico em "Salvar" → a linha vira uma pessoa de verdade na lista. "Descartar" (X) remove a linha sem salvar.
- **Anti-goals de produto:** não é modal/formulário novo (a linha é a superfície); não é segundo cadastro paralelo de pessoa (ficha continua `Contact`); não substitui os fluxos ricos das listas especializadas.
## Objetivo e aceite
- Botão **"Nova pessoa"** visível ao lado do seletor de colunas (barra de filtros) para quem pode criar (staff; escopo de carteira preservado para assessor).
- Clicar abre **uma linha-draft no topo da lista**, destacada, com: Nome e Contato (telefone) editáveis inline, as três colunas de capacidade vazias, e na coluna de ações um botão **Salvar** (+ descartar).
- **Salvar** cria a ficha (e, com C128, os vínculos de capacidade já adicionados na linha) e a pessoa aparece na lista; **Descartar** fecha a linha sem escrever nada.
- Salvar sem nome (ou só com nome, sem capacidade) não quebra a lista: pessoa sem capacidade continua fora da lista (regra C100) — a linha avisa quando isso acontecer em vez de "sumir" com a pessoa.
- A linha-draft respeita os mesmos limites das células normais (escopo de acesso, chips de território, "Salvador" agregado de C131).
- Sem regressão nas linhas normais: a linha-draft nunca conta como linha real (ordenação, filtros, paginação, facetas não a incluem).
## Dados (intenção)
- **Vou apresentar dados?** Não — cadastro, sem métrica.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/app/(campaign)/campanha/(app)/pessoas/page.tsx` (botão na barra de filtros, linha-draft no topo da tabela), células de capacidade em modo `draft` (as células de município já têm contrato de draft local), ação de criação de pessoa no estilo das ações de domínio existentes (`src/app/(campaign)/campanha/actions/…`), escrita transacional Contact + capacidades.
- **Precedente a olhar:** C128 (criação de entidade pela primeira cidade), criação de liderança/dobradinha/contato nas listas especializadas (findOrCreate de Contact + entidade, transação), células `draft` (`MunicipalityPortfolioCell.draft`), C116 (células sempre-input).
- **Risco de acoplamento:** a linha-draft lida com "pessoa que ainda não existe" — toda célula que hoje parte de `ownerId` existente precisa do caminho de criação; a regra C100 (sem capacidade não há linha) determina o que acontece ao salvar.
## Dependências
- **C128** (ordem/forte) — sem o ciclo de vida "primeira cidade cria a entidade", a linha-draft não consegue amarrar capacidades no momento da criação.
- Suave com C130 (a linha-draft usa as mesmas colunas/larguras da tabela ajustada).
## Fora de escopo
- Wizard rico de criação (telefones múltiplos, partido, organizações, convite) — a linha é o cadastro mínimo; o resto segue nas fichas.
- Edição de dados da linha-draft além de nome/telefone/capacidades (e-mail fica para depois da criação).
- Criação a partir do mobile (cards) no v1 — a pedido é o botão ao lado de Colunas (desktop).
## Rabbit holes de produto
- **Linha-draft vira mini-wizard**: se cada capacidade pedir seu próprio fluxo de criação na linha, a superfície explode. **Corte:** a linha só repete o comportamento das células normais (com C128 criando as entidades); sem campos extras além de nome/telefone.
- **"Salvar sem capacidade some a pessoa"**: a regra C100 confunde se a linha apenas "desaparece". **Corte:** aviso claro na linha ao salvar sem capacidade ("a pessoa não aparece na lista enquanto não tiver uma capacidade") + manter o cadastro feito (a ficha existe para o admin).
- **Duas linhas-draft simultâneas**: só uma linha de criação por vez (abrir outra descarta/fecha a atual sem salvar).
## Questões em aberto (produto)
- **Salvar pessoa sem nenhuma capacidade?** **Opções:** (a) salva a ficha e avisa que ela não aparece na lista (regra C100 mantida); (b) bloqueia o Salvar até ter uma capacidade. **Recomendação:** (a) — cadastro não deve ser impedido, mas o destino fica explícito no aviso. _(assumido — validar no gate)_
- **Quem pode usar "Nova pessoa"?** **Opções:** (a) todo staff da página (assessor cria na carteira dele); (b) só coordenação/candidato. **Recomendação:** (a) — com escopo de carteira igual às células; coordenação continua vendo tudo. _(assumido — validar no gate)_
## Referências
- Canvas UI (gate): plan-pessoas-ajustes-ui-draft.canvas.tsx (seção C132)
- Planos: [pessoas-qualquer-capacidade-para-toda-pessoa.md](pessoas-qualquer-capacidade-para-toda-pessoa.md) (C128 — criação pela primeira cidade), [pessoas-lista-unificada.md](pessoas-lista-unificada.md) (C100 — regra de visibilidade por capacidade), [pessoas-edicao-inplace-lista.md](pessoas-edicao-inplace-lista.md) (C116)
- `AGENTS.md` — transações multi-coleção, escopo de acesso por capacidade