mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
The picker shipped as a capability of CampaignTable rather than a municípios feature, so every list that already uses the shared table got it: pass `columnVisibility` and the table mounts the menu and filters the columns. Persistence is a cookie, not localStorage, and the reason is a type rather than a preference: CampaignTableColumn.cell is a function, so the column array never crosses the server/client boundary and the client cannot rebuild the table without it. The server filters, so a hidden column does not reach the RSC payload either. `campaign_columns` carries a listId -> hidden ids map in a compact form with no JSON and no percent-encoding, because document.cookie and Next's cookies() disagree about decoding. Closing the menu is the commit. The first cut debounced at 400 ms, which batches a burst rather than a reading pace, so unchecking four columns while reading their labels was four full-route refreshes — on /campanha/liderancas each one re-pays the ~229 ms / 371-statement render already ledgered P1. The timer survives at 3 s as a safety net for a menu left open; the price is that the table behind the menu no longer updates per checkbox. `defaultVisible` is gone: it was true on 10 columns and false on none, and storing only the hidden set is what makes a newly shipped column visible to everyone who never touched the picker. In exchange `label` is required, which made `head` optional — 24 columns had a head that only restated the new label. Each surface declares one mandatory column, re-filtered on the server so a stale cookie cannot leave a row without its only linked cell. The bug review caught is this codebase's recurring one: the RSC payload of toggle A arrives inside the window of toggle B, and adopting the server there undid B and then committed the set without it. Reconciliation now runs only when no write is pending, pinned red against the previous code. Municipality column ids are typed against MunicipalityListColumnId now, so the id, the B22 description and the picker label are one key; the union member `nivel` was renamed `level` to match the id and the ?level= filter. The picker's own content comparison is sameIdSet, widened to string ids.
47 lines
5.5 KiB
Text
47 lines
5.5 KiB
Text
---
|
|
description: Mapa navegacional do repositório Teqo — onde cada coisa vive e em que direção o código depende. Atualize no MESMO PR que mover módulos (par com docs/ARCHITECTURE.md).
|
|
alwaysApply: true
|
|
---
|
|
|
|
# Codebase map (Teqo)
|
|
|
|
Conceitos e decisões: `docs/ARCHITECTURE.md`. Operação (setup/seeds/deploy/segurança Payload): `AGENTS.md`.
|
|
|
|
## Direção de dependência
|
|
|
|
`lib/` (puro, client-safe) → `utilities/` (Payload/Next; loaders com `import 'server-only'`) → `components/` → `app/`. `lib/` nunca importa `utilities/` — agora enforcement programático: ESLint `no-restricted-imports` em `src/lib`, `pnpm check:cycles` (madge, 0 ciclos) e `codebaseConventions.unit.spec.ts` (exige `server-only` em utilities acoplados ao Payload). Client components importam de módulos server só `import type` — valores compartilhados vivem em módulos de contrato (`municipalityMapContract`, `votePledgeViews`, `lib/campaignRoles`, `lib/campaignConsentKeys`).
|
|
|
|
## Onde vive o quê
|
|
|
|
- `src/app/(frontend)` — site público (ISR, tag `posts`); actions públicas em `(frontend)/actions/*`.
|
|
- `src/app/(payload)` — admin `/admin` (auth `users`).
|
|
- `src/app/(campaign)/campanha` — ferramenta interna (auth `campaignUser`, cookie `campaign-token`; barreira no layout de `(app)`). Server actions em `campanha/actions/*`; `formActions.ts` por rota são cascas sobre `runCampaignFormAction` / `runCampaignRedirectFormAction` (`utilities/campaignFormActionError.ts`).
|
|
- `src/collections/` + `src/globals/` — modelo de dados Payload; TODA collection declara `access`; schema muda só por migration (`pnpm migrate:create`).
|
|
- `src/utilities/access/` — RBAC por domínio; `campaignAccess.ts` é o barrel de re-export; predicados de role client-safe em `src/lib/campaignRoles.ts`.
|
|
- `src/components/campaign/<domínio>/` — municipality, activity, supporter, leadership, advisor, demand, organization, stateDeputy, votePledge, invite, map, dashboard, auth, shell (sidebar/nav/PWA), shared (sistema de listas + widgets genéricos).
|
|
- `src/components/ui/` — kit shadcn da casa (sem peças mortas; `shadcn add` re-adiciona).
|
|
- `src/lib/schemas/` — zod de entrada; `src/lib/` também guarda catálogos estáticos e o artefato TSE (`electionAggregates/`).
|
|
- `src/migrations/` — história congelada; nunca editar retroativamente.
|
|
- `tests/` — `unit` (DATABASE_URL inválida), `int` (Payload real em `teqo_test`, fixtures em `tests/helpers/campaignFixtures.ts`), `e2e` (Playwright smoke).
|
|
|
|
## Sistema de listas (Pass 2 W1 — use, não reinvente)
|
|
|
|
- URL: `utilities/campaignListUrl.ts` (parse/canonicalize/`resolveListUrl`) + módulo por domínio (`municipalityListUrl.ts`, `supporterUi.ts`, `activityUi.ts`, parsers nos `*Data.ts`). Contrato de URL congelado (B18).
|
|
- Tabela: `shared/CampaignTable.tsx` — colunas como dado (`id`, `label` **obrigatório**, `head` opcional (default = `<CampaignTableHead>{label}</CampaignTableHead>`; declare só para alinhamento, `description` do B22 ou ilha de head), `cell`, `mandatory`); heads ricos = ilhas (`municipality/MunicipalitySortableHead` + `MunicipalityHeaderFilter`). Seletor de colunas (B17): passe `columnVisibility` — **um** valor, devolvido por `readCampaignColumnVisibility(listId)` (`utilities/campaignColumnVisibilityCookie.ts`, `server-only`) em quem renderiza a tabela — e ela monta o `CampaignColumnPicker` e filtra por `resolveVisibleColumns`; regra pura em `lib/campaignColumnVisibility.ts`. Só o conjunto de **ocultas** é guardado (`defaultVisible` não existe): coluna nova nasce visível.
|
|
- Shells: `CampaignSearchForm`, `CampaignFilterChips`, `CampaignListFooter`, `CampaignListEmptyState`, `CampaignListPagination`, pending compartilhado em `CampaignListPending.tsx` (dima o resultado, não o controle).
|
|
- Relação por chips numa célula: `shared/MunicipalityPortfolioCell` (B34) — sempre montada em `pointer-fine`, Drawer em `pointer-coarse`; costuras `minItems`/`maxItems`/`addableIds`/`draft`/`commitAction`. Serve lideranças e assessores; não escrever uma terceira. Lógica pura em `lib/municipalityPortfolio.ts`, índice em `utilities/municipalityPortfolioIndex.ts`.
|
|
- Exceções documentadas: atividades = cards; `TerritoryOverviewTable` = sort no cliente; comparação de candidatos, LeaderContacts e preview de import ficam fora.
|
|
- Detalhe por tabs: `utilities/detailTabUi.ts` (factory) + sections RSC colocadas na rota (`municipios/[slug]/MunicipalityDetailTabs.tsx`, `atividades/[slug]/ActivityOverviewTab.tsx`).
|
|
|
|
## Achar o dono de uma feature
|
|
|
|
1. Rota em `src/app/(campaign)/campanha/(app)/<área>/page.tsx` → 2. loader `utilities/<área>Data.ts` ou `*PageData.ts` → 3. view models (`*ViewModels`/`*Views`) → 4. componentes em `components/campaign/<domínio>/` → 5. escrita em `campanha/actions/<área>.ts` (+ `formActions.ts` da rota) → 6. acesso em `utilities/access/<domínio>.ts` → 7. pins em `tests/unit|int` com o mesmo prefixo.
|
|
|
|
## Invariantes que nunca se violam
|
|
|
|
- Local API com `user` sempre com `overrideAccess: false`; bypass admin só com comentário justificando.
|
|
- Escrita multi-collection = transação (`withPayloadTransaction`, `req: { transactionID }`).
|
|
- `leader` é lockdown; view models são por papel; liderança nunca vê `estimatedVotes`.
|
|
- Consent LGPD resolve por chave estável (`lib/campaignConsentKeys.ts`) e falha fechado.
|
|
- Copy pt-BR / identificadores em inglês; "Praça"/"Núcleo" não voltam.
|
|
- Gate por entrega: `tsc --noEmit`, `pnpm lint` (0 warnings), `pnpm format:check`, `pnpm exec knip` (exports em ERROR), `pnpm check:cycles` (0 ciclos), `pnpm test`, `pnpm build`.
|