feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -0,0 +1,270 @@
# 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<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