Pular para conteúdo

0013 — Inquilinos e Imóveis conforme o design system

Status CONCLUÍDA
Branch spec/0013-inquilinos-imoveis-design-system
Criada em 2026-08-02
Apps afetados api · web · e2e

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

As telas de Inquilinos (/renters, rotuladas "Pessoas") e Imóveis (/properties) ainda usam <Table> do MUI e destoam do padrão de lista que a Análise de Crédito firmou como referência do design system (o apps/web/CLAUDE.md diz explicitamente que as demais listas devem convergir para lá). Além do visual inconsistente:

  • Inquilinos não tem busca — em um tenant com centenas de pessoas, achar alguém exige paginar na mão.
  • O filtro de tipo de Imóvel é client-side (properties.filter): filtra só a página carregada, então "Residencial" numa lista de 200 mostra só os residenciais daquela página, e a contagem mente. É um bug de paginação, não só estética.
  • Detalhe (show) e formulário (form) das duas entidades não seguem o padrão canônico (FormPanel / DataRow) por inteiro.

2. Objetivo

Inquilinos e Imóveis — lista, detalhe e formulário — passam a seguir o design system: a lista usa o padrão da Análise de Crédito (Paper + FilterBar + DataGrid no desktop, MobileFilterBar + RecordCard no mobile), com busca e filtros server-side que funcionam corretamente entre páginas. O pacote @garantora/design-system sobe para 2.9.1.

3. Fora de escopo

  • Nenhuma coluna/campo novo de negócio em Renter ou Property — a busca reaproveita colunas existentes; não há migration de schema.
  • Modelo de permissões inalterado — usa renters:list/delete e properties:list/delete já existentes; nada de papéis novos.
  • As outras listas (imobiliárias, usuários, papéis, contratos, tipos de documento) compartilham o mesmo alvo, mas não entram nesta spec.
  • A referência (Análise de Crédito) já está pronta — não é retocada aqui.
  • Regras de criação/edição/remoção (o que o form salva, o que o delete apaga) não mudam; o form muda só na apresentação (layout/design system), não no contrato de escrita.

4. Regras de negócio

  • RN1. Busca de Inquilinos (search, opcional): casa substring, case-insensitive em name, cpf, cnpj e email. Sem termo → lista tudo.
  • RN2. Busca de Imóveis (search, opcional): casa substring, case-insensitive no endereço — street, number, neighborhood, city. (Ver Decisão em aberto sobre owner_name.)
  • RN3. O filtro de tipo de imóvel (property_type: RESIDENTIAL | COMMERCIAL | MIXED, ou ausente = todos) passa a ser server-side. O filtro de imobiliária parceira (partner_agency_id) permanece server-side, como já é.
  • RN4. Busca e filtros combinam com a paginação server-side (skip/limit); o total retornado reflete o conjunto filtrado. A ordenação segue o padrão estável do BaseRepository (created_at desc, id desc como desempate) — não introduzir OFFSET/LIMIT sem ORDER BY.
  • RN5. Permissões: renters:list / properties:list para ver; renters:delete / properties:delete para excluir. Revisada durante a implementação: a redação dizia que AGENCY_USER já era escopado "por comportamento herdado do service atual". Não era — PropertyService aceitava partner_agency_id só da query, então bastava trocar o id na URL (ou omiti-lo) para listar a carteira de outra imobiliária. Decidido implementar o recorte, como CreditAnalysisService já fazia: o recorte vem do usuário, não da query, e vale também no detalhe (_get_authorized), de onde update e delete dependem. Inquilinos ficam fora deste recorte por desenho do modelo: Renter não tem partner_agency_id — pessoa não pertence a imobiliária, o contrato de garantia é do inquilino com a garantidora. Ali o recorte é por tenant, e só.
  • RN6. property_type inválido → HTTP 422. Falha de listagem no front → Alert com ação "Tentar novamente" (mantém o comportamento atual).

5. Impacto por app

apps/api

  • Modelos/migrations: sem mudança de schema, sem migration — a busca usa colunas já existentes e indexadas onde importa (cpf, cnpj já são index=True).
  • Endpoints:
  • GET /api/v1/renters — novo query param search (str, opcional).
  • GET /api/v1/properties — novos query params search (str, opcional) e property_type (enum, opcional); partner_agency_id mantido.
  • Regras no repository: renter_repo e property_repo ganham cláusula de busca (ILIKE combinado com OR nas colunas da RN1/RN2) e, em properties, filtro por property_type; preservar o ORDER BY estável e o filtro de tenant/agency.
  • Regras no service: RenterService.list e PropertyService.list recebem e repassam os novos filtros, sem afrouxar o escopo de AGENCY_USER.
  • Docstrings + Swagger: atualizar summary/description/response_description e os Query(..., description=...) dos dois endpoints (regra do CLAUDE.md da API).

apps/web

  • Páginas/componentes:
  • pages/renters/index.tsx e pages/properties/index.tsx → trocar <Table> por <Paper> + <FilterBar> (SearchField + FilterSelect) + <DataGrid> (garantoraDataGridProps / dataGridSx de @garantora/design-system/theme/dataGrid) no desktop; MobileFilterBar + RecordCard no mobile. Remover o filtro client-side de Imóveis (passa a chamar a API com property_type) e a busca de Inquilinos passa a debouncar contra a API (como em credit-analyses/index.tsx).
  • pages/renters/show.tsx e pages/properties/show.tsx → alinhar ao padrão de detalhe (DataRow / MobileDetailSection).
  • pages/renters/form.tsx e pages/properties/form.tsx → alinhar ao FormPanel / docs/ui-patterns.md (incl. InputLabelProps.shrink).
  • data-testid: preservar todos os atuais (renters-new-button, renter-row-{id}, renter-view/edit/delete-{id}, renters-pagination, renter-delete-dialog/confirm/cancel/error; idem properties/property). Novos: renters-search-input, properties-search-input; manter properties-type-filter e properties-agency-filter (migrados para o FilterBar).
  • Sider: sem mudança — os recursos renters e properties já existem em App.tsx e em components/custom-sider/index.tsx.
  • Responsividade & PWA: DataGrid no desktop, RecordCard/MobileFilterBar no mobile (mesmo comportamento da referência). Sem scroll horizontal em 375px. PWA: o bump do pacote troca assets do bundle → o service worker recacheia no próximo load; não há tela offline nova.
  • Pacote: @garantora/design-system 2.3.0 → 2.9.1 (package.json + pnpm-lock.yaml); revalidar theme/theme/dataGrid e o color-mode.

apps/e2e

  • Specs: renters-list.spec.ts e properties-list.spec.ts — busca server-side, filtro de tipo server-side (troca de página mantém o filtro e a contagem), paginação, e o modo mobile (RecordCard). Escrever também os cenários de show/form se o padrão mudou o suficiente para afetar seletores.
  • Seeds: volume suficiente para paginar/filtrar em tests/fixtures/seeds/ — N inquilinos (com CPF/CNPJ/e-mail variados) e N imóveis (misturando property_type e imobiliárias).

6. Contrato de API

GET /api/v1/renters?skip=0&limit=50&search=maria
Authorization: Bearer <token>

GET /api/v1/properties?skip=0&limit=50&search=avenida&property_type=RESIDENTIAL&partner_agency_id=<uuid>
Authorization: Bearer <token>
{ "items": [ { "id": "uuid", "...": "..." } ], "total": 42 }
  • Shape de resposta inalterado ({ items, total }).
  • property_type fora do enum → 422 (RequestValidationError).
  • Sem permissão → 403; sem token → 401.

7. Critérios de aceite

  • [x] CA1. Na lista de Inquilinos, buscar "maria" filtra no servidor por nome/CPF/CNPJ/e-mail, e a paginação reflete o total filtrado.
  • [x] CA2. Na lista de Imóveis, filtrar por tipo passa a filtrar no servidor: mudar de página mantém o filtro e o total corresponde ao filtrado (não filtra só a página).
  • [x] CA3. Buscar Imóveis por parte do endereço retorna os correspondentes de todas as páginas, não só da atual.
  • [x] CA4. Desktop: as duas listas usam DataGrid com o visual do design system (Paper + FilterBar + rodapé fixo); não há mais <Table> do MUI nessas telas.
  • [x] CA5. Mobile (375px): as duas listas usam RecordCard + MobileFilterBar, sem scroll horizontal (scrollWidth - clientWidth == 0).
  • [x] CA6. show e form das duas entidades seguem o padrão do design system (detalhe com DataRow; formulário com FormPanel e a regra de shrink).
  • [x] CA7. Pacote em 2.9.1; make test verde; tsc do web limpo; sem apontamento novo de ruff nos arquivos tocados.
  • [x] CA8. Permissões preservadas: VIEWER vê e não exclui; AGENCY_USER só vê o escopo do seu partner_agency_id — inclusive com busca/filtros aplicados.

8. Plano de implementação

Ordem obrigatória: api → web → e2e.

  • [x] apps/api — renter_repo/property_repo: busca (ILIKE/OR) + property_type, preservando ORDER BY e escopo
  • [x] apps/api — RenterService.list/PropertyService.list: repassar filtros
  • [x] apps/api — routers: novos Query params + docstrings/Swagger
  • [x] apps/api — testes: busca casa/não-casa, property_type server-side, combinação com paginação, 422
  • [x] apps/web — bump 2.3.0 → 2.9.1 e revalidação do tema/dataGrid
  • [x] apps/web — renters/index e properties/index para o padrão (DataGrid + FilterBar + RecordCard), data-testid completos, desktop e mobile
  • [x] apps/web — show e form das duas entidades no padrão do design system
  • [x] apps/e2e — specs de lista (busca/filtro/paginação/mobile) + seeds
  • [x] make test e make lint passando (sem apontamento novo nos arquivos tocados)
  • [x] Validado no browser em desktop e mobile (screenshot nos dois viewports)

9. Decisões em aberto

  • [x] Busca de Imóveis inclui owner_name? Ficou o default: não. A camada de API já havia sido implementada assim, e a UI seguiu — o placeholder diz "endereço, bairro ou cidade", sem prometer o que não faz.
  • [x] Bump 2.3.0 → 2.9.1 são 6 minors. Verificado: o theme/ está idêntico entre as duas versões — o diff é só a linha de versão no package.json. As seis minors mexeram no UI kit e nos componentes de referência, nada que o app importe. tsc limpo depois do bump, sem uma linha de adaptação.

10. Registro

2026-08-02 — camada de API concluída (parcial). Implementada e verificada a parte apps/api da spec:

  • renter_repo.search_with_count (busca por nome/CPF/CNPJ/e-mail) e property_repo.search_with_count (busca por endereço + filtro property_type e partner_agency_id server-side), ambos preservando o ORDER BY estável e o recorte de tenant/soft-delete.
  • Services repassam os filtros; routers expõem search (renters) e search/property_type (properties) com docstrings/Swagger. property_type inválido → 422 (validação do enum).
  • Testes: tests/test_renters_search.py (6) e tests/test_properties_search.py (5). Suíte completa: 196 passed, 0 falhas.

Pendente (não implementado): todo o apps/web (bump 2.3.0 → 2.9.1, converter renters/index e properties/index para o padrão DataGrid + FilterBar + RecordCard, e alinhar show/form) e o apps/e2e. O bump para 2.9.1 ficou bloqueado no host (o shell default caiu para Node v17; o pnpm exige ≥18 — usar um Node do nvm, ex. v22).

Ainda não verificado nesta camada: o escopo de AGENCY_USER (CA8) não ganhou teste dedicado — o service preserva o comportamento atual (recorte por tenant_id + filtros de query), sem introduzir auto-scoping por partner_agency_id. (Resolvido depois — ver a seção do CA8 abaixo.)


2026-08-02 — camada web e e2e concluídas.

O bump 2.3.0 → 2.9.1 saiu de graça: o theme/ é byte a byte o mesmo, e as seis minors entregaram, entre outras coisas, as telas de referência de Inquilinos (2.7.0) e Imóveis (2.8.0) — que é o alvo desta spec. Foi delas que vieram as decisões de coluna, o texto do vazio e o comportamento do proprietário ausente.

As duas listas foram para o padrão: Paper + FilterBar + DataGrid no desktop, MobileFilterBar + RecordCard no mobile, tudo server-side. Os data-testid antigos foram preservados e entraram renters-search-input, properties-search-input, renter-card-{id}, property-card-{id} e property-owner-empty.

O que ficou de fora, e por quê: a tela de referência tem filtros de "Vínculo" (inquilino) e "Situação" (imóvel), e uma coluna de contagem de contratos/análises. Nada disso existe na API, e resolver no cliente reintroduziria exatamente o defeito que a spec veio corrigir. Ficaram fora, com a razão registrada aqui.

A barra não afirma contagem enquanto carrega. Medindo, vi "0 imóveis" piscar antes da primeira página chegar — a lista parecia vazia por um instante. Agora a contagem só aparece quando existe. Efeito colateral bom: dois testes meus falharam por ler o número cedo demais, o que confirmou a mudança.

Um susto que não era: a primeira verificação do CA2 deu "662 sem filtro, 662 com Residencial" e parecia filtro quebrado. Era o contrário — todos os 662 imóveis do tenant são residenciais, e o teste escolhera um valor que casa tudo. Conferido direto na API (COMMERCIAL → 0), o filtro estava certo desde o início. O spec e2e passou a criar um imóvel comercial no setup, justamente para o recorte ter o que recortar.

Detalhe do imóvel: sem proprietário, a seção deixou de sumir e passou a explicar — "Não informado. O dado é opcional — a garantidora não paga o proprietário.", como a referência 2.8.0 define. Some a dúvida de se o dado faltou ou nem existe. Os formulários já usavam FormPanel e os detalhes já seguiam o padrão de Field/SectionTitle do ui-patterns.md; não precisaram de conversão.

Verificação: tsc limpo; ESLint em 53 apontamentos, o mesmo baseline; suíte da API 195 passed, 1 failed — a falha é test_partner_agency_search::test_busca_alcanca_quem_ esta_fora_da_primeira_pagina, intermitente desde af2b7d5 e alheia a esta spec (está registrada no CLAUDE.md da raiz). e2e novo 11/11 no chromium contra o dev server. Medido no browser nos dois viewports: DataGrid no desktop, cartões no mobile, nenhuma <Table> antiga, zero glifo do MUI e zero rolagem horizontal.

Ruff: a camada de API tinha deixado 4 apontamentos novos (três E501 nas descrições de Swagger e um I001 de import fora de ordem no property_repo). Conferido contra a main arquivo a arquivo para separar o que era novo do que já existia — os 3 do renter_service são anteriores. Os 4 novos foram corrigidos; os arquivos tocados passam limpos.


2026-08-02 — CA8 fechado, e ele não era o que a spec dizia.

Ao ir escrever o teste, os fatos não casavam com o critério:

  • Renter não tem partner_agency_id. "AGENCY_USER só vê os inquilinos da sua imobiliária" não é expressável no modelo — e faz sentido pelo domínio: o contrato de garantia é do inquilino com a garantidora, a imobiliária é canal.
  • Property tem, mas PropertyService.list não recortava por current_user.partner_agency_id; só honrava o parâmetro da query. Um AGENCY_USER listava a carteira inteira do tenant, e trocar o id na URL abria a de qualquer parceira.
  • CreditAnalysisService.list já recortava. O padrão existia na plataforma, aplicado a análises e não a imóveis.

E a spec se contradizia: a RN5 mandava "preservar o comportamento atual" (que era não recortar) enquanto o CA8 afirmava que o recorte existia. Levado ao usuário com as duas saídas — implementar o recorte ou reescrever o critério —, a decisão foi implementar.

O que entrou em PropertyService:

  • list — effective_agency_id = current_user.partner_agency_id or partner_agency_id. O recorte vem do usuário; aceitar o da query era justamente o furo.
  • _get_authorized — recortar só a lista deixaria o detalhe aberto por id, e por ele passam update e delete. Responde 404, não 403: quem não pode ver não deve descobrir que o registro existe.

Um bug vivo achado no caminho: o web checava persons:delete — permissão que não existe; a API concede renters:delete. O botão de excluir inquilino nunca apareceu para ninguém, nem para administrador, em index.tsx e show.tsx. Ninguém notou porque "não aparece" era o esperado do lado do VIEWER. Corrigido, e o e2e passou a afirmar os dois lados: VIEWER sem o botão, ADMIN_TENANT com.

Testes: 3 novos na API (isolamento entre tenants sob busca em inquilinos e imóveis; recorte por imobiliária, inclusive a tentativa de burla via ?partner_agency_id=<outra>; detalhe alheio por id → 404) e 2 no e2e (permissões e recorte). Suíte da API: 200 passed, 0 falhas — a intermitente das imobiliárias passou nesta rodada. e2e da spec: 13/13 no chromium.

Consequência operacional: usuários de imobiliária passam a ver menos imóveis no dev-remote — só a própria carteira. É o efeito pretendido da decisão, mas é mudança visível, não refinamento invisível.