116 lines
3.5 KiB
Markdown
116 lines
3.5 KiB
Markdown
# 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
|
|
|
|
```ts
|
|
// Adição aos campos existentes:
|
|
q?: string // busca textual livre
|
|
sort?: SortOption
|
|
```
|
|
|
|
### `SortOption` (novo tipo)
|
|
|
|
```ts
|
|
type SortOption =
|
|
| 'relevance' // default — equivale a created_at DESC no backend
|
|
| 'price_asc'
|
|
| 'price_desc'
|
|
| 'area_desc'
|
|
| 'newest'
|
|
```
|
|
|
|
### `ViewMode` (novo tipo local — apenas frontend)
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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`
|
|
|
|
```ts
|
|
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:
|
|
|
|
```python
|
|
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
|
|
```
|