Pular para conteúdo

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ÍDA

Fonte 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ças EM BREVE, Sinistros EM 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. Itens EM BREVE nã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 = PJ exige endereço — cep, street, number, neighborhood, city, state obrigatórios (complement opcional). Faltando qualquer um → 422 RENTER_ADDRESS_REQUIRED.
  • renter_type = PF não coleta endereço — os campos de endereço enviados são ignorados e persistidos como null (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_color do 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_GLOBAL continua 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-layout passa a renderizar, no mobile, um MobileShell (top app bar + BottomTabBar + MobileDrawer) no lugar da CustomSider + header desktop. MobileDrawer reusa a mesma fonte de itens do components/custom-sider (extrair a definição de groups/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 do public/site.webmanifest (theme_color/background_color da marca). O manifest e os ícones já existem em public/.
  • 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 reusa renter-type-input e passa a ter renter-address-section (presente só em PJ).
  • Sider: o custom-sider desktop não muda de itens; apenas extrai a lista de navegação para compartilhar com o MobileDrawer (evita dois lugares).

apps/api

  • Modelos/migrations: sem mudança de schema — os campos de endereço do Renter já existem e seguem nullable. A regra RN6 é de serviço/validação, não de coluna.
  • Endpoints: POST /api/v1/renters e PATCH /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ão BusinessRuleError("RENTER_ADDRESS_REQUIRED", …), HTTP 422); PF zera os campos de endereço. Nova chave i18n renter.address_required (pt/en).
  • Swagger/docstrings: atualizar o router renters (descrição da regra + erro RENTER_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ço null. (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_color da 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. Assumido vite-plugin-pwa salvo objeção na implementação.
  • Campos mínimos de endereço PJ: a spec fixa cep, street, number, neighborhood, city, state como obrigatórios. Se o negócio aceitar complement/neighborhood opcionais, 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 com MobileDetailSection como 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.)