From d5d1d0c30e026d8cb6bb066739e7ad28ed18c4a9 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sun, 9 Aug 2026 09:34:25 -0300 Subject: [PATCH] chore(B181): remove o aviso de janela vazia da agenda MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sem bloco 'Nenhum compromisso nesta janela e neste filtro' nem botão 'Criar atividade' acima do calendário: a criação segue pelo slot click (C91), botão dos filtros e FAB. buildActivityCreateHref permanece com 2 consumidores. Teste unitário de regressão + impl plan + changelog. --- docs/CHANGELOG-AGENTS.md | 2 + docs/plans/agenda-remover-aviso-vazio-impl.md | 83 +++++++++++++++++++ .../campaign/activity/ActivityAgenda.tsx | 18 +--- .../activityAgendaInteractions.unit.spec.tsx | 10 +++ 4 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 docs/plans/agenda-remover-aviso-vazio-impl.md diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 006a7d0e..2a5f61ba 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-09):** **B181** (Issue #480) — a `/campanha/agenda` não exibe mais o bloco "Nenhum compromisso nesta janela e neste filtro" + botão "Criar atividade" acima do calendário em janela vazia (só ocupava espaço, redundante com slot-click C91, botão "Nova atividade" dos filtros e FAB C94). Remoção cirúrgica em `ActivityAgenda.tsx` (bloco condicional + órfãos `emptyHref`/imports `Link` e `buildActivityCreateHref`); `buildActivityCreateHref` permanece com 2 consumidores ("Mais detalhes" no inline create, "Nova atividade" nos filtros); loading/erro e `noEventsText` do FullCalendar (vista "Lista") intactos. Teste unitário de regressão: janela vazia não renderiza aviso nem link de criação. Sem migration, access, Consent ou schema. Intenção [`agenda-remover-aviso-vazio.md`](plans/agenda-remover-aviso-vazio.md); impl [`agenda-remover-aviso-vazio-impl.md`](plans/agenda-remover-aviso-vazio-impl.md). + **Recently resolved (2026-08-09):** **C93** (Issue #437) — botão "Link de import" da agenda destravado **sem filtros** (sucessor direto do C16): com zero filtros ativos, staff gera o link do feed **completo dentro do escopo de leitura do criador** — coordinator/candidate = tudo; advisor = portfólio dos municípios que administra — com fail-closed/revogação de C16 intactos. O servidor já cobria o caso (vazio = escopo do criador em `buildFeedWhere`/`resolveFeedCreatorAccess`; descrição do campo `filterMunicipality`); a entrega removeu o gate `hasFilters → disabled` de `CalendarFeedButton` (prop morto `state` incluído), manteve `hasFilters` só no "Limpar filtros" e neutralizou a copy do diálogo ("este recorte" → "a agenda"). Testes: 3 int de conteúdo sem filtro (coordinator = A+B; advisor = só o portfólio, nada fora vaza; advisor sem município → feed vazio fail-closed) + unit component (botão habilitado com zero filtros e fluxo nomear→criar). Sem migration, access, Consent ou schema. Intenção [`c93-gerar-link-import-sem-filtros.md`](plans/c93-gerar-link-import-sem-filtros.md); impl [`c93-gerar-link-import-sem-filtros-impl.md`](plans/c93-gerar-link-import-sem-filtros-impl.md). **Recently resolved (2026-08-09):** **B176** (Issue #458) — `/campanha/municipios` ganhou três dimensões de filtro na omnibox — **Dobradinha** (nome do contato), **Liderança** (nome do contato) e **Partido** (sigla) — com recortes "Sem dobradinha"/"Sem liderança"/"Sem partido" (precedente "Sem nível"). Params aditivos `stateDeputy`/`leadership`/`party` na URL congelada (sentinelas `sem_dobradinha`/`sem_lideranca`, reuso de `sem_partido`); `buildMunicipalityListWhere` ganhou um catálogo request-scoped fail-closed (`municipalityRelationSets.ts`, lazy: metade liderança só com filtro ativo) que vira as leituras reversa (`leadership.municipalities`) e cruzada (`stateDeputy.party`) em `where` — sem migration, paginação nativa mantida; facets estendidas (`stateDeputyIDs`/`leadershipOptions`/`parties`) e mapa compartilha o catálogo. **Coluna Dobradinhas liberada ao Assessor** (staff-wide): servidor já estava scoped (B37/B157 `reloadStaffActor` + `canUpdateMunicipality`), a mudança foi só de gate de UI (lista/mobile/picker) + catálogo full-staff para a célula (decisão do gate humano 2026-08-09). Extrato do par "sentinela + `exists:false`" em `campaignListUrl` (`NO_PARTY_FILTER_VALUE`, `splitAbsenceFilterValues`, `collapseListWhereOrBranches`) com 5 call sites. E2E: assessor vê/edita a coluna no portfólio; filtros pelas três dimensões; teste de coluna-restrita invertido. Intenção [`filtros-municipios-dobradinha-lideranca.md`](plans/filtros-municipios-dobradinha-lideranca.md); impl [`filtros-municipios-dobradinha-lideranca-impl.md`](plans/filtros-municipios-dobradinha-lideranca-impl.md). diff --git a/docs/plans/agenda-remover-aviso-vazio-impl.md b/docs/plans/agenda-remover-aviso-vazio-impl.md new file mode 100644 index 00000000..0514533b --- /dev/null +++ b/docs/plans/agenda-remover-aviso-vazio-impl.md @@ -0,0 +1,83 @@ +# Impl: B181 — Agenda: remover o aviso "Nenhum compromisso nesta janela e neste filtro" + +Status: aprovado +Atualizado em: 2026-08-09 +Issue: #480 +Intenção: docs/plans/agenda-remover-aviso-vazio.md +Appetite restante: herdado (~0,5 dia eng; item de ~20 min de diff) + +## Leitura da intenção + +- **Outcome:** janela sem compromissos na `/campanha/agenda` não exibe mais o bloco + "Nenhum compromisso nesta janela e neste filtro" + botão "Criar atividade", em nenhuma + largura de tela; o calendário começa no topo da área; nada novo entra na tela. +- **O que NÃO negociar:** não substituir o aviso por outro empty-state; não mudar affordances + de criação (slot click C91, FAB C94, botão dos filtros); loading e erro permanecem; texto + interno do FullCalendar (`noEventsText`, vista "Lista") fica (questão em aberto A). +- **O que reavaliar:** a intenção previa que `buildActivityCreateHref` pudesse ficar órfão — + **não fica**: `ActivityInlineCreate.tsx:111` ("Mais detalhes") e `ActivityAgendaFilters.tsx:144` + (botão "Nova atividade" desktop) seguem usando. Só o uso local em `ActivityAgenda` sai. + +## Abordagem recomendada + +Remoção cirúrgica de um bloco JSX + seus órfãos locais no único componente que o renderiza. +Sem mudança em `activityUi.ts`, sem migration, sem mudança de acesso. + +**Opções consideradas:** A | B +**Recomendação:** A — remover só o bloco no `ActivityAgenda.tsx` e os imports locais que +ficam órfãos. É o menor diff que mantém o aceite; os caminhos de criação permanecem todos. +**Rejeitadas:** B — remover também `noEventsText` do FullCalendar (vista "Lista"). A intenção +recomenda manter (opção A da questão em aberto): é contexto in-loco da lista vazia, não um +header que rouba espaço no mobile, e sair dela expandiria o escopo deste chore sem pedido. + +### Componentes / mudanças + +- **`ActivityAgenda`** (`src/components/campaign/activity/ActivityAgenda.tsx`): + - remover o bloco `!isLoading && !loadError && events.length === 0` (linhas 306–313); + - remover `const emptyHref = buildActivityCreateHref(state)` (linha 277) — órfão; + - remover `import Link from 'next/link'` (linha 21) — único uso era o `` do bloco; + - remover `buildActivityCreateHref` do import de `@/utilities/activityUi` (linha 42); + - manter `Button` (usado no bloco de erro, linha 295), `activity-agenda-notice` CSS + (loading 287 + erro 293) e `noEventsText="Nenhum compromisso nesta janela"` (343). +- **`activityUi.ts`** (`src/utilities/activityUi.ts`): **sem mudança** — + `buildActivityCreateHref` continua com 2 consumidores. +- **`ActivityAgendaFilters.tsx` / `ActivityInlineCreate.tsx`**: sem mudança — os botões de + criação que o aceite exige manter. +- **Migration:** sem migration (nenhuma coleção/global/campo tocado). +- **Access / Consent:** sem mudança. +- **UI:** Impeccable A — remoção de elemento; nenhum shape/craft necessário. A classe + `.activity-agenda-notice` permanece no CSS porque loading/erro continuam usando-a. + +## Fases verificáveis + +1. **Diff** — edição do `ActivityAgenda.tsx` (remoção do bloco + 3 órfãos de import). +2. **Teste de regressão** — novo `it` em `tests/unit/activityAgendaInteractions.unit.spec.tsx`: + janela vazia (`loadEvents` → `[]`) não renderiza mais "Nenhum compromisso nesta janela e + neste filtro" nem botão "Criar atividade", e o slot-click (C91) segue abrindo o inline + create. Os testes existentes já renderizam com `[]`, então nenhum outro muda. +3. **Gates** — `pnpm gate:fast` (inclui tsc, lint, knip, ciclos, testes unit+int); + `pnpm push` para fechar (inclui gates completos; knip valida que não sobrou órfão). + +## Rabbit holes / Não escopo (engenharia) + +- Não mexer no `noEventsText` (vista "Lista") — fora de escopo, questão em aberto A. +- Não "melhorar" o topo da agenda nem o empty-state da grade (`dayGrid`/`timeGrid` vazios + mostram a grade vazia do FullCalendar — comportamento desejado, é o "calendário no topo"). +- Não tocar em outros empty-states do `/campanha`. +- Não extrair nada para helper novo — remover é o oposto de abstrair. + +## Riscos e mitigação + +- **Órfãos de import/knip:** removidos no mesmo edit (`Link`, `buildActivityCreateHref`); + `pnpm gate:fast`/knip confirma. +- **Regressão de affordance de criação:** nenhum botão de criação é removido — só o do + aviso, que some junto com ele; filtros (desktop) e inline create (C91) intactos, ambos + cobertos por testes existentes (unit + e2e). +- **Estado "janela vazia" no teste unitário:** o mock do FullCalendar já exercita `[]`; + o novo `it` usa o mesmo mock sem mudança. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto (janela vazia sem aviso; nada novo na tela) +- [ ] Invariantes AGENTS/engineering-standards (nenhum access/Consent/transação tocado) +- [ ] Testes de domínio previstos (unit) onde o comportamento visual muda diff --git a/src/components/campaign/activity/ActivityAgenda.tsx b/src/components/campaign/activity/ActivityAgenda.tsx index 43b1f5db..9b32d839 100644 --- a/src/components/campaign/activity/ActivityAgenda.tsx +++ b/src/components/campaign/activity/ActivityAgenda.tsx @@ -18,7 +18,6 @@ import themePlugin from '@fullcalendar/react/themes/classic' import '@fullcalendar/react/themes/classic/theme.css' import timeGridPlugin from '@fullcalendar/react/timegrid' import { UserRoundCheckIcon } from 'lucide-react' -import Link from 'next/link' import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' @@ -37,11 +36,7 @@ import { activityStatusLabels, type ActivityStatus, } from '@/lib/schemas/activity' -import { - activitySlotPrefill, - buildActivityCreateHref, - type ActivityAgendaState, -} from '@/utilities/activityUi' +import { activitySlotPrefill, type ActivityAgendaState } from '@/utilities/activityUi' import type { ActivityAgendaEvent } from '@/utilities/activityViewModels' import './ActivityAgenda.css' @@ -274,8 +269,6 @@ export const ActivityAgenda = ({ setCreateDraft({ ...prefill, anchor }) }, []) - const emptyHref = buildActivityCreateHref(state) - return (
@@ -303,15 +296,6 @@ export const ActivityAgenda = ({
) : null} - {!isLoading && !loadError && events.length === 0 ? ( -
- Nenhum compromisso nesta janela e neste filtro. - -
- ) : null} -
0} ref={containerRef}> { expect((screen.getByLabelText('Término') as HTMLInputElement).value).toBe('2026-08-07T13:30') expect(mocks.routerPush).not.toHaveBeenCalled() }) + + it('não exibe o aviso de janela vazia nem o botão de criação acima do calendário', async () => { + mocks.loadEvents.mockResolvedValue([]) + render() + + await waitFor(() => expect(mocks.loadEvents).toHaveBeenCalled()) + + expect(screen.queryByText('Nenhum compromisso nesta janela e neste filtro.')).toBeNull() + expect(screen.queryByRole('link', { name: 'Criar atividade' })).toBeNull() + }) })