sass-imobiliaria/specs/024-filtro-busca-avancada/data-model.md

7.7 KiB
Raw Permalink Blame History

Data Model: Filtro de Busca Avançada — FilterSidebar

Feature: 024-filtro-busca-avancada | Phase: 1 | Date: 2026-04-20


Resumo

Sem novas tabelas ou migrations. A única alteração de "modelo de dados" é a extensão dos schemas Pydantic de saída com o campo calculado property_count: int = 0. No frontend, os tipos TypeScript espelham a mudança com property_count?: number (opcional para backward-compatibility).

São adicionados dois tipos internos do TypeScript exclusivos ao FilterSidebar.tsx, sem impacto em outros componentes.


Backend — Schemas Pydantic (catalog.py)

Antes e depois

# ANTES
class PropertyTypeOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    parent_id: int | None
    subtypes: list["PropertyTypeOut"] = []

class CityOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    state: str

class NeighborhoodOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    city_id: int

# DEPOIS (adição de property_count: int = 0 em cada schema)
class PropertyTypeOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    parent_id: int | None
    subtypes: list["PropertyTypeOut"] = []
    property_count: int = 0          # ← NOVO

class CityOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    state: str
    property_count: int = 0          # ← NOVO

class NeighborhoodOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    slug: str
    city_id: int
    property_count: int = 0          # ← NOVO

Regras de validação:

  • property_count >= 0 (COUNT nunca é negativo)
  • Default 0 garante que entidades sem imóveis ativos retornem um valor válido
  • Campo somente-leitura (nenhum endpoint de escrita o aceita como input)

Backend — Queries (routes)

list_cities() — locations.py

from sqlalchemy import func
from app.models.property import Property

rows = (
    db.session.query(City, func.count(Property.id).label("cnt"))
    .outerjoin(
        Property,
        (Property.city_id == City.id) & (Property.is_active.is_(True))
    )
    .group_by(City.id)
    .order_by(City.state, City.name)
    .all()
)
return jsonify([
    {**CityOut.model_validate(city).model_dump(), "property_count": cnt}
    for city, cnt in rows
])

list_neighborhoods() — locations.py

q = (
    db.session.query(Neighborhood, func.count(Property.id).label("cnt"))
    .outerjoin(
        Property,
        (Property.neighborhood_id == Neighborhood.id) & (Property.is_active.is_(True))
    )
    .group_by(Neighborhood.id)
)
if city_id:
    q = q.filter(Neighborhood.city_id == int(city_id))
rows = q.order_by(Neighborhood.name).all()
return jsonify([
    {**NeighborhoodOut.model_validate(n).model_dump(), "property_count": cnt}
    for n, cnt in rows
])

list_property_types() — catalog.py

A hierarquia pai → subtypes torna a injeção de property_count ligeiramente diferente: o COUNT é calculado por subtype_id (leaf nodes), não por parent. Os tipos pai não recebem property_count (deixam default 0).

from sqlalchemy import func
from app.models.property import Property

# 1. Calcular counts por subtype em uma query plana
subtype_rows = (
    db.session.query(
        PropertyType.id,
        func.count(Property.id).label("cnt")
    )
    .filter(PropertyType.parent_id.isnot(None))  # apenas leaf nodes
    .outerjoin(
        Property,
        (Property.subtype_id == PropertyType.id) & (Property.is_active.is_(True))
    )
    .group_by(PropertyType.id)
    .all()
)
count_map: dict[int, int] = {row.id: row.cnt for row in subtype_rows}

# 2. Buscar hierarquia normalmente
categories = (
    PropertyType.query.filter_by(parent_id=None).order_by(PropertyType.id).all()
)

# 3. Serializar injetando property_count nos subtypes
def serialize_category(cat: PropertyType) -> dict:
    data = PropertyTypeOut.model_validate(cat).model_dump(mode="json")
    data["subtypes"] = [
        {**sub, "property_count": count_map.get(sub["id"], 0)}
        for sub in data["subtypes"]
    ]
    return data

return jsonify([serialize_category(c) for c in categories])

Frontend — Tipos TypeScript (catalog.ts)

// ANTES
export interface PropertyType {
    id: number; name: string; slug: string
    parent_id: number | null; subtypes: PropertyType[]
}
export interface City {
    id: number; name: string; slug: string; state: string
}
export interface Neighborhood {
    id: number; name: string; slug: string; city_id: number
}

// DEPOIS
export interface PropertyType {
    id: number; name: string; slug: string
    parent_id: number | null; subtypes: PropertyType[]
    property_count?: number   // ← NOVO (opcional — backward-compat)
}
export interface City {
    id: number; name: string; slug: string; state: string
    property_count?: number   // ← NOVO
}
export interface Neighborhood {
    id: number; name: string; slug: string; city_id: number
    property_count?: number   // ← NOVO
}

Frontend — Tipos Internos (FilterSidebar.tsx)

Estes tipos são declarados localmente no arquivo FilterSidebar.tsx e não exportados.

SectionKey

type SectionKey =
    | 'imobiliaria'
    | 'localizacao'
    | 'tipo'
    | 'preco'
    | 'quartos'
    | 'area'
    | 'comodidades'

FilterSuggestion

interface FilterSuggestion {
    category: string           // Label do grupo exibido na UI (ex.: "Tipo de imóvel")
    sectionKey: SectionKey     // Chave para expandir a seção correta ao selecionar
    label: string              // Texto exibido na sugestão
    filterKey: keyof PropertyFilters
    value: number | string | undefined
    isAmenity?: boolean        // true quando a ação é toggle em amenity_ids
    amenityId?: number         // preenchido quando isAmenity = true
}

Mapeamento de categorias:

category (label UI) filterKey sectionKey Fonte
"Tipo de imóvel" subtype_id tipo propertyTypes[*].subtypes[*]
"Cidade" city_id localizacao cities
"Bairro" neighborhood_id localizacao neighborhoods
"Comodidade" n/a (amenity_ids toggle) comodidades amenities

initOpenSections — lógica de estado inicial

function initOpenSections(filters: PropertyFilters): Record<SectionKey, boolean> {
    return {
        imobiliaria: filters.imobiliaria_id != null,
        localizacao: filters.city_id != null || filters.neighborhood_id != null,
        tipo:        filters.subtype_id != null,
        preco:       true,   // sempre aberta (FR-009)
        quartos:     filters.bedrooms_min != null
                     || filters.bathrooms_min != null
                     || filters.parking_min != null,
        area:        filters.area_min != null || filters.area_max != null,
        comodidades: (filters.amenity_ids?.length ?? 0) > 0,
    }
}

Estado do Banco de Dados

Aspecto Situação
Novas tabelas Nenhuma
Migrations Alembic Nenhuma
Colunas adicionadas Nenhuma
Índices adicionados Nenhum (os índices em city_id, neighborhood_id, subtype_id em properties já existem)
Triggers Nenhum

property_count é calculado em tempo de execução. O overhead da query é negligenciável:

  • cities: esperado < 50 linhas × JOIN com properties (< 5 k linhas) → < 5 ms
  • neighborhoods: esperado < 200 linhas → < 10 ms
  • property_types: contagem por subtypes separada das categorias → < 5 ms