181 lines
5.6 KiB
Markdown
181 lines
5.6 KiB
Markdown
# Data Model: Property Detail Page (004)
|
||
|
||
**Feature**: `004-property-detail-page`
|
||
**Date**: 2026-04-13
|
||
|
||
---
|
||
|
||
## Entidades Modificadas
|
||
|
||
### Property (existente — colunas adicionadas)
|
||
|
||
| Campo | Tipo SQL | Tipo Python | Nullable | Notas |
|
||
|-------|----------|-------------|----------|-------|
|
||
| `id` | UUID PK | `UUID` | não | existente |
|
||
| `title` | VARCHAR(200) | `str` | não | existente |
|
||
| `slug` | VARCHAR(220) UNIQUE | `str` | não | existente |
|
||
| **`code`** | **VARCHAR(30) UNIQUE** | **`str \| None`** | **sim** | **novo** — ex: `"AP-00042"` |
|
||
| **`description`** | **TEXT** | **`str \| None`** | **sim** | **novo** — descrição narrativa |
|
||
| `address` | VARCHAR(300) | `str \| None` | sim | existente |
|
||
| `price` | NUMERIC(12,2) | `Decimal` | não | existente |
|
||
| `condo_fee` | NUMERIC(10,2) | `Decimal \| None` | sim | existente |
|
||
| `type` | ENUM(venda,aluguel) | `Literal["venda","aluguel"]` | não | existente |
|
||
| `subtype_id` | INT FK → property_types | `int \| None` | sim | existente |
|
||
| `bedrooms` | INT | `int` | não | existente |
|
||
| `bathrooms` | INT | `int` | não | existente |
|
||
| `parking_spots` | INT | `int` | não | existente |
|
||
| `area_m2` | INT | `int` | não | existente |
|
||
| `city_id` | INT FK → cities | `int \| None` | sim | existente |
|
||
| `neighborhood_id` | INT FK → neighborhoods | `int \| None` | sim | existente |
|
||
| `is_featured` | BOOLEAN | `bool` | não | existente |
|
||
| `is_active` | BOOLEAN | `bool` | não | existente |
|
||
| `created_at` | TIMESTAMP | `datetime` | não | existente |
|
||
|
||
**Relacionamentos existentes** (sem mudança):
|
||
- `photos`: 1:M → `PropertyPhoto` (cascade delete-orphan, order by `display_order`)
|
||
- `subtype`: M:1 → `PropertyType` (joined)
|
||
- `city`: M:1 → `City` (joined)
|
||
- `neighborhood`: M:1 → `Neighborhood` (joined)
|
||
- `amenities`: M:M via `property_amenity`
|
||
|
||
---
|
||
|
||
## Entidades Novas
|
||
|
||
### ContactLead (novo)
|
||
|
||
Registra cada solicitação de contato feita por um visitante em relação a um imóvel.
|
||
|
||
| Campo | Tipo SQL | Tipo Python | Nullable | Restrições |
|
||
|-------|----------|-------------|----------|------------|
|
||
| `id` | SERIAL PK | `int` | não | autoincrement |
|
||
| `property_id` | UUID FK → properties | `UUID \| None` | **sim** | ON DELETE SET NULL |
|
||
| `name` | VARCHAR(150) | `str` | não | |
|
||
| `email` | VARCHAR(254) | `str` | não | |
|
||
| `phone` | VARCHAR(20) | `str \| None` | sim | |
|
||
| `message` | TEXT | `str` | não | |
|
||
| `created_at` | TIMESTAMP WITH TIME ZONE | `datetime` | não | server_default=NOW() |
|
||
|
||
**Índices**:
|
||
- `ix_contact_leads_property_id` em `property_id` (consultas futuras de admin)
|
||
- `ix_contact_leads_created_at` em `created_at` (ordenação)
|
||
|
||
**Relacionamento**:
|
||
- `ContactLead.property` → `Property` (lazy="select", nullable via FK SET NULL)
|
||
|
||
**Sem cascade delete**: leads são preservados mesmo se o imóvel for deletado (histórico de negócio).
|
||
|
||
---
|
||
|
||
## Schemas Pydantic (backend)
|
||
|
||
### PropertyDetailOut (novo — herda de PropertyOut)
|
||
|
||
```python
|
||
class PropertyDetailOut(PropertyOut):
|
||
model_config = ConfigDict(from_attributes=True)
|
||
address: str | None
|
||
code: str | None
|
||
description: str | None
|
||
```
|
||
|
||
Usado exclusivamente pelo endpoint `GET /api/v1/properties/<slug>`.
|
||
|
||
### ContactLeadIn (novo — input de validação)
|
||
|
||
```python
|
||
class ContactLeadIn(BaseModel):
|
||
name: Annotated[str, Field(min_length=2, max_length=150)]
|
||
email: EmailStr
|
||
phone: Annotated[str | None, Field(max_length=20)] = None
|
||
message: Annotated[str, Field(min_length=10, max_length=2000)]
|
||
```
|
||
|
||
### ContactLeadCreatedOut (novo — resposta 201)
|
||
|
||
```python
|
||
class ContactLeadCreatedOut(BaseModel):
|
||
id: int
|
||
message: str
|
||
```
|
||
|
||
---
|
||
|
||
## Types TypeScript (frontend)
|
||
|
||
### PropertyDetail (novo — herda de Property)
|
||
|
||
```typescript
|
||
export interface PropertyDetail extends Property {
|
||
address: string | null
|
||
code: string | null
|
||
description: string | null
|
||
}
|
||
```
|
||
|
||
### ContactFormData (novo)
|
||
|
||
```typescript
|
||
export interface ContactFormData {
|
||
name: string
|
||
email: string
|
||
phone: string
|
||
message: string
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Estado do React — PropertyDetailPage
|
||
|
||
```
|
||
PropertyDetailPage
|
||
├── property: PropertyDetail | null (fetch por slug)
|
||
├── notFound: boolean (true se 404)
|
||
├── loading: boolean
|
||
└── ContactSection
|
||
├── formData: ContactFormData
|
||
├── submitting: boolean
|
||
├── submitStatus: 'idle' | 'success' | 'error'
|
||
└── fieldErrors: Partial<Record<keyof ContactFormData, string>>
|
||
```
|
||
|
||
---
|
||
|
||
## Validações de Estado
|
||
|
||
| Regra | Onde aplicada |
|
||
|-------|---------------|
|
||
| `is_active = false` → 404 | Backend (GET /properties/\<slug\>) |
|
||
| slug inexistente → 404 | Backend |
|
||
| `name` obrigatório, 2–150 chars | Backend (Pydantic) + Frontend (HTML validation) |
|
||
| `email` formato válido | Backend (EmailStr) + Frontend (type="email") |
|
||
| `phone` max 20 chars, opcional | Backend (Pydantic) |
|
||
| `message` obrigatório, 10–2000 chars | Backend (Pydantic) + Frontend |
|
||
| `property_id` via slug (nunca do cliente) | Backend |
|
||
|
||
---
|
||
|
||
## Transições de Estado do Formulário de Contato
|
||
|
||
```
|
||
idle → [usuário clica Enviar]
|
||
↓ validação frontend falha → exibe erros de campo → idle
|
||
↓ validação ok → submitting (botão desabilitado)
|
||
↓ 201 Created → success (mensagem de confirmação, formulário limpo)
|
||
↓ 4xx/5xx → error (mensagem de erro, dados preservados)
|
||
error → [usuário edita] → idle
|
||
```
|
||
|
||
---
|
||
|
||
## Agrupamento de Amenidades
|
||
|
||
| Valor `group` no BD | Label exibido |
|
||
|--------------------|---------------|
|
||
| `caracteristica` | Características |
|
||
| `lazer` | Lazer |
|
||
| `condominio` | Condomínio |
|
||
| `seguranca` | Segurança |
|
||
|
||
Grupos sem amenidade associada ao imóvel **não são renderizados** (FR-F07).
|