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,181 @@
# 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, 2150 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, 102000 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).