# 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 ```python # 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 ```python 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 ```python 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`). ```python 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) ```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` ```ts type SectionKey = | 'imobiliaria' | 'localizacao' | 'tipo' | 'preco' | 'quartos' | 'area' | 'comodidades' ``` ### `FilterSuggestion` ```ts 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 ```ts function initOpenSections(filters: PropertyFilters): Record { 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