Merge pull request #485 from fsolla/B181-agenda-remover-o-aviso-nenhum-compromisso-nesta-janela-

B181 — Agenda: remover o aviso "Nenhum compromisso nesta janela e neste filtro"
This commit is contained in:
Francisco Solla 2026-08-09 09:55:00 -03:00 committed by GitHub
commit bbe93da4ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 96 additions and 17 deletions

View file

@ -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).

View file

@ -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 `<Link>` 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

View file

@ -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 (
<section className="activity-agenda-shell" aria-label="Calendário de atividades">
<div className="sr-only" aria-live="polite">
@ -303,15 +296,6 @@ export const ActivityAgenda = ({
</div>
) : null}
{!isLoading && !loadError && events.length === 0 ? (
<div className="activity-agenda-notice" role="status">
<span>Nenhum compromisso nesta janela e neste filtro.</span>
<Button asChild variant="outline" className="min-h-11">
<Link href={emptyHref}>Criar atividade</Link>
</Button>
</div>
) : null}
<div className="activity-agenda" aria-busy={isLoading || savingCount > 0} ref={containerRef}>
<FullCalendar
ref={calendarRef}

View file

@ -149,4 +149,14 @@ describe('ActivityAgenda schedule failures', () => {
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(<ActivityAgenda state={{}} />)
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()
})
})