Camadas do app React (modules/frontend)

TLDR: páginas e componentes não têm lógica; toda lógica vive em hooks; todo dado passa por services/ — a camada que amanhã troca de mock para HTTP real sem tocar em mais nada.

Contexto

O app nasceu como port do protótipo dc-runtime (Sistema de Cobrança.dc.html), um único componente com um único state compartilhado pelas 14 telas. Portar isso para React exigiu decidir duas coisas antes de escrever a primeira tela: onde a lógica passaria a morar, e como o front sobreviveria à chegada do back-end real — que na época sequer tinha stack definida.

Decisão

Camadas

``` pages/ → uma página por rota, monta hooks + components, sem lógica própria components/ → apenas apresentação (props in, JSX out) layout/ → AppShell, Sidebar, NavItem, PageHeader, PageLayout — genéricos, usados em toda página logada ui/ → PillGroup, StatusChip, DataTable, Pagination, SearchInput, InfoHint, InfoToggle, Button, StepList, Card, Bar — genéricos, reaproveitáveis em qualquer domínio

/ → components/painel, components/lotes, components/pagamentos, components/clientes, components/clienteDetalhe, components/negativacao, components/contratos, components/juridico, components/bots, components/colaboradores — compõem os componentes de ui/ para uma tela específica (ex: KpiCard, LoteAtivoCard, PagamentosTable) hooks/ → toda lógica de estado/efeito/derivação mora aqui shared/ → usePagination, useToggleIndex, useAsyncData — genéricos, sem conhecimento de domínio auth/, nav/, painel/, lotes/, loteDetalhe/, negativacao/, contratos/, pagamentos/, clientes/, clienteDetalhe/, juridico/, bots/, colaboradores/ → hooks por domínio services/ → uma função por operação (ex: fetchPainelDados), hoje resolvendo contra mocks/, amanhã contra a API real mocks/ → dados estáticos determinísticos (equivalentes aos arrays hardcoded do protótipo original: NOMES_LOTE, MESES, etc.) types/ → tipos compartilhados por domínio routing/ → guards de rota (RequireAuth, RequireRole) e o mapa de navegação utils/ → funções puras sem estado (ex: formatBRL) ``` ```mermaid graph LR P["pages/"] --> C["components/"] P --> H["hooks/"] H --> S["services/"] S --> M["mocks/"] S -.->|"futuro"| API["API real"] C --> UI["components/ui + layout"] style S fill:#1f2937,color:#fff style API fill:#374151,color:#fff,stroke-dasharray: 4 4 ``` ### Camada de serviço isolada Toda tela busca dados via uma função em `services/` (ex: `fetchPainelDados`, `fetchLotesAtivos`, `fetchLoteDetalhe`). Hoje essas funções apenas devolvem dados de `mocks/`, mas já são `async` — quando o back-end real existir, a integração é trocar a implementação dessas funções (por um `fetch`/client HTTP real), sem tocar em hooks, componentes ou páginas. O hook `hooks/shared/useAsyncData` centraliza o padrão "buscar ao montar, ignorar resposta se o componente já desmontou", usado por praticamente todo hook que consome um `service`. Uma exceção é `useImportarLote`: ele usa o fetch inicial apenas como *seed* de um estado que é mutado localmente depois (ao importar um novo lote), então não se encaixa no formato "buscar e exibir" do `useAsyncData`. ### Componentes sem lógica Toda lógica de estado, cálculo e efeito colateral vive em hooks customizados (`hooks//use*`). Os componentes (`components/`, `pages/`) recebem dados já prontos via props e só decidem o que renderizar. ### Roteamento e papéis `routing/RequireAuth` e `routing/RequireRole` substituem o `tela`/`permitidas` do protótipo original por rotas reais via `react-router`. `hooks/nav/useNav` deriva a navegação (itens, seleção, badges) a partir do papel do usuário e da rota atual. Telas ainda não portadas (fases futuras) usam `pages/ComingSoonPage` como placeholder, para que a navegação completa já funcione ponta a ponta mesmo antes de todas as telas existirem. ### Quando promover um hook local para Context compartilhado O protótipo original é um único componente com um único `state` — várias telas liam e escreviam nos mesmos dados. Ao portar para hooks isolados por página, isso se perde por padrão (cada `usePagamentos()`/`useContratos()` teria seu próprio fetch e sua própria cópia do estado). Sempre que **duas ou mais páginas** precisam enxergar a mesma mutação em tempo real, promovemos o hook para um Context compartilhado (padrão igual ao `SessionContext` da Fase 1): - **`PagamentosContext`** (`hooks/pagamentos/PagamentosContext.tsx`) — a tela Pagamentos (duplicar pagamento) e a aba Pagamentos do Detalhe do cliente (pagamento avulso) escrevem no mesmo array; uma cobrança criada em um lugar precisa aparecer no outro imediatamente. - **`FilaNegativacaoContext`** (`hooks/negativacao/FilaNegativacaoContext.tsx`), **`ContratosContext`** (`hooks/contratos/ContratosContext.tsx`) e **`CasosJuridicosContext`** (`hooks/juridico/CasosJuridicosContext.tsx`) — a tela em si E o badge da barra lateral (`hooks/nav/useNavBadges`) precisam do mesmo estado ao vivo; sem o Context, o badge mostraria uma contagem congelada no valor inicial, nunca refletindo o que o usuário fez na tela. Todos os Context ficam montados em `AppLayout`, que envolve todas as rotas autenticadas — por isso sobrevivem à navegação entre páginas. Quando um hook é usado por **uma única página** e nada mais precisa da mesma instância ao vivo (ex: `useLoteConfig`, `useClientes`), ele continua sendo um hook local comum — promover para Context sem essa necessidade real seria complexidade desnecessária. ## Consequências | Consequência | Efeito | |---|---| | Integração com o back-end é local | A "regra de ouro" das specs INT: trocar só `services/*.ts`; se um hook precisar mudar, a spec tem que justificar | | Lógica testável sem renderizar | Cálculos do painel, paginação e regras de importação de lote são testados nos `*.test.ts` ao lado de cada hook | | Componentes de `ui/`/`layout/` reaproveitáveis | Qualquer tela futura os compõe passando dados diferentes via props | | Estado compartilhado exige decisão explícita | Promover a Context é a exceção justificada, não o padrão — o custo é lembrar de fazê-lo quando um badge depende do estado de uma tela | O motor do simulador de Bots (`hooks/bots/criarEstadoInicial.ts` e `hooks/bots/processarMensagem.ts`) é o exemplo mais completo do padrão "extrair lógica complexa como função pura, sem depender de React" — `enviarMensagemTexto` e `selecionarOpcaoMenu` recebem e devolvem um `BotSimState` comum, sem `useState` nem efeito colateral, o que permitiu testar toda a máquina de estados (validações, tentativas, round-robin) sem montar nenhum componente. `useBotSimulador` é só a casca fina que liga isso ao React (estado, scroll automático). ## Referências - [Camadas do app Rails](backend_layers.md) — a contraparte no back-end - [Quirks do protótipo dc-runtime](../learnings/prototype_dc_quirks.md) - Specs do port: [Fase 1](../specs/20260720144029_react_port_phase1_foundation.md) · [Fase 2](../specs/20260720172958_react_port_phase2_clientes_pagamentos.md) · [Fase 3](../specs/20260721082709_react_port_phase3_juridico_bots_colaboradores.md)