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:
commit
bbe93da4ad
4 changed files with 96 additions and 17 deletions
|
|
@ -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).
|
||||
|
|
|
|||
83
docs/plans/agenda-remover-aviso-vazio-impl.md
Normal file
83
docs/plans/agenda-remover-aviso-vazio-impl.md
Normal 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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue