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/deleteeproperties:list/deletejá 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 emname,cpf,cnpjeemail. 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 sobreowner_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); ototalretornado reflete o conjunto filtrado. A ordenação segue o padrão estável doBaseRepository(created_atdesc,iddesc como desempate) — não introduzirOFFSET/LIMITsemORDER BY. - RN5. Permissões:
renters:list/properties:listpara ver;renters:delete/properties:deletepara excluir. Revisada durante a implementação: a redação dizia queAGENCY_USERjá era escopado "por comportamento herdado do service atual". Não era —PropertyServiceaceitavapartner_agency_idsó 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, comoCreditAnalysisServicejá fazia: o recorte vem do usuário, não da query, e vale também no detalhe (_get_authorized), de ondeupdateedeletedependem. Inquilinos ficam fora deste recorte por desenho do modelo:Renternão tempartner_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_typeinválido → HTTP 422. Falha de listagem no front →Alertcom 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,cnpjjá sãoindex=True). - Endpoints:
GET /api/v1/renters— novo query paramsearch(str, opcional).GET /api/v1/properties— novos query paramssearch(str, opcional) eproperty_type(enum, opcional);partner_agency_idmantido.- Regras no repository:
renter_repoeproperty_repoganham cláusula de busca (ILIKEcombinado comORnas colunas da RN1/RN2) e, em properties, filtro porproperty_type; preservar oORDER BYestável e o filtro de tenant/agency. - Regras no service:
RenterService.listePropertyService.listrecebem e repassam os novos filtros, sem afrouxar o escopo deAGENCY_USER. - Docstrings + Swagger: atualizar
summary/description/response_descriptione osQuery(..., description=...)dos dois endpoints (regra do CLAUDE.md da API).
apps/web¶
- Páginas/componentes:
pages/renters/index.tsxepages/properties/index.tsx→ trocar<Table>por<Paper>+<FilterBar>(SearchField+FilterSelect) +<DataGrid>(garantoraDataGridProps/dataGridSxde@garantora/design-system/theme/dataGrid) no desktop;MobileFilterBar+RecordCardno mobile. Remover o filtro client-side de Imóveis (passa a chamar a API comproperty_type) e a busca de Inquilinos passa a debouncar contra a API (como emcredit-analyses/index.tsx).pages/renters/show.tsxepages/properties/show.tsx→ alinhar ao padrão de detalhe (DataRow/MobileDetailSection).pages/renters/form.tsxepages/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; idemproperties/property). Novos:renters-search-input,properties-search-input; manterproperties-type-filtereproperties-agency-filter(migrados para oFilterBar).- Sider: sem mudança — os recursos
rentersepropertiesjá existem emApp.tsxe emcomponents/custom-sider/index.tsx. - Responsividade & PWA: DataGrid no desktop,
RecordCard/MobileFilterBarno 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-system2.3.0 → 2.9.1 (package.json+pnpm-lock.yaml); revalidartheme/theme/dataGride ocolor-mode.
apps/e2e¶
- Specs:
renters-list.spec.tseproperties-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 (misturandoproperty_typee 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>
- Shape de resposta inalterado (
{ items, total }). property_typefora 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
totalcorresponde 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.
showeformdas duas entidades seguem o padrão do design system (detalhe comDataRow; formulário comFormPanele a regra deshrink). - [x] CA7. Pacote em 2.9.1;
make testverde;tscdo web limpo; sem apontamento novo de ruff nos arquivos tocados. - [x] CA8. Permissões preservadas:
VIEWERvê e não exclui;AGENCY_USERsó vê o escopo do seupartner_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, preservandoORDER BYe escopo - [x]
apps/api—RenterService.list/PropertyService.list: repassar filtros - [x]
apps/api— routers: novosQueryparams + docstrings/Swagger - [x]
apps/api— testes: busca casa/não-casa,property_typeserver-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/indexeproperties/indexpara o padrão (DataGrid + FilterBar + RecordCard),data-testidcompletos, desktop e mobile - [x]
apps/web—showeformdas duas entidades no padrão do design system - [x]
apps/e2e— specs de lista (busca/filtro/paginação/mobile) + seeds - [x]
make testemake lintpassando (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 nopackage.json. As seis minors mexeram no UI kit e nos componentes de referência, nada que o app importe.tsclimpo 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) eproperty_repo.search_with_count(busca por endereço + filtroproperty_typeepartner_agency_idserver-side), ambos preservando oORDER BYestável e o recorte de tenant/soft-delete.- Services repassam os filtros; routers expõem
search(renters) esearch/property_type(properties) com docstrings/Swagger.property_typeinválido → 422 (validação do enum). - Testes:
tests/test_renters_search.py(6) etests/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:
Renternão tempartner_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.Propertytem, masPropertyService.listnão recortava porcurrent_user.partner_agency_id; só honrava o parâmetro da query. UmAGENCY_USERlistava a carteira inteira do tenant, e trocar o id na URL abria a de qualquer parceira.CreditAnalysisService.listjá 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 passamupdateedelete. 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.