5.6 KiB
5.6 KiB
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 bydisplay_order)subtype: M:1 →PropertyType(joined)city: M:1 →City(joined)neighborhood: M:1 →Neighborhood(joined)amenities: M:M viaproperty_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_idemproperty_id(consultas futuras de admin)ix_contact_leads_created_atemcreated_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)
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)
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)
class ContactLeadCreatedOut(BaseModel):
id: int
message: str
Types TypeScript (frontend)
PropertyDetail (novo — herda de Property)
export interface PropertyDetail extends Property {
address: string | null
code: string | null
description: string | null
}
ContactFormData (novo)
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).