diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index ddc81eb8..8679bb92 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-11):** **C124** (Issue #668) — tarefa de atividade fica só com **texto, responsável e concluída**: o campo `due` (Prazo, datetime) sai do modelo e da UI. Remoção em cadeia num único commit simétrico: campo do array `tasks` da `activity` (o guardrail de liderança perde a comparação de `due` no mesmo item — continua fail-closed sobre título/responsável/contagem), `taskSchema` zod, `parseTasksFormData`/`ParsedActivityTask` (formData), `ActivityFormTaskViewModel`/`ActivityTaskViewModel`, input `datetime-local` do `ActivityTaskFields` (grid `2fr_1fr`) e linha "Prazo: …" do `ActivityTaskChecklist` (subtítulo condicional + `break-words` no título — P2 dos reviewers). `doneAt` ("concluída em") permanece como derivado invisível; `taskTotal`/`taskDoneCount` intocados. **Migration** `20260811_183016_drop_activity_task_due` (gerada; diff do gerador tinha no-op de `google_calendar_sync` por snapshot stale — limpa à mão para só o `DROP COLUMN "due"` de `activity_tasks`; dados descartados conforme decidido no gate). Grep pós: zero `due` fora das migrations (histórico frozen); nenhum consumidor fora da vertical (notificações, Google sync, quick actions). Gates: unit+int 736/736, e2e atividade 9/9 (1 flake ECONNRESET no run paralelo — rerun limpo), build OK. Intenção [`atividades-tarefas-sem-prazo.md`](plans/atividades-tarefas-sem-prazo.md); impl [`atividades-tarefas-sem-prazo-impl.md`](plans/atividades-tarefas-sem-prazo-impl.md). + **Recently resolved (2026-08-11):** **B197+** (Issue #642) — o menu do picker de colunas de assessores nasce da **mesma fonte** que os `` do `AdvisorsTable` (fim do espelho): o módulo client declara `advisorTableColumns` (id/label/mandatory + `headerClassName` + `visuallyHiddenLabel` para o `sr-only` de Ações) e exporta `advisorPickerColumns = toCampaignColumnPickerColumns(...)` (precedente `supporterPickerColumns`); o header renderiza por `resolveVisibleColumns(advisorTableColumns, hiddenColumnIds)` — mesmo helper B17 das outras listas — e `emailHidden` deriva do mesmo `visibleColumns`. **Descoberta do /simplify incorporada:** o corpo só condiciona a coluna `email` (células à mão + exceção do draft), então `phone`/`municipalities`/`actions` são `mandatory: true` — esconder um deles (cookie velho do toggle inerte do B197) derrubaria o `` sobre um corpo de 5 células; os checkboxes viram "sempre visível" (honestos). `assessores/page.tsx` consome a fonte exportada e perdeu o array espelho + imports órfãos. Comportamento da tabela idêntico (ids/labels/ordem, exceção do draft intacta); e2e `campaignColumnPicker` ganhou pin dos mandatory. Sem migration/access/Consent/UI/cache. Observações de ambiente (não da mudança): o e2e do picker falha no harness dev sob carga desta máquina (painel do popover não monta; falha idêntica na base e em lideranças — intocado; conjunto falho deriva com a carga; verificado funcional em Chromium real); warning React de key pré-existente no assessores (`CampaignListOmnibox` — base também). Intenção [`escala-dry-pos-b197.md`](plans/escala-dry-pos-b197.md); impl [`escala-dry-pos-b197-impl.md`](plans/escala-dry-pos-b197-impl.md). **Recently resolved (2026-08-11):** **C116** (Issue #655) — `/campanha/pessoas` desktop vira **edit-where-you-see**: toda célula (exceto Ações) é um input permanente sem destaque — salva no blur/Enter, Esc descarta, vazio mostra "—" apagado. **Nome** mantém o valor como link para a ficha (`/campanha/liderancas/[id]`) com o mecanismo travado no gate: input invisível (`text-transparent`, z-0) sobre o link (z-10, `pointer-events-auto`, `w-auto` — cobre só o texto; o resto da célula é a área clicável do input), draft renderizado **no texto do link** (o valor real do input nunca é o display), clique no link navega, teclado/área fora do texto edita. **Células de municípios** (Assessora, Lidera, Aliada em) e **Assessorado** usam chips internos com: grupo novo **"Salvador (19)"** (`buildMunicipalityPortfolioChips` ganha kind `city`, checado antes do território — senão as 19 ZEs colapsariam como "Metropolitano de Salvador"), **chip batch clicável expande in loco** (estado de apresentação do wrapper `PeopleMunicipalityCell` — clique expande, mouse-leave colapsa se todos os membros seguem atribuídos; X num membro remove e impede o recolapso), **"+X"** no toggle de overflow do B170, `quiet` (sem tint de hover/foco; indicador de foco discreto só no input), commit por **match exato** no Enter/espaço (digitar "salvador-ze-2"+Enter adiciona a ZE certa, não a pre-selecionada — regra da intenção). Escrita: `updatePersonContactRecord` (escopo "edita o que vê" — assessor só com entidade da pessoa na carteira; `contactFieldUpdateSchema` ganhou variante `city`, convivendo com `phones` do C112), `setPersonAssessoraMembershipRecord` (unrestricted-only, single-account — multi-conta nega com mensagem segura e célula read-only; toggle extraído para `toggleMunicipalityAdvisorMembership` em `municipality.ts`, reusado pelo record B27) e `setPersonAdvisorMembershipRecord` (union person-centric: delta de assessor nas DUAS entidades quando ambas existem). Matriz por papel na página: assessor edita Lidera/Aliada com `addableIds`=carteira e **read-only** em Assessora/Assessorado (regras das collections donas); tooltips de label (WhatsApp/Convidar/Apagar) via `CampaignHoverTooltip`; `CampaignListSheetProvider` na tabela (guard miss #52 — wrapper de composição registrado no `sharedSheetHostConventions`). Mobile intacto. Sem migration. Testes: unit (grupo Salvador + expand + match exato; guard do sheet host), int novo `campaignPersonCellEditing` (11 — escopo por papel, assessora single/multi/sem-conta, union nas duas entidades), e2e `campaignPeople` 4/4. Intenção [`pessoas-edicao-inplace-lista.md`](plans/pessoas-edicao-inplace-lista.md); impl [`pessoas-edicao-inplace-lista-impl.md`](plans/pessoas-edicao-inplace-lista-impl.md). diff --git a/docs/plans/atividades-tarefas-sem-prazo-impl.md b/docs/plans/atividades-tarefas-sem-prazo-impl.md new file mode 100644 index 00000000..c78c0fdf --- /dev/null +++ b/docs/plans/atividades-tarefas-sem-prazo-impl.md @@ -0,0 +1,73 @@ +# Impl: C124 — Atividades: tarefa só com texto, responsável e concluída (sem data/hora) + +Status: aprovado +Atualizado em: 2026-08-11 +Issue: #668 +Intenção: docs/plans/atividades-tarefas-sem-prazo.md +Appetite restante: herdado (~0,5–1 dia eng; mudança é remoção pura, sem migração de dados) + +## Leitura da intenção + +- **Outcome:** tarefa de atividade passa a ter exatamente três controles — título (texto), responsável (pessoa) e "concluída" (checkbox); o prazo (data/hora) sai do schema, da UI e do guardrail de liderança; `doneAt` permanece. +- **O que NÃO negociar:** guardrail de liderança fail-closed (só marca concluída, comparação perde `due`); dados existentes com prazo são **descartados** (drop da coluna, decidido no gate); anti-goal: prazo não migra para outro campo. +- **O que reavaliar:** a hipótese de direção da intenção estava completa — confirmei por grep que `due` de tarefa só existe em 6 arquivos (collections, zod, formData, viewModels, 2 componentes) + payload-types gerado + migration antiga (frozen). Nenhum consumidor fora da vertical de atividades (notificações, Google sync, activityUi, quick actions: nenhum lê `task.due`). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[Activity.ts
campo due sai do array tasks
guardrail líder perde comparação due] --> M[migration:
DROP COLUMN due
em activity_tasks] + C[lib/schemas/activity.ts
taskSchema: due sai] --> D[activityFormData.ts
ParsedActivityTask + parsing due saem] + D --> E[activityViewModels.ts
due sai dos dois view models] + E --> F[ActivityTaskFields.tsx
input datetime sai
grid 2fr_1fr] + E --> G[ActivityTaskChecklist.tsx
linha Prazo sai] + M --> T[generate:types
payload-types refresca] + T --> G2[Gates: tsc lint format knip cycles unit int e2e build] +``` + +**Opções consideradas:** A) remoção completa (campo + coluna + UI) · B) manter coluna morta (`due` nullable, nunca escrita) · C) manter campo no schema, só esconder da UI +**Recomendação:** A — a intenção e o gate decidiram "descartar e remover"; remover na origem elimina a coluna fantasma, o peso no guardrail e a tentação de re-exibir. Zero risco de perda: nenhum consumidor lê o valor (verificado por grep em `src/` e `tests/`), e dado de campanha interno sem valor operacional. +**Rejeitadas:** B (anti-goal "limpeza real, sem coluna fantasma" — matéria do gate) · C (mantém API e guardrail poluídos com campo morto; anti-goal: campo sai do modelo, não só da UI). + +### Componentes / mudanças + +- **`Activity`** (`src/collections/Activity.ts`): remove o field `due` do array `tasks` (linhas 575–583); remove `(taskRecord.due ?? null) !== (previousTask.due ?? null)` do guardrail de liderança em `deriveActivityFields` (linha 361) — mesmo item, senão a comparação lê propriedade inexistente e o fail-closed perde a cobertura. `doneAt`/`taskTotal`/`taskDoneCount` intocados. +- **`src/lib/schemas/activity.ts`**: `taskSchema` perde `due: z.string().datetime().optional().nullable()` (linha 74). Types `ActivityCreateInput`/`ActivityUpdateInput` derivam de zod — atualizam sozinhos. +- **`src/utilities/activityFormData.ts`**: `ParsedActivityTask` perde `due?: string`; `parseTasksFormData` perde o parsing/validação de prazo (linhas 81–85) e a propagação `...(due ? { due } : {})` (linha 91). O resto do parse fica intacto. +- **`src/utilities/activityViewModels.ts`**: `ActivityFormTaskViewModel` e `ActivityTaskViewModel` perdem `due` (linhas 244, 346) e os mapeamentos (linhas 294, 395). `doneAt` permanece no checklist VM. +- **`ActivityTaskFields.tsx`**: `ActivityTaskFieldValue` e `initialTasks` perdem `due`; `serializeTasks` deixa de emitir `due`; estado inicial e `addTask` perdem o default; o input `datetime-local` do prazo sai; grid `sm:grid-cols-[2fr_1fr_1fr]` → `sm:grid-cols-[2fr_1fr]`; import `formatIsoAsBahiaDateTimeInput` sai. +- **`ActivityTaskChecklist.tsx`**: a entrada `Prazo: …` do subtítulo sai (linha 63); import `formatBahiaDateTimeLabel` sai (o helper continua usado em outras superfícies — ActivityCard/agenda — knip valida). +- **Migration:** `pnpm migrate:create drop_activity_task_due` — gera `ALTER TABLE "activity_tasks" DROP COLUMN IF EXISTS "due";` (+ down). A coluna é nullable e sem índice próprio, então o drop é barato. Aplica em prod via build (Vercel) — revisar o SQL gerado antes. +- **Access / Consent:** nenhuma mudança — mesmo access, mesma coleção, sem Consent. +- **UI:** Impeccable B — encaixe nas superfícies existentes; remoção pura, sem shape novo. + +### Dados → forma (se aplicável) + +- Não se aplica — sem dado novo para apresentar; o contador `taskDoneCount` segue exatamente como está. + +## Fases verificáveis + +1. **Schema + server** — `Activity.ts` (campo + guardrail), `taskSchema` zod, `parseTasksFormData`. Verificar: `tsc --noEmit` + int spec `campaignActivity.int.spec.ts` (cobre o guardrail de liderança com `tasks` sem `due`). +2. **UI + view models** — `activityViewModels.ts`, `ActivityTaskFields.tsx`, `ActivityTaskChecklist.tsx`. Verificar: `tsc`, `lint`, unit specs de UI (activity\*) e `knip` (orfandade de helpers). +3. **Migration + tipos** — `pnpm migrate:create drop_activity_task_due`, revisar SQL, `pnpm migrate` local (teqo_wt), `pnpm generate:types`. Verificar: `migrate:status` nas duas DBs (dev + test). +4. **Gates** — `tsc --noEmit`, `lint` (0 warnings), `format:check`, `knip`, `check:cycles`, `pnpm test`, `pnpm test:e2e` (specs de atividade: `campaignActivity`, `campaignAgenda*`), `pnpm build` contra DB local. + +## Rabbit holes / Não escopo (engenharia) + +- **Migração de conteúdo (preservar `due` em outra coluna):** corte — nada consome; drop limpo. +- **Prazo em demandas/atividades em geral:** fora de escopo (não criar equivalente). +- **Tocar `doneAt`/`taskDoneCount`:** fora de escopo. +- **C123 (overlay da agenda):** roda depois, nasce já com o modelo simplificado — nada a preparar aqui além do modelo limpo. + +## Riscos e mitigação + +- **Guardrail de liderança quebrado ao mudar o shape:** mitigado — a comparação de `due` é removida no mesmo commit do field; o int spec `denies leaders from updating activities` e o de toggle de tarefa da liderança continuam exercitando o caminho fail-closed. +- **Migração destrutiva em prod (via build):** coluna nullable, dados já sem consumidor (grep `src/`+`tests/`); SQL revisado no passo 3 antes de qualquer deploy. +- **`payload-types.ts` desatualizado no meio do caminho:** `due` some do tipo gerado no passo 3 (`generate:types`) — até lá `tsc` pega qualquer uso remanescente. +- **Serialização client × parse server fora de sincronia:** `serializeTasks` para de emitir `due` e o parse para de ler — mudança simétrica num único commit; nenhum cliente antigo persiste (formas stateless). + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto: 3 controles por tarefa; checklist sem "Prazo: …"; schema sem prazo; dados descartados; guardrail de liderança intacto; `doneAt` permanece +- [ ] Invariantes AGENTS/engineering-standards: sem toque em access/Consent; transações intactas; copy pt-BR; identificadores em inglês +- [ ] Testes de domínio previstos: int spec existente do guardrail continua verde (sem novo teste — remoção pura, superfície coberta) diff --git a/src/collections/Activity.ts b/src/collections/Activity.ts index aa7474b7..8812bfd2 100644 --- a/src/collections/Activity.ts +++ b/src/collections/Activity.ts @@ -357,8 +357,7 @@ const deriveActivityFields: CollectionBeforeChangeHook = ({ const taskRecord = task as Record if ( trimmedText(taskRecord.title) !== trimmedText(previousTask.title) || - relationshipId(taskRecord.responsible) !== relationshipId(previousTask.responsible) || - (taskRecord.due ?? null) !== (previousTask.due ?? null) + relationshipId(taskRecord.responsible) !== relationshipId(previousTask.responsible) ) { throw new APIError('Lideranças só podem marcar tarefas como concluídas.', 403) } @@ -571,16 +570,6 @@ export const Activity: CollectionConfig = { relationTo: 'contact', label: 'Responsável', }, - { - name: 'due', - type: 'date', - label: 'Prazo', - admin: { - date: { - pickerAppearance: 'dayAndTime', - }, - }, - }, { name: 'done', type: 'checkbox', diff --git a/src/components/campaign/activity/ActivityTaskChecklist.tsx b/src/components/campaign/activity/ActivityTaskChecklist.tsx index 30cffffc..b24171b1 100644 --- a/src/components/campaign/activity/ActivityTaskChecklist.tsx +++ b/src/components/campaign/activity/ActivityTaskChecklist.tsx @@ -5,7 +5,6 @@ import { toast } from 'sonner' import { toggleActivityTaskAction } from '@/app/(campaign)/campanha/(app)/atividades/[slug]/taskActions' import { Checkbox } from '@/components/ui/Checkbox' -import { formatBahiaDateTimeLabel } from '@/lib/campaignTime' import type { ActivityTaskViewModel } from '@/utilities/activityViewModels' type ActivityTaskChecklistProps = { @@ -53,18 +52,15 @@ export const ActivityTaskChecklist = ({ activityId, tasks }: ActivityTaskCheckli aria-label={`Marcar tarefa "${task.title}" como ${task.done ? 'pendente' : 'concluída'}`} className="mt-0.5" /> -
- +
+ {task.title} - - {[ - task.responsibleName ? `Resp: ${task.responsibleName}` : null, - task.due ? `Prazo: ${formatBahiaDateTimeLabel(task.due)}` : null, - ] - .filter(Boolean) - .join(' · ')} - + {task.responsibleName ? ( + Resp: {task.responsibleName} + ) : null}
))} diff --git a/src/components/campaign/activity/ActivityTaskFields.tsx b/src/components/campaign/activity/ActivityTaskFields.tsx index 5d8c2895..284956ca 100644 --- a/src/components/campaign/activity/ActivityTaskFields.tsx +++ b/src/components/campaign/activity/ActivityTaskFields.tsx @@ -10,13 +10,11 @@ import { import { Button } from '@/components/ui/button' import { Field, FieldError, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' -import { formatIsoAsBahiaDateTimeInput } from '@/lib/campaignTime' export type ActivityTaskFieldValue = { key: string title: string responsible: ContactComboboxOption | null - due: string done: boolean } @@ -25,7 +23,6 @@ type ActivityTaskFieldsProps = { id: string | null title: string responsible: ContactComboboxOption | null - due: string | null done: boolean }> searchContacts: (query: string) => Promise @@ -45,7 +42,6 @@ const serializeTasks = (tasks: ActivityTaskFieldValue[]) => .map((task) => ({ title: task.title.trim(), ...(task.responsible ? { responsible: task.responsible.id } : {}), - ...(task.due ? { due: task.due } : {}), done: task.done, })), ) @@ -61,7 +57,6 @@ export const ActivityTaskFields = ({ key: task.id ?? nextTaskKey(), title: task.title, responsible: task.responsible, - due: task.due ? formatIsoAsBahiaDateTimeInput(task.due) : '', done: task.done, })), ) @@ -73,7 +68,7 @@ export const ActivityTaskFields = ({ const addTask = () => { setTasks((current) => [ ...current, - { key: nextTaskKey(), title: '', responsible: null, due: '', done: false }, + { key: nextTaskKey(), title: '', responsible: null, done: false }, ]) } @@ -91,7 +86,7 @@ export const ActivityTaskFields = ({ key={task.key} className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-start" > -
+
updateTask(task.key, { responsible: contact })} /> - updateTask(task.key, { due: event.target.value })} - />