Compare commits
2 commits
main
...
plans/plan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b60bcbe58 | ||
|
|
bd71e2e19d |
BIN
docs/plans/demandas-responsaveis-ui-draft-criacao.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/plans/demandas-responsaveis-ui-draft-detalhe.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
167
docs/plans/demandas-responsaveis-ui-draft.html
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<!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 — Demandas: responsáveis explícitos</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
||||
<main class="mx-auto max-w-6xl p-8">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Demanda só aparece para candidato/coordenador e para quem está marcado como responsável —
|
||||
nem assessor do município vê fora disso. Cenas: detalhe com seção Responsáveis + editor,
|
||||
formulário de criação com sugestão.
|
||||
</p>
|
||||
|
||||
<!-- Cena 1: detalhe da demanda — visão de coordenador, com responsáveis -->
|
||||
<section data-shot="detalhe" class="mt-6 overflow-hidden rounded-xl border border-zinc-800">
|
||||
<div class="bg-zinc-100 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Material de campanha</span
|
||||
>
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Em aberto</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 text-lg font-bold text-zinc-900">
|
||||
Cartazes para a caminhada do fim de semana
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-zinc-500">Ilhéus · liderança: Nome da liderança</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="text-sm font-bold text-zinc-900">Descrição</div>
|
||||
<div class="mt-2 text-sm leading-relaxed text-zinc-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua.
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-between border-t border-zinc-100 pt-3">
|
||||
<div class="text-xs text-zinc-500">Custo estimado</div>
|
||||
<div class="text-sm font-bold text-zinc-900">R$ 1.200,00</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="text-xs text-zinc-500">Comprovantes</div>
|
||||
<div class="text-sm text-zinc-600">2 anexos</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-between border-t border-zinc-100 pt-3">
|
||||
<div class="text-xs text-zinc-500">Visível para</div>
|
||||
<div class="text-sm text-zinc-600">Responsáveis · Candidato · Coordenador</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-bold text-zinc-900">Responsáveis pela demanda</div>
|
||||
<div class="text-xs text-zinc-400">Só quem é responsável vê esta demanda</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700"
|
||||
>Assessora de campo</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700"
|
||||
>Outro assessor</span
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-6 items-center rounded bg-zinc-100 px-2 text-xs text-zinc-500"
|
||||
>+2</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 h-9 w-full rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Adicionar responsável…
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-zinc-500">
|
||||
Criador já entra como responsável automaticamente. Assessores do município aparecem
|
||||
como sugestão, mas só quem for marcado enxerga.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-md bg-zinc-100 px-4 py-2 text-xs leading-relaxed text-zinc-600">
|
||||
Assessor do município que não é responsável: a demanda não aparece na lista dele nem
|
||||
abre por URL — mesmo com permissão "Visão Tudo".
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cena 2: criação de demanda — campo responsáveis -->
|
||||
<section data-shot="criacao" class="mt-6 overflow-hidden rounded-xl border border-zinc-800">
|
||||
<div class="bg-zinc-100 px-8 py-6">
|
||||
<div class="text-base font-bold text-zinc-900">Nova demanda</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-xs font-bold text-zinc-700">Título</div>
|
||||
<div
|
||||
class="mt-1 h-9 w-full rounded-md border border-zinc-300 bg-white px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Cartazes para a caminhada do fim de semana
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold text-zinc-700">Município</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 w-full items-center justify-between rounded-md border border-zinc-300 bg-white px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Ilhéus
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold text-zinc-700">Tipo</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 w-full items-center justify-between rounded-md border border-zinc-300 bg-white px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Material de campanha
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-4">
|
||||
<div class="text-xs font-bold text-zinc-700">Responsáveis pela demanda</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
<span class="rounded bg-zinc-300 px-2 py-1 text-xs font-bold text-zinc-800"
|
||||
>Você (criador)</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700"
|
||||
>Assessora de campo</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 h-9 w-full rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Buscar assessor…
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-zinc-500">
|
||||
Sugestões: assessores do município. Se ninguém for marcado além de você, só você,
|
||||
candidato e coordenador verão a demanda.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex justify-end gap-2">
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg border border-zinc-300 px-4 text-sm font-bold text-zinc-700"
|
||||
>
|
||||
Cancelar
|
||||
</div>
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg bg-red-600 px-4 text-sm font-extrabold text-white"
|
||||
>
|
||||
Criar demanda
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
88
docs/plans/demandas-responsaveis.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Demandas visíveis apenas para responsáveis explícitos + candidato/coordenador
|
||||
|
||||
Status: aguardando execução
|
||||
Atualizado em: 2026-08-19
|
||||
Issue: #106
|
||||
Priority: P1
|
||||
Model: cursor-grok-4.5-high
|
||||
Impeccable: B — encaixe no detalhe de demanda (`/campanha/demandas/[slug]`) e na criação
|
||||
Rascunho UI: docs/plans/demandas-responsaveis-ui-draft.html
|
||||
Appetite: ~1–1,5 dias eng; campo de responsáveis + regra de visibilidade fail-closed
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
Demandas carregam custo estimado e comprovantes (controle interno de gastos). Hoje **qualquer assessor que administra o município da demanda a vê** — e a mesa descobriu que isso é amplo demais: a demanda deve ser vista **só por quem é responsável por ela**, pelo candidato e pelo coordenador geral. Mesmo um assessor do município relacionado, se não estiver marcado como responsável, não vê. Decidido no gate (2026-08-19, opção B): responsável é **vínculo explícito por demanda**.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** Alex (Coordenador Geral) e o Candidato acompanhando custos e decisões; assessores responsáveis acompanhando "suas" demandas; assessores do município que NÃO devem esbarrar em demandas que não são deles.
|
||||
- **Job principal:** quem cria a demanda (ou a coordenação) define os responsáveis; só eles — além de candidato/coordenador — enxergam e tocam a demanda.
|
||||
- **Fluxo desejado:**
|
||||
1. Um assessor cria uma demanda no município da carteira: o formulário pede "Responsáveis pela demanda" (criador já vem marcado; sugestões = assessores do município).
|
||||
2. No detalhe da demanda, quem pode atualizá-la adiciona/remove responsáveis.
|
||||
3. Um assessor do município que não é responsável: a demanda não aparece na lista dele, não abre por URL e não aparece em buscas — sem erro "sem permissão", simplesmente não existe para ele.
|
||||
- **Anti-goals de produto:** responsável herdado automaticamente do município (a decisão do gate foi explícita); visibilidade por município como default; auditoria de quem viu (fora de escopo).
|
||||
|
||||
### Esboço de fluxo (B/C/D)
|
||||
|
||||
```text
|
||||
[criar demanda] → [campo "Responsáveis" — criador pré-marcado, sugestão do município]
|
||||
→ [criar] → [criador vira responsável; lista só para responsáveis + candidato/coordenador]
|
||||
[detalhe] → [responsáveis gerenciam responsáveis]
|
||||
[assessor não-responsável] → [demanda invisível: lista, URL e busca]
|
||||
```
|
||||
|
||||
### Rascunho UI (B/C/D)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- A demanda tem um conjunto de **responsáveis** (pessoas da campanha); candidato e coordenador sempre a veem, responsável ou não.
|
||||
- **Visibilidade fail-closed:** sem responsável marcado além do criador, a demanda é visível só ao criador + candidato/coordenador. Assessor do município relacionado, se não for responsável, **não vê** — nem na lista, nem por URL, nem na busca.
|
||||
- O **criador** entra automaticamente como responsável (garante pelo menos um dono e que a criação não suma do criador).
|
||||
- Quem pode **atualizar** a demanda pode gerenciar os responsáveis (mesma regra de visibilidade: responsáveis + candidato/coordenador).
|
||||
- A criação pede os responsáveis (sugestão = assessores do município da demanda), mas não obriga além do criador.
|
||||
- Guardrails: leader lockdown intacto (liderança nunca vê demandas); decisão de escalada continua candidato/coordenador; custo/comprovantes seguem a visibilidade da demanda.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — regra de visibilidade e controle de acesso.
|
||||
- **Decisões desbloqueadas:** coordenação decide quem acompanha cada demanda.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** campo de responsáveis na demanda em `src/collections/CampaignDemand.ts`; regra de leitura/atualização em `src/utilities/access/demands.ts` (hoje `canReadCampaignDemand` = escopo do município — passa a ser "unrestricted ou responsável"); server actions `src/app/(campaign)/campanha/actions/demand.ts`; UI do detalhe `src/components/campaign/demand/` e do fluxo de criação (registrar pedido).
|
||||
- **Precedente a olhar:** o fragmento de escopo P3-D (não re-escrever o `{ municipality: { in: ids } }` por aí — a regra nova substitui o escopo municipal de demandas); `getFreshCampaignUser`/`memoizePerRequest` para a leitura por request; o padrão de "membership" de `municipality.advisors` para o controle de responsáveis.
|
||||
- **Risco de acoplamento:** atividades geram demandas (C90) e a lista/omnibox de demandas filtra por escopo — a regra nova muda o que assessores veem em todos os pontos; o Sollinha usa as mesmas queries de demanda (escopo por user).
|
||||
|
||||
## Dependências
|
||||
|
||||
- Nenhuma dura. **Guardrail:** C141 (perfis por assessor) nunca contorna esta regra — Visão "Tudo" não abre demandas fora da responsabilidade.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Perfil de permissão por assessor (C141) e apresentação por perfil (C142).
|
||||
- Notificação/feed ao ser marcado responsável.
|
||||
- Auditoria de leituras da demanda.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Responsável = assessor do município, simples."** Isso é exatamente o modelo de hoje, que o gate rejeitou. **Corte:** vínculo explícito; sugestões sim, herança automática não.
|
||||
- **"Vou usar o histórico de quem criou."** `createdBy` existe, mas só o criador não cobre quem vai executar a demanda. **Corte:** campo próprio de responsáveis; criador vira um responsável, não o único.
|
||||
- **"Vou esconder no cliente."** Esconder na UI não segura URL direta nem a API. **Corte:** regra no acesso de dados (fail-closed) — lista, URL e busca respeitam o mesmo where.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
Fechadas no gate. Assunções operacionais (sem pergunta nova): criador entra automaticamente como responsável; quem atualiza a demanda gerencia responsáveis; sugestões do campo = assessores do município da demanda; visível para = "Responsáveis · Candidato · Coordenador" (rótulo exibido no detalhe). _Assumidas — validar se contrariarem a mesa._
|
||||
|
||||
## Referências
|
||||
|
||||
- Decisão do gate 2026-08-19 (resposta 1): responsáveis explícitos; assessor do município não-responsável não vê.
|
||||
- Item irmão: `docs/plans/permissao-granular-assessores.md` (C141).
|
||||
- Rascunho UI (gate): `docs/plans/demandas-responsaveis-ui-draft.html` + PNGs acima.
|
||||
- Para abrir primeiro: `src/utilities/access/demands.ts`, `src/collections/CampaignDemand.ts`, `docs/plans/demandas-campanha.md`.
|
||||
- `AGENTS.md` — convenção P3-D do escopo de assessor.
|
||||
BIN
docs/plans/permissao-granular-assessores-ui-draft-desktop.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/plans/permissao-granular-assessores-ui-draft-detalhe.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/plans/permissao-granular-assessores-ui-draft-mobile.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
322
docs/plans/permissao-granular-assessores-ui-draft.html
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
<!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 — Perfis de permissão por assessor</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
||||
<main class="mx-auto max-w-6xl p-8">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Coordenador/candidato define por assessor o que ele vê (Tudo | Carteira) e o que ele pode
|
||||
editar (Tudo | Carteira | Somente leitura) — cenas: lista com coluna Permissão + editor,
|
||||
mobile, detalhe do assessor.
|
||||
</p>
|
||||
|
||||
<!-- Cena 1: lista desktop com editor aberto numa linha -->
|
||||
<section data-shot="desktop" class="mt-6 overflow-hidden rounded-xl border border-zinc-800">
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-zinc-200 bg-zinc-100 px-6 py-4"
|
||||
>
|
||||
<div>
|
||||
<div class="h-5 w-44 rounded bg-zinc-500"></div>
|
||||
<div class="mt-1 h-2 w-64 rounded bg-zinc-400"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-9 w-44 rounded-full border border-zinc-300 bg-white px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Buscar assessor…
|
||||
</div>
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg bg-red-600 px-4 text-sm font-extrabold text-white"
|
||||
>
|
||||
+ Novo assessor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-100 px-6 pb-6">
|
||||
<!-- tabela -->
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-200 bg-white">
|
||||
<div
|
||||
class="grid grid-cols-[1.3fr_1.7fr_1.1fr_1.7fr_1.5fr] gap-2 border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-bold text-zinc-500"
|
||||
>
|
||||
<div>Nome</div>
|
||||
<div>E-mail</div>
|
||||
<div>Celular</div>
|
||||
<div>Municípios</div>
|
||||
<div>Permissão</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.3fr_1.7fr_1.1fr_1.7fr_1.5fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="font-semibold text-zinc-900">Nome do assessor</div>
|
||||
<div class="text-zinc-600">assessor@exemplo.org</div>
|
||||
<div class="text-zinc-600">(71) 90000-0000</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700"
|
||||
>Salvador (19)</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700"
|
||||
>Feira de Santana</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700">+3</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Carteira · Edita carteira</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.3fr_1.7fr_1.1fr_1.7fr_1.5fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="font-semibold text-zinc-900">Outra assessora</div>
|
||||
<div class="text-zinc-600">outra@exemplo.org</div>
|
||||
<div class="text-zinc-600">(71) 90000-0001</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700"
|
||||
>Camaçari</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Tudo · Somente leitura</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative grid grid-cols-[1.3fr_1.7fr_1.1fr_1.7fr_1.5fr] items-center gap-2 bg-zinc-50 px-4 py-3 text-sm pb-[28rem]"
|
||||
>
|
||||
<div class="font-semibold text-zinc-900">Assessora de campo</div>
|
||||
<div class="text-zinc-600">campo@exemplo.org</div>
|
||||
<div class="text-zinc-600">(71) 90000-0002</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700">Ilhéus</span>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-700">Itabuna</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Carteira · Edita carteira</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- popover editor -->
|
||||
<div
|
||||
class="absolute left-4 top-6 z-10 w-80 rounded-lg border border-zinc-200 bg-white p-4 shadow-xl"
|
||||
>
|
||||
<div class="text-sm font-bold text-zinc-900">Permissão da conta</div>
|
||||
<div class="mt-1 text-xs text-zinc-500">Vale para todas as telas do /campanha.</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-xs font-bold text-zinc-700">Visão</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 items-center justify-between rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Carteira — municípios que administra
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-zinc-500">
|
||||
Tudo: enxerga todos os municípios e lideranças.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-xs font-bold text-zinc-700">Edição</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 items-center justify-between rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Carteira — edita só o que administra
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-zinc-500">
|
||||
Somente leitura: nenhum controle de edição em lugar nenhum.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 rounded-md bg-zinc-100 px-3 py-2 text-[11px] leading-relaxed text-zinc-600"
|
||||
>
|
||||
Demandas seguem regra própria: só quem é responsável pela demanda (e
|
||||
candidato/coordenador) a vê — mesmo com visão "Tudo".
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg border border-zinc-300 px-4 text-sm font-bold text-zinc-700"
|
||||
>
|
||||
Cancelar
|
||||
</div>
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg bg-red-600 px-4 text-sm font-extrabold text-white"
|
||||
>
|
||||
Salvar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-zinc-500">
|
||||
Padrão de contas novas: Carteira · Edita carteira (comportamento atual). Alterações
|
||||
valem na hora — ninguém precisa reenviar senha.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cena 2: mobile — lista em cards -->
|
||||
<section
|
||||
data-shot="mobile"
|
||||
class="mt-6 w-[390px] overflow-hidden rounded-xl border border-zinc-800"
|
||||
>
|
||||
<div class="bg-zinc-100 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="h-5 w-36 rounded bg-zinc-500"></div>
|
||||
<div
|
||||
class="inline-flex h-9 items-center rounded-lg bg-red-600 px-4 text-sm font-extrabold text-white"
|
||||
>
|
||||
+ Novo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 h-9 w-full rounded-full border border-zinc-300 bg-white px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Buscar assessor…
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg border border-zinc-200 bg-white p-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Nome do assessor</div>
|
||||
<span
|
||||
class="shrink-0 rounded bg-zinc-300 px-2 py-0.5 text-[11px] font-bold text-zinc-800"
|
||||
>Carteira · Edita</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-600">assessor@exemplo.org · (71) 90000-0000</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700"
|
||||
>Salvador (19)</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700"
|
||||
>Feira de Santana</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700">+3</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 rounded-lg border border-zinc-200 bg-white p-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Outra assessora</div>
|
||||
<span
|
||||
class="shrink-0 rounded bg-zinc-300 px-2 py-0.5 text-[11px] font-bold text-zinc-800"
|
||||
>Tudo · Só leitura</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-600">outra@exemplo.org · (71) 90000-0001</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700"
|
||||
>Camaçari</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 rounded-lg border border-zinc-200 bg-white p-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Assessora de campo</div>
|
||||
<span
|
||||
class="shrink-0 rounded bg-zinc-300 px-2 py-0.5 text-[11px] font-bold text-zinc-800"
|
||||
>Carteira · Edita</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-600">campo@exemplo.org · (71) 90000-0002</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700"
|
||||
>Ilhéus</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[11px] text-zinc-700"
|
||||
>Itabuna</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cena 3: detalhe do assessor com seção de permissão -->
|
||||
<section data-shot="detalhe" class="mt-6 overflow-hidden rounded-xl border border-zinc-800">
|
||||
<div class="bg-zinc-100 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-10 w-10 rounded-full bg-zinc-400"></div>
|
||||
<div>
|
||||
<div class="text-lg font-bold text-zinc-900">Nome do assessor</div>
|
||||
<div class="text-sm text-zinc-500">assessor@exemplo.org · (71) 90000-0000</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-bold text-zinc-700"
|
||||
>
|
||||
Enviar link de redefinição de senha
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="text-sm font-bold text-zinc-900">Permissão da conta</div>
|
||||
<div class="mt-3">
|
||||
<div class="text-xs font-bold text-zinc-700">Visão</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 items-center justify-between rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Carteira — municípios que administra
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="text-xs font-bold text-zinc-700">Edição</div>
|
||||
<div
|
||||
class="mt-1 flex h-9 items-center justify-between rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-800"
|
||||
>
|
||||
Carteira — edita só o que administra
|
||||
<span class="text-xs text-zinc-400">▾</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 rounded-md bg-zinc-100 px-3 py-2 text-[11px] leading-relaxed text-zinc-600"
|
||||
>
|
||||
Demandas ficam sempre limitadas aos municípios de responsabilidade. Decisões de
|
||||
coordenação (nível de envolvimento, contas, demandas escaladas) continuam restritas
|
||||
a coordenador e candidato.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="text-sm font-bold text-zinc-900">Municípios administrados</div>
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700"
|
||||
>Salvador (19)</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700"
|
||||
>Feira de Santana</span
|
||||
>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700">Camaçari</span>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700">Ilhéus</span>
|
||||
<span class="rounded bg-zinc-200 px-2 py-1 text-xs text-zinc-700">Itabuna</span>
|
||||
<span
|
||||
class="inline-flex h-6 items-center rounded bg-zinc-100 px-2 text-xs text-zinc-500"
|
||||
>+8</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 h-9 w-full rounded-md border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Adicionar município…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
101
docs/plans/permissao-granular-assessores.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Perfis de permissão por assessor (visão × edição), configuráveis pelo coordenador/candidato
|
||||
|
||||
Status: aguardando execução
|
||||
Atualizado em: 2026-08-19
|
||||
Issue: #105
|
||||
Priority: P1
|
||||
Model: cursor-grok-4.5-high
|
||||
Impeccable: B — encaixe em `/campanha/assessores` (lista + detalhe + novo)
|
||||
Rascunho UI: docs/plans/permissao-granular-assessores-ui-draft.html
|
||||
Appetite: ~2–3 dias eng; um modelo de permissão + configuração + enforcement fail-closed
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
Hoje todo assessor tem exatamente um perfil: **vê e edita só os municípios da carteira**. No campo, a equipe descobriu que isso não cobre a mesa real: há assessor que precisa **ver tudo** (panorama, relatório, cobertura) sem poder **alterar nada**; há assessor que **vê tudo** mas só pode mexer **naquilo pelo que é responsável**; há assessor que só vê o que é seu — com ou sem edição. E **demandas** carregam custo e comprovantes (controle interno): só candidato, coordenador e as **pessoas responsáveis pela demanda** as veem — regra própria, detalhada no item irmão (C143).
|
||||
|
||||
O coordenador/candidato precisa configurar isso **por conta**, na vertical `/campanha`, sem depender do Payload admin.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** Alex (Coordenador Geral) e o Candidato na mesa — montar e ajustar o time de assessores conforme o papel real de cada um; e o assessor, operando o `/campanha` dentro do que lhe foi liberado.
|
||||
- **Job principal:** para cada assessor, definir **o que ele vê** e **o que ele pode editar**, em dois controles simples; e ter isso aplicado na hora, em todas as telas.
|
||||
- **Fluxo desejado:**
|
||||
1. Alex abre `/campanha/assessores` e vê uma coluna "Permissão" por assessor (ex.: "Carteira · Edita carteira").
|
||||
2. Toca no perfil de um assessor e escolhe **Visão** (Tudo | Carteira) e **Edição** (Tudo | Carteira | Somente leitura); a tela explica cada combinação e lembra que demandas seguem regra própria.
|
||||
3. Salva. O assessor entra e opera exatamente dentro do perfil — se "Somente leitura", não encontra nenhum controle de edição; se "Tudo", navega o estado inteiro (demandas fora, que seguem a regra de responsáveis).
|
||||
4. Conta nova nasce no padrão atual (Carteira · Carteira) — nada muda até alguém mexer.
|
||||
- **Anti-goals de produto:** papel novo (só o assessor ganha perfis; liderança continua em lockdown e candidato/coordenador intocados); IAM tipo SaaS (grupos, herança, policy engine); permissão fina por coleção ("vê lideranças mas não atividades"); autoatendimento do próprio perfil.
|
||||
|
||||
### Esboço de fluxo (B/C/D)
|
||||
|
||||
```text
|
||||
[assessores lista] → [coluna Permissão] → [editor: Visão × Edição + avisos]
|
||||
→ [salvar] → [perfil ativo imediatamente]
|
||||
[assessor entra] → [telas renderizam e respondem dentro do perfil — fail-closed no servidor]
|
||||
```
|
||||
|
||||
### Rascunho UI (B/C/D)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- O coordenador/candidato define, por assessor: **Visão** (Tudo | Carteira) e **Edição** (Tudo | Carteira | Somente leitura), nas superfícies de `/campanha/assessores` (lista, detalhe, novo).
|
||||
- Contas novas e existentes mantêm o perfil atual por default — zero mudança de comportamento até configurarem.
|
||||
- O assessor vê/edita **exatamente** o que o perfil permite, em todas as telas do `/campanha` e nas APIs — a checagem falha-fechada no servidor (nunca só ocultar botão no cliente).
|
||||
- **Demandas:** ficam fora dos eixos — mesmo com Visão "Tudo", o assessor só vê demandas de que é responsável (regra explícita do item C143); Visão "Tudo" não abre demandas nem custo/comprovantes.
|
||||
- Edição "Tudo" **não** libera escritas de coordenação: gerenciar contas, atribuir assessores a municípios, mover nível de envolvimento e decidir demandas escaladas seguem restritos a candidato/coordenador.
|
||||
- Combinação incoerente (ex.: editar o que não vê) não é oferecida.
|
||||
- **Apoiadores** (PII) continuam capados na carteira para todo staff abaixo de candidato/coordenador, mesmo com Visão "Tudo" _(decidido no gate)_.
|
||||
- Configuração em três pontos: coluna + editor inline na lista, seção no detalhe e campos no formulário de novo assessor _(decidido no gate)_. Edit-where-you-see.
|
||||
- Guardrails do repo intactos: liderança em lockdown, assimetria de votos (estimativas staff-only), postura LGPD fail-closed.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — superfície de configuração de contas; sem KPI eleitoral.
|
||||
- **Decisões desbloqueadas:** coordenador decide quem vê o quê e quem edita o quê, por pessoa.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** perfis na conta em `src/collections/CampaignUser.ts`; enforcement nos módulos de acesso `src/utilities/access/*` (o prologue `resolveActorScopedRead`/`resolveAccessibleIds` em `shared.ts` e os módulos por domínio — municípios, lideranças, pledges, demandas, atividades, organizações, dobradinhas, apoiadores); gates de escrita nas server actions `src/app/(campaign)/campanha/actions/*.ts`; UI em `src/components/campaign/advisor/` (`AdvisorsTable`, detalhe `[id]`, formulário novo) e helpers de papel client-safe em `src/lib/campaignRoles.ts`.
|
||||
- **Precedente a olhar:** B19 (`gerenciar-assessores.md`) — a superfície e os gates de conta; a convenção P3-D do escopo de assessor (uma só grafia do fragmento `{ municipality(s): { in: ids } }`); C116/C116+ — células edit-in-place com estados read-only por papel.
|
||||
- **Risco de acoplamento:** o escopo do assessor é central e compartilhado (mapa, quantis por escopo, listas, dossiê, Sollinha); qualquer mudança no "o que o assessor vê" propaga. Respeitar leader lockdown e o "não re-escrever o fragmento de escopo" da convenção.
|
||||
|
||||
## Dependências
|
||||
|
||||
- Nenhuma dura. **Guardrail:** a regra de demandas (C143) não depende deste item, e Visão "Tudo" nunca a contorna. C142 (superfícies respeitando o perfil) depende deste.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Perfis para `coordinator`/`candidate`/`leader` (congelados por política de produto).
|
||||
- Demanda com "responsáveis" explícitos por demanda (vínculo por pessoa) — ver Questões; se confirmado, item sucessor.
|
||||
- Permissão fina por coleção ou por campo além da regra de demandas.
|
||||
- Autoatendimento/edição do próprio perfil.
|
||||
- Histórico/auditoria de mudanças de perfil.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Já que tem visão, vou adicionar um role novo."** Papel novo (`viewer`, `auditor`) recria o modelo de papéis e o seed de contas. **Corte:** perfis sobre o papel `advisor` existente — um assessor "visão Tudo · Somente leitura" É o leitor.
|
||||
- **"Vou pedir permissão por coleção."** Cada campo novo vira matriz N×M e a tela vira IAM. **Corte:** dois eixos só; o que não couber nos eixos fica com a regra das demandas ou fora.
|
||||
- **"Vou esconder botão no cliente e pronto."** Acesso de verdade mora no servidor; esconder botão é cosmético. **Corte:** enforcement no acesso de dados e nas actions primeiro; a apresentação é a entrega irmã (item seguinte).
|
||||
- **"Visão Tudo abre as planilhas PII."** Apoiadores/custo de demandas são sensíveis (LGPD/controle interno). **Corte:** apoiadores capados na carteira (decidido); demandas seguem a regra de responsáveis (C143).
|
||||
|
||||
## Decisões do gate (2026-08-19)
|
||||
|
||||
- **Responsável por uma demanda = vínculo explícito por demanda** (opção B da rodada) — mesmo o assessor do município da demanda, se não estiver marcado como responsável, não vê. A regra vive no item irmão **C143**; este item apenas garante que Visão "Tudo" não a contorna.
|
||||
- **Visão "Tudo" não abre superfícies sensíveis** (opção A) — apoiadores (PII) seguem capados na carteira; demandas seguem a regra de responsáveis (C143).
|
||||
- **Edição "Tudo" não libera escritas de coordenação** (opção A).
|
||||
- **Configuração na lista + detalhe + formulário de novo assessor** (opção A).
|
||||
|
||||
## Referências
|
||||
|
||||
- Pedido de produto 2026-08-19 (vertical `/campanha` — granularidade de acesso por conta); decisões do gate na seção acima.
|
||||
- Item irmão: `docs/plans/demandas-responsaveis.md` (C143 — regra de responsáveis de demanda).
|
||||
- Rascunho UI (gate): `docs/plans/permissao-granular-assessores-ui-draft.html` + PNGs acima.
|
||||
- Para abrir primeiro: `src/utilities/access/shared.ts` (`resolveActorScopedRead`), `src/utilities/access/demands.ts` (regra atual de demandas = escopo de leitura), `src/components/campaign/advisor/AdvisorsTable.tsx`, `docs/plans/gerenciar-assessores.md` (B19).
|
||||
- `AGENTS.md` — modelo de papéis de campanha, convenção P3-D do escopo de assessor.
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 39 KiB |
230
docs/plans/ui-respeita-perfil-permissao-ui-draft.html
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
<!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 — Interface respeita o perfil de permissão</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
||||
<main class="mx-auto max-w-6xl p-8">
|
||||
<p class="text-sm text-zinc-400">
|
||||
Depois do perfil de permissão (C141): as telas param de oferecer edição a quem não pode
|
||||
editar e passam a renderizar o escopo completo para quem vê tudo — cenas: lista de
|
||||
municípios em modo leitura, lista com visão total, detalhe de demanda somente leitura.
|
||||
</p>
|
||||
|
||||
<!-- Cena 1: lista de municípios — assessor somente leitura -->
|
||||
<section
|
||||
data-shot="municipios-leitura"
|
||||
class="mt-6 overflow-hidden rounded-xl border border-zinc-800"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-zinc-200 bg-zinc-100 px-6 py-4"
|
||||
>
|
||||
<div>
|
||||
<div class="h-5 w-44 rounded bg-zinc-500"></div>
|
||||
<div class="mt-1 h-2 w-64 rounded bg-zinc-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-100 px-6 pb-6">
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-200 bg-white">
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] gap-2 border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-bold text-zinc-500"
|
||||
>
|
||||
<div>Município</div>
|
||||
<div>Tendência</div>
|
||||
<div>Nível</div>
|
||||
<div>Classe</div>
|
||||
<div>Cobertura</div>
|
||||
<div>Últ. atualização</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Salvador (19 zonas)</div>
|
||||
<div class="min-w-0 text-zinc-600">Favorável</div>
|
||||
<div class="min-w-0 text-zinc-600">N3</div>
|
||||
<div class="min-w-0 text-zinc-600">Reduto</div>
|
||||
<div class="min-w-0 text-zinc-600">82%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 2 dias</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Feira de Santana</div>
|
||||
<div class="min-w-0 text-zinc-600">Neutro</div>
|
||||
<div class="min-w-0 text-zinc-600">N2</div>
|
||||
<div class="min-w-0 text-zinc-600">Expansão</div>
|
||||
<div class="min-w-0 text-zinc-600">41%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 5 dias</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Ilhéus</div>
|
||||
<div class="min-w-0 text-zinc-600">Favorável</div>
|
||||
<div class="min-w-0 whitespace-nowrap text-zinc-600">—</div>
|
||||
<div class="min-w-0 text-zinc-600">Manutenção</div>
|
||||
<div class="min-w-0 text-zinc-600">57%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 1 dia</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-zinc-500">
|
||||
Mesma lista de sempre — mas sem autosave, sem chips editáveis, sem "Registrar
|
||||
atualização": nenhum caminho de escrita visível.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cena 2: mesma lista para assessor com visão Tudo -->
|
||||
<section
|
||||
data-shot="municipios-visao-tudo"
|
||||
class="mt-6 overflow-hidden rounded-xl border border-zinc-800"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-zinc-200 bg-zinc-100 px-6 py-4"
|
||||
>
|
||||
<div>
|
||||
<div class="h-5 w-44 rounded bg-zinc-500"></div>
|
||||
<div class="mt-1 h-2 w-64 rounded bg-zinc-400"></div>
|
||||
</div>
|
||||
<div
|
||||
class="h-9 w-44 rounded-full border border-zinc-300 bg-white px-3 text-sm text-zinc-500"
|
||||
>
|
||||
Buscar município…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-100 px-6 pb-6">
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-200 bg-white">
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] gap-2 border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-bold text-zinc-500"
|
||||
>
|
||||
<div>Município</div>
|
||||
<div>Tendência</div>
|
||||
<div>Nível</div>
|
||||
<div>Classe</div>
|
||||
<div>Cobertura</div>
|
||||
<div>Últ. atualização</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Salvador (19 zonas)</div>
|
||||
<div class="min-w-0 text-zinc-600">Favorável</div>
|
||||
<div class="min-w-0 text-zinc-600">N3</div>
|
||||
<div class="min-w-0 text-zinc-600">Reduto</div>
|
||||
<div class="min-w-0 text-zinc-600">82%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 2 dias</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Vitória da Conquista</div>
|
||||
<div class="min-w-0 text-zinc-600">Neutro</div>
|
||||
<div class="min-w-0 text-zinc-600">N1</div>
|
||||
<div class="min-w-0 text-zinc-600">Marginal</div>
|
||||
<div class="min-w-0 text-zinc-600">12%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 9 dias</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 border-b border-zinc-100 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Barreiras</div>
|
||||
<div class="min-w-0 text-zinc-600">Favorável</div>
|
||||
<div class="min-w-0 text-zinc-600">N2</div>
|
||||
<div class="min-w-0 text-zinc-600">Expansão</div>
|
||||
<div class="min-w-0 text-zinc-600">38%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 4 dias</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_1.05fr_0.95fr_1.05fr_0.8fr_1.1fr] items-center gap-2 px-4 py-3 text-sm"
|
||||
>
|
||||
<div class="min-w-0 truncate font-semibold text-zinc-900">Juazeiro</div>
|
||||
<div class="min-w-0 text-zinc-600">Desfavorável</div>
|
||||
<div class="min-w-0 text-zinc-600">N1</div>
|
||||
<div class="min-w-0 text-zinc-600">Sem base</div>
|
||||
<div class="min-w-0 text-zinc-600">9%</div>
|
||||
<div class="min-w-0 text-zinc-600">há 12 dias</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-zinc-500">
|
||||
Mesma mesa de trabalho de um coordenador (menos apoiadores fora da carteira e demandas
|
||||
de que não é responsável) — ver regras no plano C141.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cena 3: detalhe de demanda em modo leitura -->
|
||||
<section
|
||||
data-shot="demanda-leitura"
|
||||
class="mt-6 overflow-hidden rounded-xl border border-zinc-800"
|
||||
>
|
||||
<div class="bg-zinc-100 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Material de campanha</span
|
||||
>
|
||||
<span class="rounded bg-zinc-300 px-2 py-0.5 text-xs font-bold text-zinc-800"
|
||||
>Em aberto</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 text-lg font-bold text-zinc-900">
|
||||
Cartazes para a caminhada do fim de semana
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-zinc-500">Ilhéus · liderança: Nome da liderança</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="text-sm font-bold text-zinc-900">Descrição</div>
|
||||
<div class="mt-2 text-sm leading-relaxed text-zinc-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua.
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-between border-t border-zinc-100 pt-3">
|
||||
<div class="text-xs text-zinc-500">Custo estimado</div>
|
||||
<div class="text-sm font-bold text-zinc-900">R$ 1.200,00</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="text-xs text-zinc-500">Comprovantes</div>
|
||||
<div class="text-sm text-zinc-600">2 anexos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div class="text-sm font-bold text-zinc-900">Histórico de status</div>
|
||||
<div class="mt-3 space-y-3">
|
||||
<div class="flex gap-3">
|
||||
<div class="h-2 w-2 shrink-0 rounded-full bg-zinc-400"></div>
|
||||
<div class="text-xs text-zinc-600">
|
||||
<span class="font-bold text-zinc-800">Em aberto</span> — criada por Assessora de
|
||||
campo · 12 ago
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="h-2 w-2 shrink-0 rounded-full bg-zinc-400"></div>
|
||||
<div class="text-xs text-zinc-600">
|
||||
<span class="font-bold text-zinc-800">Encaminhada</span> — decidida por
|
||||
Coordenador Geral · 14 ago
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-4 rounded-md bg-zinc-100 px-3 py-2 text-[11px] leading-relaxed text-zinc-600"
|
||||
>
|
||||
Sem botões de transição, sem "registrar pedido", sem edição — só leitura do
|
||||
andamento.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
88
docs/plans/ui-respeita-perfil-permissao.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Interface respeita o perfil de permissão (assessor somente-leitura sem controles de edição; visão Tudo no escopo completo)
|
||||
|
||||
Status: aguardando execução
|
||||
Atualizado em: 2026-08-19
|
||||
Issue: #107
|
||||
Priority: P1
|
||||
Model: cursor-grok-4.5-medium
|
||||
Impeccable: B — passada de apresentação sobre as superfícies staff do `/campanha`
|
||||
Rascunho UI: docs/plans/ui-respeita-perfil-permissao-ui-draft.html
|
||||
Appetite: ~1–2 dias eng; telas passam a refletir o perfil (sem 403s à vista, escopo coerente)
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
O item irmão (perfis de permissão por assessor) entrega o modelo e o enforcement no servidor. Sem esta passada, um assessor "Somente leitura" ainda **vê** todos os botões, células autosave, FABs e sheets de edição — e cada toque morre com erro de permissão, o oposto do produto que a mesa espera. E um assessor que ganhou "Visão Tudo" precisa que listas, detalhes e mapa carreguem o estado inteiro de forma coerente (contagens, facetas, filtros), não meia-lista.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** o assessor com perfil restrito, operando o `/campanha` no celular e no desktop — não pode, não deve e não quer tentar editar; e o assessor de visão total, querendo a mesa de trabalho completa.
|
||||
- **Job principal:** a interface diz a verdade sobre o perfil: quem não edita não encontra edição; quem vê tudo vê tudo.
|
||||
- **Fluxo desejado:**
|
||||
1. Assessor "Carteira · Somente leitura" abre `/campanha/municipios`: células são leitura pura, sem chips editáveis, sem "Registrar atualização", sem FAB de ações — nenhum controle de escrita à vista, sem aviso: a tela simplesmente não oferece edição.
|
||||
2. Assessor "Tudo · Edita carteira" abre as mesmas listas: vê o estado inteiro (todos os municípios/lideranças) e edita apenas o que administra — o controle de edição aparece só onde a carteira alcança.
|
||||
3. Assessor "Carteira · Edita carteira": exatamente o comportamento de hoje (nada muda).
|
||||
- **Anti-goals de produto:** bloquear por mensagem de erro em vez de ocultar/desabilitar; cinza-fantasma que confunde o que é editável; redesenho de superfícies que não precisam mudar.
|
||||
|
||||
### Esboço de fluxo (B/C/D)
|
||||
|
||||
```text
|
||||
[login assessor] → [perfil da conta resolve o que renderizar]
|
||||
→ [visão: escopo completo ou carteira] × [edição: controles presentes ou ausentes]
|
||||
→ [nenhum caminho leva a 403; nenhum dado fora do escopo é oferecido]
|
||||
```
|
||||
|
||||
### Rascunho UI (B/C/D)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- Assessor sem permissão de edição **não encontra nenhum controle de escrita** nas superfícies staff: células de autosave, chips removíveis, "Registrar atualização", FAB/ações rápidas, wizards, sheets e triggers de Popover — no desktop e no mobile. Sem aviso de modo leitura: a presença/ausência dos controles é a única linguagem _(decidido no gate: opção B)_.
|
||||
- Assessor "Visão Tudo" navega listas, detalhes e mapa no escopo completo, com contagens, facetas e filtros coerentes com o que ele vê.
|
||||
- Atalhos de escrita (ex.: busca global) seguem a mesma regra: destino de escrita some para quem não pode escrever; atalhos de navegação permanecem _(decidido no gate: opção A)_.
|
||||
- Nada muda para coordenador/candidato/liderança e para o assessor "Carteira · Edita carteira".
|
||||
- O enforcement de segurança não é relaxado em nenhum ponto: a apresentação acompanha o servidor, nunca o substitui.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — passada de apresentação sobre superfícies existentes.
|
||||
- **Decisões desbloqueadas:** nenhuma decisão de dado nova.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** componentes `src/components/campaign/<domínio>/*` — células edit-in-place e cascas compartilhadas (`CampaignCellEditOverlay`, `CampaignListSheetProvider`, colunas por papel), triggers de autosave por lista, FAB/ações rápidas, botões "Registrar atualização" em municípios/atividades, controles de status em demandas; predicados client-safe de perfil em `src/lib/campaignRoles.ts` (ou o helper do item irmão); dashboards/mapa (`MunicipalityMapPanel` e loaders escopados).
|
||||
- **Precedente a olhar:** estados `readOnly` já existentes (`RelationChipCell`/`RelationOptionCell` com `readOnly`), a matriz por papel da célula de pessoas (C116: assessor edita só onde a carteira alcança, resto read-only), card mobile de municípios (B193).
|
||||
- **Risco de acoplamento:** o mapa e as listas já leem "escopo do ator" — cuidado para não duplicar lógica de escopo no cliente; o fragmento de escopo continua vivendo uma vez só (convenção P3-D).
|
||||
|
||||
## Dependências
|
||||
|
||||
- **C141** (modelo + enforcement + configuração) — este item só faz sentido depois do perfil existir e valer no servidor.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Qualquer mudança no modelo de permissão ou no enforcement (é o item irmão).
|
||||
- Redesenho de superfícies não relacionadas a permissão.
|
||||
- Estado de leitura para liderança (lockdown já é outra superfície, intocada).
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Vou esconder tudo com um flag e rezar."** A passada precisa achar **cada** caminho de escrita real (autosave, sheets, wizards, links para formulários). **Corte:** varrer por superfície com a matriz de papéis, como a célula de pessoas já faz; o que não for achado agora aparece no aceite do item.
|
||||
- **"Vou redesenhar a lista de municípios."** Tocar layout por tocar não é o pedido. **Corte:** manter layouts; mudar só presença/ausência de controles e o aviso de modo leitura.
|
||||
- **"Cinza-fantasma em tudo."** Desabilitar tudo visualmente polui mais do que ajuda. **Corte:** controles de edição **ocultos** para leitura; aviso discreto no topo.
|
||||
|
||||
## Decisões do gate (2026-08-19)
|
||||
|
||||
- **Modo leitura sem aviso** (opção B) — controles ocultos; presença/ausência de edição é a única linguagem.
|
||||
- **Atalhos de escrita seguem a regra** (opção A) — destino de escrita some para quem não pode escrever.
|
||||
|
||||
## Referências
|
||||
|
||||
- Item irmão: `docs/plans/permissao-granular-assessores.md` (C141) e `docs/plans/demandas-responsaveis.md` (C143 — a regra de demandas muda o que a demanda renderiza para quem não é responsável).
|
||||
- Rascunho UI (gate): `docs/plans/ui-respeita-perfil-permissao-ui-draft.html` + PNGs acima.
|
||||
- Para abrir primeiro: `src/components/campaign/shared/CampaignCellEditOverlay.tsx` (casca dos overlays de edição), `src/lib/campaignRoles.ts` (predicados por papel), a matriz por papel em `pessoas-*` (C116).
|
||||
- `AGENTS.md` — convenção P3-D do escopo de assessor.
|
||||