7.7 KiB
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
0garante 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 comproperties(< 5 k linhas) → < 5 msneighborhoods: esperado < 200 linhas → < 10 msproperty_types: contagem por subtypes separada das categorias → < 5 ms