feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
181
.specify/features/004-property-detail-page/data-model.md
Normal file
181
.specify/features/004-property-detail-page/data-model.md
Normal 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, 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue