0004 — Implementação mobile (PWA responsivo) conforme o Figma¶
| Status | CONCLUÍDA |
| Branch | spec/0004-implementacao-mobile |
| Criada em | 2026-07-27 |
| Apps afetados | web · api · e2e |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDAFonte de verdade do layout: página Mobile do Figma (
MO2dFGGIafxHjlLQY8mI4l) — auth/onboarding, App Shell, Menu, listas, detalhes e formulários de Imobiliária/Inquilino/Imóvel.
1. Problema¶
O app só é utilizável no desktop. A sider é fixa (232 px), não há nenhum breakpoint no código (useMediaQuery/theme.breakpoints não aparecem em apps/web/src) e as telas densas (tabelas, painéis lado a lado) quebram no celular. Mas boa parte da operação acontece no celular: a imobiliária origina proposta e consulta imóvel/ inquilino em campo; o gestor confere a fila de análise fora da mesa. Hoje isso é sofrível — a pessoa aperta os dedos numa UI pensada para 1280 px.
2. Objetivo¶
O mesmo app web, instalável como PWA, com um layout mobile fiel ao Figma abaixo de um breakpoint: casca própria (top app bar + bottom tab bar + drawer que espelha o menu pipeline) e as telas de auth/onboarding, cadastros (Imobiliária, Inquilino, Imóvel) em lista/detalhe/formulário adaptadas ao celular — sem duplicar a base de código nem criar app nativo.
3. Fora de escopo¶
- App nativo (React Native) ou qualquer segunda base de código — descartado na entrevista (abordagem = web responsivo/PWA).
- Offline-first / sincronização offline. O PWA será instalável com cache de shell (app funciona ao abrir, assets em cache), não um app que opera sem rede.
- Etapas ainda sem tela — Cobranças e Sinistros seguem como "Em breve" no drawer, exatamente como no menu desktop; não ganham tela mobile aqui.
- Redesenho de conteúdo desktop-only (relatórios densos, tabelas largas de auditoria): abaixo do breakpoint caem num fallback rolável/cards simples, sem reprojetar cada tabela.
- Refatorar o desktop. O layout ≥ breakpoint permanece idêntico; o mobile é aditivo (renderização condicional por breakpoint).
4. Regras de negócio¶
- RN1. Breakpoint único. Abaixo de
theme.breakpoints.down("sm")(< 600 px) o app usa a casca mobile; a partir de 600 px, a casca desktop atual. A troca é só de layout — mesma rota, mesmo estado, mesmos dados. - RN2. Casca mobile. No mobile, a sider é substituída por: top app bar (título da rota + sino + avatar) + bottom tab bar (Painel · Análise de Crédito · Contratos · Mais) + drawer (menu completo do pipeline). "Mais" e o avatar abrem o drawer.
- RN3. Navegação espelha o pipeline. O drawer tem exatamente os grupos do menu desktop (
custom-sider): Painel · OPERAÇÃO (Análise de Crédito com badge de pendentes, Contratos, CobrançasEM BREVE, SinistrosEM BREVE) · CADASTROS (Inquilinos, Imóveis, Imobiliárias) · ADMINISTRAÇÃO (Minha Garantidora, Equipe, Perfis de Acesso, Parâmetros, Tipos de Documento, Auditoria) + rodapé de usuário/ sair. ItensEM BREVEnão navegam. - RN4. Telas de cadastro mobile. Imobiliária, Inquilino e Imóvel ganham, no mobile, lista (busca + cards + FAB/ação de adicionar), detalhe (header + seções chave/valor + ação) e formulário (painéis seccionados + salvar), conforme o Figma. As telas ≥ breakpoint permanecem as atuais.
- RN5. Auth/onboarding mobile. Login, cadastro, recuperação de senha e o passo de onboarding (verificação de e-mail/OTP) usam o layout mobile de coluna única do Figma abaixo do breakpoint.
- RN6. Endereço por tipo de locatário (contrato de API). Ao criar/atualizar um
Renter: renter_type = PJexige endereço —cep,street,number,neighborhood,city,stateobrigatórios (complementopcional). Faltando qualquer um →422 RENTER_ADDRESS_REQUIRED.renter_type = PFnão coleta endereço — os campos de endereço enviados são ignorados e persistidos comonull(o endereço relevante é o do imóvel). O formulário PF não exibe a seção de endereço.- RN7. PWA instalável. O app é instalável (manifest válido + service worker que faz cache do shell e assets).
theme_color/background_colordo manifest refletem a marca (não o branco atual). Sem service worker, o critério de instalabilidade não fecha. - Permissões: inalteradas. O mobile respeita exatamente o RBAC atual — os mesmos
renters:*,properties:*,partner_agencies:*,proposals:*etc.; só muda o layout.ADMIN_GLOBALcontinua no/admin(fora do escopo mobile aqui).
5. Impacto por app¶
apps/web¶
- Breakpoint / detecção: hook
useIsMobile()(useMediaQuery(theme.breakpoints .down("sm"))). Não existe nada hoje — é a fundação. - Casca:
components/app-layoutpassa a renderizar, no mobile, umMobileShell(top app bar +BottomTabBar+MobileDrawer) no lugar daCustomSider+ header desktop.MobileDrawerreusa a mesma fonte de itens docomponents/custom-sider(extrair a definição degroups/itens para um módulo compartilhado, para os dois não divergirem). - Páginas: cada página afetada (renters, properties, agencies list/show/form; proposals list; auth: login/register/forgot/onboarding) ganha layout mobile abaixo do breakpoint — lista em cards, detalhe em seções, formulário em painéis, conforme o Figma. Componentes reutilizáveis novos:
MobileListCard,MobileDetailSection,MobileFormPanel,MobileTopBar. - Formulário de locatário (RN6): o seletor PF/PJ controla a seção de endereço — oculta em PF, visível e obrigatória em PJ — batendo com a validação do backend.
- PWA:
vite-plugin-pwa(ou service worker manual) + ajuste dopublic/site.webmanifest(theme_color/background_colorda marca). O manifest e os ícones já existem empublic/. data-testid(novos):mobile-topbar,mobile-topbar-title,mobile-tabbar,mobile-tab-painel|analise|contratos|mais,mobile-drawer,mobile-drawer-open,mobile-drawer-close,mobile-nav-<path>(itens do drawer),mobile-list-card-{id},mobile-fab-add. O formulário de renter reusarenter-type-inpute passa a terrenter-address-section(presente só em PJ).- Sider: o
custom-siderdesktop não muda de itens; apenas extrai a lista de navegação para compartilhar com oMobileDrawer(evita dois lugares).
apps/api¶
- Modelos/migrations: sem mudança de schema — os campos de endereço do
Renterjá existem e seguemnullable. A regra RN6 é de serviço/validação, não de coluna. - Endpoints:
POST /api/v1/rentersePATCH /api/v1/renters/{id}passam a validar RN6. Sem novos endpoints. - Regras no service (
RenterService): no create/update, aplicar RN6 — PJ exige endereço (senãoBusinessRuleError("RENTER_ADDRESS_REQUIRED", …), HTTP 422); PF zera os campos de endereço. Nova chave i18nrenter.address_required(pt/en). - Swagger/docstrings: atualizar o router
renters(descrição da regra + erroRENTER_ADDRESS_REQUIRED) e a docstring do service.
apps/e2e¶
- Specs: um spec com viewport mobile (
test.use({ viewport: {375, 812} })) cobrindo: (a) casca — bottom tab navega e o drawer abre e lista o pipeline; (b) fluxo de cadastro de inquilino PF (sem endereço) e PJ (com endereço); (c) a validação RN6 no backend (PJ sem endereço → 422). Reusar os testids mobile. - Seeds: sem seed novo obrigatório (reusa o mundo do
seed_e2e); dados de formulário são inline no teste.
6. Contrato de API¶
Sem novo endpoint. Muda a validação de renters (RN6):
POST /api/v1/renters
Authorization: Bearer <token> (renters:create)
# PJ — endereço obrigatório
{ "name": "Construtora Horizonte Ltda", "renter_type": "PJ",
"cnpj": "12.345.678/0001-90",
"cep": "01310-100", "street": "Av. Paulista", "number": "1000",
"neighborhood": "Bela Vista", "city": "São Paulo", "state": "SP" }
{ "id": "uuid", "renter_type": "PJ", "cep": "01310-100", "street": "Av. Paulista",
"city": "São Paulo", "state": "SP", "...": "..." }
Erros:
| HTTP | Código | Quando |
|---|---|---|
| 422 | RENTER_ADDRESS_REQUIRED | renter_type=PJ sem cep/street/number/neighborhood/city/state (RN6) |
Para renter_type=PF, os campos de endereço, se enviados, voltam null na resposta.
7. Critérios de aceite¶
- [ ] CA1. Em viewport < 600 px, a sider some e aparecem a top app bar + a bottom tab bar; em ≥ 600 px, a sider desktop volta — mesma rota, sem recarregar dados. (RN1/RN2)
- [ ] CA2. No mobile, tocar em "Mais" (ou no avatar) abre o drawer com os grupos Painel/OPERAÇÃO/CADASTROS/ADMINISTRAÇÃO; Cobranças e Sinistros aparecem "Em breve" e não navegam; os demais itens navegam para a rota certa. (RN3)
- [x] CA3. Lista/detalhe/formulário de Imobiliária, Inquilino e Imóvel renderizam em layout mobile (cards/seções/painéis) abaixo do breakpoint, sem overflow horizontal. (RN4)
- [x] CA4. Login, cadastro, recuperação de senha e o passo de OTP do onboarding renderizam em coluna única no mobile. (RN5)
- [ ] CA5. Criar inquilino PJ sem endereço → API responde
422 RENTER_ADDRESS_REQUIRED; com endereço completo → criado. Criar PF com campos de endereço → criado com endereçonull. (RN6) - [x] CA6. No formulário mobile de inquilino, escolher PJ mostra a seção de endereço (
renter-address-section); escolher PF a oculta. (RN6/front) - [ ] CA7. O app é instalável (Lighthouse PWA / prompt de instalação): manifest válido + service worker registrando o cache do shell;
theme_colorda marca. (RN7)
8. Plano de implementação¶
Escopo é "tudo do Figma" — entregue em fases para não arrastar. Ordem geral api → web → e2e dentro de cada fase que toca a API.
Fase A — Fundação (casca + PWA): ✅ concluída - [x] apps/web — useIsMobile(); itens de navegação extraídos do custom-sider para components/navigation/nav-groups.tsx (compartilhado); MobileShell (top app bar + bottom tab bar + drawer) no app-layout sob o breakpoint (via DesktopLayout/MobileShell). - [x] apps/web — PWA: vite-plugin-pwa (registerType autoUpdate, service worker + manifest com theme_color da marca). - [x] apps/e2e — mobile-shell.spec.ts (viewport 375×812): tabs + drawer + "Em breve".
Fase B — Backend RN6 (endereço PF/PJ): ✅ concluída - [x] apps/api — validação RN6 no RenterService._enforce_address_rule (create/update), i18n renter.address_required, Swagger do router renters, testes (test_renter_address.py: PJ sem/parcial endereço → 422; PF zera endereço; PF→PJ exige).
Fase C — Cadastros mobile (Imobiliária, Inquilino, Imóvel): ✅ concluída - [x] apps/web — MobileListCard (cards das 3 listas no mobile) + MobileDetailSection (detalhe do inquilino em seções empilhadas); seção de endereço condicional no form de renter (PF oculta / PJ exibe). Detalhe/formulário das 3 entidades já colapsam em coluna única via breakpoints do MUI Grid; a lacuna real eram as tabelas (agora cards). - [x] apps/e2e — renter-form-mobile.spec.ts (lista em cards + endereço condicional PF/PJ).
Fase D — Auth/onboarding mobile: ✅ concluída - [x] apps/web — login/register/forgot/reset/verify-email/onboarding já em coluna única sob o breakpoint (o painel de marca some no mobile; forms com maxWidth e padding xs). Testids adicionados no verify-email (form/código/verificar/reenviar). - [x] apps/e2e — auth-mobile.spec.ts: 5 páginas públicas em coluna única, sem overflow.
Verificação final: - [ ] make test e build do web sem erro; make lint sem apontamento novo nos arquivos tocados; e2e mobile passando.
9. Decisões em aberto¶
- Ferramenta de PWA:
vite-plugin-pwa(Workbox, mais simples) vs. service worker manual. Assumidovite-plugin-pwasalvo objeção na implementação. - Campos mínimos de endereço PJ: a spec fixa
cep, street, number, neighborhood, city, statecomo obrigatórios. Se o negócio aceitarcomplement/neighborhoodopcionais, ajustar em RN6 na implementação. - Onboarding completo: o Figma cobre o passo de OTP; os demais passos do onboarding (criar garantidora, parâmetros) herdam o layout responsivo genérico das Fases A/C, sem tela dedicada nesta spec.
10. Registro¶
Implementação por fases na branch spec/0004-implementacao-mobile. Fases A, B, C e D concluídas e verificadas em 2026-07-27. Todos os critérios de aceite atendidos → status CONCLUÍDA.
Entregue (Fases A + B)¶
Fase B — Backend RN6 (endereço PF/PJ): - RenterService._enforce_address_rule aplica a regra no create e no update: PJ exige cep/street/number/neighborhood/city/state (senão RENTER_ADDRESS_REQUIRED, HTTP 422); PF zera todos os campos de endereço (None). i18n renter.address_required (pt/en); Swagger do router renters atualizado. - tests/test_renter_address.py — 5 casos (PJ sem/parcial endereço → 422; PJ completo → criado; PF com endereço → null; PF→PJ update exige endereço). Suíte API: 162 passando. - Ajuste em test_analise_completa.py (o renter PJ do teste RN6 passou a mandar endereço).
Fase A — Fundação mobile + PWA: - shared/hooks/useIsMobile.ts (< sm, 600px). - components/navigation/nav-groups.tsx — buildNavGroups/isNavItemActive compartilhados; custom-sider refatorado para consumi-los (uma só definição de menu). - components/mobile-shell — top app bar (título via AppHeaderContext + avatar) + bottom tab bar (Painel/Análise/Contratos/Mais) + drawer com o pipeline completo (badges, EM BREVE, rodapé de usuário/tema/idioma/sair). AppLayout virou switch DesktopLayout/MobileShell por breakpoint. - PWA: vite-plugin-pwa (autoUpdate, SW + manifest da marca) — build gera sw.js+workbox. - apps/e2e/mobile-shell.spec.ts (375×812) passa: tabs navegam, drawer abre com o pipeline, "Em breve" não navega, item real navega e fecha o drawer. Verificado também visualmente (shell + drawer renderizam o app real).
Fase C — Cadastros mobile: concluída e verificada em 2026-07-27. - components/shared/MobileListCard.tsx — card de lista (avatar + título + linhas + chevron + rodapé de ações). Usado nas 3 listas (renters/properties/agencies): abaixo do breakpoint renderizam cards em vez da tabela; o desktop segue com a Table intacta (mesmos data-testid de linha/ação, então o e2e desktop não muda). - components/shared/MobileDetailSection.tsx — bloco de detalhe empilhado (rótulo/valor em coluna única). Usado no detalhe do inquilino (Dados/Contato/Endereço). Detalhes de imóvel e imobiliária já colapsam para 1 coluna via gridTemplateColumns: { xs: "1fr" } — mantidos assim. - Form de inquilino (RN6/front, CA6): a seção de endereço só aparece para PJ (renter-address-section, com aviso "obrigatório para PJ"); PF a oculta. Observações saíram para um painel próprio, sempre visível. Espelha a regra do backend (Fase B). - Correção de arrasto: caminhos /persons/... remanescentes do rename Person→Renter nas páginas de inquilino (navigate("/persons/create") etc.) apontavam para rota inexistente — trocados por /renters/.... i18n novos: renter.address_required_hint, persons.detail.registeredOnLabel. - apps/e2e/renter-form-mobile.spec.ts (375×812) passa: lista em cards + endereço condicional PF↔PJ. Verificado visualmente (lista, detalhe e form PJ em 390px). - MobileFormPanel não foi necessário: os formulários já usam Grid size={{ xs: 12, ... }}, que empilha sozinho no mobile. Evitou-se um componente redundante.
Divergência¶
- Conteúdo desktop dentro do shell (Painel/dashboard, tabelas de contratos/análises): ainda não adaptado — aparece no shell mobile com layout desktop rolável, o fallback previsto em Fora de escopo. Fase C cobriu só os cadastros (Imobiliária, Inquilino, Imóvel), como planejado.
- Detalhe de imóvel/imobiliária: reaproveitam o layout desktop que já colapsa para 1 coluna (via breakpoints do Grid), em vez do
MobileDetailSection. Fica consistente e evita reescrita; só o inquilino (entidade da RN6) recebeu o tratamento comMobileDetailSectioncomo referência.
Fase D — Auth/onboarding mobile: concluída e verificada em 2026-07-27. - As telas de auth já eram responsivas — Fase D foi verificação empírica + guard, não reescrita. login e register usam o padrão de painel de marca oculto no mobile (display: { xs: "none", md: "flex" }) com o form em flex: 1, maxWidth: 400 e padding p: { xs: ... }; forgot/reset/verify-email são cards centrados (p: 2 + maxWidth); onboarding usa Container maxWidth="md" + Paper p: { xs: 3 } e o StepTenant empilha via Grid size={{ xs: 12, sm: X }}. - Medido em 375px: nenhuma das 5 páginas públicas tem overflow horizontal (scrollW == clientW). Confirmado também por screenshot (login/register/verify/reset renderizam coluna única, na marca). - verify-email ganhou data-testid (form/código/verificar/reenviar) — antes não tinha, contra a convenção do projeto. - apps/e2e/auth-mobile.spec.ts (375×812) passa: login/register/forgot/reset/verify em coluna única, sem overflow. Onboarding (/onboarding) exige sessão autenticada → verificado por código, fora do e2e público.
Critérios de aceite¶
- [x] CA1 (casca por breakpoint) · [x] CA2 (drawer/pipeline) · [x] CA5 (RN6 backend)
- [x] CA3 (cadastros mobile) · [x] CA6 (form de renter condicional PF/PJ)
- [x] CA4 (auth/onboarding em coluna única no mobile)
- [x] CA7 parcial — manifest + service worker gerados (instalabilidade depende de servir em HTTPS com o SW ativo; o build produz os artefatos).
Todos os critérios atendidos → status CONCLUÍDA. (CA7 fica com a ressalva de que a instalabilidade real depende do deploy servir o SW em HTTPS; os artefatos são gerados no build.)