sass-imobiliaria/specs/023-ux-melhorias-imoveis/data-model.md

3.5 KiB

Data Model — Melhorias UX/UI (023)

Nenhuma migration de banco necessária. Todos os campos utilizados já existem no modelo Property.


Entidades Existentes (campos utilizados nesta feature)

Property (backend/app/models/property.py)

Campo Tipo SQLAlchemy Tipo Python Sprint Utilização
title VARCHAR(200) str 1 Busca textual q (ILIKE)
address VARCHAR(300) str | None 1 Busca textual q (ILIKE)
code VARCHAR(30) str | None 1 Busca textual q (ILIKE)
neighborhood_id INTEGER FK → neighborhoods.id int | None 1 Join para busca q em Neighborhood.name
price NUMERIC(12,2) Decimal 2 Ordenação price_asc / price_desc
area_m2 INTEGER int 2 Ordenação area_desc
created_at DATETIME datetime 2/3 Ordenação newest; badge "Novo" (frontend)
is_featured BOOLEAN bool 3 Badge "Destaque" no card

Neighborhood (backend/app/models/location.py)

Campo Tipo SQLAlchemy Utilização
id INTEGER PK Join com Property.neighborhood_id
name VARCHAR Busca textual q (ILIKE)

Tipos Frontend Adicionados (frontend/src/services/properties.ts)

PropertyFilters — campos novos

// Adição aos campos existentes:
q?: string      // busca textual livre
sort?: SortOption

SortOption (novo tipo)

type SortOption =
  | 'relevance'   // default — equivale a created_at DESC no backend
  | 'price_asc'
  | 'price_desc'
  | 'area_desc'
  | 'newest'

ViewMode (novo tipo local — apenas frontend)

type ViewMode = 'list' | 'grid'
// Persiste em localStorage com key 'imoveis_view_mode'

Entidades de UI (apenas frontend — sem persistência no banco)

ActiveFilterChip

Tipo derivado calculado a partir de PropertyFilters + dados do catálogo:

interface ActiveFilterChip {
  key: string          // identificador único (ex: 'city_id', 'q', 'bedrooms_min')
  label: string        // texto exibido no chip (ex: 'São Paulo', 'Busca: "Jardins"')
  onRemove: () => void // callback que remove este filtro específico
}

EmptyStateSuggestion

interface EmptyStateSuggestion {
  label: string               // ex: 'Remover filtro de bairro'
  relaxedFilters: PropertyFilters
  count: number               // total de imóveis com o filtro relaxado
}

Validação de Entrada — Backend

O parâmetro q não é validado via Pydantic (é query param de GET, sem body). Sanitização aplicada diretamente na rota:

q = args.get("q", "").strip()
# Comprimento máximo razoável (evitar payloads abusivos)
if len(q) > 200:
    q = q[:200]

O parâmetro sort é validado via whitelist implícita no sort_map.get(sort, default).


Diagrama de Relacionamentos (campos relevantes)

Property
  ├── title (VARCHAR 200)         ─── ILIKE com q
  ├── address (VARCHAR 300)       ─── ILIKE com q
  ├── code (VARCHAR 30)           ─── ILIKE com q
  ├── neighborhood_id (FK)        ─┐
  │                                ├── JOIN → Neighborhood.name ─── ILIKE com q
  ├── price (NUMERIC 12,2)        ─── ORDER BY price_asc/desc
  ├── area_m2 (INTEGER)           ─── ORDER BY area_desc
  ├── created_at (DATETIME)       ─── ORDER BY newest; badge "Novo" no frontend
  └── is_featured (BOOLEAN)       ─── badge "Destaque" no frontend