Merge pull request #429 from fsolla/plans/c90-c91-responsaveis-agenda

docs(plans): C90 — remodelar responsáveis; C91 — criar evento inline na agenda
This commit is contained in:
Francisco Solla 2026-08-08 14:00:47 -03:00 committed by GitHub
commit 728f92fe60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 203 additions and 0 deletions

View file

@ -0,0 +1,113 @@
# Criar evento inline no calendário da agenda (estilo Google Calendar)
Status: rascunho
Atualizado em: 2026-08-08
Issue: #428
Priority: P1
Model: composer-2.5
Model-local: deepseek-v4-flash-high
Impeccable: C — fluxo novo de criação inline sobre uma tela existente (`/campanha/agenda`)
Canvas UI: /home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c91-ui-draft.canvas.tsx
Appetite: ~1–1,5 dia eng; um outcome verificável — staff cria compromisso no slot certo sem sair da agenda
Responsável: —
## Dependência rápida
- **Dura: [C90 — remodelar `responsible` da Atividade](remodelar-responsaveis-atividade.md) (#426)** (campo único polimórfico multi-valor; remove `advisors`/`leadership`). O seletor de responsáveis do overlay **consome o campo remodelado**; este item não decide schema.
## Intenção
A agenda (C15) já mostra a semana e o mês, mas criar exige **sair** para `/campanha/atividades/nova` (o clique no slot só pré-preenche a URL). Quebra o ritmo de planejamento na mesa: “onde eu estava” se perde. Queremos criar **no lugar clicado**, como no Google Calendar — um popover com dia e horários já preenchidos do slot, editáveis, e o evento surgindo no calendário assim que salvar, sem reload. A mesa-alvo já vive no Google Calendar; a promessa é “mesma letra, no Teqo”.
A criação rápida também leva o **seletor de responsáveis** do fluxo completo para o overlay (popover/drawer): o responsável de um compromisso pode ser **candidato**, **assessor** (staff `campaignUser`), **liderança** ou **dobradinha** — não só um Contact avulso. Na mesa, o coordenador decide o compromisso e quem o conduz no mesmo gesto, sem abrir o formulário grande só para isso.
## Persona e fluxo
- **Persona / contexto:** coordenador (ou assessor no recorte) planejando a semana do candidato, desktop e mobile, no meio de uma conversa — quer lançar compromissos rápido e seguir.
- **Job principal:** criar um compromisso no dia/horário certo da agenda **sem sair dela**.
- **Fluxo desejado:**
1. Abre `/campanha/agenda`, aplica filtros se quiser.
2. Clica num slot vazio (semana ou mês) no desktop → popover abre **naquele ponto**, com título vazio e **dia/horário já preenchidos do slot** (editáveis).
3. Mobile → em vez de popover, um **bottom sheet** sobe com o mesmo conteúdo.
4. Digita o mínimo obrigatório (título + município) e escolhe o **responsável** (candidato, assessor, liderança ou dobradinha), salva → o evento **aparece no calendário na hora**, sem reload.
5. Precisa de mais campos? “Mais detalhes” leva ao formulário completo pré-preenchido.
- **Anti-goals de produto:** o popover **não** vira o formulário completo (tarefas, organizações, demandas, liderança vinculada e assessores responsáveis ficam no detalhe); salvar **não** navega (voltar à tela quebra o fluxo); **não** é mode de edição em massa; o seletor de responsável **não** cadastra pessoa nova — só escolhe entre entidades já existentes da campanha (candidato/assessor/liderança/dobradinha).
### Esboço de fluxo (C)
```text
[/campanha/agenda + filtros]
→ clique em slot vazio
→ [desktop] popover ancorado ao ponto / [mobile] bottom sheet
→ título + horário (pré-preenchido do slot, editável) + município + local
→ seletor de responsável (candidato · assessor · liderança · dobradinha)
→ Salvar → evento insere no calendário (sem reload)
→ “Mais detalhes” → /nova pré-preenchida (form completo)
```
## Objetivo e aceite
- Clicar num slot vazio da agenda abre criação **inline** (popover no desktop, bottom sheet no mobile) em vez de navegar para `/campanha/atividades/nova`.
- O popover/sheet abre com **início e fim pré-preenchidos do slot clicado** (ex.: o intervalo do clique; clique no dia cheio “vira” 09:00–10:00, como o comportamento atual de `dateClick`), e os horários são **editáveis**.
- Campos mínimos para salvar: **título** e **município** (obrigatórios do modelo) — local e **responsável** opcionais no overlay.
- O overlay traz um **seletor de responsáveis** que escolhe **um ou mais** entre **candidato/assessor/coordenador (staff `campaignUser`), liderança (`leadership`) ou dobradinha (`stateDeputy`)** — entidades da campanha já existentes, buscadas por nome (e celular quando fizer sentido), com rótulo indicando o tipo (ex.: “Maria (Assessora)”). Usa o campo **`responsible` polimórfico multi-valor** do remodel (C90).
- Após salvar, o evento **aparece imediatamente** na agenda sem recarregar a página; falha → toast de erro e nada some.
- “Mais detalhes” leva ao formulário completo (`/atividades/nova`) já com o que foi digitado, sem perder o que já foi feito.
- Reusa componentes shadcn já existentes (popover, drawer/sheet, inputs) — não inventar primitiva paralela.
- Liderança não acessa a agenda (inalterado).
## Dados (intenção)
- **Vou apresentar dados?** Não. `Dados: N/A` — affordance de escrita sobre a agenda; não introduz métrica, série, ranking ou mapa.
- **Decisões desbloqueadas:** ator decide “que compromisso entra neste slot” **no momento do planejamento**, sem fila de navegação.
## Direção no codebase (hipótese)
- **Áreas prováveis:**
- `src/components/campaign/activity/ActivityAgenda.tsx` — `handleDateClick` hoje só monta `buildActivityCreateHref` e redireciona; vira abrir o popover/sheet com prefill.
- `src/utilities/activityUi.ts` — `buildActivityCreateHref` / `parseActivityCreatePrefill`: o prefill do slot já existe; a criação inline precisa de um caminho que **não navegue**.
- `src/app/(campaign)/campanha/actions/activity.ts` — `createActivity` já é uma action; criar inline via action que retorna o evento (sem `redirect`), e o calendário insere no estado local (mesma família do `rescheduleActivity`).
- `src/app/(campaign)/campanha/(app)/atividades/nova/page.tsx` + `ActivityForm.tsx` — forma atual (referência de campos/validação), reutilizada só no “Mais detalhes”.
- **Responsável:** hoje `responsible` é um `Contact` único (via `ContactCombobox`); `advisors` (hasMany `campaignUser`) e `leadership` (única) são campos separados. O seletor unificado do overlay precisa agregar opções de `leadership` + `stateDeputy` + staff `campaignUser` — precedentes: `searchActivityLeadershipOptions` (`activityLeadershipOptions`), `loadStateDeputyOptions` (`campaignRelationOptions`), `searchActivityContactOptions` (`contactSearchActions`). A direção de schema (`responsible` único vs lista) é questão em aberto de produto — não travada aqui (ver Rabbit holes / Questões).
- **Precedente a olhar:** `ActivityForm.tsx` (campos/validação), `rescheduleActivity`/`loadActivityAgendaEvents` (estado local do calendário), bottom drawer já usado em listas mobile (`CampaignListSheetHost` → `Drawer`), `criar-assessor-inline-popover-municipios` (B154 — criação inline em popover como precedente de pattern).
- **Risco de acoplamento:** estado local do FullCalendar (`events`/`reloadCount` em `ActivityAgenda`) — a criação inline deve seguir o mesmo padrão de otimismo/atualização do reschedule ou a célula pode dessincronizar; não quebrar o fluxo `redirect` do formulário completo.
## Dependências
- **Dura: C90 — remodelar `responsible` da Atividade ([#426](https://github.com/fsolla/teqo/issues/426))** (campo único polimórfico multi-valor; remove `advisors`/`leadership`). O seletor de responsáveis do overlay **consome o campo remodelado**.
- **Suaves:** `loadActivityAgendaEvents` (refetch da janela) — se a criação inline só insere localmente, revalidar a janela ao fechar.
## Fora de escopo
- Campos avançados inline (tarefas, organizações, demandas, liderança vinculada, assessores responsáveis) → formulário completo `/atividades/nova`.
- **Seletor de responsável não cadastra entidade nova** — só combina entidades já existentes (staff/candidato/liderança/dobradinha). Criar um `campaignUser` ou `leadership` na hora (padrão B154) **não** entra aqui, a menos que o produto peça explicitamente.
- Edição inline de evento existente (abrir continua indo ao detalhe).
- Recorrência, resources, sync Google (C16 já cobre import/sync).
- Mudanças no formulário completo de `/atividades/nova`.
## Rabbit holes de produto
- **“Vira o formulário inteiro no popover.”** Se alguém “só completar” campos, o popover vira uma segunda página de criação. **Corte neste item:** mínimo (título + município + horário + local) e um link “Mais detalhes”.
- **Responsável = vários campos.** O remodel (C90) unifica tudo num `responsible` polimórfico multi-valor; este item **não** soma campos — **Corte:** o overlay usa o campo remodelado (vários responsáveis de qualquer papel) e pronto. Se a mesa pedir algo além disso, vira novo item, não estica o popover.
- **“Criar a pessoa se não existir.”** B154 criou assessor inline na lista; aqui não. **Corte:** o seletor só escolhe dentre o que já existe; mensagem clara quando a busca não acha.
- **“Salvar e voltar pro detalhe.”** Navegar após salvar quebra o fluxo. **Corte neste item:** salvar fecha o overlay, evento aparece, usuário segue na agenda.
- **“Mostrar também nas células do mês.”** Mês é sprint de visão; criação no slot é o job. **Corte:** funcionar na semana e dia; no mês, clique no dia abre o popover com 09:00–10:00 (comportamento atual).
## Questões em aberto (produto)
- **Intervalo pré-preenchido:** **Opções:** A) usar o próprio intervalo do clique (meia-hora tipada no slot da semana) | B) fixo 1h a partir do horário do clique | C) 1h como hoje. **Recomendação: A** — é o que mais replica o Google Calendar; onde o intervalo do clique não dá (dia cheio do mês), usamos o comportamento atual (09:00–10:00). _(assumido — validar)_
- **“Mais detalhes” preserva o rascunho?** **Opções:** A) prefere popular o form completo via URL (já existe) | B) mantém o texto do popover no `defaultValue`. **Recomendação: A** + preencher `title` no link (novo parâmetro). _(assumido — validar)_
- **Após salvar, refetch ou inserção local?** **Opções (produto):** o evento deve **aparecer já** (inserção otimista/local) | ou **refetch da janela** (mais caro, mas “correto” com filtros). **Recomendação: refetch da janela** no fechamento — é o mesmo contrato dos filtros e evita estado duplicado; a sensação de “sem reload” vem de não navegar. _(decidir — se parecer lento no app, volta para inserção local)_
- **Validação de município no popover:** **Opções:** A) combobox com opções do escopo do ator (já carregadas na página) | B) select nativo | C) em branco e erra ao salvar. **Recomendação: A** — combobox leve, com o município do filtro ativo como opção inicial se houver filtro. _(assumido — validar)_
- **Seletor de responsáveis — decisão tomada no gate:** campo **`responsible` polimórfico multi-valor** (staff `campaignUser` nas funções assessor/candidato/coordenador, `leadership`, `stateDeputy`) — **remodel em C90**. Este item (C91) **consome** o campo remodelado no overlay; não decide schema. _Removido como questão em aberto — travada._
- **Responsável obrigatório ou opcional no overlay?** **Opções:** A) opcional (como o form completo hoje) | B) obrigatório para criar rápido. **Recomendação: A** — obrigatório só `title` e `municipality`; responsável é preenchido na primeira chance, sem travar a criação. _(assumido — validar)_
## Referências
- GitHub Issue: [#428](https://github.com/fsolla/teqo/issues/428)
- Canvas UI (gate): `/home/fsolla/.cursor/projects/home-fsolla-Code-teqo/canvases/plan-c91-ui-draft.canvas.tsx`
- `src/components/campaign/activity/ActivityAgenda.tsx` — `handleDateClick` (prefill do slot + redirect atual)
- `src/utilities/activityUi.ts` — `buildActivityCreateHref`, `parseActivityCreatePrefill`
- `src/app/(campaign)/campanha/actions/activity.ts` — `createActivity`, `rescheduleActivity`, `loadActivityAgendaEvents`
- `src/components/campaign/activity/ActivityForm.tsx` — campos e validação do form completo
- `docs/plans/criar-assessor-inline-popover-municipios.md` — precedente de criação inline em popover (B154)

View file

@ -0,0 +1,90 @@
# Remodelar responsáveis da Atividade — campo único polimórfico multi-valor
Status: rascunho
Atualizado em: 2026-08-08
Issue: #426
Priority: P1
Model: cursor-grok-4.5-medium
Model-local: deepseek-v4-high
Impeccable: B — muda o card "Pessoas e organizações" do form completo + detalhe (campos somem/somam)
Appetite: ~1–1,5 dia eng; um outcome verificável — um compromisso pode ter vários responsáveis de papéis distintos
Responsável: —
## Intenção
Hoje um compromisso tem **três campos separados**: `responsible` (um Contact avulso), `advisors` (assessores/campaignUser, vários) e `leadership` (uma liderança). Na mesa, o coordenador pensa em **“quem é responsável por este compromisso”** — e isso pode ser o candidato, um assessor, uma liderança ou uma dobradinha, **um ou vários ao mesmo tempo**. Três campos forçam a mesa a decidir arbitrariamente onde cada pessoa entra, e o candidato/coordenador não tem onde entrar (não são Contact nem cabem em assessor/liderança). Unificar num **campo único `responsible`, polimórfico (staff `campaignUser` nas funções assessor/candidato/coordenador, `leadership`, `stateDeputy`) e de múltiplos valores**, removendo `advisors` e `leadership`.
## Persona e fluxo
- **Persona / contexto:** coordenador (ou assessor com acesso) montando eventos na agenda ou no form completo; precisa registrar quem conduz, sem enquadrar pessoas erradas.
- **Job principal:** indicar **um ou mais responsáveis** de qualquer papel da campanha num único seletor.
- **Fluxo desejado:** form completo (ou o overlay da criação rápida na agenda) → seletor "Responsáveis" → escolhe vários (candidato, assessor, liderança, dobradinha) → salvar → detalhe mostra todos com seus tipos.
- **Anti-goals de produto:** não é segundo cadastro de pessoa (só escolhe entidades existentes); não é "responsável por município" já coberto por outra entidade; não introduz papel novo nem altera quem pode entrar.
### Esboço de fluxo (B)
```text
[form completo de atividade] → "Responsáveis" → busca por nome → marca 1+ de
qualquer papel (candidato/assessor/liderança/dobradinha) → salvar → detalhe lista
```
## Objetivo e aceite
- A Atividade passa a ter **um campo `responsible`** que aceita **vários** valores e pode referenciar **pessoas de qualquer papel**: assessor, candidato ou coordenador (staff `campaignUser`), liderança (`leadership`), dobradinha (`stateDeputy`).
- Os campos `advisors` e `leadership` **saem do modelo e da UI** (form completo, detalhe, agenda); a informação que morava neles migra para `responsible`.
- Assessor continua podendo **ver/editar** os compromissos em que está listado como responsável (mesmo direito que `advisors` lhe dava, agora via `responsible`).
- Acesso de criação/edição segue as regras atuais (staff cria; assessor edita o que administra + o que lhe é imputado).
- `responsible` opcional no form completo (como hoje); obrigatório só título/município.
- Não cadastra entidade nova no seletor (sem padrão B154 aqui).
## Dados (intenção)
- **Vou apresentar dados?** Não. `Dados: N/A` — remodel de escrita/relacionamento; não introduz métrica, série, ranking nem mapa.
- **Decisões desbloqueadas:** coordenador define quem conduz o compromisso **sem enquadrar papel em campo errado**, com múltiplos responsáveis quando fizer sentido.
## Direção no codebase (hipótese)
- **Áreas prováveis:**
- `src/collections/Activity.ts` — campo `responsible` passa a relationship **polimórfica e hasMany** (`relationTo` staff/leadership/stateDeputy); remove `advisors` e `leadership`; hooks `validateActivityAdvisors`, `deriveActivityFields` (auto-inclusão de assessor), `validateDeputyPresentTimeGate` re-expressos sobre `responsible`.
- `src/utilities/access/activities.ts` — `canReadActivity` usa `advisors contains id` como escopo do assessor; passa a usar `responsible` (player id) + escopo de município.
- `src/utilities/activityViewModels.ts`, `src/utilities/activityFormData.ts`, `src/app/(campaign)/campanha/actions/activity.ts` — tipos de input/view e parse do formulário acompanham a mudança.
- `src/components/campaign/activity/ActivityForm.tsx` — card "Pessoas e organizações": um seletor multi-polimórfico `Responsáveis`; some o grupo de assessores e o combobox de liderança.
- `src/app/(campaign)/campanha/(app)/atividades/[slug]/ActivityOverviewTab.tsx` — detalhe lista os responsáveis com tipo.
- Busca: agregar opções de `leadership` + `stateDeputy` + staff `campaignUser` (precedentes: `searchActivityLeadershipOptions`, `loadStateDeputyOptions`, `searchActivityContactOptions`).
- **Precedente a olhar:** `criar-assessor-inline-popover` (B154), `remodelar-atividades-para-agenda` (C14), e2e `campaignActivity.e2e.spec.ts`.
- **Risco de acoplamento:** mudança de **schema** (migration) + **access** do assessor + **form completo** + **detalhe** + **e2e** — item serializado, sem paralelismo de agents; é pré-requisito do item "criar evento inline na agenda" (que consome o novo seletor).
## Dependências
- **Nenhuma dura** para si. Desbloqueia o item "Criar evento inline na agenda" (que depende deste).
- Chess com qualquer outra change que toque `Activity`/migrations (serialização).
## Fora de escopo
- Criar entidade nova no seletor (B154 fica para outro item, se o produto pedir).
- Mudar o acesso/regras do `deputyPresent` time gate (só re-expressar em `responsible`).
- Tarefas (`tasks[].responsible`) mantêm o próprio `responsible` de Contact — não coberto aqui.
- Rewrite do form completo além do card "Pessoas e organizações".
## Rabbit holes de produto
- **“Vira segundo cadastro de pessoa.”** Se a busca não achar, a tentação é criar inline (B154). **Corte:** só escolher entre existentes; sem criação no seletor.
- **“Polimorfismo é problema só de schema; produto já decidiu.”** Sim — produto decidiu (C). Este plano só garante que o **acesso do assessor** não quebre e que o **detalhe** mostre tipos.
- **“Vários de tudo.”** Multi-valor não significa "misturar assessores de outros municípios fora do escopo". **Corte:** a escolha respeta o escopo de leitura do ator (não busca quem o ator não pode ver).
## Questões em aberto (produto)
- **Exibição no detalhe/card da agenda:** **Opções:** A) lista "N resp do tipo X" colapsada com tooltip | B) chips individuais por responsável | C) contar só "Resp: N". **Recomendação: B** — chips com tipo, mesmo padrão do card de hoje ("Resp: nome") mas agora pode ser vários. _(assumido — validar)_
- **Ordem/relação com `tasks[].responsible`:** campo de tarefa (Contact) fica como está? **Recomendação: sim** — tarefa tem responsável próprio de execução; não acoplar. _(assumido — validar)_
## Decisões travadas (gate 2026-08-08)
- **(C)** Campo `responsible` **polimórfico multi-valor**: staff `campaignUser` (roles assessor/candidato/coordenador), `leadership`, `stateDeputy`.
- **Remover** `advisors` e `leadership` do modelo/form/detalhe; informação migra para `responsible`.
- Acesso do assessor via `responsible` (mantém o direito que `advisors` dava).
## Referências
- GitHub Issue: [#426](https://github.com/fsolla/teqo/issues/426)
- `src/collections/Activity.ts`, `src/utilities/access/activities.ts`, `src/components/campaign/activity/ActivityForm.tsx`
- Deps: item "Criar evento inline na agenda" ([#428](https://github.com/fsolla/teqo/issues/428), C91) consome este seletor.